package scanner import ( "fmt" "os" "path/filepath" "strings" "github.com/Silo-Server/silo-server/internal/lang" ) // subtitleExtensions lists the recognized external subtitle file extensions. var subtitleExtensions = map[string]bool{ ".srt": true, ".vtt": true, ".ass": true, ".ssa": true, ".sub": true, } // DetectExternalSubtitles scans the directory containing the media file for // sidecar subtitle files that match the media file's basename. // // Naming conventions: // // Movie.srt -> no language // Movie.eng.srt -> language: "eng" // Movie.en.srt -> language: "en" // Movie.en.forced.srt -> language: "en", forced: true // Movie.forced.eng.srt -> language: "eng", forced: true func DetectExternalSubtitles(mediaFilePath string) ([]ExternalSubtitleInfo, error) { dir := filepath.Dir(mediaFilePath) mediaBase := stripExtension(filepath.Base(mediaFilePath)) entries, err := os.ReadDir(dir) if err != nil { return nil, fmt.Errorf("subtitles: read dir %s: %w", dir, err) } var results []ExternalSubtitleInfo for _, entry := range entries { if entry.IsDir() { continue } name := entry.Name() ext := strings.ToLower(filepath.Ext(name)) if !subtitleExtensions[ext] { continue } // Check that the subtitle file starts with the media basename // followed by either a dot separator or nothing (exact match). nameWithoutExt := name[:len(name)-len(ext)] if !strings.HasPrefix(nameWithoutExt, mediaBase) { continue } // The suffix after the base name must be empty or start with a dot. // This prevents "MovieExtra.srt" from matching "Movie.mkv". suffix := nameWithoutExt[len(mediaBase):] if suffix != "" && !strings.HasPrefix(suffix, ".") { continue } info := ExternalSubtitleInfo{ Path: filepath.Join(dir, name), Format: strings.TrimPrefix(ext, "."), Title: name, } parseSuffix(suffix, &info) results = append(results, info) } return results, nil } // parseSuffix extracts the language and forced flag from the suffix portion // of a subtitle filename. The suffix is the part between the media basename // and the subtitle extension, e.g. ".en.forced" or ".forced.eng" or ".eng". func parseSuffix(suffix string, info *ExternalSubtitleInfo) { if suffix == "" { return } // Split on dots, filtering out empty parts. parts := splitDots(suffix) if len(parts) == 0 { return } // Identify forced flag and language from the parts. // Valid patterns: // [lang] // [lang, "forced"] // ["forced", lang] for _, p := range parts { lower := strings.ToLower(p) if lower == "forced" { info.Forced = true } else if info.Language == "" { info.Language = lang.Canonical(p) } } } // splitDots splits a string on '.' and returns non-empty parts. func splitDots(s string) []string { raw := strings.Split(s, ".") var parts []string for _, p := range raw { if p != "" { parts = append(parts, p) } } return parts } // stripExtension removes the file extension from a filename. func stripExtension(name string) string { ext := filepath.Ext(name) return name[:len(name)-len(ext)] }