refactor(lyrics): share cue end-time normalization across layers

buildLyricCues (subsonic response) reimplemented the same fill-from-next /
fallback / all-or-none end-time logic that normalizeCueLine (model) already
ran at parse time. They operate on different groupings -- the response
builder normalizes per-agent cue subsets, the model the whole line -- so they
cannot be collapsed, but the algorithm itself was duplicated.

Extract model.NormalizeCueEnds([]Cue, fallbackEnd) as the single
implementation (now also clamping ends within the model line normalization),
and have buildLyricCues delegate to it and only map to the response type.
This commit is contained in:
Deluan 2026-06-18 21:02:23 -04:00
parent a42b27cfce
commit 7796faa1b2
2 changed files with 57 additions and 72 deletions

View File

@ -467,39 +467,56 @@ func normalizeCueLine(line Line, fallbackEnd *int64) Line {
if len(line.Cue) == 0 {
return line
}
for i := range line.Cue {
if line.Cue[i].End != nil {
continue
}
if i+1 < len(line.Cue) && line.Cue[i+1].Start != nil {
v := *line.Cue[i+1].Start
line.Cue[i].End = &v
continue
}
if fallbackEnd != nil {
v := *fallbackEnd
line.Cue[i].End = &v
}
}
for i := range line.Cue {
if line.Cue[i].End == nil {
line.Cue = clearCueEnds(line.Cue)
return NormalizeLineTiming(line)
}
}
line.Cue = NormalizeCueEnds(line.Cue, fallbackEnd)
return NormalizeLineTiming(line)
}
func clearCueEnds(cues []Cue) []Cue {
normalized := make([]Cue, len(cues))
copy(normalized, cues)
for i := range normalized {
normalized[i].End = nil
// NormalizeCueEnds resolves missing cue end times within a single ordered cue
// group: each end is filled from the next cue's start, then from fallbackEnd,
// and is clamped so it never precedes the cue's own start nor overruns the next
// cue. End times are all-or-none — if any cue still lacks an end afterwards, all
// ends in the group are cleared. The input slice is never mutated.
func NormalizeCueEnds(cues []Cue, fallbackEnd *int64) []Cue {
if len(cues) == 0 {
return cues
}
return normalized
out := slices.Clone(cues)
for i := range out {
end := out[i].End
if end == nil {
if i+1 < len(out) && out[i+1].Start != nil {
end = out[i+1].Start
} else {
end = fallbackEnd
}
}
if end != nil && i+1 < len(out) && out[i+1].Start != nil && *end > *out[i+1].Start {
end = out[i+1].Start
}
if end != nil && out[i].Start != nil && *end < *out[i].Start {
end = out[i].Start
}
out[i].End = ptrOrNil(end)
}
for i := range out {
if out[i].End == nil {
for j := range out {
out[j].End = nil
}
break
}
}
return out
}
// ptrOrNil returns a fresh copy of *v so callers never alias another cue's
// Start/End pointer into the result.
func ptrOrNil(v *int64) *int64 {
if v == nil {
return nil
}
c := *v
return &c
}

View File

@ -606,20 +606,18 @@ func buildLyricCues(cues []model.Cue, lineEnd *int64) []responses.LyricCue {
return nil
}
hasAnyEnd := false
for i := range cues {
if cues[i].End != nil {
hasAnyEnd = true
break
}
// Only resolve end times when at least one cue carries one; otherwise the
// group is start-only and must stay that way.
hasAnyEnd := slices.ContainsFunc(cues, func(c model.Cue) bool { return c.End != nil })
if hasAnyEnd {
cues = model.NormalizeCueEnds(cues, lineEnd)
}
normalized := make([]responses.LyricCue, 0, len(cues))
out := make([]responses.LyricCue, 0, len(cues))
for i := range cues {
if cues[i].Start == nil {
continue
}
cue := responses.LyricCue{
Start: *cues[i].Start,
Value: cues[i].Value,
@ -627,41 +625,11 @@ func buildLyricCues(cues []model.Cue, lineEnd *int64) []responses.LyricCue {
ByteEnd: cues[i].ByteEnd,
}
if hasAnyEnd {
end := cues[i].End
if end == nil {
if i+1 < len(cues) && cues[i+1].Start != nil {
v := *cues[i+1].Start
end = &v
} else if lineEnd != nil {
v := *lineEnd
end = &v
}
}
if end != nil && i+1 < len(cues) && cues[i+1].Start != nil && *end > *cues[i+1].Start {
v := *cues[i+1].Start
end = &v
}
if end != nil && *end < cue.Start {
v := cue.Start
end = &v
}
cue.End = end
cue.End = cues[i].End
}
normalized = append(normalized, cue)
out = append(out, cue)
}
if hasAnyEnd {
for i := range normalized {
if normalized[i].End == nil {
for j := range normalized {
normalized[j].End = nil
}
break
}
}
}
return normalized
return out
}
func buildLyricsList(mf *model.MediaFile, lyricsList model.LyricList, enhanced bool) *responses.LyricsList {