Downloads confirm flow can select a Dutch subtitle that is written beside the movie on import. Co-authored-by: Cursor <cursoragent@cursor.com>
254 lines
5.7 KiB
Go
254 lines
5.7 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 SubtitleFile struct {
|
|
Language string
|
|
Ext string
|
|
Content []byte
|
|
}
|
|
|
|
type Result struct {
|
|
ImportedPath string
|
|
SubtitlePath string
|
|
}
|
|
|
|
type Options struct {
|
|
SourcePath string
|
|
DestDir string
|
|
Title string
|
|
Year *int
|
|
ReleaseTags string
|
|
QualityHint string
|
|
Subtitle *SubtitleFile
|
|
}
|
|
|
|
// ImportMovie moves the largest video into destDir as:
|
|
//
|
|
// Title (Year) - <release tags>.ext
|
|
//
|
|
// and optionally writes a sidecar subtitle next to it.
|
|
func ImportMovie(opts Options) (*Result, error) {
|
|
sourcePath := filepath.Clean(strings.TrimSpace(opts.SourcePath))
|
|
destDir := filepath.Clean(strings.TrimSpace(opts.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(opts.Title)
|
|
if opts.Year != nil && *opts.Year > 1900 {
|
|
base = fmt.Sprintf("%s (%d)", base, *opts.Year)
|
|
}
|
|
|
|
tags := strings.TrimSpace(opts.ReleaseTags)
|
|
if tags == "" {
|
|
tags = ExtractReleaseTags(filepath.Base(video), opts.Title, opts.Year)
|
|
}
|
|
if tags == "" {
|
|
tags = strings.TrimSpace(opts.QualityHint)
|
|
}
|
|
if tags == "" {
|
|
tags = "2160p"
|
|
}
|
|
tags = sanitizeName(tags)
|
|
|
|
destName := fmt.Sprintf("%s - %s%s", base, tags, ext)
|
|
destPath := filepath.Join(destDir, destName)
|
|
|
|
if err := moveFile(video, destPath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := &Result{ImportedPath: destPath}
|
|
|
|
if opts.Subtitle != nil && len(opts.Subtitle.Content) > 0 {
|
|
lang := strings.ToLower(strings.TrimSpace(opts.Subtitle.Language))
|
|
if lang == "" {
|
|
lang = "nl"
|
|
}
|
|
subExt := strings.ToLower(strings.TrimSpace(opts.Subtitle.Ext))
|
|
if subExt == "" {
|
|
subExt = ".srt"
|
|
}
|
|
if !strings.HasPrefix(subExt, ".") {
|
|
subExt = "." + subExt
|
|
}
|
|
stem := strings.TrimSuffix(destName, ext)
|
|
subPath := filepath.Join(destDir, fmt.Sprintf("%s.%s%s", stem, lang, subExt))
|
|
if err := os.WriteFile(subPath, opts.Subtitle.Content, 0o644); err != nil {
|
|
return nil, fmt.Errorf("ondertitel schrijven: %w", err)
|
|
}
|
|
result.SubtitlePath = subPath
|
|
}
|
|
|
|
cleanupSource(sourcePath, video)
|
|
return result, nil
|
|
}
|
|
|
|
// ExtractReleaseTags keeps quality/group tokens from a release name.
|
|
func ExtractReleaseTags(release, title string, year *int) string {
|
|
s := release
|
|
s = regexp.MustCompile(`(?i)\.(mkv|mp4|m4v|avi|ts|m2ts|srt|ass|sub)$`).ReplaceAllString(s, "")
|
|
s = strings.ReplaceAll(s, ".", " ")
|
|
s = strings.ReplaceAll(s, "_", " ")
|
|
for _, w := range strings.Fields(title) {
|
|
if len(w) < 2 {
|
|
continue
|
|
}
|
|
re := regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(w) + `\b`)
|
|
s = re.ReplaceAllString(s, " ")
|
|
}
|
|
if year != nil && *year > 1900 {
|
|
re := regexp.MustCompile(`\b` + fmt.Sprintf("%d", *year) + `\b`)
|
|
s = re.ReplaceAllString(s, " ")
|
|
}
|
|
return strings.Join(strings.Fields(s), " ")
|
|
}
|
|
|
|
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
|
|
}
|
|
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 {
|
|
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])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sanitizeName(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
replacer := strings.NewReplacer(
|
|
"<", "", ">", "", ":", "", `"`, "", "/", "", "\\", "",
|
|
"|", "", "?", "", "*", "",
|
|
)
|
|
s = replacer.Replace(s)
|
|
return strings.Join(strings.Fields(s), " ")
|
|
}
|