package resolve import ( "path/filepath" "strings" "testing" ) func TestParseM3UBasic(t *testing.T) { input := `#EXTM3U #EXTINF:120,Artist - Song One http://example.com/song1.mp3 #EXTINF:180,Artist - Song Two http://example.com/song2.mp3 ` entries, err := parseM3U(strings.NewReader(input), "") if err != nil { t.Fatalf("parseM3U error: %v", err) } if len(entries) != 2 { t.Fatalf("got %d entries, want 2", len(entries)) } if entries[0].Title != "Artist - Song One" { t.Errorf("entry[0].Title = %q, want %q", entries[0].Title, "Artist - Song One") } if entries[0].Duration != 120 { t.Errorf("entry[0].Duration = %d, want 120", entries[0].Duration) } if entries[0].Path != "http://example.com/song1.mp3" { t.Errorf("entry[0].Path = %q", entries[0].Path) } if entries[1].Title != "Artist - Song Two" { t.Errorf("entry[1].Title = %q", entries[1].Title) } if entries[1].Duration != 180 { t.Errorf("entry[1].Duration = %d, want 180", entries[1].Duration) } } func TestParseM3UNoHeader(t *testing.T) { // M3U without #EXTM3U header should still work input := `http://example.com/stream1.mp3 http://example.com/stream2.mp3 ` entries, err := parseM3U(strings.NewReader(input), "") if err != nil { t.Fatalf("parseM3U error: %v", err) } if len(entries) != 2 { t.Fatalf("got %d entries, want 2", len(entries)) } if entries[0].Duration != -1 { t.Errorf("bare entry Duration = %d, want -1", entries[0].Duration) } } func TestParseM3UBOM(t *testing.T) { // UTF-8 BOM prefix should be stripped input := "\xef\xbb\xbf#EXTM3U\n#EXTINF:60,Song\nhttp://example.com/song.mp3\n" entries, err := parseM3U(strings.NewReader(input), "") if err != nil { t.Fatalf("parseM3U error: %v", err) } if len(entries) != 1 { t.Fatalf("got %d entries, want 1", len(entries)) } if entries[0].Title != "Song" { t.Errorf("Title = %q, want Song", entries[0].Title) } } func TestParseM3USkipsComments(t *testing.T) { input := `#EXTM3U # This is a comment #EXTINF:60,Song http://example.com/song.mp3 #EXTVLCOPT:some-option ` entries, err := parseM3U(strings.NewReader(input), "") if err != nil { t.Fatalf("parseM3U error: %v", err) } if len(entries) != 1 { t.Fatalf("got %d entries, want 1", len(entries)) } } func TestParseM3URelativePaths(t *testing.T) { input := `#EXTM3U #EXTINF:60,Song music/song.mp3 ` entries, err := parseM3U(strings.NewReader(input), "/home/user") if err != nil { t.Fatalf("parseM3U error: %v", err) } if len(entries) != 1 { t.Fatalf("got %d entries, want 1", len(entries)) } if entries[0].Path != "/home/user/music/song.mp3" { t.Errorf("Path = %q, want /home/user/music/song.mp3", entries[0].Path) } } func TestParseM3UAbsolutePaths(t *testing.T) { input := "/absolute/path/song.mp3\n" entries, err := parseM3U(strings.NewReader(input), "/home/user") if err != nil { t.Fatalf("parseM3U error: %v", err) } if entries[0].Path != "/absolute/path/song.mp3" { t.Errorf("Path = %q, want /absolute/path/song.mp3", entries[0].Path) } } func TestParseM3UEmpty(t *testing.T) { entries, err := parseM3U(strings.NewReader(""), "") if err != nil { t.Fatalf("parseM3U error: %v", err) } if len(entries) != 0 { t.Fatalf("got %d entries, want 0", len(entries)) } } func TestParseM3URadioStream(t *testing.T) { // Radio stream with -1 duration input := `#EXTM3U #EXTINF:-1,Radio Station http://radio.example.com/stream ` entries, err := parseM3U(strings.NewReader(input), "") if err != nil { t.Fatalf("parseM3U error: %v", err) } if len(entries) != 1 { t.Fatalf("got %d entries, want 1", len(entries)) } if entries[0].Duration != -1 { t.Errorf("Duration = %d, want -1", entries[0].Duration) } if entries[0].Title != "Radio Station" { t.Errorf("Title = %q, want Radio Station", entries[0].Title) } } func TestM3UEntryToTrackStream(t *testing.T) { e := m3uEntry{ Path: "http://radio.example.com/stream", Title: "Radio Station", Duration: -1, } tr := m3uEntryToTrack(e) if !tr.Stream { t.Error("Stream should be true for URL") } if !tr.Realtime { t.Error("Realtime should be true for stream with -1 duration") } if tr.DurationSecs != 0 { t.Errorf("DurationSecs = %d, want 0 (negative clamped)", tr.DurationSecs) } if tr.Title != "Radio Station" { t.Errorf("Title = %q, want Radio Station", tr.Title) } } func TestM3UEntryToTrackFile(t *testing.T) { e := m3uEntry{ Path: "/home/user/song.mp3", Title: "My Song", Duration: 180, } tr := m3uEntryToTrack(e) if tr.Stream { t.Error("Stream should be false for local file") } if tr.Realtime { t.Error("Realtime should be false for local file") } if tr.DurationSecs != 180 { t.Errorf("DurationSecs = %d, want 180", tr.DurationSecs) } } func TestResolveM3UPathWindows(t *testing.T) { tests := []struct { name string baseDir string entry string want string }{ { name: "relative backslash", baseDir: `C:\Music`, entry: `artist\song.mp3`, want: filepath.Clean(filepath.Join(`C:\Music`, filepath.FromSlash(`artist\song.mp3`))), }, { name: "relative forward slash", baseDir: `C:\Music`, entry: `artist/song.mp3`, want: filepath.Clean(filepath.Join(`C:\Music`, filepath.FromSlash(`artist/song.mp3`))), }, { name: "absolute drive-letter", baseDir: `C:\Music`, entry: `D:\Other\track.mp3`, want: filepath.Clean(`D:\Other\track.mp3`), }, { name: "absolute UNC path", baseDir: `\\server\share`, entry: `sub\file.mp3`, want: filepath.Clean(filepath.Join(`\\server\share`, filepath.FromSlash(`sub\file.mp3`))), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := resolveM3UPath(tt.baseDir, tt.entry) if got != tt.want { t.Fatalf("resolveM3UPath(%q, %q) = %q, want %q", tt.baseDir, tt.entry, got, tt.want) } }) } }