254 lines
6.5 KiB
Go
254 lines
6.5 KiB
Go
package playlist
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/dhowden/tag"
|
|
)
|
|
|
|
func TestTrackMeta(t *testing.T) {
|
|
t.Run("nil map returns empty", func(t *testing.T) {
|
|
tr := Track{Title: "Test"}
|
|
if got := tr.Meta("navidrome.id"); got != "" {
|
|
t.Errorf("Meta on nil map = %q, want empty", got)
|
|
}
|
|
})
|
|
|
|
t.Run("existing key", func(t *testing.T) {
|
|
tr := Track{
|
|
Title: "Test",
|
|
ProviderMeta: map[string]string{"navidrome.id": "abc123"},
|
|
}
|
|
if got := tr.Meta("navidrome.id"); got != "abc123" {
|
|
t.Errorf("Meta = %q, want %q", got, "abc123")
|
|
}
|
|
})
|
|
|
|
t.Run("missing key", func(t *testing.T) {
|
|
tr := Track{
|
|
Title: "Test",
|
|
ProviderMeta: map[string]string{"navidrome.id": "abc123"},
|
|
}
|
|
if got := tr.Meta("jellyfin.id"); got != "" {
|
|
t.Errorf("Meta = %q, want empty", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTrackDisplayName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
track Track
|
|
want string
|
|
}{
|
|
{
|
|
name: "artist and title",
|
|
track: Track{Artist: "Radiohead", Title: "Creep"},
|
|
want: "Radiohead - Creep",
|
|
},
|
|
{
|
|
name: "title only",
|
|
track: Track{Title: "Unknown Song"},
|
|
want: "Unknown Song",
|
|
},
|
|
{
|
|
name: "empty",
|
|
track: Track{},
|
|
want: "",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tt.track.DisplayName(); got != tt.want {
|
|
t.Errorf("DisplayName() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTrackIsLive(t *testing.T) {
|
|
t.Run("realtime", func(t *testing.T) {
|
|
tr := Track{Realtime: true}
|
|
if !tr.IsLive() {
|
|
t.Error("IsLive() = false, want true")
|
|
}
|
|
})
|
|
|
|
t.Run("not realtime", func(t *testing.T) {
|
|
tr := Track{Realtime: false}
|
|
if tr.IsLive() {
|
|
t.Error("IsLive() = true, want false")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileURL(t *testing.T) {
|
|
got := fileURL(filepath.Join("tmp", "cover art.jpg"))
|
|
if !strings.HasPrefix(got, "file:///") {
|
|
t.Fatalf("fileURL = %q, want file URL", got)
|
|
}
|
|
if runtime.GOOS != "windows" && !strings.Contains(got, "cover%20art.jpg") {
|
|
t.Fatalf("fileURL = %q, want escaped spaces", got)
|
|
}
|
|
}
|
|
|
|
func TestCacheAlbumArtUsesContentHash(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
|
|
first := cacheAlbumArt(&tag.Picture{Ext: "jpeg", Data: []byte("same cover")})
|
|
second := cacheAlbumArt(&tag.Picture{Ext: "jpg", Data: []byte("same cover")})
|
|
if first == "" || first != second {
|
|
t.Fatalf("cacheAlbumArt URLs = %q and %q, want same non-empty URL", first, second)
|
|
}
|
|
|
|
matches, err := filepath.Glob(filepath.Join(home, ".local", "share", "cliamp", albumArtCacheDir, "*"))
|
|
if err != nil {
|
|
t.Fatalf("Glob: %v", err)
|
|
}
|
|
if len(matches) != 1 {
|
|
t.Fatalf("cached files = %d, want 1: %v", len(matches), matches)
|
|
}
|
|
}
|
|
|
|
func TestCleanupAlbumArtCacheKeepsCurrentFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
oldPath := filepath.Join(dir, "old.jpg")
|
|
keepPath := filepath.Join(dir, "keep.jpg")
|
|
if err := os.WriteFile(oldPath, []byte(strings.Repeat("o", 80)), 0o644); err != nil {
|
|
t.Fatalf("WriteFile old: %v", err)
|
|
}
|
|
if err := os.WriteFile(keepPath, []byte(strings.Repeat("k", 80)), 0o644); err != nil {
|
|
t.Fatalf("WriteFile keep: %v", err)
|
|
}
|
|
oldTime := time.Now().Add(-time.Hour)
|
|
if err := os.Chtimes(oldPath, oldTime, oldTime); err != nil {
|
|
t.Fatalf("Chtimes old: %v", err)
|
|
}
|
|
|
|
cleanupAlbumArtCache(dir, 100, keepPath)
|
|
if _, err := os.Stat(oldPath); !os.IsNotExist(err) {
|
|
t.Fatalf("old cache file still exists or stat failed: %v", err)
|
|
}
|
|
if _, err := os.Stat(keepPath); err != nil {
|
|
t.Fatalf("current cache file was removed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRefreshEmbeddedMetadataPreservesSavedFieldsWhenUnreadable(t *testing.T) {
|
|
track := Track{
|
|
Path: filepath.Join(t.TempDir(), "missing.mp3"),
|
|
Title: "Saved title",
|
|
Artist: "Saved artist",
|
|
EmbeddedLyrics: "old lyrics",
|
|
AlbumArtURL: "file:///old.jpg",
|
|
}
|
|
|
|
got := RefreshEmbeddedMetadata(track)
|
|
if got.EmbeddedLyrics != track.EmbeddedLyrics || got.AlbumArtURL != track.AlbumArtURL {
|
|
t.Fatalf("embedded metadata changed: got %q, %q want %q, %q", got.EmbeddedLyrics, got.AlbumArtURL, track.EmbeddedLyrics, track.AlbumArtURL)
|
|
}
|
|
if got.Title != track.Title || got.Artist != track.Artist {
|
|
t.Fatalf("non-embedded fields changed: got %+v want title/artist from %+v", got, track)
|
|
}
|
|
}
|
|
|
|
func TestRefreshEmbeddedMetadataClearsSavedFieldsWhenTagsUnreadable(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "empty.mp3")
|
|
if err := os.WriteFile(path, nil, 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
track := Track{
|
|
Path: path,
|
|
Title: "Saved title",
|
|
EmbeddedLyrics: "old lyrics",
|
|
AlbumArtURL: "file:///old.jpg",
|
|
}
|
|
|
|
got := RefreshEmbeddedMetadata(track)
|
|
if got.EmbeddedLyrics != "" || got.AlbumArtURL != "" {
|
|
t.Fatalf("embedded metadata = %q, %q; want empty refreshed fields", got.EmbeddedLyrics, got.AlbumArtURL)
|
|
}
|
|
if got.Title != track.Title {
|
|
t.Fatalf("title changed: got %q want %q", got.Title, track.Title)
|
|
}
|
|
}
|
|
|
|
func TestTrackFromURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
wantTtl string
|
|
stream bool
|
|
}{
|
|
{
|
|
name: "with filename",
|
|
url: "https://example.com/music/song.mp3",
|
|
wantTtl: "song",
|
|
stream: true,
|
|
},
|
|
{
|
|
name: "stream path fallback to hostname",
|
|
url: "https://radio.example.com/stream",
|
|
wantTtl: "radio.example.com",
|
|
stream: true,
|
|
},
|
|
{
|
|
name: "rest path fallback to hostname",
|
|
url: "https://api.example.com/rest",
|
|
wantTtl: "api.example.com",
|
|
stream: true,
|
|
},
|
|
{
|
|
name: "query params ignored",
|
|
url: "https://example.com/song.mp3?token=abc",
|
|
wantTtl: "song",
|
|
stream: true,
|
|
},
|
|
{
|
|
name: "root path fallback to hostname",
|
|
url: "https://radio.example.com/",
|
|
wantTtl: "radio.example.com",
|
|
stream: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tr := TrackFromPath(tt.url)
|
|
if tr.Title != tt.wantTtl {
|
|
t.Errorf("Title = %q, want %q", tr.Title, tt.wantTtl)
|
|
}
|
|
if tr.Stream != tt.stream {
|
|
t.Errorf("Stream = %v, want %v", tr.Stream, tt.stream)
|
|
}
|
|
if tr.Path != tt.url {
|
|
t.Errorf("Path = %q, want %q", tr.Path, tt.url)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRepeatModeString(t *testing.T) {
|
|
tests := []struct {
|
|
mode RepeatMode
|
|
want string
|
|
}{
|
|
{RepeatOff, "Off"},
|
|
{RepeatAll, "All"},
|
|
{RepeatOne, "One"},
|
|
{RepeatMode(99), "Off"}, // unknown defaults to "Off"
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.want, func(t *testing.T) {
|
|
if got := tt.mode.String(); got != tt.want {
|
|
t.Errorf("String() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|