package media import ( "path/filepath" "strings" "unicode" ) const maxComponentRunes = 80 const maxComponentBytes = 60 // RelativeVideoPath は worker と web が共有する保存先の相対パスを返す。 func RelativeVideoPath(seriesTitle, episodeTitle, episodeID string) string { series := sanitizeComponent(seriesTitle) episode := sanitizeComponent(episodeTitle) id := sanitizeComponent(episodeID) return filepath.Join(series, series+" "+episode+" ["+id+"].mp4") } func sanitizeComponent(value string) string { var result []rune lastReplacement := false for _, r := range value { invalid := unicode.IsControl(r) || strings.ContainsRune(`<>:"/\|?*`, r) if invalid { if !lastReplacement { result = append(result, '_') lastReplacement = true } continue } if unicode.IsSpace(r) { r = ' ' } result = append(result, r) lastReplacement = false } clean := strings.Trim(string(result), " .") if clean == "" || clean == "." || clean == ".." { return "名称未設定" } runes := []rune(clean) if len(runes) > maxComponentRunes { runes = runes[:maxComponentRunes] } var limited []rune bytes := 0 for _, r := range runes { size := len(string(r)) if bytes+size > maxComponentBytes { break } limited = append(limited, r) bytes += size } clean = strings.Trim(string(limited), " .") if clean == "" || clean == "." || clean == ".." { return "名称未設定" } return clean }