refactor(lyrics): share pointer helpers via gg.Clone and gg.If

Add a nil-safe generic gg.Clone[T](*T) *T to utils/gg alongside the existing
V and If helpers, and route the lyrics package's bespoke pointer-copy helpers
through it: ptrOrNil (model/lyrics.go) and cloneTTMLCue (model/lyrics_ttml.go)
are removed in favor of gg.Clone. Also replaces the local positiveOrDefault
helper with gg.If at its three TTML timing-param call sites.

This removes duplicated single-purpose plumbing in favor of shared, generic
utilities. Behavior is unchanged. Adds tests for gg.Clone covering copy,
non-aliasing, and nil input.
This commit is contained in:
Deluan 2026-06-18 22:35:17 -04:00
parent 60377c9485
commit c66d92c724
4 changed files with 39 additions and 32 deletions

View File

@ -10,6 +10,7 @@ import (
"unicode"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils/gg"
"github.com/navidrome/navidrome/utils/str"
)
@ -542,7 +543,7 @@ func NormalizeCueEnds(cues []Cue, fallbackEnd *int64) []Cue {
if end != nil && out[i].Start != nil && *end < *out[i].Start {
end = out[i].Start
}
out[i].End = ptrOrNil(end)
out[i].End = gg.Clone(end)
}
for i := range out {
@ -555,13 +556,3 @@ func NormalizeCueEnds(cues []Cue, fallbackEnd *int64) []Cue {
}
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

@ -13,6 +13,7 @@ import (
"unicode"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils/gg"
"github.com/navidrome/navidrome/utils/str"
)
@ -487,7 +488,7 @@ func splitTTMLPiecesByNewline(pieces []ttmlPiece) [][]ttmlPiece {
if start < i {
lines[len(lines)-1] = append(lines[len(lines)-1], ttmlPiece{
raw: raw[start:i],
cue: cloneTTMLCue(piece.cue),
cue: gg.Clone(piece.cue),
})
}
lines = append(lines, []ttmlPiece{})
@ -496,7 +497,7 @@ func splitTTMLPiecesByNewline(pieces []ttmlPiece) [][]ttmlPiece {
if start < len(raw) {
lines[len(lines)-1] = append(lines[len(lines)-1], ttmlPiece{
raw: raw[start:],
cue: cloneTTMLCue(piece.cue),
cue: gg.Clone(piece.cue),
})
}
}
@ -562,15 +563,6 @@ func ttmlPiecesContainCue(pieces []ttmlPiece) bool {
return false
}
func cloneTTMLCue(cue *Cue) *Cue {
if cue == nil {
return nil
}
cloned := *cue
return &cloned
}
func (p *ttmlParser) toLyricList() LyricList {
res := make(LyricList, 0, len(p.mainLangOrder)+len(p.translationLangOrder)+len(p.pronunciationLangOrder))
for _, lang := range p.mainLangOrder {
@ -979,9 +971,9 @@ func (p *ttmlParser) updateTimingParams(attrs []xml.Attr) {
}
}
p.params.frameRate = positiveOrDefault(frameRate, defaultTTMLFrameRate)
p.params.subFrameRate = positiveOrDefault(subFrameRate, defaultTTMLSubFrameRate)
p.params.tickRate = positiveOrDefault(tickRate, defaultTTMLTickRate)
p.params.frameRate = gg.If(frameRate > 0, frameRate, defaultTTMLFrameRate)
p.params.subFrameRate = gg.If(subFrameRate > 0, subFrameRate, defaultTTMLSubFrameRate)
p.params.tickRate = gg.If(tickRate > 0, tickRate, defaultTTMLTickRate)
}
func parseTTMLDurationExpression(expr string, params ttmlTimingParams) (int64, bool) {
@ -1262,10 +1254,3 @@ func linesAreSynced(lines []Line) bool {
}
return false
}
func positiveOrDefault(v float64, fallback float64) float64 {
if v <= 0 {
return fallback
}
return v
}

View File

@ -16,3 +16,13 @@ func If[T any](cond bool, v1, v2 T) T {
}
return v2
}
// Clone returns a pointer to a fresh copy of *p, or nil if p is nil. Use it to
// avoid aliasing the pointed-to value when a separate *T is needed.
func Clone[T any](p *T) *T {
if p == nil {
return nil
}
v := *p
return &v
}

View File

@ -46,4 +46,25 @@ var _ = Describe("GG", func() {
Expect(gg.If(false, 1.1, 2.2)).To(Equal(2.2))
})
})
Describe("Clone", func() {
It("returns a pointer to a copy of the value", func() {
original := 123
cloned := gg.Clone(&original)
Expect(cloned).To(HaveValue(Equal(123)))
Expect(cloned).NotTo(BeIdenticalTo(&original))
})
It("does not alias the original value", func() {
original := 123
cloned := gg.Clone(&original)
original = 456
Expect(*cloned).To(Equal(123))
})
It("returns nil when the input is nil", func() {
var v *int
Expect(gg.Clone(v)).To(BeNil())
})
})
})