diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go
index 619371b..f08a225 100644
--- a/internal/backends/jspf/jspf.go
+++ b/internal/backends/jspf/jspf.go
@@ -18,12 +18,12 @@ Scotty. If not, see .
package jspf
import (
- "encoding/json"
"os"
"time"
"github.com/spf13/viper"
"go.uploadedlobster.com/scotty/internal/models"
+ "go.uploadedlobster.com/scotty/pkg/jspf"
)
type JSPFBackend struct {
@@ -31,7 +31,7 @@ type JSPFBackend struct {
title string
creator string
identifier string
- tracks []Track
+ tracks []jspf.Track
}
func (b *JSPFBackend) Name() string { return "jspf" }
@@ -41,7 +41,7 @@ func (b *JSPFBackend) FromConfig(config *viper.Viper) models.Backend {
b.title = config.GetString("title")
b.creator = config.GetString("username")
b.identifier = config.GetString("identifier")
- b.tracks = make([]Track, 0)
+ b.tracks = make([]jspf.Track, 0)
return b
}
@@ -75,13 +75,13 @@ func (b *JSPFBackend) ImportLoves(export models.LovesResult, importResult models
return importResult, nil
}
-func listenAsTrack(l models.Listen) Track {
+func listenAsTrack(l models.Listen) jspf.Track {
l.FillAdditionalInfo()
track := trackAsTrack(l.Track)
extension := makeMusicBrainzExtension(l.Track)
extension.AddedAt = l.ListenedAt
extension.AddedBy = l.UserName
- track.Extension[MusicBrainzTrackExtensionId] = extension
+ track.Extension[jspf.MusicBrainzTrackExtensionId] = extension
if l.RecordingMbid != "" {
track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(l.RecordingMbid))
@@ -90,13 +90,13 @@ func listenAsTrack(l models.Listen) Track {
return track
}
-func loveAsTrack(l models.Love) Track {
+func loveAsTrack(l models.Love) jspf.Track {
l.FillAdditionalInfo()
track := trackAsTrack(l.Track)
extension := makeMusicBrainzExtension(l.Track)
extension.AddedAt = l.Created
extension.AddedBy = l.UserName
- track.Extension[MusicBrainzTrackExtensionId] = extension
+ track.Extension[jspf.MusicBrainzTrackExtensionId] = extension
if l.RecordingMbid != "" {
track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(l.RecordingMbid))
@@ -105,8 +105,8 @@ func loveAsTrack(l models.Love) Track {
return track
}
-func trackAsTrack(t models.Track) Track {
- track := Track{
+func trackAsTrack(t models.Track) jspf.Track {
+ track := jspf.Track{
Title: t.TrackName,
Album: t.ReleaseName,
Creator: t.ArtistName(),
@@ -117,8 +117,8 @@ func trackAsTrack(t models.Track) Track {
return track
}
-func makeMusicBrainzExtension(t models.Track) MusicBrainzTrackExtension {
- extension := MusicBrainzTrackExtension{
+func makeMusicBrainzExtension(t models.Track) jspf.MusicBrainzTrackExtension {
+ extension := jspf.MusicBrainzTrackExtension{
AdditionalMetadata: t.AdditionalInfo,
ArtistIdentifiers: make([]string, len(t.ArtistMbids)),
}
@@ -137,9 +137,9 @@ func makeMusicBrainzExtension(t models.Track) MusicBrainzTrackExtension {
return extension
}
-func (b JSPFBackend) writeJSPF(tracks []Track) error {
- playlist := JSPF{
- Playlist: Playlist{
+func (b JSPFBackend) writeJSPF(tracks []jspf.Track) error {
+ playlist := jspf.JSPF{
+ Playlist: jspf.Playlist{
Title: b.title,
Creator: b.creator,
Identifier: b.identifier,
@@ -154,11 +154,5 @@ func (b JSPFBackend) writeJSPF(tracks []Track) error {
}
defer file.Close()
- jspfJson, err := json.MarshalIndent(playlist, "", "\t")
- if err != nil {
- return err
- }
-
- _, err = file.Write(jspfJson)
- return err
+ return playlist.Write(file)
}
diff --git a/pkg/jspf/COPYING b/pkg/jspf/COPYING
new file mode 100644
index 0000000..f2bd793
--- /dev/null
+++ b/pkg/jspf/COPYING
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright © 2023 Philipp Wolfer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/pkg/jspf/extensions.go b/pkg/jspf/extensions.go
new file mode 100644
index 0000000..fe5fef2
--- /dev/null
+++ b/pkg/jspf/extensions.go
@@ -0,0 +1,86 @@
+/*
+Copyright © 2023 Philipp Wolfer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package jspf
+
+import "time"
+
+const (
+ // The identifier for the MusicBrainz / ListenBrainz JSPF playlist extension
+ MusicBrainzPlaylistExtensionId = "https://musicbrainz.org/doc/jspf#playlist"
+ // The identifier for the MusicBrainz / ListenBrainz JSPF track extension
+ MusicBrainzTrackExtensionId = "https://musicbrainz.org/doc/jspf#track"
+)
+
+// MusicBrainz / ListenBrainz JSPF track extension
+//
+// This extension allows storing additional metadata for playlists.
+//
+// See "https://musicbrainz.org/doc/jspf#playlist"
+type MusicBrainzPlaylistExtension struct {
+ // The ListenBrainz user who created this playlist.
+ Creator string `json:"added_by,omitempty"`
+ // Which ListenBrainz user was the playlist generated for? This is for music
+ // recommendation bots generating playlists for users.
+ CreatedFor string `json:"created_for,omitempty"`
+ // Who are the ListenBrainz users who have access to edit this playlist?
+ Collaborators []string `json:"collaborators,omitempty"`
+ // This field identifies a playlist, using the identifier syntax,
+ // from which this playlist was copied from.
+ CopiedFrom string `json:"copied_from,omitempty"`
+ // If the source playlist that this playlist has been copied from has been
+ // deleted, this field will be set to true and the copied_from field will not
+ // be returned.
+ CopiedFromDeleted bool `json:"copied_from_deleted,omitempty"`
+ // Indicates if this playlist is public or private. Must contain the value
+ // "true" or "false".
+ Public string `json:"public,omitempty"`
+ // The timestamp for when this playlist was last modified.
+ LastModifiedAt time.Time `json:"last_modified_at,omitempty"`
+ // This dict allows a playlist creator to submit additional track metadata
+ // that may be used by playlist generation tools. The content of this field
+ // is defined by the playlist generation tools and is beyond the scope of
+ // this document.
+ AdditionalMetadata map[string]any `json:"additional_metadata,omitempty"`
+}
+
+// MusicBrainz / ListenBrainz JSPF track extension
+//
+// This extension allows storing additional metadata for tracks.
+//
+// See "https://musicbrainz.org/doc/jspf#track"
+type MusicBrainzTrackExtension struct {
+ // The timestamp for when this track was added to the playlist.
+ AddedAt time.Time `json:"added_at,omitempty"`
+ // The ListenBrainz user who added this track.
+ AddedBy string `json:"added_by,omitempty"`
+ // The MusicBrainz ID URI for the release that contained this track.
+ ReleaseIdentifier string `json:"release_identifier,omitempty"`
+ // A list of MusicBrainz Artist URIs that identify the artist that are part
+ // of the MusicBrainz artist credit id for this track.
+ ArtistIdentifiers []string `json:"artist_identifiers,omitempty"`
+ // This dict allows a playlist creator to submit additional track metadata
+ // that may be used by playlist generation tools. The content of this field
+ // is defined by the playlist generation tools and is beyond the scope of
+ // this document.
+ AdditionalMetadata map[string]any `json:"additional_metadata,omitempty"`
+}
diff --git a/pkg/jspf/extensions_test.go b/pkg/jspf/extensions_test.go
new file mode 100644
index 0000000..8d8653d
--- /dev/null
+++ b/pkg/jspf/extensions_test.go
@@ -0,0 +1,74 @@
+/*
+Copyright © 2023 Philipp Wolfer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package jspf_test
+
+import (
+ "bytes"
+ "fmt"
+ "log"
+ "time"
+
+ "go.uploadedlobster.com/scotty/pkg/jspf"
+)
+
+func ExampleMusicBrainzTrackExtension() {
+ pl := jspf.JSPF{
+ Playlist: jspf.Playlist{
+ Date: time.Date(2023, 11, 24, 07, 47, 50, 0, time.UTC),
+ Tracks: []jspf.Track{
+ {
+ Title: "Oweynagat",
+ Extension: map[string]any{
+ jspf.MusicBrainzTrackExtensionId: jspf.MusicBrainzTrackExtension{
+ AddedAt: time.Date(2023, 11, 24, 07, 47, 50, 0, time.UTC),
+ AddedBy: "scotty",
+ },
+ },
+ },
+ },
+ },
+ }
+ buf := bytes.NewBuffer(make([]byte, 0, 10))
+ err := pl.Write(buf)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Print(buf.String())
+ // Output:
+ // {
+ // "playlist": {
+ // "date": "2023-11-24T07:47:50Z",
+ // "track": [
+ // {
+ // "title": "Oweynagat",
+ // "extension": {
+ // "https://musicbrainz.org/doc/jspf#track": {
+ // "added_at": "2023-11-24T07:47:50Z",
+ // "added_by": "scotty"
+ // }
+ // }
+ // }
+ // ]
+ // }
+ // }
+}
diff --git a/pkg/jspf/io.go b/pkg/jspf/io.go
new file mode 100644
index 0000000..f25f6a6
--- /dev/null
+++ b/pkg/jspf/io.go
@@ -0,0 +1,52 @@
+/*
+Copyright © 2023 Philipp Wolfer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+// Package for reading and writing music playlists in the JSPF format.
+//
+// JSPF is the JSON representation of XSPF, see https://www.xspf.org/jspf
+package jspf
+
+import (
+ "encoding/json"
+ "io"
+)
+
+// Reads the JSPF playlist in JSON format from the given io.Reader
+func (j *JSPF) Read(in io.Reader) error {
+ bytes, err := io.ReadAll(in)
+ if err != nil {
+ return err
+ }
+ err = json.Unmarshal(bytes, j)
+ return err
+}
+
+// Writes the JSPF playlist in JSON format to the given io.Writer
+func (j *JSPF) Write(out io.Writer) error {
+ jspfJson, err := json.MarshalIndent(j, "", "\t")
+ if err != nil {
+ return err
+ }
+
+ _, err = out.Write(jspfJson)
+ return err
+}
diff --git a/pkg/jspf/io_test.go b/pkg/jspf/io_test.go
new file mode 100644
index 0000000..e18bbce
--- /dev/null
+++ b/pkg/jspf/io_test.go
@@ -0,0 +1,86 @@
+/*
+Copyright © 2023 Philipp Wolfer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package jspf_test
+
+import (
+ "bytes"
+ "fmt"
+ "log"
+ "os"
+ "time"
+
+ "go.uploadedlobster.com/scotty/pkg/jspf"
+)
+
+func ExampleJSPF_Read() {
+ file, err := os.Open("testdata/simple.jspf")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ defer file.Close()
+
+ pl := jspf.JSPF{}
+ err = pl.Read(file)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("Playlist '%v' with %v tracks",
+ pl.Playlist.Title, len(pl.Playlist.Tracks))
+ // Output: Playlist 'Two Songs From Thriller' with 2 tracks
+}
+
+func ExampleJSPF_Write() {
+ date, _ := time.Parse(time.RFC3339, "2023-11-23T23:56:00Z")
+ pl := jspf.JSPF{
+ Playlist: jspf.Playlist{
+ Title: "The Playlist",
+ Creator: "Scotty",
+ Date: date,
+ Tracks: []jspf.Track{
+ {
+ Title: "The Track",
+ },
+ },
+ },
+ }
+ buf := bytes.NewBuffer(make([]byte, 0, 10))
+ err := pl.Write(buf)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Print(buf.String())
+ // Output:
+ // {
+ // "playlist": {
+ // "title": "The Playlist",
+ // "creator": "Scotty",
+ // "date": "2023-11-23T23:56:00Z",
+ // "track": [
+ // {
+ // "title": "The Track"
+ // }
+ // ]
+ // }
+ // }
+}
diff --git a/internal/backends/jspf/models.go b/pkg/jspf/models.go
similarity index 59%
rename from internal/backends/jspf/models.go
rename to pkg/jspf/models.go
index c286b96..d910367 100644
--- a/internal/backends/jspf/models.go
+++ b/pkg/jspf/models.go
@@ -1,24 +1,31 @@
/*
Copyright © 2023 Philipp Wolfer
-This file is part of Scotty.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
-Scotty is free software: you can redistribute it and/or modify it under the
-terms of the GNU General Public License as published by the Free Software
-Foundation, either version 3 of the License, or (at your option) any later version.
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
-Scotty is distributed in the hope that it will be useful, but WITHOUT ANY
-WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License along with
-Scotty. If not, see .
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
*/
package jspf
import "time"
+// A JSPF playlist
+//
// See https://xspf.org/jspf
type JSPF struct {
Playlist Playlist `json:"playlist"`
@@ -61,15 +68,3 @@ type Attribution map[string]string
type Link map[string]string
type Meta map[string]string
-
-// Extension for "https://musicbrainz.org/doc/jspf#track"
-// as used by ListenBrainz.
-const MusicBrainzTrackExtensionId = "https://musicbrainz.org/doc/jspf#track"
-
-type MusicBrainzTrackExtension struct {
- AddedAt time.Time `json:"added_at,omitempty"`
- AddedBy string `json:"added_by,omitempty"`
- ReleaseIdentifier string `json:"release_identifier,omitempty"`
- ArtistIdentifiers []string `json:"artist_identifiers,omitempty"`
- AdditionalMetadata map[string]any `json:"additional_metadata,omitempty"`
-}
diff --git a/internal/backends/jspf/models_test.go b/pkg/jspf/models_test.go
similarity index 71%
rename from internal/backends/jspf/models_test.go
rename to pkg/jspf/models_test.go
index ddd7c01..c83c24f 100644
--- a/internal/backends/jspf/models_test.go
+++ b/pkg/jspf/models_test.go
@@ -1,25 +1,29 @@
/*
Copyright © 2023 Philipp Wolfer
-This file is part of Scotty.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
-Scotty is free software: you can redistribute it and/or modify it under the
-terms of the GNU General Public License as published by the Free Software
-Foundation, either version 3 of the License, or (at your option) any later version.
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
-Scotty is distributed in the hope that it will be useful, but WITHOUT ANY
-WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License along with
-Scotty. If not, see .
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
*/
package jspf_test
import (
"encoding/json"
- "fmt"
"io"
"os"
"testing"
@@ -27,7 +31,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "go.uploadedlobster.com/scotty/internal/backends/jspf"
+ "go.uploadedlobster.com/scotty/pkg/jspf"
)
func TestUnmarshalSimple(t *testing.T) {
@@ -82,7 +86,6 @@ func TestUnmarshalListenBrainzPlaylist(t *testing.T) {
assert.Equal(
"https://musicbrainz.org/recording/3f2bdbbd-063e-478c-a394-6da0cb303302",
track1.Identifier[0])
- fmt.Printf("Ext: %v\n", track1.Extension["https://musicbrainz.org/doc/jspf#track"])
extension := track1.Extension["https://musicbrainz.org/doc/jspf#track"].(map[string]any)
assert.NotNil(extension)
assert.Equal("outsidecontext", extension["added_by"])
diff --git a/internal/backends/jspf/testdata/comprehensive.jspf b/pkg/jspf/testdata/comprehensive.jspf
similarity index 100%
rename from internal/backends/jspf/testdata/comprehensive.jspf
rename to pkg/jspf/testdata/comprehensive.jspf
diff --git a/internal/backends/jspf/testdata/lb-playlist.jspf b/pkg/jspf/testdata/lb-playlist.jspf
similarity index 100%
rename from internal/backends/jspf/testdata/lb-playlist.jspf
rename to pkg/jspf/testdata/lb-playlist.jspf
diff --git a/internal/backends/jspf/testdata/simple.jspf b/pkg/jspf/testdata/simple.jspf
similarity index 100%
rename from internal/backends/jspf/testdata/simple.jspf
rename to pkg/jspf/testdata/simple.jspf