stremio/node/media-node/internal/importmedia/sidecars.go
Jos Vooges | STH 9de3ee62d7 Serve local sidecar subtitles via existing client search/fetch and Stremio.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 03:37:08 +02:00

117 lines
2.6 KiB
Go

package importmedia
import (
"encoding/base64"
"os"
"path/filepath"
"regexp"
"strings"
)
var langSuffixRe = regexp.MustCompile(`(?i)\.(nl|nld|dut|dutch|en|eng|english|forced|sdh|hi)(?:\.(forced|sdh|hi))?$`)
var subtitleExt = map[string]string{
".srt": "srt",
".vtt": "vtt",
".ass": "ass",
".ssa": "ass",
}
const maxSidecarBytes = 2 * 1024 * 1024
type SidecarHit struct {
FileName string
Language string
Format string
Content []byte
}
// FindSidecarsBesideVideo returns subtitle files in the same folder that match the video stem.
func FindSidecarsBesideVideo(videoPath string) ([]SidecarHit, error) {
videoPath = filepath.Clean(strings.TrimSpace(videoPath))
if videoPath == "" {
return nil, nil
}
info, err := os.Stat(videoPath)
if err != nil || info.IsDir() {
return nil, err
}
dir := filepath.Dir(videoPath)
stem := strings.TrimSuffix(filepath.Base(videoPath), filepath.Ext(videoPath))
stemLower := strings.ToLower(stem)
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var out []SidecarHit
for _, ent := range entries {
if ent.IsDir() {
continue
}
name := ent.Name()
ext := strings.ToLower(filepath.Ext(name))
format, ok := subtitleExt[ext]
if !ok {
continue
}
base := strings.TrimSuffix(name, filepath.Ext(name))
baseLower := strings.ToLower(base)
if !sidecarMatchesStem(stemLower, baseLower) {
continue
}
full := filepath.Join(dir, name)
fi, err := ent.Info()
if err != nil || fi.Size() <= 0 || fi.Size() > maxSidecarBytes {
continue
}
raw, err := os.ReadFile(full)
if err != nil || len(raw) == 0 {
continue
}
out = append(out, SidecarHit{
FileName: name,
Language: guessLangFromName(baseLower, stemLower),
Format: format,
Content: raw,
})
}
return out, nil
}
func sidecarMatchesStem(stemLower, subBaseLower string) bool {
if subBaseLower == stemLower {
return true
}
if strings.HasPrefix(subBaseLower, stemLower+".") {
return true
}
// Strip known lang tags and compare
stripped := langSuffixRe.ReplaceAllString(subBaseLower, "")
return stripped == stemLower
}
func guessLangFromName(subBaseLower, stemLower string) string {
rest := strings.TrimPrefix(subBaseLower, stemLower)
rest = strings.TrimPrefix(rest, ".")
if rest == "" {
return "nl"
}
parts := strings.Split(rest, ".")
for _, p := range parts {
switch p {
case "nl", "nld", "dut", "dutch":
return "nl"
case "en", "eng", "english":
return "en"
}
}
return "nl"
}
// EncodeSidecarBase64 helper for control payloads.
func EncodeSidecarBase64(b []byte) string {
return base64.StdEncoding.EncodeToString(b)
}