feat(plugins): content-sniff plugin lyrics for all formats

Replace model.ToLyrics (LRC/plain only) with model.ParseEmbedded so plugin
responses are content-sniffed for TTML, SRT, YAML, LRC, and plain text.
ParseEmbedded returns a LyricList, so the loop now flattens multiple tracks
per response entry.

The test-lyrics WASM plugin gains a "ttml" format mode (configured via
pdk.GetConfig) that returns a minimal TTML document; rebuilt with the
standard Go wasip1 toolchain (GOOS=wasip1 GOARCH=wasm). A new Ginkgo test
asserts Synced==true and the exact cue value, which the old plain-text path
could not produce.

GetLyrics doc comment updated to reflect content-sniffing; a later task will
retarget it to ParseLyrics once that function is introduced.
This commit is contained in:
Deluan 2026-06-19 14:15:07 -04:00
commit 0ec7a818fb
3 changed files with 45 additions and 5 deletions

View file

@ -31,8 +31,9 @@ type LyricsPlugin struct {
plugin *plugin
}
// GetLyrics calls the plugin to fetch lyrics, then parses the raw text responses
// using model.ToLyrics.
// GetLyrics calls the plugin to fetch lyrics, then content-sniffs each response
// via model.ParseEmbedded (TTML/SRT/YAML/LRC/plain). A later task retargets this
// to ParseLyrics once that function is introduced.
func (l *LyricsPlugin) GetLyrics(ctx context.Context, mf *model.MediaFile) (model.LyricList, error) {
req := capabilities.GetLyricsRequest{
Track: mediaFileToTrackInfo(l.plugin, mf),
@ -50,13 +51,15 @@ func (l *LyricsPlugin) GetLyrics(ctx context.Context, mf *model.MediaFile) (mode
if lang == "" {
lang = "xxx"
}
parsed, err := model.ToLyrics(lang, lt.Text)
parsed, err := model.ParseEmbedded(lang, lt.Text)
if err != nil {
log.Warn(ctx, "Error parsing plugin lyrics", "plugin", l.name, err)
continue
}
if parsed != nil && !parsed.IsEmpty() {
result = append(result, *parsed)
for _, lyric := range parsed {
if !lyric.IsEmpty() {
result = append(result, lyric)
}
}
}
return result, nil

View file

@ -83,6 +83,25 @@ var _ = Describe("LyricsPlugin", Ordered, func() {
_, err := p.GetLyrics(GinkgoT().Context(), track)
Expect(err).To(HaveOccurred())
})
It("content-sniffs TTML returned by the plugin", func() {
manager, _ := createTestManagerWithPlugins(map[string]map[string]string{
"test-lyrics": {"format": "ttml"},
}, "test-lyrics"+PackageExtension)
p, ok := manager.LoadLyricsProvider("test-lyrics")
Expect(ok).To(BeTrue())
track := &model.MediaFile{ID: "track-1", Title: "Test Song", Artist: "Test Artist"}
result, err := p.GetLyrics(GinkgoT().Context(), track)
Expect(err).ToNot(HaveOccurred())
Expect(result).To(HaveLen(1))
// Verify the result is parsed as TTML (synced, with the exact cue text),
// not mangled as plain text with raw XML markup.
Expect(result[0].Synced).To(BeTrue())
Expect(result[0].Line).To(HaveLen(1))
Expect(result[0].Line[0].Value).To(Equal("plugin ttml line"))
})
})
Describe("PluginNames", func() {

View file

@ -21,6 +21,24 @@ func (t *testLyrics) GetLyrics(input lyrics.GetLyricsRequest) (lyrics.GetLyricsR
return lyrics.GetLyricsResponse{}, fmt.Errorf("%s", errMsg)
}
// Return a minimal TTML document to exercise content-sniffing for rich formats.
format, hasFormat := pdk.GetConfig("format")
if hasFormat && format == "ttml" {
const ttml = `<?xml version="1.0" encoding="UTF-8"?>
<tt xmlns="http://www.w3.org/ns/ttml">
<body xml:lang="eng">
<div>
<p begin="00:00.000" end="00:01.000">plugin ttml line</p>
</div>
</body>
</tt>`
return lyrics.GetLyricsResponse{
Lyrics: []lyrics.LyricsText{
{Lang: "eng", Text: ttml},
},
}, nil
}
// Check if we should omit language (to test default language handling)
noLang, hasNoLang := pdk.GetConfig("no_lang")
lang := "eng"