package lyrics import "testing" func TestParsePlainLRC(t *testing.T) { content := `[la:en] [00:12.34]First line [00:15.00]Second line [00:20.50]Third line` p := Parse(content) if p.Lang != "en" { t.Errorf("Lang = %q, want en", p.Lang) } if p.Enhanced { t.Error("Enhanced = true, want false for plain LRC") } if len(p.Lines) != 3 { t.Fatalf("Lines = %v, want 3", p.Lines) } if p.Lines[0].StartMs != 12340 || p.Lines[0].Text != "First line" { t.Errorf("Lines[0] = %+v", p.Lines[0]) } if p.Lines[2].StartMs != 20500 { t.Errorf("Lines[2].StartMs = %d, want 20500", p.Lines[2].StartMs) } } func TestParseLinesSortedByTime(t *testing.T) { // Deliberately out of order in the source file. content := "[00:20.00]Later\n[00:05.00]Earlier\n[00:10.00]Middle" p := Parse(content) if len(p.Lines) != 3 { t.Fatalf("Lines = %v, want 3", p.Lines) } if p.Lines[0].Text != "Earlier" || p.Lines[1].Text != "Middle" || p.Lines[2].Text != "Later" { t.Errorf("lines not sorted by time: %+v", p.Lines) } } func TestParseRepeatedTimestamps(t *testing.T) { content := "[00:05.00][00:45.00]Chorus line" p := Parse(content) if len(p.Lines) != 2 { t.Fatalf("Lines = %v, want 2 (one per timestamp)", p.Lines) } if p.Lines[0].Text != "Chorus line" || p.Lines[1].Text != "Chorus line" { t.Errorf("both repeated lines should have the same text: %+v", p.Lines) } } func TestParseEnhancedLRCWordTiming(t *testing.T) { content := "[00:12.00]<00:12.00>Hello <00:12.50>world <00:13.00>test" p := Parse(content) if !p.Enhanced { t.Fatal("Enhanced = false, want true for word-timed input") } if len(p.Lines) != 1 { t.Fatalf("Lines = %v, want 1", p.Lines) } line := p.Lines[0] if line.Text != "Hello world test" { t.Errorf("Text = %q, want %q", line.Text, "Hello world test") } if len(line.Words) != 3 { t.Fatalf("Words = %v, want 3", line.Words) } if line.Words[0].StartMs != 12000 || line.Words[0].Text != "Hello" { t.Errorf("Words[0] = %+v", line.Words[0]) } if line.Words[1].StartMs != 12500 || line.Words[1].Text != "world" { t.Errorf("Words[1] = %+v", line.Words[1]) } if line.Words[2].StartMs != 13000 || line.Words[2].Text != "test" { t.Errorf("Words[2] = %+v", line.Words[2]) } } func TestParsePlainLRCHasNoWords(t *testing.T) { // Regression guard for "do not fake enhanced timing from ordinary // plain lyrics": a plain synced line must never synthesize word cues. content := "[00:12.00]Just a normal line with several words" p := Parse(content) if p.Enhanced { t.Fatal("Enhanced = true for a plain LRC line") } if len(p.Lines[0].Words) != 0 { t.Fatalf("Words = %v, want none for a plain line", p.Lines[0].Words) } } func TestParseUTF8Content(t *testing.T) { // Japanese and emoji content, to exercise multi-byte UTF-8 handling // (relevant later for byte-offset computation in the OpenSubsonic // wire format). content := "[00:01.00]こんにちは世界\n[00:05.00]🎵 music note 🎶" p := Parse(content) if len(p.Lines) != 2 { t.Fatalf("Lines = %v, want 2", p.Lines) } if p.Lines[0].Text != "こんにちは世界" { t.Errorf("Lines[0].Text = %q", p.Lines[0].Text) } if p.Lines[1].Text != "🎵 music note 🎶" { t.Errorf("Lines[1].Text = %q", p.Lines[1].Text) } } func TestParseEnhancedUTF8Words(t *testing.T) { content := "[00:01.00]<00:01.00>日本語<00:01.50>のテスト" p := Parse(content) if len(p.Lines[0].Words) != 2 { t.Fatalf("Words = %v, want 2", p.Lines[0].Words) } if p.Lines[0].Words[0].Text != "日本語" { t.Errorf("Words[0].Text = %q", p.Lines[0].Words[0].Text) } if p.Lines[0].Words[1].Text != "のテスト" { t.Errorf("Words[1].Text = %q", p.Lines[0].Words[1].Text) } } func TestParseOffset(t *testing.T) { p := Parse("[offset:+500]\n[00:10.00]Line") if p.OffsetMs != 500 { t.Errorf("OffsetMs = %d, want 500", p.OffsetMs) } } func TestParseEmptyInput(t *testing.T) { p := Parse("") if len(p.Lines) != 0 { t.Errorf("Lines = %v, want empty", p.Lines) } } func TestParseIgnoresUnrecognizedLines(t *testing.T) { content := "[ar:Some Artist]\n[ti:Some Title]\nplain text with no timestamp\n[00:01.00]Actual line" p := Parse(content) if len(p.Lines) != 1 { t.Fatalf("Lines = %v, want 1 (junk lines skipped)", p.Lines) } } func TestParseCentisecondsVsMilliseconds(t *testing.T) { // [mm:ss.xx] (centiseconds, 2 digits) vs [mm:ss.xxx] (milliseconds, 3 digits). p := Parse("[00:01.50]Two digit\n[00:02.500]Three digit") if p.Lines[0].StartMs != 1500 { t.Errorf("two-digit frac: StartMs = %d, want 1500", p.Lines[0].StartMs) } if p.Lines[1].StartMs != 2500 { t.Errorf("three-digit frac: StartMs = %d, want 2500", p.Lines[1].StartMs) } }