package model import ( "regexp" "strconv" "strings" "github.com/navidrome/navidrome/utils/str" ) var ( srtTimeRegex = regexp.MustCompile(`^\s*(\d{1,2}):(\d{2}):(\d{2})[,.](\d{1,3})\s*$`) srtBlockSeparatorRegex = regexp.MustCompile(`\n\s*\n`) ) func parseSRT(language string, contents []byte) (LyricList, error) { raw := strings.ReplaceAll(string(contents), "\r\n", "\n") raw = strings.ReplaceAll(raw, "\r", "\n") blocks := splitSRTBlocks(raw) lines := make([]Line, 0, len(blocks)) for _, block := range blocks { line, ok, err := parseSRTBlock(block) if err != nil { return nil, err } if ok { lines = append(lines, line) } } if len(lines) == 0 { return nil, nil } lyrics := normalizeLyrics(Lyrics{ Lang: normalizeLyricLang(language), Line: lines, Synced: true, }) return LyricList{lyrics}, nil } func splitSRTBlocks(raw string) []string { raw = strings.TrimSpace(raw) if raw == "" { return nil } parts := srtBlockSeparatorRegex.Split(raw, -1) blocks := make([]string, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if part != "" { blocks = append(blocks, part) } } return blocks } func parseSRTBlock(block string) (Line, bool, error) { rawLines := strings.Split(block, "\n") lines := make([]string, 0, len(rawLines)) for _, line := range rawLines { lines = append(lines, strings.TrimSpace(line)) } if len(lines) == 0 { return Line{}, false, nil } startIdx := 0 if digitsOnly(lines[0]) { startIdx = 1 } if startIdx >= len(lines) { return Line{}, false, nil } timing := strings.Split(lines[startIdx], "-->") if len(timing) != 2 { return Line{}, false, nil } startMs, err := parseSRTTime(timing[0]) if err != nil { return Line{}, false, err } endMs, err := parseSRTTime(timing[1]) if err != nil { return Line{}, false, err } textLines := make([]string, 0, len(lines)-startIdx-1) for _, line := range lines[startIdx+1:] { if line == "" { continue } textLines = append(textLines, line) } value := str.SanitizeText(strings.Join(textLines, "\n")) if value == "" { return Line{}, false, nil } return Line{ Start: &startMs, End: &endMs, Value: value, }, true, nil } func parseSRTTime(value string) (int64, error) { match := srtTimeRegex.FindStringSubmatch(strings.TrimSpace(value)) if match == nil { return 0, strconv.ErrSyntax } hours, err := strconv.ParseInt(match[1], 10, 64) if err != nil { return 0, err } minutes, err := strconv.ParseInt(match[2], 10, 64) if err != nil { return 0, err } seconds, err := strconv.ParseInt(match[3], 10, 64) if err != nil { return 0, err } millis, err := strconv.ParseInt(match[4], 10, 64) if err != nil { return 0, err } switch len(match[4]) { case 1: millis *= 100 case 2: millis *= 10 } return (((hours*60)+minutes)*60+seconds)*1000 + millis, nil } func digitsOnly(value string) bool { if value == "" { return false } for _, ch := range value { if ch < '0' || ch > '9' { return false } } return true }