Skip to content

Commit 5852a81

Browse files
committed
feat(strm): add STRM driver with independent sync flow
- implement STRM mount and generate behavior with alias mapping - support local save modes: insert, update, sync - keep config compatibility and remove previous intermediate history
1 parent 39c236b commit 5852a81

File tree

5 files changed

+588
-0
lines changed

5 files changed

+588
-0
lines changed

drivers/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import (
6666
_ "github.com/alist-org/alist/v3/drivers/seafile"
6767
_ "github.com/alist-org/alist/v3/drivers/sftp"
6868
_ "github.com/alist-org/alist/v3/drivers/smb"
69+
_ "github.com/alist-org/alist/v3/drivers/strm"
6970
_ "github.com/alist-org/alist/v3/drivers/teambition"
7071
_ "github.com/alist-org/alist/v3/drivers/terabox"
7172
_ "github.com/alist-org/alist/v3/drivers/thunder"

drivers/strm/driver.go

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package strm
2+
3+
import (
4+
"context"
5+
"errors"
6+
stdpath "path"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/alist-org/alist/v3/internal/driver"
11+
"github.com/alist-org/alist/v3/internal/errs"
12+
"github.com/alist-org/alist/v3/internal/fs"
13+
"github.com/alist-org/alist/v3/internal/model"
14+
)
15+
16+
type Strm struct {
17+
model.Storage
18+
Addition
19+
20+
aliases map[string][]string
21+
autoFlatten bool
22+
singleRootKey string
23+
24+
mediaExtSet map[string]struct{}
25+
downloadExtSet map[string]struct{}
26+
normalizedMode string
27+
normalizedPrefix string
28+
}
29+
30+
func (d *Strm) Config() driver.Config {
31+
return config
32+
}
33+
34+
func (d *Strm) GetAddition() driver.Additional {
35+
return &d.Addition
36+
}
37+
38+
func (d *Strm) Init(ctx context.Context) error {
39+
if strings.TrimSpace(d.Paths) == "" {
40+
return errors.New("paths is required")
41+
}
42+
if d.SaveStrmToLocal && strings.TrimSpace(d.SaveStrmLocalPath) == "" {
43+
return errors.New("SaveStrmLocalPath is required")
44+
}
45+
46+
d.aliases = parseAliases(d.Paths)
47+
if len(d.aliases) == 0 {
48+
return errors.New("no valid path mapping found")
49+
}
50+
51+
d.autoFlatten = len(d.aliases) == 1
52+
d.singleRootKey = ""
53+
if d.autoFlatten {
54+
for k := range d.aliases {
55+
d.singleRootKey = k
56+
}
57+
}
58+
59+
d.mediaExtSet = parseExtSet(defaultIfEmpty(d.FilterFileTypes, defaultMediaExt))
60+
d.downloadExtSet = parseExtSet(defaultIfEmpty(d.DownloadFileTypes, defaultDownloadExt))
61+
d.normalizedPrefix = normalizePrefix(defaultIfEmpty(d.PathPrefix, "/d"))
62+
d.normalizedMode = normalizeSaveMode(d.SaveLocalMode)
63+
64+
if d.Version != 5 {
65+
d.FilterFileTypes = mergeDefaultExtCSV(d.FilterFileTypes, defaultMediaExt)
66+
d.DownloadFileTypes = mergeDefaultExtCSV(d.DownloadFileTypes, defaultDownloadExt)
67+
d.PathPrefix = "/d"
68+
d.Version = 5
69+
}
70+
if d.SaveLocalMode == "" {
71+
d.SaveLocalMode = SaveLocalInsertMode
72+
}
73+
return nil
74+
}
75+
76+
func (d *Strm) Drop(ctx context.Context) error {
77+
d.aliases = nil
78+
d.mediaExtSet = nil
79+
d.downloadExtSet = nil
80+
return nil
81+
}
82+
83+
func (Addition) GetRootPath() string {
84+
return "/"
85+
}
86+
87+
func (d *Strm) Get(ctx context.Context, path string) (model.Obj, error) {
88+
path = cleanPath(path)
89+
root, sub := d.splitVirtualPath(path)
90+
targets, ok := d.aliases[root]
91+
if !ok {
92+
return nil, errs.ObjectNotFound
93+
}
94+
95+
for _, targetRoot := range targets {
96+
realPath := stdpath.Join(targetRoot, sub)
97+
obj, err := fs.Get(ctx, realPath, &fs.GetArgs{NoLog: true})
98+
if err != nil {
99+
continue
100+
}
101+
if obj.IsDir() {
102+
return wrapObj(path, obj, 0), nil
103+
}
104+
return wrapObj(realPath, obj, obj.GetSize()), nil
105+
}
106+
107+
if strings.HasSuffix(strings.ToLower(path), ".strm") {
108+
return nil, errs.NotSupport
109+
}
110+
return nil, errs.ObjectNotFound
111+
}
112+
113+
func (d *Strm) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
114+
virtualDir := cleanPath(dir.GetPath())
115+
if virtualDir == "/" && !d.autoFlatten {
116+
objs := d.listVirtualRoots()
117+
d.syncLocalDir(ctx, virtualDir, objs)
118+
return objs, nil
119+
}
120+
121+
root, sub := d.splitVirtualPath(virtualDir)
122+
targets, ok := d.aliases[root]
123+
if !ok {
124+
return nil, errs.ObjectNotFound
125+
}
126+
127+
out := make([]model.Obj, 0)
128+
for _, targetRoot := range targets {
129+
realDir := stdpath.Join(targetRoot, sub)
130+
objs, err := fs.List(ctx, realDir, &fs.ListArgs{NoLog: true, Refresh: args.Refresh})
131+
if err != nil {
132+
continue
133+
}
134+
out = append(out, d.mapListedObjects(ctx, realDir, objs)...)
135+
}
136+
137+
d.syncLocalDir(ctx, virtualDir, out)
138+
return out, nil
139+
}
140+
141+
func (d *Strm) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
142+
if file.GetID() == "strm" {
143+
line := d.buildStrmLine(ctx, file.GetPath())
144+
return &model.Link{MFile: model.NewNopMFile(strings.NewReader(line + "\n"))}, nil
145+
}
146+
return d.linkRealFile(ctx, file.GetPath(), args)
147+
}
148+
149+
func (d *Strm) listVirtualRoots() []model.Obj {
150+
objs := make([]model.Obj, 0, len(d.aliases))
151+
for k := range d.aliases {
152+
objs = append(objs, &model.Object{
153+
Path: "/" + k,
154+
Name: k,
155+
IsFolder: true,
156+
Modified: d.Modified,
157+
})
158+
}
159+
return objs
160+
}
161+
162+
func (d *Strm) mapListedObjects(ctx context.Context, realDir string, listed []model.Obj) []model.Obj {
163+
ret := make([]model.Obj, 0, len(listed))
164+
for _, obj := range listed {
165+
if obj.IsDir() {
166+
ret = append(ret, &model.Object{
167+
Name: obj.GetName(),
168+
Path: "",
169+
IsFolder: true,
170+
Modified: obj.ModTime(),
171+
})
172+
continue
173+
}
174+
175+
realPath := stdpath.Join(realDir, obj.GetName())
176+
ext := fileExt(obj.GetName())
177+
178+
if _, ok := d.downloadExtSet[ext]; ok {
179+
ret = append(ret, d.cloneWithPath(obj, realPath, obj.GetName(), "", obj.GetSize()))
180+
continue
181+
}
182+
if _, ok := d.mediaExtSet[ext]; ok {
183+
strmName := strings.TrimSuffix(obj.GetName(), stdpath.Ext(obj.GetName())) + ".strm"
184+
size := int64(len(d.buildStrmLine(ctx, realPath)) + 1)
185+
ret = append(ret, d.cloneWithPath(obj, realPath, strmName, "strm", size))
186+
}
187+
}
188+
return ret
189+
}
190+
191+
func (d *Strm) cloneWithPath(src model.Obj, realPath, name, id string, size int64) model.Obj {
192+
baseObj := model.Object{
193+
ID: id,
194+
Path: realPath,
195+
Name: name,
196+
Size: size,
197+
Modified: src.ModTime(),
198+
IsFolder: src.IsDir(),
199+
}
200+
thumb, ok := model.GetThumb(src)
201+
if !ok {
202+
return &baseObj
203+
}
204+
return &model.ObjThumb{Object: baseObj, Thumbnail: model.Thumbnail{Thumbnail: thumb}}
205+
}
206+
207+
func (d *Strm) splitVirtualPath(path string) (string, string) {
208+
if d.autoFlatten {
209+
return d.singleRootKey, path
210+
}
211+
trimmed := strings.TrimPrefix(path, "/")
212+
parts := strings.SplitN(trimmed, "/", 2)
213+
if len(parts) == 1 {
214+
return parts[0], ""
215+
}
216+
return parts[0], parts[1]
217+
}
218+
219+
func cleanPath(path string) string {
220+
if path == "" {
221+
return "/"
222+
}
223+
return filepath.ToSlash(stdpath.Clean("/" + strings.TrimPrefix(path, "/")))
224+
}
225+
226+
func wrapObj(path string, src model.Obj, size int64) model.Obj {
227+
return &model.Object{
228+
Path: path,
229+
Name: src.GetName(),
230+
Size: size,
231+
Modified: src.ModTime(),
232+
IsFolder: src.IsDir(),
233+
HashInfo: src.GetHash(),
234+
}
235+
}
236+
237+
var _ driver.Driver = (*Strm)(nil)

