navidrome/model/lyrics_srt.go

167 lines
3.2 KiB
Go

package model
import (
"bytes"
"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(contents []byte) (LyricList, error) {
return parseSRTWithLanguage(contents, "xxx")
}
func parseSRTWithLanguage(contents []byte, language string) (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) {
scanner := bytes.Split([]byte(block), []byte("\n"))
if len(scanner) == 0 {
return Line{}, false, nil
}
lines := make([]string, 0, len(scanner))
for _, line := range scanner {
lines = append(lines, strings.TrimSpace(string(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
}