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), " ") }