mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-17 23:07:16 +02:00
refactor(lyrics): move LRC parser to its own lyrics_lrc.go
Extract parseLRC, the enhanced-LRC helpers (parseEnhancedLine, adjustGroup, stripEnhancedMarkers, shiftELRCCues), parseTime, and the LRC regexes from lyrics.go into lyrics_lrc.go, with the parseLRC tests in lyrics_lrc_test.go. This makes the layout symmetric — one file per format (lrc/srt/ttml/yaml) — and leaves lyrics.go holding only shared types and cue normalization. All moved symbols were already LRC-private; no behavior change.
This commit is contained in:
parent
8190e3a535
commit
2ee3857e42
4 changed files with 569 additions and 556 deletions
343
model/lyrics.go
343
model/lyrics.go
|
|
@ -1,17 +1,11 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/utils/gg"
|
||||
"github.com/navidrome/navidrome/utils/str"
|
||||
)
|
||||
|
||||
type Cue struct {
|
||||
|
|
@ -55,20 +49,6 @@ const (
|
|||
LyricKindPronunciation = "pronunciation"
|
||||
)
|
||||
|
||||
// support the standard [mm:ss.mm], as well as [hh:*] and [*.mmm]
|
||||
const timeRegexString = `\[([0-9]{1,2}:)?([0-9]{1,2}):([0-9]{1,2})(\.[0-9]{1,3})?\]`
|
||||
|
||||
var (
|
||||
// Should either be at the beginning of file, or beginning of line
|
||||
syncRegex = regexp.MustCompile(`(^|\n)\s*` + timeRegexString)
|
||||
timeRegex = regexp.MustCompile(timeRegexString)
|
||||
lrcIdRegex = regexp.MustCompile(`\[(ar|ti|offset|lang):([^]]+)]`)
|
||||
|
||||
// Enhanced LRC: inline word-level timing markers like <00:12.34>
|
||||
enhancedLRCTimeString = `<([0-9]{1,2}:)?([0-9]{1,2}):([0-9]{1,2})(\.[0-9]{1,3})?>`
|
||||
enhancedLRCRegex = regexp.MustCompile(enhancedLRCTimeString)
|
||||
)
|
||||
|
||||
func (l Lyrics) IsEmpty() bool {
|
||||
return len(l.Line) == 0
|
||||
}
|
||||
|
|
@ -89,329 +69,6 @@ func (l Lyrics) EffectiveKind() string {
|
|||
return l.Kind
|
||||
}
|
||||
|
||||
func parseLRC(language, text string) (*Lyrics, error) {
|
||||
text = str.SanitizeText(text)
|
||||
|
||||
lines := strings.Split(text, "\n")
|
||||
structuredLines := make([]Line, 0, len(lines)*2)
|
||||
|
||||
artist := ""
|
||||
title := ""
|
||||
var offset *int64 = nil
|
||||
|
||||
synced := syncRegex.MatchString(text)
|
||||
priorLine := ""
|
||||
validLine := false
|
||||
repeated := false
|
||||
var timestamps []int64
|
||||
|
||||
for _, line := range lines {
|
||||
line := strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
if validLine {
|
||||
priorLine += "\n"
|
||||
}
|
||||
continue
|
||||
}
|
||||
var text string
|
||||
var time *int64 = nil
|
||||
|
||||
if synced {
|
||||
idTag := lrcIdRegex.FindStringSubmatch(line)
|
||||
if idTag != nil {
|
||||
switch idTag[1] {
|
||||
case "ar":
|
||||
artist = str.SanitizeText(strings.TrimSpace(idTag[2]))
|
||||
case "lang":
|
||||
language = str.SanitizeText(strings.TrimSpace(idTag[2]))
|
||||
case "offset":
|
||||
{
|
||||
off, err := strconv.ParseInt(strings.TrimSpace(idTag[2]), 10, 64)
|
||||
if err != nil {
|
||||
log.Warn("Error parsing offset", "offset", idTag[2], "error", err)
|
||||
} else {
|
||||
offset = &off
|
||||
}
|
||||
}
|
||||
case "ti":
|
||||
title = str.SanitizeText(strings.TrimSpace(idTag[2]))
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
times := timeRegex.FindAllStringSubmatchIndex(line, -1)
|
||||
if len(times) > 1 {
|
||||
repeated = true
|
||||
}
|
||||
|
||||
// The second condition is for when there is a timestamp in the middle of
|
||||
// a line (after any text)
|
||||
if times == nil || times[0][0] != 0 {
|
||||
if validLine {
|
||||
priorLine += "\n" + line
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if validLine {
|
||||
value, baseCues := parseEnhancedLine(priorLine)
|
||||
for idx := range timestamps {
|
||||
startCopy := timestamps[idx]
|
||||
structuredLines = append(structuredLines, Line{
|
||||
Start: &startCopy,
|
||||
Value: value,
|
||||
Cue: shiftELRCCues(baseCues, timestamps[idx]-timestamps[0]),
|
||||
})
|
||||
}
|
||||
timestamps = nil
|
||||
}
|
||||
|
||||
end := 0
|
||||
|
||||
// [fullStart, fullEnd, hourStart, hourEnd, minStart, minEnd, secStart, secEnd, msStart, msEnd]
|
||||
for _, match := range times {
|
||||
// for multiple matches, we need to check that later matches are not
|
||||
// in the middle of the string
|
||||
if end != 0 {
|
||||
middle := strings.TrimSpace(line[end:match[0]])
|
||||
if middle != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
end = match[1]
|
||||
timeInMillis, err := parseTime(line, match)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timestamps = append(timestamps, timeInMillis)
|
||||
}
|
||||
|
||||
if end >= len(line) {
|
||||
priorLine = ""
|
||||
} else {
|
||||
priorLine = strings.TrimSpace(line[end:])
|
||||
}
|
||||
|
||||
validLine = true
|
||||
} else {
|
||||
text = line
|
||||
structuredLines = append(structuredLines, Line{
|
||||
Start: time,
|
||||
Value: text,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if validLine {
|
||||
value, baseCues := parseEnhancedLine(priorLine)
|
||||
for idx := range timestamps {
|
||||
startCopy := timestamps[idx]
|
||||
structuredLines = append(structuredLines, Line{
|
||||
Start: &startCopy,
|
||||
Value: value,
|
||||
Cue: shiftELRCCues(baseCues, timestamps[idx]-timestamps[0]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If there are repeated values, there is no guarantee that they are in order
|
||||
// In this, case, sort the lyrics by start time
|
||||
if repeated {
|
||||
slices.SortFunc(structuredLines, func(a, b Line) int {
|
||||
return cmp.Compare(*a.Start, *b.Start)
|
||||
})
|
||||
}
|
||||
|
||||
lyrics := Lyrics{
|
||||
DisplayArtist: artist,
|
||||
DisplayTitle: title,
|
||||
Lang: language,
|
||||
Line: NormalizeCueLines(structuredLines),
|
||||
Offset: offset,
|
||||
Synced: synced,
|
||||
}
|
||||
return &lyrics, nil
|
||||
}
|
||||
|
||||
// parseEnhancedLine extracts word-level timing cues from Enhanced LRC inline markers
|
||||
// and computes UTF-8 byte offsets against the final stripped line value.
|
||||
func parseEnhancedLine(text string) (string, []Cue) {
|
||||
matches := enhancedLRCRegex.FindAllStringSubmatchIndex(text, -1)
|
||||
if len(matches) == 0 {
|
||||
return strings.TrimSpace(text), nil
|
||||
}
|
||||
|
||||
type segment struct {
|
||||
start int64
|
||||
rawStart int
|
||||
rawEnd int
|
||||
}
|
||||
|
||||
segments := make([]segment, 0, len(matches))
|
||||
var rawValue strings.Builder
|
||||
for i, match := range matches {
|
||||
timeMs, err := parseTime(
|
||||
// Rewrite <...> as [...] so parseTime can handle it with the same logic
|
||||
"["+text[match[0]+1:match[1]-1]+"]",
|
||||
// Adjust match indices to point into our rewritten string (need start/end pairs for each group)
|
||||
[]int{
|
||||
0, match[1] - match[0],
|
||||
adjustGroup(match, 2), adjustGroup(match, 3),
|
||||
adjustGroup(match, 4), adjustGroup(match, 5),
|
||||
adjustGroup(match, 6), adjustGroup(match, 7),
|
||||
adjustGroup(match, 8), adjustGroup(match, 9),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Text runs from after this marker to the start of the next marker (or end of string)
|
||||
textStart := match[1]
|
||||
var textEnd int
|
||||
if i+1 < len(matches) {
|
||||
textEnd = matches[i+1][0]
|
||||
} else {
|
||||
textEnd = len(text)
|
||||
}
|
||||
|
||||
word := text[textStart:textEnd]
|
||||
if word == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
rawStart := rawValue.Len()
|
||||
rawValue.WriteString(word)
|
||||
segments = append(segments, segment{
|
||||
start: timeMs,
|
||||
rawStart: rawStart,
|
||||
rawEnd: rawValue.Len(),
|
||||
})
|
||||
}
|
||||
|
||||
if len(segments) == 0 {
|
||||
return strings.TrimSpace(stripEnhancedMarkers(text)), nil
|
||||
}
|
||||
|
||||
finalRaw := rawValue.String()
|
||||
leftTrimBytes := len(finalRaw) - len(strings.TrimLeftFunc(finalRaw, unicode.IsSpace))
|
||||
rightTrimBytes := len(finalRaw) - len(strings.TrimRightFunc(finalRaw, unicode.IsSpace))
|
||||
trimmedEnd := len(finalRaw) - rightTrimBytes
|
||||
if trimmedEnd < leftTrimBytes {
|
||||
trimmedEnd = leftTrimBytes
|
||||
}
|
||||
|
||||
cues := make([]Cue, 0, len(segments))
|
||||
for _, seg := range segments {
|
||||
start := seg.start
|
||||
byteStart := max(seg.rawStart, leftTrimBytes)
|
||||
byteEnd := min(seg.rawEnd, trimmedEnd)
|
||||
if byteStart >= byteEnd {
|
||||
continue
|
||||
}
|
||||
|
||||
cues = append(cues, Cue{
|
||||
Start: &start,
|
||||
Value: finalRaw[byteStart:byteEnd],
|
||||
ByteStart: byteStart - leftTrimBytes,
|
||||
ByteEnd: byteEnd - leftTrimBytes - 1,
|
||||
})
|
||||
}
|
||||
|
||||
return strings.TrimSpace(finalRaw), cues
|
||||
}
|
||||
|
||||
// adjustGroup remaps a capture group index from the original match to our rewritten "[...]" string.
|
||||
// The rewrite shifts by -1 (removed '<', added '[') so positions within the brackets stay the same.
|
||||
func adjustGroup(match []int, groupIdx int) int {
|
||||
orig := match[groupIdx]
|
||||
if orig == -1 {
|
||||
return -1
|
||||
}
|
||||
// Offset is: original position minus the position of '<' in the original, plus 1 for '['
|
||||
return orig - match[0]
|
||||
}
|
||||
|
||||
// stripEnhancedMarkers removes all <mm:ss.mm> inline markers from text,
|
||||
// returning the plain lyric text.
|
||||
func stripEnhancedMarkers(text string) string {
|
||||
return enhancedLRCRegex.ReplaceAllString(text, "")
|
||||
}
|
||||
|
||||
// shiftELRCCues returns a deep copy of baseCues with each cue's Start/End
|
||||
// timestamps shifted by offsetMs. Inline ELRC word markers parse to absolute
|
||||
// timestamps anchored at the line's first occurrence, so repeated-line LRC
|
||||
// inputs of the form `[t0][t1]...` must shift the cues by (t1-t0) for the
|
||||
// second occurrence to point at the correct moment. Returned *int64 pointers
|
||||
// are freshly allocated so the input slice is never aliased into the result.
|
||||
func shiftELRCCues(baseCues []Cue, offsetMs int64) []Cue {
|
||||
if len(baseCues) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]Cue, len(baseCues))
|
||||
for i, c := range baseCues {
|
||||
out[i] = c
|
||||
if c.Start != nil {
|
||||
s := *c.Start + offsetMs
|
||||
out[i].Start = &s
|
||||
}
|
||||
if c.End != nil {
|
||||
e := *c.End + offsetMs
|
||||
out[i].End = &e
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseTime(line string, match []int) (int64, error) {
|
||||
var hours, millis int64
|
||||
var err error
|
||||
|
||||
hourStart := match[2]
|
||||
if hourStart != -1 {
|
||||
// subtract 1 because group has : at the end
|
||||
hourEnd := match[3] - 1
|
||||
hours, err = strconv.ParseInt(line[hourStart:hourEnd], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
minutes, err := strconv.ParseInt(line[match[4]:match[5]], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
sec, err := strconv.ParseInt(line[match[6]:match[7]], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
msStart := match[8]
|
||||
if msStart != -1 {
|
||||
msEnd := match[9]
|
||||
// +1 offset since this capture group contains .
|
||||
millis, err = strconv.ParseInt(line[msStart+1:msEnd], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
length := msEnd - msStart
|
||||
|
||||
if length == 3 {
|
||||
millis *= 10
|
||||
} else if length == 2 {
|
||||
millis *= 100
|
||||
}
|
||||
}
|
||||
|
||||
timeInMillis := (((((hours * 60) + minutes) * 60) + sec) * 1000) + millis
|
||||
return timeInMillis, nil
|
||||
}
|
||||
|
||||
type LyricList []Lyrics
|
||||
|
||||
// MarshalJSON keeps the lyrics column invariant: empty/nil serializes to [], never null.
|
||||
|
|
|
|||
350
model/lyrics_lrc.go
Normal file
350
model/lyrics_lrc.go
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/utils/str"
|
||||
)
|
||||
|
||||
// support the standard [mm:ss.mm], as well as [hh:*] and [*.mmm]
|
||||
const timeRegexString = `\[([0-9]{1,2}:)?([0-9]{1,2}):([0-9]{1,2})(\.[0-9]{1,3})?\]`
|
||||
|
||||
var (
|
||||
// Should either be at the beginning of file, or beginning of line
|
||||
syncRegex = regexp.MustCompile(`(^|\n)\s*` + timeRegexString)
|
||||
timeRegex = regexp.MustCompile(timeRegexString)
|
||||
lrcIdRegex = regexp.MustCompile(`\[(ar|ti|offset|lang):([^]]+)]`)
|
||||
|
||||
// Enhanced LRC: inline word-level timing markers like <00:12.34>
|
||||
enhancedLRCTimeString = `<([0-9]{1,2}:)?([0-9]{1,2}):([0-9]{1,2})(\.[0-9]{1,3})?>`
|
||||
enhancedLRCRegex = regexp.MustCompile(enhancedLRCTimeString)
|
||||
)
|
||||
|
||||
func parseLRC(language, text string) (*Lyrics, error) {
|
||||
text = str.SanitizeText(text)
|
||||
|
||||
lines := strings.Split(text, "\n")
|
||||
structuredLines := make([]Line, 0, len(lines)*2)
|
||||
|
||||
artist := ""
|
||||
title := ""
|
||||
var offset *int64 = nil
|
||||
|
||||
synced := syncRegex.MatchString(text)
|
||||
priorLine := ""
|
||||
validLine := false
|
||||
repeated := false
|
||||
var timestamps []int64
|
||||
|
||||
for _, line := range lines {
|
||||
line := strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
if validLine {
|
||||
priorLine += "\n"
|
||||
}
|
||||
continue
|
||||
}
|
||||
var text string
|
||||
var time *int64 = nil
|
||||
|
||||
if synced {
|
||||
idTag := lrcIdRegex.FindStringSubmatch(line)
|
||||
if idTag != nil {
|
||||
switch idTag[1] {
|
||||
case "ar":
|
||||
artist = str.SanitizeText(strings.TrimSpace(idTag[2]))
|
||||
case "lang":
|
||||
language = str.SanitizeText(strings.TrimSpace(idTag[2]))
|
||||
case "offset":
|
||||
{
|
||||
off, err := strconv.ParseInt(strings.TrimSpace(idTag[2]), 10, 64)
|
||||
if err != nil {
|
||||
log.Warn("Error parsing offset", "offset", idTag[2], "error", err)
|
||||
} else {
|
||||
offset = &off
|
||||
}
|
||||
}
|
||||
case "ti":
|
||||
title = str.SanitizeText(strings.TrimSpace(idTag[2]))
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
times := timeRegex.FindAllStringSubmatchIndex(line, -1)
|
||||
if len(times) > 1 {
|
||||
repeated = true
|
||||
}
|
||||
|
||||
// The second condition is for when there is a timestamp in the middle of
|
||||
// a line (after any text)
|
||||
if times == nil || times[0][0] != 0 {
|
||||
if validLine {
|
||||
priorLine += "\n" + line
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if validLine {
|
||||
value, baseCues := parseEnhancedLine(priorLine)
|
||||
for idx := range timestamps {
|
||||
startCopy := timestamps[idx]
|
||||
structuredLines = append(structuredLines, Line{
|
||||
Start: &startCopy,
|
||||
Value: value,
|
||||
Cue: shiftELRCCues(baseCues, timestamps[idx]-timestamps[0]),
|
||||
})
|
||||
}
|
||||
timestamps = nil
|
||||
}
|
||||
|
||||
end := 0
|
||||
|
||||
// [fullStart, fullEnd, hourStart, hourEnd, minStart, minEnd, secStart, secEnd, msStart, msEnd]
|
||||
for _, match := range times {
|
||||
// for multiple matches, we need to check that later matches are not
|
||||
// in the middle of the string
|
||||
if end != 0 {
|
||||
middle := strings.TrimSpace(line[end:match[0]])
|
||||
if middle != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
end = match[1]
|
||||
timeInMillis, err := parseTime(line, match)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timestamps = append(timestamps, timeInMillis)
|
||||
}
|
||||
|
||||
if end >= len(line) {
|
||||
priorLine = ""
|
||||
} else {
|
||||
priorLine = strings.TrimSpace(line[end:])
|
||||
}
|
||||
|
||||
validLine = true
|
||||
} else {
|
||||
text = line
|
||||
structuredLines = append(structuredLines, Line{
|
||||
Start: time,
|
||||
Value: text,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if validLine {
|
||||
value, baseCues := parseEnhancedLine(priorLine)
|
||||
for idx := range timestamps {
|
||||
startCopy := timestamps[idx]
|
||||
structuredLines = append(structuredLines, Line{
|
||||
Start: &startCopy,
|
||||
Value: value,
|
||||
Cue: shiftELRCCues(baseCues, timestamps[idx]-timestamps[0]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If there are repeated values, there is no guarantee that they are in order
|
||||
// In this, case, sort the lyrics by start time
|
||||
if repeated {
|
||||
slices.SortFunc(structuredLines, func(a, b Line) int {
|
||||
return cmp.Compare(*a.Start, *b.Start)
|
||||
})
|
||||
}
|
||||
|
||||
lyrics := Lyrics{
|
||||
DisplayArtist: artist,
|
||||
DisplayTitle: title,
|
||||
Lang: language,
|
||||
Line: NormalizeCueLines(structuredLines),
|
||||
Offset: offset,
|
||||
Synced: synced,
|
||||
}
|
||||
return &lyrics, nil
|
||||
}
|
||||
|
||||
// parseEnhancedLine extracts word-level timing cues from Enhanced LRC inline markers
|
||||
// and computes UTF-8 byte offsets against the final stripped line value.
|
||||
func parseEnhancedLine(text string) (string, []Cue) {
|
||||
matches := enhancedLRCRegex.FindAllStringSubmatchIndex(text, -1)
|
||||
if len(matches) == 0 {
|
||||
return strings.TrimSpace(text), nil
|
||||
}
|
||||
|
||||
type segment struct {
|
||||
start int64
|
||||
rawStart int
|
||||
rawEnd int
|
||||
}
|
||||
|
||||
segments := make([]segment, 0, len(matches))
|
||||
var rawValue strings.Builder
|
||||
for i, match := range matches {
|
||||
timeMs, err := parseTime(
|
||||
// Rewrite <...> as [...] so parseTime can handle it with the same logic
|
||||
"["+text[match[0]+1:match[1]-1]+"]",
|
||||
// Adjust match indices to point into our rewritten string (need start/end pairs for each group)
|
||||
[]int{
|
||||
0, match[1] - match[0],
|
||||
adjustGroup(match, 2), adjustGroup(match, 3),
|
||||
adjustGroup(match, 4), adjustGroup(match, 5),
|
||||
adjustGroup(match, 6), adjustGroup(match, 7),
|
||||
adjustGroup(match, 8), adjustGroup(match, 9),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Text runs from after this marker to the start of the next marker (or end of string)
|
||||
textStart := match[1]
|
||||
var textEnd int
|
||||
if i+1 < len(matches) {
|
||||
textEnd = matches[i+1][0]
|
||||
} else {
|
||||
textEnd = len(text)
|
||||
}
|
||||
|
||||
word := text[textStart:textEnd]
|
||||
if word == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
rawStart := rawValue.Len()
|
||||
rawValue.WriteString(word)
|
||||
segments = append(segments, segment{
|
||||
start: timeMs,
|
||||
rawStart: rawStart,
|
||||
rawEnd: rawValue.Len(),
|
||||
})
|
||||
}
|
||||
|
||||
if len(segments) == 0 {
|
||||
return strings.TrimSpace(stripEnhancedMarkers(text)), nil
|
||||
}
|
||||
|
||||
finalRaw := rawValue.String()
|
||||
leftTrimBytes := len(finalRaw) - len(strings.TrimLeftFunc(finalRaw, unicode.IsSpace))
|
||||
rightTrimBytes := len(finalRaw) - len(strings.TrimRightFunc(finalRaw, unicode.IsSpace))
|
||||
trimmedEnd := len(finalRaw) - rightTrimBytes
|
||||
if trimmedEnd < leftTrimBytes {
|
||||
trimmedEnd = leftTrimBytes
|
||||
}
|
||||
|
||||
cues := make([]Cue, 0, len(segments))
|
||||
for _, seg := range segments {
|
||||
start := seg.start
|
||||
byteStart := max(seg.rawStart, leftTrimBytes)
|
||||
byteEnd := min(seg.rawEnd, trimmedEnd)
|
||||
if byteStart >= byteEnd {
|
||||
continue
|
||||
}
|
||||
|
||||
cues = append(cues, Cue{
|
||||
Start: &start,
|
||||
Value: finalRaw[byteStart:byteEnd],
|
||||
ByteStart: byteStart - leftTrimBytes,
|
||||
ByteEnd: byteEnd - leftTrimBytes - 1,
|
||||
})
|
||||
}
|
||||
|
||||
return strings.TrimSpace(finalRaw), cues
|
||||
}
|
||||
|
||||
// adjustGroup remaps a capture group index from the original match to our rewritten "[...]" string.
|
||||
// The rewrite shifts by -1 (removed '<', added '[') so positions within the brackets stay the same.
|
||||
func adjustGroup(match []int, groupIdx int) int {
|
||||
orig := match[groupIdx]
|
||||
if orig == -1 {
|
||||
return -1
|
||||
}
|
||||
// Offset is: original position minus the position of '<' in the original, plus 1 for '['
|
||||
return orig - match[0]
|
||||
}
|
||||
|
||||
// stripEnhancedMarkers removes all <mm:ss.mm> inline markers from text,
|
||||
// returning the plain lyric text.
|
||||
func stripEnhancedMarkers(text string) string {
|
||||
return enhancedLRCRegex.ReplaceAllString(text, "")
|
||||
}
|
||||
|
||||
// shiftELRCCues returns a deep copy of baseCues with each cue's Start/End
|
||||
// timestamps shifted by offsetMs. Inline ELRC word markers parse to absolute
|
||||
// timestamps anchored at the line's first occurrence, so repeated-line LRC
|
||||
// inputs of the form `[t0][t1]...` must shift the cues by (t1-t0) for the
|
||||
// second occurrence to point at the correct moment. Returned *int64 pointers
|
||||
// are freshly allocated so the input slice is never aliased into the result.
|
||||
func shiftELRCCues(baseCues []Cue, offsetMs int64) []Cue {
|
||||
if len(baseCues) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]Cue, len(baseCues))
|
||||
for i, c := range baseCues {
|
||||
out[i] = c
|
||||
if c.Start != nil {
|
||||
s := *c.Start + offsetMs
|
||||
out[i].Start = &s
|
||||
}
|
||||
if c.End != nil {
|
||||
e := *c.End + offsetMs
|
||||
out[i].End = &e
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseTime(line string, match []int) (int64, error) {
|
||||
var hours, millis int64
|
||||
var err error
|
||||
|
||||
hourStart := match[2]
|
||||
if hourStart != -1 {
|
||||
// subtract 1 because group has : at the end
|
||||
hourEnd := match[3] - 1
|
||||
hours, err = strconv.ParseInt(line[hourStart:hourEnd], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
minutes, err := strconv.ParseInt(line[match[4]:match[5]], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
sec, err := strconv.ParseInt(line[match[6]:match[7]], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
msStart := match[8]
|
||||
if msStart != -1 {
|
||||
msEnd := match[9]
|
||||
// +1 offset since this capture group contains .
|
||||
millis, err = strconv.ParseInt(line[msStart+1:msEnd], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
length := msEnd - msStart
|
||||
|
||||
if length == 3 {
|
||||
millis *= 10
|
||||
} else if length == 2 {
|
||||
millis *= 100
|
||||
}
|
||||
}
|
||||
|
||||
timeInMillis := (((((hours * 60) + minutes) * 60) + sec) * 1000) + millis
|
||||
return timeInMillis, nil
|
||||
}
|
||||
219
model/lyrics_lrc_test.go
Normal file
219
model/lyrics_lrc_test.go
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("parseLRC", func() {
|
||||
It("should parse tags with spaces", func() {
|
||||
lyrics, err := parseLRC("xxx", "[lang: eng ]\n[offset: 1551 ]\n[ti: A title ]\n[ar: An artist ]\n[00:00.00]Hi there")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Lang).To(Equal("eng"))
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.DisplayArtist).To(Equal("An artist"))
|
||||
Expect(lyrics.DisplayTitle).To(Equal("A title"))
|
||||
Expect(lyrics.Offset).To(Equal(new(int64(1551))))
|
||||
})
|
||||
|
||||
It("Should ignore bad offset", func() {
|
||||
lyrics, err := parseLRC("xxx", "[offset: NotANumber ]\n[00:00.00]Hi there")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Offset).To(BeNil())
|
||||
})
|
||||
|
||||
It("should accept lines with no text and weird times", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:00.00]Hi there\n\n\n[00:10.040]\n[00:40]Test\n[01:00:00]late")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(0)), Value: "Hi there"},
|
||||
{Start: new(int64(10040)), Value: ""},
|
||||
{Start: new(int64(40000)), Value: "Test"},
|
||||
{Start: new(int64(1000 * 60 * 60)), Value: "late"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Should support multiple timestamps per line", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:00.00] [00:10.00]Repeated\n[13:00][51:00:00.00]")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(0)), Value: "Repeated"},
|
||||
{Start: new(int64(10000)), Value: "Repeated"},
|
||||
{Start: new(int64(13 * 60 * 1000)), Value: ""},
|
||||
{Start: new(int64(1000 * 60 * 60 * 51)), Value: ""},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Should support parsing multiline string", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:00.00]This is\na multiline \n\n [:0] string\n[10:00.001]This is\nalso one")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(0)), Value: "This is\na multiline\n\n[:0] string"},
|
||||
{Start: new(int64(10*60*1000 + 1)), Value: "This is\nalso one"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Does not match timestamp in middle of line", func() {
|
||||
lyrics, err := parseLRC("xxx", "This could [00:00:00] be a synced file")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeFalse())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Value: "This could [00:00:00] be a synced file"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Allows timestamp in middle of line if also at beginning", func() {
|
||||
lyrics, err := parseLRC("xxx", " [00:00] This is [00:00:00] be a synced file\n [00:01]Line 2")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(0)), Value: "This is [00:00:00] be a synced file"},
|
||||
{Start: new(int64(1000)), Value: "Line 2"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Ignores lines in synchronized lyric prior to first timestamp", func() {
|
||||
lyrics, err := parseLRC("xxx", "This is some prelude\nThat doesn't\nmatter\n[00:00]Text")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(0)), Value: "Text"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Handles all possible ms cases", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:00.001]a\n[00:00.01]b\n[00:00.1]c")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(1)), Value: "a"},
|
||||
{Start: new(int64(10)), Value: "b"},
|
||||
{Start: new(int64(100)), Value: "c"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Properly sorts repeated lyrics out of order", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:00.00] [13:00]Repeated\n[00:10.00][51:00:00.00]Test\n[00:40.00]Not repeated")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(0)), Value: "Repeated"},
|
||||
{Start: new(int64(10000)), Value: "Test"},
|
||||
{Start: new(int64(40000)), Value: "Not repeated"},
|
||||
{Start: new(int64(13 * 60 * 1000)), Value: "Repeated"},
|
||||
{Start: new(int64(1000 * 60 * 60 * 51)), Value: "Test"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("should parse Enhanced LRC with word-level timing", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:01.00]<00:01.00>Some <00:01.50>lyrics <00:02.00>here\n[00:03.00]<00:03.00>More <00:03.50>words")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(HaveLen(2))
|
||||
|
||||
t1000, t1500, t2000, t3000, t3500 := int64(1000), int64(1500), int64(2000), int64(3000), int64(3500)
|
||||
|
||||
line0 := lyrics.Line[0]
|
||||
Expect(line0.Start).To(Equal(&t1000))
|
||||
Expect(line0.End).To(Equal(&t3000))
|
||||
Expect(line0.Value).To(Equal("Some lyrics here"))
|
||||
Expect(line0.Cue).To(Equal([]Cue{
|
||||
{Start: &t1000, End: &t1500, Value: "Some ", ByteStart: 0, ByteEnd: 4},
|
||||
{Start: &t1500, End: &t2000, Value: "lyrics ", ByteStart: 5, ByteEnd: 11},
|
||||
{Start: &t2000, End: &t3000, Value: "here", ByteStart: 12, ByteEnd: 15},
|
||||
}))
|
||||
|
||||
line1 := lyrics.Line[1]
|
||||
Expect(line1.Start).To(Equal(&t3000))
|
||||
Expect(line1.End).To(Equal(&t3500))
|
||||
Expect(line1.Value).To(Equal("More words"))
|
||||
Expect(line1.Cue).To(Equal([]Cue{
|
||||
{Start: &t3000, Value: "More ", ByteStart: 0, ByteEnd: 4},
|
||||
{Start: &t3500, Value: "words", ByteStart: 5, ByteEnd: 9},
|
||||
}))
|
||||
|
||||
Expect(line1.Cue[1].End).To(BeNil())
|
||||
})
|
||||
|
||||
It("should not parse malformed Enhanced LRC timing markers", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:01.00]<00:01a50>Not a marker")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(1000)), Value: "<00:01a50>Not a marker"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("should handle mixed Enhanced and plain LRC lines", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:01.00]<00:01.00>Some <00:01.50>lyrics\n[00:03.00]Plain line\n[00:05.00]<00:05.00>More <00:05.50>words")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Line).To(HaveLen(3))
|
||||
|
||||
t1000, t1500, t5000, t5500 := int64(1000), int64(1500), int64(5000), int64(5500)
|
||||
t3000 := int64(3000)
|
||||
|
||||
Expect(lyrics.Line[0].Cue).To(Equal([]Cue{
|
||||
{Start: &t1000, End: &t1500, Value: "Some ", ByteStart: 0, ByteEnd: 4},
|
||||
{Start: &t1500, End: &t3000, Value: "lyrics", ByteStart: 5, ByteEnd: 10},
|
||||
}))
|
||||
Expect(lyrics.Line[0].Value).To(Equal("Some lyrics"))
|
||||
Expect(lyrics.Line[0].End).To(Equal(&t3000))
|
||||
|
||||
Expect(lyrics.Line[1].Cue).To(BeNil())
|
||||
Expect(lyrics.Line[1].Value).To(Equal("Plain line"))
|
||||
|
||||
Expect(lyrics.Line[2].Cue).To(Equal([]Cue{
|
||||
{Start: &t5000, Value: "More ", ByteStart: 0, ByteEnd: 4},
|
||||
{Start: &t5500, Value: "words", ByteStart: 5, ByteEnd: 9},
|
||||
}))
|
||||
Expect(lyrics.Line[2].Value).To(Equal("More words"))
|
||||
})
|
||||
|
||||
It("should preserve byte offsets for Enhanced LRC cues", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:00.00]<00:00.00>Oh <00:00.90>love<00:01.30> me <00:01.60>tonight")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Line).To(HaveLen(1))
|
||||
|
||||
t0, t900, t1300, t1600 := int64(0), int64(900), int64(1300), int64(1600)
|
||||
line := lyrics.Line[0]
|
||||
Expect(line.Value).To(Equal("Oh love me tonight"))
|
||||
Expect(line.Cue).To(Equal([]Cue{
|
||||
{Start: &t0, Value: "Oh ", ByteStart: 0, ByteEnd: 2},
|
||||
{Start: &t900, Value: "love", ByteStart: 3, ByteEnd: 6},
|
||||
{Start: &t1300, Value: " me ", ByteStart: 7, ByteEnd: 10},
|
||||
{Start: &t1600, Value: "tonight", ByteStart: 11, ByteEnd: 17},
|
||||
}))
|
||||
})
|
||||
|
||||
It("should shift inline ELRC word timestamps for each repeated line occurrence", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:10.00][00:30.00]<00:10.10>Hello <00:10.50>world")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Line).To(HaveLen(2))
|
||||
|
||||
t10000 := int64(10000)
|
||||
t10100 := int64(10100)
|
||||
t10500 := int64(10500)
|
||||
t30000 := int64(30000)
|
||||
t30100 := int64(30100)
|
||||
t30500 := int64(30500)
|
||||
|
||||
Expect(lyrics.Line[0].Start).To(Equal(&t10000))
|
||||
Expect(lyrics.Line[0].End).To(Equal(&t30000))
|
||||
Expect(lyrics.Line[0].Value).To(Equal("Hello world"))
|
||||
Expect(lyrics.Line[0].Cue).To(Equal([]Cue{
|
||||
{Start: &t10100, End: &t10500, Value: "Hello ", ByteStart: 0, ByteEnd: 5},
|
||||
{Start: &t10500, End: &t30000, Value: "world", ByteStart: 6, ByteEnd: 10},
|
||||
}))
|
||||
|
||||
Expect(lyrics.Line[1].Start).To(Equal(&t30000))
|
||||
Expect(lyrics.Line[1].End).To(Equal(&t30500))
|
||||
Expect(lyrics.Line[1].Value).To(Equal("Hello world"))
|
||||
Expect(lyrics.Line[1].Cue).To(Equal([]Cue{
|
||||
{Start: &t30100, Value: "Hello ", ByteStart: 0, ByteEnd: 5},
|
||||
{Start: &t30500, Value: "world", ByteStart: 6, ByteEnd: 10},
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
|
@ -5,219 +5,6 @@ import (
|
|||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("parseLRC", func() {
|
||||
It("should parse tags with spaces", func() {
|
||||
lyrics, err := parseLRC("xxx", "[lang: eng ]\n[offset: 1551 ]\n[ti: A title ]\n[ar: An artist ]\n[00:00.00]Hi there")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Lang).To(Equal("eng"))
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.DisplayArtist).To(Equal("An artist"))
|
||||
Expect(lyrics.DisplayTitle).To(Equal("A title"))
|
||||
Expect(lyrics.Offset).To(Equal(new(int64(1551))))
|
||||
})
|
||||
|
||||
It("Should ignore bad offset", func() {
|
||||
lyrics, err := parseLRC("xxx", "[offset: NotANumber ]\n[00:00.00]Hi there")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Offset).To(BeNil())
|
||||
})
|
||||
|
||||
It("should accept lines with no text and weird times", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:00.00]Hi there\n\n\n[00:10.040]\n[00:40]Test\n[01:00:00]late")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(0)), Value: "Hi there"},
|
||||
{Start: new(int64(10040)), Value: ""},
|
||||
{Start: new(int64(40000)), Value: "Test"},
|
||||
{Start: new(int64(1000 * 60 * 60)), Value: "late"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Should support multiple timestamps per line", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:00.00] [00:10.00]Repeated\n[13:00][51:00:00.00]")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(0)), Value: "Repeated"},
|
||||
{Start: new(int64(10000)), Value: "Repeated"},
|
||||
{Start: new(int64(13 * 60 * 1000)), Value: ""},
|
||||
{Start: new(int64(1000 * 60 * 60 * 51)), Value: ""},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Should support parsing multiline string", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:00.00]This is\na multiline \n\n [:0] string\n[10:00.001]This is\nalso one")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(0)), Value: "This is\na multiline\n\n[:0] string"},
|
||||
{Start: new(int64(10*60*1000 + 1)), Value: "This is\nalso one"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Does not match timestamp in middle of line", func() {
|
||||
lyrics, err := parseLRC("xxx", "This could [00:00:00] be a synced file")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeFalse())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Value: "This could [00:00:00] be a synced file"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Allows timestamp in middle of line if also at beginning", func() {
|
||||
lyrics, err := parseLRC("xxx", " [00:00] This is [00:00:00] be a synced file\n [00:01]Line 2")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(0)), Value: "This is [00:00:00] be a synced file"},
|
||||
{Start: new(int64(1000)), Value: "Line 2"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Ignores lines in synchronized lyric prior to first timestamp", func() {
|
||||
lyrics, err := parseLRC("xxx", "This is some prelude\nThat doesn't\nmatter\n[00:00]Text")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(0)), Value: "Text"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Handles all possible ms cases", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:00.001]a\n[00:00.01]b\n[00:00.1]c")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(1)), Value: "a"},
|
||||
{Start: new(int64(10)), Value: "b"},
|
||||
{Start: new(int64(100)), Value: "c"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Properly sorts repeated lyrics out of order", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:00.00] [13:00]Repeated\n[00:10.00][51:00:00.00]Test\n[00:40.00]Not repeated")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(0)), Value: "Repeated"},
|
||||
{Start: new(int64(10000)), Value: "Test"},
|
||||
{Start: new(int64(40000)), Value: "Not repeated"},
|
||||
{Start: new(int64(13 * 60 * 1000)), Value: "Repeated"},
|
||||
{Start: new(int64(1000 * 60 * 60 * 51)), Value: "Test"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("should parse Enhanced LRC with word-level timing", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:01.00]<00:01.00>Some <00:01.50>lyrics <00:02.00>here\n[00:03.00]<00:03.00>More <00:03.50>words")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(HaveLen(2))
|
||||
|
||||
t1000, t1500, t2000, t3000, t3500 := int64(1000), int64(1500), int64(2000), int64(3000), int64(3500)
|
||||
|
||||
line0 := lyrics.Line[0]
|
||||
Expect(line0.Start).To(Equal(&t1000))
|
||||
Expect(line0.End).To(Equal(&t3000))
|
||||
Expect(line0.Value).To(Equal("Some lyrics here"))
|
||||
Expect(line0.Cue).To(Equal([]Cue{
|
||||
{Start: &t1000, End: &t1500, Value: "Some ", ByteStart: 0, ByteEnd: 4},
|
||||
{Start: &t1500, End: &t2000, Value: "lyrics ", ByteStart: 5, ByteEnd: 11},
|
||||
{Start: &t2000, End: &t3000, Value: "here", ByteStart: 12, ByteEnd: 15},
|
||||
}))
|
||||
|
||||
line1 := lyrics.Line[1]
|
||||
Expect(line1.Start).To(Equal(&t3000))
|
||||
Expect(line1.End).To(Equal(&t3500))
|
||||
Expect(line1.Value).To(Equal("More words"))
|
||||
Expect(line1.Cue).To(Equal([]Cue{
|
||||
{Start: &t3000, Value: "More ", ByteStart: 0, ByteEnd: 4},
|
||||
{Start: &t3500, Value: "words", ByteStart: 5, ByteEnd: 9},
|
||||
}))
|
||||
|
||||
Expect(line1.Cue[1].End).To(BeNil())
|
||||
})
|
||||
|
||||
It("should not parse malformed Enhanced LRC timing markers", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:01.00]<00:01a50>Not a marker")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Synced).To(BeTrue())
|
||||
Expect(lyrics.Line).To(Equal([]Line{
|
||||
{Start: new(int64(1000)), Value: "<00:01a50>Not a marker"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("should handle mixed Enhanced and plain LRC lines", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:01.00]<00:01.00>Some <00:01.50>lyrics\n[00:03.00]Plain line\n[00:05.00]<00:05.00>More <00:05.50>words")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Line).To(HaveLen(3))
|
||||
|
||||
t1000, t1500, t5000, t5500 := int64(1000), int64(1500), int64(5000), int64(5500)
|
||||
t3000 := int64(3000)
|
||||
|
||||
Expect(lyrics.Line[0].Cue).To(Equal([]Cue{
|
||||
{Start: &t1000, End: &t1500, Value: "Some ", ByteStart: 0, ByteEnd: 4},
|
||||
{Start: &t1500, End: &t3000, Value: "lyrics", ByteStart: 5, ByteEnd: 10},
|
||||
}))
|
||||
Expect(lyrics.Line[0].Value).To(Equal("Some lyrics"))
|
||||
Expect(lyrics.Line[0].End).To(Equal(&t3000))
|
||||
|
||||
Expect(lyrics.Line[1].Cue).To(BeNil())
|
||||
Expect(lyrics.Line[1].Value).To(Equal("Plain line"))
|
||||
|
||||
Expect(lyrics.Line[2].Cue).To(Equal([]Cue{
|
||||
{Start: &t5000, Value: "More ", ByteStart: 0, ByteEnd: 4},
|
||||
{Start: &t5500, Value: "words", ByteStart: 5, ByteEnd: 9},
|
||||
}))
|
||||
Expect(lyrics.Line[2].Value).To(Equal("More words"))
|
||||
})
|
||||
|
||||
It("should preserve byte offsets for Enhanced LRC cues", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:00.00]<00:00.00>Oh <00:00.90>love<00:01.30> me <00:01.60>tonight")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Line).To(HaveLen(1))
|
||||
|
||||
t0, t900, t1300, t1600 := int64(0), int64(900), int64(1300), int64(1600)
|
||||
line := lyrics.Line[0]
|
||||
Expect(line.Value).To(Equal("Oh love me tonight"))
|
||||
Expect(line.Cue).To(Equal([]Cue{
|
||||
{Start: &t0, Value: "Oh ", ByteStart: 0, ByteEnd: 2},
|
||||
{Start: &t900, Value: "love", ByteStart: 3, ByteEnd: 6},
|
||||
{Start: &t1300, Value: " me ", ByteStart: 7, ByteEnd: 10},
|
||||
{Start: &t1600, Value: "tonight", ByteStart: 11, ByteEnd: 17},
|
||||
}))
|
||||
})
|
||||
|
||||
It("should shift inline ELRC word timestamps for each repeated line occurrence", func() {
|
||||
lyrics, err := parseLRC("xxx", "[00:10.00][00:30.00]<00:10.10>Hello <00:10.50>world")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.Line).To(HaveLen(2))
|
||||
|
||||
t10000 := int64(10000)
|
||||
t10100 := int64(10100)
|
||||
t10500 := int64(10500)
|
||||
t30000 := int64(30000)
|
||||
t30100 := int64(30100)
|
||||
t30500 := int64(30500)
|
||||
|
||||
Expect(lyrics.Line[0].Start).To(Equal(&t10000))
|
||||
Expect(lyrics.Line[0].End).To(Equal(&t30000))
|
||||
Expect(lyrics.Line[0].Value).To(Equal("Hello world"))
|
||||
Expect(lyrics.Line[0].Cue).To(Equal([]Cue{
|
||||
{Start: &t10100, End: &t10500, Value: "Hello ", ByteStart: 0, ByteEnd: 5},
|
||||
{Start: &t10500, End: &t30000, Value: "world", ByteStart: 6, ByteEnd: 10},
|
||||
}))
|
||||
|
||||
Expect(lyrics.Line[1].Start).To(Equal(&t30000))
|
||||
Expect(lyrics.Line[1].End).To(Equal(&t30500))
|
||||
Expect(lyrics.Line[1].Value).To(Equal("Hello world"))
|
||||
Expect(lyrics.Line[1].Cue).To(Equal([]Cue{
|
||||
{Start: &t30100, Value: "Hello ", ByteStart: 0, ByteEnd: 5},
|
||||
{Start: &t30500, Value: "world", ByteStart: 6, ByteEnd: 10},
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("NormalizeCueLines", func() {
|
||||
It("should not mutate caller cue slices when filling missing cue end times", func() {
|
||||
start0, start1, nextLineStart := int64(1000), int64(1500), int64(3000)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue