perf(lyrics): drop []byte/string round-trips in parsers

Apply code-review feedback to remove avoidable allocations in the lyrics
parsers. isTTMLDocument now takes []byte directly, so parseTTMLWithDefaultLang
no longer copies its buffer into a string before the TTML probe. parseSRTBlock
splits its block with strings.Split instead of converting to []byte and back
per line. ParseLyrics hoists strings.ToLower(suffix) out of the format loop.

No behavior change; the dropped len(scanner)==0 SRT guard was dead (strings.Split
never returns an empty slice, and the existing len(lines)==0 check still covers
empty input).

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2026-06-19 17:06:00 -04:00
commit 76ce0b8553
3 changed files with 10 additions and 14 deletions

View file

@ -30,13 +30,14 @@ var lyricFormats = []struct {
// a structured parser that does not match falls back to the LRC/plain-text floor.
func ParseLyrics(suffix, lang string, contents []byte) (LyricList, error) {
contents = stripBOM(contents)
sniff := suffix == "" || strings.EqualFold(suffix, "auto")
suffix = strings.ToLower(suffix)
sniff := suffix == "" || suffix == "auto"
// Sniffing tries every format in order; a known suffix selects just its own.
// Unmatched suffixes leave no candidates, so parseFirstMatch falls to plain.
candidates := make([]lyricParser, 0, len(lyricFormats))
for _, f := range lyricFormats {
if sniff || slices.Contains(f.suffixes, strings.ToLower(suffix)) {
if sniff || slices.Contains(f.suffixes, suffix) {
candidates = append(candidates, f.parse)
}
}

View file

@ -1,7 +1,6 @@
package model
import (
"bytes"
"regexp"
"strconv"
"strings"
@ -61,14 +60,10 @@ func splitSRTBlocks(raw string) []string {
}
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)))
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 {

View file

@ -102,8 +102,8 @@ type ttmlParser struct {
metadataSeq int
}
func isTTMLDocument(text string) bool {
decoder := xml.NewDecoder(strings.NewReader(strings.TrimSpace(text)))
func isTTMLDocument(contents []byte) bool {
decoder := xml.NewDecoder(bytes.NewReader(bytes.TrimSpace(contents)))
for {
token, err := decoder.Token()
if err != nil {
@ -120,7 +120,7 @@ func parseTTMLWithDefaultLang(defaultLang string, contents []byte) (LyricList, e
// Skip non-TTML content so sniffing doesn't run the XML decoder on plain text.
// Checked after the encoding fixup so UTF-16-declared documents are recognized.
if !isTTMLDocument(string(contents)) {
if !isTTMLDocument(contents) {
return nil, nil
}