Support m3u playlist files to define chapter order
The chapter order on the tonie is already fully API-controlled via SortChaptersOfTonie/PATCH. This adds an explicit way to define that order via an .m3u/.m3u8 file placed in the tonie folder, instead of relying on alphabetical filenames: - internal/syncer/playlist.go: finds a single playlist file in the tonie folder, parses its entries (ignoring blanks/#-comments), and reorders local tracks accordingly. Tracks not referenced by the playlist are appended afterwards so nothing is silently dropped; playlist entries without a matching local file are ignored. Multiple playlist files in the same folder is an error (ambiguous order). - ListLocalTracks now prefers this playlist-defined order, falling back to alphabetical filename order when no playlist is present. - Tests covering playlist ordering, unreferenced-track handling, missing entries, and the multiple-playlists error case. - README documents the .m3u/.m3u8 convention. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package syncer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeFile(t *testing.T, dir, name, content string) string {
|
||||
t.Helper()
|
||||
p := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func TestListLocalTracksUsesM3UOrderWhenPresent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFile(t, dir, "b-track.mp3", "data")
|
||||
writeFile(t, dir, "a-track.mp3", "data")
|
||||
writeFile(t, dir, "c-track.mp3", "data")
|
||||
writeFile(t, dir, "playlist.m3u", "#EXTM3U\nb-track.mp3\nc-track.mp3\na-track.mp3\n")
|
||||
|
||||
tracks, err := ListLocalTracks(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := titles(tracks)
|
||||
want := []string{"b-track", "c-track", "a-track"}
|
||||
if !equalStrings(got, want) {
|
||||
t.Fatalf("expected order %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListLocalTracksAppendsUnreferencedTracksAfterPlaylist(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFile(t, dir, "a-track.mp3", "data")
|
||||
writeFile(t, dir, "b-track.mp3", "data")
|
||||
writeFile(t, dir, "z-new-track.mp3", "data")
|
||||
// Playlist doesn't mention the newly added track yet.
|
||||
writeFile(t, dir, "playlist.m3u", "b-track.mp3\na-track.mp3\n")
|
||||
|
||||
tracks, err := ListLocalTracks(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := titles(tracks)
|
||||
want := []string{"b-track", "a-track", "z-new-track"}
|
||||
if !equalStrings(got, want) {
|
||||
t.Fatalf("expected order %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListLocalTracksIgnoresMissingPlaylistEntries(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFile(t, dir, "a-track.mp3", "data")
|
||||
writeFile(t, dir, "playlist.m3u", "does-not-exist.mp3\na-track.mp3\n")
|
||||
|
||||
tracks, err := ListLocalTracks(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := titles(tracks)
|
||||
want := []string{"a-track"}
|
||||
if !equalStrings(got, want) {
|
||||
t.Fatalf("expected order %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListLocalTracksFallsBackToAlphabeticalWithoutPlaylist(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFile(t, dir, "b-track.mp3", "data")
|
||||
writeFile(t, dir, "a-track.mp3", "data")
|
||||
|
||||
tracks, err := ListLocalTracks(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := titles(tracks)
|
||||
want := []string{"a-track", "b-track"}
|
||||
if !equalStrings(got, want) {
|
||||
t.Fatalf("expected order %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListLocalTracksErrorsOnMultiplePlaylists(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writeFile(t, dir, "a-track.mp3", "data")
|
||||
writeFile(t, dir, "one.m3u", "a-track.mp3\n")
|
||||
writeFile(t, dir, "two.m3u8", "a-track.mp3\n")
|
||||
|
||||
if _, err := ListLocalTracks(dir); err == nil {
|
||||
t.Fatal("expected error when multiple playlist files are present")
|
||||
}
|
||||
}
|
||||
|
||||
func titles(tracks []Track) []string {
|
||||
titles := make([]string, len(tracks))
|
||||
for i, t := range tracks {
|
||||
titles[i] = t.Title
|
||||
}
|
||||
return titles
|
||||
}
|
||||
Reference in New Issue
Block a user