Files
MeBox/internal/service/strm_tree_import.go
T
2026-06-30 20:18:02 +08:00

142 lines
4.4 KiB
Go

package service
import (
"context"
"errors"
"os"
"path"
"path/filepath"
"strings"
)
type GenerateSTRMTreeOptions struct {
Provider string `json:"provider"`
TreeText string `json:"tree_text,omitempty"`
Paths []string `json:"paths,omitempty"`
SourceRoot string `json:"source_root,omitempty"`
OutputPrefix string `json:"output_prefix,omitempty"`
OutputDir string `json:"output_dir"`
BaseURL string `json:"base_url,omitempty"`
Overwrite bool `json:"overwrite"`
Cleanup bool `json:"cleanup"`
}
type strmTreeSource struct {
Provider string
Path string
RefPath string
}
func (s *STRMService) GenerateFromTree(ctx context.Context, opts GenerateSTRMTreeOptions) (*GenerateSTRMResult, error) {
provider := normalizeSTRMTreeProvider(opts.Provider)
if provider == "" {
return nil, errors.New("provider required")
}
outputDir := resolveMappedDestinationPath(strings.TrimSpace(opts.OutputDir))
if outputDir == "" || outputDir == "." {
return nil, errors.New("output_dir required")
}
if err := os.MkdirAll(outputDir, 0o755); err != nil { // #nosec G301 -- STRM output directories must be readable by media players.
return nil, err
}
result := &GenerateSTRMResult{LibraryID: provider, OutputDir: outputDir}
expectedFiles := make(map[string]struct{})
for _, source := range collectSTRMTreeSources(opts) {
select {
case <-ctx.Done():
return result, ctx.Err()
default:
}
item := generateTreeSTRMItem(outputDir, source, opts)
if item.FilePath != "" && item.Action != "error" {
expectedFiles[filepath.Clean(item.FilePath)] = struct{}{}
}
result.addItem(item)
}
if opts.Cleanup && len(expectedFiles) > 0 {
cleanupDir := outputDir
if prefix, err := strmTreeOutputPrefixPath(opts.OutputPrefix); err == nil && prefix != "" {
cleanupDir = filepath.Join(outputDir, prefix)
}
cleaned, err := removeStaleSTRMFiles(cleanupDir, expectedFiles)
result.Cleaned += cleaned
if err != nil {
result.Errors = append(result.Errors, err.Error())
}
}
return result, nil
}
func generateTreeSTRMItem(outputDir string, source strmTreeSource, opts GenerateSTRMTreeOptions) GenerateSTRMItem {
relSource := strmTreeRelativeSource(source.Path, opts.SourceRoot)
relPath, err := strmTreeOutputRelativePath(relSource)
item := GenerateSTRMItem{Title: strings.TrimSuffix(path.Base(source.Path), path.Ext(source.Path))}
if err != nil {
item.Action = "error"
item.Reason = err.Error()
return item
}
prefix, err := strmTreeOutputPrefixPath(opts.OutputPrefix)
if err != nil {
item.Action = "error"
item.Reason = err.Error()
return item
}
filePath := filepath.Join(outputDir, prefix, relPath)
item.FilePath = filePath
item.URL = absolutizeSTRMURL(BuildRelativeCloudPlayURL(source.Provider, strmTreeCloudRef(source.cloudRefPath(), opts.SourceRoot)), opts.BaseURL)
if _, err := os.Stat(filePath); err == nil && !opts.Overwrite {
item.Action = "skipped"
item.Reason = "target exists"
return item
}
action := "generated"
if _, err := os.Stat(filePath); err == nil {
action = "updated"
}
if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil { // #nosec G301 -- STRM output directories must be readable by media players.
item.Action = "error"
item.Reason = err.Error()
return item
}
if err := os.WriteFile(filePath, []byte(item.URL+"\n"), 0o644); err != nil { // #nosec G306 -- STRM files are media sidecars intended to be readable by players.
item.Action = "error"
item.Reason = err.Error()
return item
}
item.Action = action
return item
}
func collectSTRMTreeSources(opts GenerateSTRMTreeOptions) []strmTreeSource {
fallbackProvider := normalizeSTRMTreeProvider(opts.Provider)
out := make([]strmTreeSource, 0, len(opts.Paths))
seen := map[string]struct{}{}
add := func(value string) {
source := normalizeSTRMTreeSourceWithProvider(value, fallbackProvider)
if source.Provider == "" || source.Path == "" || !strmTreeSourceIsVideo(source.Path) {
return
}
key := strings.ToLower(source.Provider) + "\x00" + strings.ToLower(source.Path) + "\x00" + strings.ToLower(source.cloudRefPath())
if _, ok := seen[key]; ok {
return
}
seen[key] = struct{}{}
out = append(out, source)
}
for _, value := range opts.Paths {
add(value)
}
for _, value := range parseSTRMTreeText(opts.TreeText) {
add(value)
}
return out
}
func (s strmTreeSource) cloudRefPath() string {
if strings.TrimSpace(s.RefPath) != "" {
return s.RefPath
}
return s.Path
}