package lrc import ( "os" "path/filepath" "strings" "testing" "sub-cli/internal/model" ) func TestParse(t *testing.T) { // Create a temporary test file content := `[ti:Test LRC File] [ar:Test Artist] [al:Test Album] [by:Test Creator] [00:01.00]This is the first line. [00:05.00]This is the second line. [00:09.50]This is the third line. ` tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.lrc") if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Test parsing lyrics, err := Parse(testFile) if err != nil { t.Fatalf("Parse failed: %v", err) } // Verify results if len(lyrics.Timeline) != 3 { t.Errorf("Expected 3 timeline entries, got %d", len(lyrics.Timeline)) } if len(lyrics.Content) != 3 { t.Errorf("Expected 3 content entries, got %d", len(lyrics.Content)) } // Check metadata if lyrics.Metadata["ti"] != "Test LRC File" { t.Errorf("Expected title 'Test LRC File', got '%s'", lyrics.Metadata["ti"]) } if lyrics.Metadata["ar"] != "Test Artist" { t.Errorf("Expected artist 'Test Artist', got '%s'", lyrics.Metadata["ar"]) } if lyrics.Metadata["al"] != "Test Album" { t.Errorf("Expected album 'Test Album', got '%s'", lyrics.Metadata["al"]) } if lyrics.Metadata["by"] != "Test Creator" { t.Errorf("Expected creator 'Test Creator', got '%s'", lyrics.Metadata["by"]) } // Check first timeline entry if lyrics.Timeline[0].Hours != 0 || lyrics.Timeline[0].Minutes != 0 || lyrics.Timeline[0].Seconds != 1 || lyrics.Timeline[0].Milliseconds != 0 { t.Errorf("First entry time: expected 00:01.00, got %+v", lyrics.Timeline[0]) } // Check third timeline entry if lyrics.Timeline[2].Hours != 0 || lyrics.Timeline[2].Minutes != 0 || lyrics.Timeline[2].Seconds != 9 || lyrics.Timeline[2].Milliseconds != 500 { t.Errorf("Third entry time: expected 00:09.50, got %+v", lyrics.Timeline[2]) } // Check content if lyrics.Content[0] != "This is the first line." { t.Errorf("First entry content: expected 'This is the first line.', got '%s'", lyrics.Content[0]) } } func TestGenerate(t *testing.T) { // Create test lyrics lyrics := model.Lyrics{ Metadata: map[string]string{ "ti": "Test LRC File", "ar": "Test Artist", }, Timeline: []model.Timestamp{ {Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0}, {Hours: 0, Minutes: 0, Seconds: 5, Milliseconds: 0}, }, Content: []string{ "This is the first line.", "This is the second line.", }, } // Generate LRC file tempDir := t.TempDir() outputFile := filepath.Join(tempDir, "output.lrc") err := Generate(lyrics, outputFile) if err != nil { t.Fatalf("Generate failed: %v", err) } // Verify generated content content, err := os.ReadFile(outputFile) if err != nil { t.Fatalf("Failed to read output file: %v", err) } // Check content lines := strings.Split(string(content), "\n") if len(lines) < 4 { t.Fatalf("Expected at least 4 lines, got %d", len(lines)) } hasTitleLine := false hasFirstTimeline := false for _, line := range lines { if line == "[ti:Test LRC File]" { hasTitleLine = true } if line == "[00:01.000]This is the first line." { hasFirstTimeline = true } } if !hasTitleLine { t.Errorf("Expected title line '[ti:Test LRC File]' not found") } if !hasFirstTimeline { t.Errorf("Expected timeline line '[00:01.000]This is the first line.' not found") } } func TestConvertToSubtitle(t *testing.T) { // Create a temporary test file content := `[ti:Test LRC File] [ar:Test Artist] [00:01.00]This is the first line. [00:05.00]This is the second line. ` tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.lrc") if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Convert to subtitle subtitle, err := ConvertToSubtitle(testFile) if err != nil { t.Fatalf("ConvertToSubtitle failed: %v", err) } // Check result if subtitle.Format != "lrc" { t.Errorf("Expected format 'lrc', got '%s'", subtitle.Format) } if subtitle.Title != "Test LRC File" { t.Errorf("Expected title 'Test LRC File', got '%s'", subtitle.Title) } if len(subtitle.Entries) != 2 { t.Errorf("Expected 2 entries, got %d", len(subtitle.Entries)) } // Check first entry if subtitle.Entries[0].Text != "This is the first line." { t.Errorf("Expected first entry text 'This is the first line.', got '%s'", subtitle.Entries[0].Text) } // Check metadata if subtitle.Metadata["ar"] != "Test Artist" { t.Errorf("Expected metadata 'ar' to be 'Test Artist', got '%s'", subtitle.Metadata["ar"]) } } func TestConvertFromSubtitle(t *testing.T) { // Create test subtitle subtitle := model.NewSubtitle() subtitle.Format = "lrc" subtitle.Title = "Test LRC File" subtitle.Metadata["ar"] = "Test Artist" entry1 := model.NewSubtitleEntry() entry1.StartTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0} entry1.EndTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 4, Milliseconds: 0} entry1.Text = "This is the first line." entry2 := model.NewSubtitleEntry() entry2.StartTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 5, Milliseconds: 0} entry2.EndTime = model.Timestamp{Hours: 0, Minutes: 0, Seconds: 8, Milliseconds: 0} entry2.Text = "This is the second line." subtitle.Entries = append(subtitle.Entries, entry1, entry2) // Convert from subtitle to LRC tempDir := t.TempDir() outputFile := filepath.Join(tempDir, "output.lrc") err := ConvertFromSubtitle(subtitle, outputFile) if err != nil { t.Fatalf("ConvertFromSubtitle failed: %v", err) } // Verify by parsing back lyrics, err := Parse(outputFile) if err != nil { t.Fatalf("Failed to parse output file: %v", err) } if len(lyrics.Timeline) != 2 { t.Errorf("Expected 2 entries, got %d", len(lyrics.Timeline)) } if lyrics.Content[0] != "This is the first line." { t.Errorf("Expected first entry content 'This is the first line.', got '%s'", lyrics.Content[0]) } if lyrics.Metadata["ti"] != "Test LRC File" { t.Errorf("Expected title metadata 'Test LRC File', got '%s'", lyrics.Metadata["ti"]) } } func TestFormat(t *testing.T) { // Create test LRC file with inconsistent timestamp formatting content := `[ti:Test LRC File] [ar:Test Artist] [0:1.0]This is the first line. [0:5]This is the second line. ` tempDir := t.TempDir() testFile := filepath.Join(tempDir, "test.lrc") if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } // Format the file err := Format(testFile) if err != nil { t.Fatalf("Format failed: %v", err) } // Verify by parsing back lyrics, err := Parse(testFile) if err != nil { t.Fatalf("Failed to parse formatted file: %v", err) } // Check that timestamps are formatted correctly if lyrics.Timeline[0].Seconds != 1 || lyrics.Timeline[0].Milliseconds != 0 { t.Errorf("Expected first timestamp to be 00:01.000, got %+v", lyrics.Timeline[0]) } // Verify metadata is preserved if lyrics.Metadata["ti"] != "Test LRC File" { t.Errorf("Expected title 'Test LRC File', got '%s'", lyrics.Metadata["ti"]) } } func TestParseTimestamp(t *testing.T) { testCases := []struct { name string input string expected model.Timestamp hasError bool }{ { name: "Simple minute and second", input: "01:30", expected: model.Timestamp{Hours: 0, Minutes: 1, Seconds: 30, Milliseconds: 0}, hasError: false, }, { name: "With milliseconds (1 digit)", input: "01:30.5", expected: model.Timestamp{Hours: 0, Minutes: 1, Seconds: 30, Milliseconds: 500}, hasError: false, }, { name: "With milliseconds (2 digits)", input: "01:30.75", expected: model.Timestamp{Hours: 0, Minutes: 1, Seconds: 30, Milliseconds: 750}, hasError: false, }, { name: "With milliseconds (3 digits)", input: "01:30.123", expected: model.Timestamp{Hours: 0, Minutes: 1, Seconds: 30, Milliseconds: 123}, hasError: false, }, { name: "With hours, minutes, seconds", input: "01:30:45", expected: model.Timestamp{Hours: 1, Minutes: 30, Seconds: 45, Milliseconds: 0}, hasError: false, }, { name: "With hours, minutes, seconds and milliseconds", input: "01:30:45.5", expected: model.Timestamp{Hours: 1, Minutes: 30, Seconds: 45, Milliseconds: 500}, hasError: false, }, { name: "Invalid format (single number)", input: "123", expected: model.Timestamp{}, hasError: true, }, { name: "Invalid format (too many parts)", input: "01:30:45:67", expected: model.Timestamp{}, hasError: true, }, { name: "Invalid minute (not a number)", input: "aa:30", expected: model.Timestamp{}, hasError: true, }, { name: "Invalid second (not a number)", input: "01:bb", expected: model.Timestamp{}, hasError: true, }, { name: "Invalid millisecond (not a number)", input: "01:30.cc", expected: model.Timestamp{}, hasError: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result, err := ParseTimestamp(tc.input) if tc.hasError && err == nil { t.Errorf("Expected error for input '%s', but got none", tc.input) } if !tc.hasError && err != nil { t.Errorf("Unexpected error for input '%s': %v", tc.input, err) } if !tc.hasError && result != tc.expected { t.Errorf("For input '%s', expected %+v, got %+v", tc.input, tc.expected, result) } }) } } func TestParse_FileErrors(t *testing.T) { // Test with non-existent file _, err := Parse("/nonexistent/file.lrc") if err == nil { t.Error("Expected error when parsing non-existent file, got nil") } } func TestParse_EdgeCases(t *testing.T) { // Test with empty file tempDir := t.TempDir() emptyFile := filepath.Join(tempDir, "empty.lrc") if err := os.WriteFile(emptyFile, []byte{}, 0644); err != nil { t.Fatalf("Failed to create empty test file: %v", err) } lyrics, err := Parse(emptyFile) if err != nil { t.Fatalf("Parse failed on empty file: %v", err) } if len(lyrics.Timeline) != 0 || len(lyrics.Content) != 0 { t.Errorf("Expected empty lyrics for empty file, got %d timeline entries and %d content entries", len(lyrics.Timeline), len(lyrics.Content)) } // Test with invalid timestamps invalidFile := filepath.Join(tempDir, "invalid.lrc") content := `[ti:Test LRC File] [ar:Test Artist] [invalidtime]This should be ignored. [00:01.00]This is a valid line. ` if err := os.WriteFile(invalidFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create invalid test file: %v", err) } lyrics, err = Parse(invalidFile) if err != nil { t.Fatalf("Parse failed on file with invalid timestamps: %v", err) } if len(lyrics.Timeline) != 1 || len(lyrics.Content) != 1 { t.Errorf("Expected 1 valid timeline entry, got %d timeline entries and %d content entries", len(lyrics.Timeline), len(lyrics.Content)) } // Test with timestamp-only lines (no content) timestampOnlyFile := filepath.Join(tempDir, "timestamp_only.lrc") content = `[ti:Test LRC File] [ar:Test Artist] [00:01.00] [00:05.00]This has content. ` if err := os.WriteFile(timestampOnlyFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create timestamp-only test file: %v", err) } lyrics, err = Parse(timestampOnlyFile) if err != nil { t.Fatalf("Parse failed on file with timestamp-only lines: %v", err) } if len(lyrics.Timeline) != 1 || len(lyrics.Content) != 1 { t.Errorf("Expected 1 valid entry (ignoring empty content), got %d timeline entries and %d content entries", len(lyrics.Timeline), len(lyrics.Content)) } } func TestGenerate_FileError(t *testing.T) { // Create test lyrics lyrics := model.Lyrics{ Metadata: map[string]string{ "ti": "Test LRC File", }, Timeline: []model.Timestamp{ {Hours: 0, Minutes: 0, Seconds: 1, Milliseconds: 0}, }, Content: []string{ "This is a test line.", }, } // Test with invalid path err := Generate(lyrics, "/nonexistent/directory/file.lrc") if err == nil { t.Error("Expected error when generating to invalid path, got nil") } } func TestFormat_FileError(t *testing.T) { // Test with non-existent file err := Format("/nonexistent/file.lrc") if err == nil { t.Error("Expected error when formatting non-existent file, got nil") } } func TestConvertToSubtitle_FileError(t *testing.T) { // Test with non-existent file _, err := ConvertToSubtitle("/nonexistent/file.lrc") if err == nil { t.Error("Expected error when converting non-existent file, got nil") } } func TestConvertToSubtitle_EdgeCases(t *testing.T) { // Test with empty lyrics (no content/timeline) tempDir := t.TempDir() emptyLyricsFile := filepath.Join(tempDir, "empty_lyrics.lrc") content := `[ti:Test LRC File] [ar:Test Artist] ` if err := os.WriteFile(emptyLyricsFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create empty lyrics test file: %v", err) } subtitle, err := ConvertToSubtitle(emptyLyricsFile) if err != nil { t.Fatalf("ConvertToSubtitle failed on empty lyrics: %v", err) } if len(subtitle.Entries) != 0 { t.Errorf("Expected 0 entries for empty lyrics, got %d", len(subtitle.Entries)) } if subtitle.Title != "Test LRC File" { t.Errorf("Expected title 'Test LRC File', got '%s'", subtitle.Title) } // Test with more content than timeline entries moreContentFile := filepath.Join(tempDir, "more_content.lrc") content = `[ti:Test LRC File] [00:01.00]This has a timestamp. This doesn't have a timestamp but is content. ` if err := os.WriteFile(moreContentFile, []byte(content), 0644); err != nil { t.Fatalf("Failed to create more content test file: %v", err) } subtitle, err = ConvertToSubtitle(moreContentFile) if err != nil { t.Fatalf("ConvertToSubtitle failed on file with more content than timestamps: %v", err) } if len(subtitle.Entries) != 1 { t.Errorf("Expected 1 entry (only the one with timestamp), got %d", len(subtitle.Entries)) } } func TestConvertFromSubtitle_FileError(t *testing.T) { // Create simple subtitle subtitle := model.NewSubtitle() subtitle.Entries = append(subtitle.Entries, model.NewSubtitleEntry()) // Test with invalid path err := ConvertFromSubtitle(subtitle, "/nonexistent/directory/file.lrc") if err == nil { t.Error("Expected error when converting to invalid path, got nil") } }