mirror of
https://github.com/navidrome/navidrome.git
synced 2025-06-01 16:11:05 +03:00
refactor: unify logic to export to M3U8
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
85a7268192
commit
71851b076c
@ -9,6 +9,7 @@ import (
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hashstructure"
|
||||
@ -330,6 +331,23 @@ func firstArtPath(currentPath string, currentDisc int, m MediaFile) (string, int
|
||||
return currentPath, currentDisc
|
||||
}
|
||||
|
||||
// ToM3U8 exports the playlist to the Extended M3U8 format, as specified in
|
||||
// https://docs.fileformat.com/audio/m3u/#extended-m3u
|
||||
func (mfs MediaFiles) ToM3U8(title string, absolutePaths bool) string {
|
||||
buf := strings.Builder{}
|
||||
buf.WriteString("#EXTM3U\n")
|
||||
buf.WriteString(fmt.Sprintf("#PLAYLIST:%s\n", title))
|
||||
for _, t := range mfs {
|
||||
buf.WriteString(fmt.Sprintf("#EXTINF:%.f,%s - %s\n", t.Duration, t.Artist, t.Title))
|
||||
if absolutePaths {
|
||||
buf.WriteString(t.AbsolutePath() + "\n")
|
||||
} else {
|
||||
buf.WriteString(t.Path + "\n")
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type MediaFileCursor iter.Seq2[MediaFile, error]
|
||||
|
||||
type MediaFileRepository interface {
|
||||
|
@ -402,6 +402,72 @@ var _ = Describe("MediaFiles", func() {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ToM3U8", func() {
|
||||
It("returns header only for empty MediaFiles", func() {
|
||||
mfs = MediaFiles{}
|
||||
result := mfs.ToM3U8("My Playlist", false)
|
||||
Expect(result).To(Equal("#EXTM3U\n#PLAYLIST:My Playlist\n"))
|
||||
})
|
||||
|
||||
DescribeTable("duration formatting",
|
||||
func(duration float32, expected string) {
|
||||
mfs = MediaFiles{{Title: "Song", Artist: "Artist", Duration: duration, Path: "song.mp3"}}
|
||||
result := mfs.ToM3U8("Test", false)
|
||||
Expect(result).To(ContainSubstring(expected))
|
||||
},
|
||||
Entry("zero duration", float32(0.0), "#EXTINF:0,"),
|
||||
Entry("whole number", float32(120.0), "#EXTINF:120,"),
|
||||
Entry("rounds 0.5 down", float32(180.5), "#EXTINF:180,"),
|
||||
Entry("rounds 0.6 up", float32(240.6), "#EXTINF:241,"),
|
||||
)
|
||||
|
||||
Context("multiple tracks", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{
|
||||
{Title: "Song One", Artist: "Artist A", Duration: 120, Path: "a/song1.mp3", LibraryPath: "/music"},
|
||||
{Title: "Song Two", Artist: "Artist B", Duration: 241, Path: "b/song2.mp3", LibraryPath: "/music"},
|
||||
{Title: "Song with \"quotes\" & ampersands", Artist: "Artist with Ümläuts", Duration: 90, Path: "special/file.mp3", LibraryPath: "/música"},
|
||||
}
|
||||
})
|
||||
|
||||
DescribeTable("generates correct output",
|
||||
func(absolutePaths bool, expectedContent string) {
|
||||
result := mfs.ToM3U8("Multi Track", absolutePaths)
|
||||
Expect(result).To(Equal(expectedContent))
|
||||
},
|
||||
Entry("relative paths",
|
||||
false,
|
||||
"#EXTM3U\n#PLAYLIST:Multi Track\n#EXTINF:120,Artist A - Song One\na/song1.mp3\n#EXTINF:241,Artist B - Song Two\nb/song2.mp3\n#EXTINF:90,Artist with Ümläuts - Song with \"quotes\" & ampersands\nspecial/file.mp3\n",
|
||||
),
|
||||
Entry("absolute paths",
|
||||
true,
|
||||
"#EXTM3U\n#PLAYLIST:Multi Track\n#EXTINF:120,Artist A - Song One\n/music/a/song1.mp3\n#EXTINF:241,Artist B - Song Two\n/music/b/song2.mp3\n#EXTINF:90,Artist with Ümläuts - Song with \"quotes\" & ampersands\n/música/special/file.mp3\n",
|
||||
),
|
||||
Entry("special characters",
|
||||
false,
|
||||
"#EXTM3U\n#PLAYLIST:Multi Track\n#EXTINF:120,Artist A - Song One\na/song1.mp3\n#EXTINF:241,Artist B - Song Two\nb/song2.mp3\n#EXTINF:90,Artist with Ümläuts - Song with \"quotes\" & ampersands\nspecial/file.mp3\n",
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
Context("path variations", func() {
|
||||
It("handles different path structures", func() {
|
||||
mfs = MediaFiles{
|
||||
{Title: "Root", Artist: "Artist", Duration: 60, Path: "song.mp3", LibraryPath: "/lib"},
|
||||
{Title: "Nested", Artist: "Artist", Duration: 60, Path: "deep/nested/song.mp3", LibraryPath: "/lib"},
|
||||
}
|
||||
|
||||
relativeResult := mfs.ToM3U8("Test", false)
|
||||
Expect(relativeResult).To(ContainSubstring("song.mp3\n"))
|
||||
Expect(relativeResult).To(ContainSubstring("deep/nested/song.mp3\n"))
|
||||
|
||||
absoluteResult := mfs.ToM3U8("Test", true)
|
||||
Expect(absoluteResult).To(ContainSubstring("/lib/song.mp3\n"))
|
||||
Expect(absoluteResult).To(ContainSubstring("/lib/deep/nested/song.mp3\n"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("MediaFile", func() {
|
||||
|
@ -1,10 +1,8 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/model/criteria"
|
||||
@ -53,17 +51,9 @@ func (pls *Playlist) RemoveTracks(idxToRemove []int) {
|
||||
pls.Tracks = newTracks
|
||||
}
|
||||
|
||||
// ToM3U8 exports the playlist to the Extended M3U8 format, as specified in
|
||||
// https://docs.fileformat.com/audio/m3u/#extended-m3u
|
||||
// ToM3U8 exports the playlist to the Extended M3U8 format
|
||||
func (pls *Playlist) ToM3U8() string {
|
||||
buf := strings.Builder{}
|
||||
buf.WriteString("#EXTM3U\n")
|
||||
buf.WriteString(fmt.Sprintf("#PLAYLIST:%s\n", pls.Name))
|
||||
for _, t := range pls.Tracks {
|
||||
buf.WriteString(fmt.Sprintf("#EXTINF:%.f,%s - %s\n", t.Duration, t.Artist, t.Title))
|
||||
buf.WriteString(t.AbsolutePath() + "\n")
|
||||
}
|
||||
return buf.String()
|
||||
return pls.MediaFiles().ToM3U8(pls.Name, true)
|
||||
}
|
||||
|
||||
func (pls *Playlist) AddTracks(mediaFileIds []string) {
|
||||
|
@ -13,13 +13,17 @@ var _ = Describe("Playlist", func() {
|
||||
pls = model.Playlist{Name: "Mellow sunset"}
|
||||
pls.Tracks = model.PlaylistTracks{
|
||||
{MediaFile: model.MediaFile{Artist: "Morcheeba feat. Kurt Wagner", Title: "What New York Couples Fight About",
|
||||
Duration: 377.84, Path: "/music/library/Morcheeba/Charango/01-06 What New York Couples Fight About.mp3"}},
|
||||
Duration: 377.84,
|
||||
LibraryPath: "/music/library", Path: "Morcheeba/Charango/01-06 What New York Couples Fight About.mp3"}},
|
||||
{MediaFile: model.MediaFile{Artist: "A Tribe Called Quest", Title: "Description of a Fool (Groove Armada's Acoustic mix)",
|
||||
Duration: 374.49, Path: "/music/library/Groove Armada/Back to Mine_ Groove Armada/01-01 Description of a Fool (Groove Armada's Acoustic mix).mp3"}},
|
||||
Duration: 374.49,
|
||||
LibraryPath: "/music/library", Path: "Groove Armada/Back to Mine_ Groove Armada/01-01 Description of a Fool (Groove Armada's Acoustic mix).mp3"}},
|
||||
{MediaFile: model.MediaFile{Artist: "Lou Reed", Title: "Walk on the Wild Side",
|
||||
Duration: 253.1, Path: "/music/library/Lou Reed/Walk on the Wild Side_ The Best of Lou Reed/01-06 Walk on the Wild Side.m4a"}},
|
||||
Duration: 253.1,
|
||||
LibraryPath: "/music/library", Path: "Lou Reed/Walk on the Wild Side_ The Best of Lou Reed/01-06 Walk on the Wild Side.m4a"}},
|
||||
{MediaFile: model.MediaFile{Artist: "Legião Urbana", Title: "On the Way Home",
|
||||
Duration: 163.89, Path: "/music/library/Legião Urbana/Música p_ acampamentos/02-05 On the Way Home.mp3"}},
|
||||
Duration: 163.89,
|
||||
LibraryPath: "/music/library", Path: "Legião Urbana/Música p_ acampamentos/02-05 On the Way Home.mp3"}},
|
||||
}
|
||||
})
|
||||
It("generates the correct M3U format", func() {
|
@ -2,7 +2,6 @@ package model
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -50,17 +49,9 @@ func (s Share) CoverArtID() ArtworkID {
|
||||
|
||||
type Shares []Share
|
||||
|
||||
// ToM3U8 exports the playlist to the Extended M3U8 format, as specified in
|
||||
// https://docs.fileformat.com/audio/m3u/#extended-m3u
|
||||
// ToM3U8 exports the share to the Extended M3U8 format.
|
||||
func (s Share) ToM3U8() string {
|
||||
buf := strings.Builder{}
|
||||
buf.WriteString("#EXTM3U\n")
|
||||
buf.WriteString(fmt.Sprintf("#PLAYLIST:%s\n", cmp.Or(s.Description, s.ID)))
|
||||
for _, t := range s.Tracks {
|
||||
buf.WriteString(fmt.Sprintf("#EXTINF:%.f,%s - %s\n", t.Duration, t.Artist, t.Title))
|
||||
buf.WriteString(t.Path + "\n")
|
||||
}
|
||||
return buf.String()
|
||||
return s.Tracks.ToM3U8(cmp.Or(s.Description, s.ID), false)
|
||||
}
|
||||
|
||||
type ShareRepository interface {
|
||||
|
Loading…
x
Reference in New Issue
Block a user