drivers/strm/hook.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package strm
2+
3+
// Local sync is triggered during STRM directory listing.

drivers/strm/meta.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package strm
2+
3+
import (
4+
"github.com/alist-org/alist/v3/internal/driver"
5+
"github.com/alist-org/alist/v3/internal/op"
6+
)
7+
8+
const (
9+
SaveLocalInsertMode = "insert"
10+
SaveLocalUpdateMode = "update"
11+
SaveLocalSyncMode = "sync"
12+
)
13+
14+
type Addition struct {
15+
Paths string `json:"paths" required:"true" type:"text"`
16+
SiteUrl string `json:"siteUrl" type:"text" required:"false" help:"The prefix URL of generated strm file"`
17+
PathPrefix string `json:"PathPrefix" type:"text" required:"false" default:"/d" help:"Path prefix in strm content"`
18+
DownloadFileTypes string `json:"downloadFileTypes" type:"text" default:"ass,srt,vtt,sub,strm" required:"false" help:"Extensions to download as local files"`
19+
FilterFileTypes string `json:"filterFileTypes" type:"text" default:"mp4,mkv,flv,avi,wmv,ts,rmvb,webm,mp3,flac,aac,wav,ogg,m4a,wma,alac" required:"false" help:"Extensions to expose as .strm"`
20+
EncodePath bool `json:"encodePath" default:"true" required:"true" help:"Encode path in strm content"`
21+
WithoutUrl bool `json:"withoutUrl" default:"false" help:"Generate path-only strm content"`
22+
WithSign bool `json:"withSign" default:"false" help:"Append sign query to generated URL"`
23+
SaveStrmToLocal bool `json:"SaveStrmToLocal" default:"false" help:"Save generated files to local disk"`
24+
SaveStrmLocalPath string `json:"SaveStrmLocalPath" type:"text" help:"Local path for generated files"`
25+
SaveLocalMode string `json:"SaveLocalMode" type:"select" help:"Local save mode" options:"insert,update,sync" default:"insert"`
26+
Version int
27+
}
28+
29+
var config = driver.Config{
30+
Name: "Strm",
31+
LocalSort: true,
32+
OnlyProxy: true,
33+
NoCache: true,
34+
NoUpload: true,
35+
DefaultRoot: "/",
36+
}
37+
38+
func init() {
39+
op.RegisterDriver(func() driver.Driver {
40+
return &Strm{Addition: Addition{EncodePath: true}}
41+
})
42+
}

0 commit comments

Comments
 (0)