From 1f2d5f662dfb2bce27db385b7a86b5eb258ab8d0 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 14 Nov 2023 00:39:17 +0100 Subject: [PATCH] JSPF loves export --- backends/backends.go | 2 + backends/jspf/jspf.go | 101 +++++++++++++++++++++++++++++++++++++ backends/jspf/jspf_test.go | 18 +++++++ backends/jspf/models.go | 10 ++++ 4 files changed, 131 insertions(+) diff --git a/backends/backends.go b/backends/backends.go index 9086404..9584a59 100644 --- a/backends/backends.go +++ b/backends/backends.go @@ -30,6 +30,7 @@ import ( "github.com/spf13/viper" "go.uploadedlobster.com/scotty/backends/dump" "go.uploadedlobster.com/scotty/backends/funkwhale" + "go.uploadedlobster.com/scotty/backends/jspf" "go.uploadedlobster.com/scotty/backends/listenbrainz" "go.uploadedlobster.com/scotty/backends/maloja" "go.uploadedlobster.com/scotty/backends/scrobblerlog" @@ -79,6 +80,7 @@ func GetBackends() []BackendInfo { var knownBackends = map[string]func() models.Backend{ "dump": func() models.Backend { return &dump.DumpBackend{} }, "funkwhale-api": func() models.Backend { return &funkwhale.FunkwhaleApiBackend{} }, + "jspf": func() models.Backend { return &jspf.JspfBackend{} }, "listenbrainz-api": func() models.Backend { return &listenbrainz.ListenBrainzApiBackend{} }, "maloja-api": func() models.Backend { return &maloja.MalojaApiBackend{} }, "scrobbler-log": func() models.Backend { return &scrobblerlog.ScrobblerLogBackend{} }, diff --git a/backends/jspf/jspf.go b/backends/jspf/jspf.go index f23ac28..9af9087 100644 --- a/backends/jspf/jspf.go +++ b/backends/jspf/jspf.go @@ -21,3 +21,104 @@ THE SOFTWARE. */ package jspf + +import ( + "encoding/json" + "os" + "time" + + "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/models" +) + +type JspfBackend struct { + filePath string + title string + creator string + identifier string +} + +func (b JspfBackend) FromConfig(config *viper.Viper) models.Backend { + b.filePath = config.GetString("file-path") + b.title = config.GetString("title") + b.creator = config.GetString("username") + b.identifier = config.GetString("identifier") + return b +} + +func (b JspfBackend) ImportLoves(loves []models.Love, oldestTimestamp time.Time) (models.ImportResult, error) { + result := models.ImportResult{ + TotalCount: len(loves), + ImportCount: 0, + LastTimestamp: oldestTimestamp, + } + + tracks := make([]Track, 0, result.TotalCount) + for _, love := range loves { + if love.Created.Unix() > result.LastTimestamp.Unix() { + result.LastTimestamp = love.Created + } + + extension := MusicBrainzTrackExtension{ + AddedAt: love.Created, + AddedBy: love.UserName, + AdditionalMetadata: love.AdditionalInfo, + ArtistIdentifiers: make([]string, len(love.ArtistMbids)), + } + + for i, mbid := range love.ArtistMbids { + extension.ArtistIdentifiers[i] = "https://musicbrainz.org/artist/" + string(mbid) + } + + if love.ReleaseMbid != "" { + extension.ReleaseIdentifier = "https://musicbrainz.org/release/" + string(love.ReleaseMbid) + } + + track := Track{ + Title: love.TrackName, + Album: love.ReleaseName, + Creator: love.ArtistName(), + TrackNum: love.TrackNumber, + Extension: map[string]any{ + "https://musicbrainz.org/doc/jspf#track": extension, + }, + } + + if love.RecordingMbid != "" { + track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(love.RecordingMbid)) + } + + tracks = append(tracks, track) + + result.ImportCount += 1 + } + + err := b.writeJspf(tracks) + return result, err +} + +func (b JspfBackend) writeJspf(tracks []Track) error { + playlist := Jspf{ + Playlist: Playlist{ + Title: b.title, + Creator: b.creator, + Identifier: b.identifier, + Date: time.Now(), + Tracks: tracks, + }, + } + + file, err := os.Create(b.filePath) + if err != nil { + return err + } + + defer file.Close() + jspfJson, err := json.MarshalIndent(playlist, "", "\t") + if err != nil { + return err + } + + _, err = file.Write(jspfJson) + return err +} diff --git a/backends/jspf/jspf_test.go b/backends/jspf/jspf_test.go index 5522c24..2f1f31f 100644 --- a/backends/jspf/jspf_test.go +++ b/backends/jspf/jspf_test.go @@ -21,3 +21,21 @@ THE SOFTWARE. */ package jspf_test + +import ( + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "go.uploadedlobster.com/scotty/backends/scrobblerlog" +) + +func TestFromConfig(t *testing.T) { + config := viper.New() + config.Set("file-path", "/foo/bar.jspf") + config.Set("title", "My Playlist") + config.Set("username", "outsidecontext") + config.Set("identifier", "http://example.com/playlist1") + backend := scrobblerlog.ScrobblerLogBackend{}.FromConfig(config) + assert.IsType(t, scrobblerlog.ScrobblerLogBackend{}, backend) +} diff --git a/backends/jspf/models.go b/backends/jspf/models.go index bb2b8b0..622a827 100644 --- a/backends/jspf/models.go +++ b/backends/jspf/models.go @@ -65,3 +65,13 @@ 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. +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"` +}