stremio/node/media-node/internal/importmedia/import.go
Jos Vooges | STH 18eb702de2 Add Synology Download Station search and 4K import pipeline.
Admin can search DS, confirm TMDB/IMDb, then auto-move finished downloads into a fixed 4K library folder via the media-node.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 17:42:12 +02:00

187 lines
4.2 KiB
Go

package importmedia
import (
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
)
var videoExt = map[string]bool{
".mkv": true, ".mp4": true, ".m4v": true,
".avi": true, ".ts": true, ".m2ts": true,
}
var sampleRe = regexp.MustCompile(`(?i)(?:^|[.\-_])(sample|trailer|preview)(?:[.\-_]|$)`)
type Result struct {
ImportedPath string
}
// ImportMovie moves the largest video from sourcePath into destDir as Title (Year) - 2160p.ext
// and cleans leftover files under the download folder when source was a directory.
func ImportMovie(sourcePath, destDir, title string, year *int, qualityHint string) (*Result, error) {
sourcePath = filepath.Clean(strings.TrimSpace(sourcePath))
destDir = filepath.Clean(strings.TrimSpace(destDir))
if sourcePath == "" || destDir == "" {
return nil, fmt.Errorf("source/dest pad ontbreekt")
}
video, err := findLargestVideo(sourcePath)
if err != nil {
return nil, err
}
if err := os.MkdirAll(destDir, 0o755); err != nil {
return nil, fmt.Errorf("doelmap aanmaken: %w", err)
}
ext := strings.ToLower(filepath.Ext(video))
base := sanitizeName(title)
if year != nil && *year > 1900 {
base = fmt.Sprintf("%s (%d)", base, *year)
}
q := strings.TrimSpace(qualityHint)
if q == "" {
q = "2160p"
}
destName := fmt.Sprintf("%s - %s%s", base, q, ext)
destPath := filepath.Join(destDir, destName)
if err := moveFile(video, destPath); err != nil {
return nil, err
}
// Cleanup download leftovers (only if source was a dir under a parent, or sibling junk)
cleanupSource(sourcePath, video)
return &Result{ImportedPath: destPath}, nil
}
func findLargestVideo(sourcePath string) (string, error) {
info, err := os.Stat(sourcePath)
if err != nil {
return "", fmt.Errorf("bron niet gevonden: %w", err)
}
var best string
var bestSize int64
consider := func(path string, size int64) {
if !videoExt[strings.ToLower(filepath.Ext(path))] {
return
}
name := filepath.Base(path)
if sampleRe.MatchString(name) {
return
}
if size > bestSize {
bestSize = size
best = path
}
}
if !info.IsDir() {
consider(sourcePath, info.Size())
if best == "" {
return "", fmt.Errorf("geen videobestand: %s", sourcePath)
}
return best, nil
}
err = filepath.Walk(sourcePath, func(path string, fi os.FileInfo, walkErr error) error {
if walkErr != nil || fi == nil || fi.IsDir() {
return nil
}
consider(path, fi.Size())
return nil
})
if err != nil {
return "", err
}
if best == "" {
return "", fmt.Errorf("geen videobestand in %s", sourcePath)
}
return best, nil
}
func moveFile(src, dst string) error {
if err := os.Rename(src, dst); err == nil {
return nil
}
// Cross-device: copy + remove
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return err
}
if _, err := io.Copy(out, in); err != nil {
out.Close()
_ = os.Remove(dst)
return err
}
if err := out.Close(); err != nil {
return err
}
return os.Remove(src)
}
func cleanupSource(sourcePath, movedVideo string) {
info, err := os.Stat(sourcePath)
if err != nil {
// source was a file that moved away
parent := filepath.Dir(sourcePath)
_ = removeEmptyDirs(parent)
return
}
if !info.IsDir() {
_ = os.Remove(sourcePath)
_ = removeEmptyDirs(filepath.Dir(sourcePath))
return
}
_ = filepath.Walk(sourcePath, func(path string, fi os.FileInfo, walkErr error) error {
if walkErr != nil || fi == nil {
return nil
}
if path == movedVideo {
return nil
}
if !fi.IsDir() {
_ = os.Remove(path)
}
return nil
})
_ = removeEmptyDirs(sourcePath)
}
func removeEmptyDirs(root string) error {
var dirs []string
_ = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
if err == nil && fi != nil && fi.IsDir() {
dirs = append(dirs, path)
}
return nil
})
for i := len(dirs) - 1; i >= 0; i-- {
_ = os.Remove(dirs[i]) // only succeeds if empty
}
return nil
}
func sanitizeName(s string) string {
s = strings.TrimSpace(s)
replacer := strings.NewReplacer(
"<", "", ">", "", ":", "", `"`, "", "/", "", "\\", "",
"|", "", "?", "", "*", "",
)
s = replacer.Replace(s)
return strings.Join(strings.Fields(s), " ")
}