diff --git a/backends/base.go b/backends/base.go index 2c4fdad..80ae4c0 100644 --- a/backends/base.go +++ b/backends/base.go @@ -76,5 +76,6 @@ func ResolveBackend[T interface{}](config *viper.Viper) (T, error) { var knownBackends = map[string]func() Backend{ "dump": func() Backend { return &DumpBackend{} }, "listenbrainz-api": func() Backend { return &ListenBrainzApiBackend{} }, + "maloja-api": func() Backend { return &MalojaApiBackend{} }, "scrobbler-log": func() Backend { return &ScrobblerLogBackend{} }, } diff --git a/backends/listenbrainz/models.go b/backends/listenbrainz/models.go index abadb15..e6f5a57 100644 --- a/backends/listenbrainz/models.go +++ b/backends/listenbrainz/models.go @@ -55,11 +55,11 @@ func (t Track) Duration() time.Duration { var duration time.Duration milliseconds, ok := t.AdditionalInfo["duration_ms"].(float64) if ok { - duration = time.Duration(int(milliseconds) * int(time.Millisecond)) + duration = time.Duration(int64(milliseconds) * int64(time.Millisecond)) } else { seconds, ok := t.AdditionalInfo["duration_ms"].(float64) if ok { - duration = time.Duration(int(seconds) * int(time.Second)) + duration = time.Duration(int64(seconds) * int64(time.Second)) } } return duration diff --git a/backends/maloja.go b/backends/maloja.go new file mode 100644 index 0000000..11975eb --- /dev/null +++ b/backends/maloja.go @@ -0,0 +1,97 @@ +/* +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 backends + +import ( + "slices" + "strings" + "time" + + "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/backends/maloja" +) + +type MalojaApiBackend struct { + client maloja.Client +} + +func (b MalojaApiBackend) FromConfig(config *viper.Viper) Backend { + b.client = maloja.New( + config.GetString("server-url"), + config.GetString("token"), + ) + return b +} + +func (b MalojaApiBackend) ExportListens(oldestTimestamp time.Time) ([]Listen, error) { + page := 0 + perPage := 1000 + + listens := make([]Listen, 0) + +out: + for { + result, err := b.client.GetListens(page, perPage) + if err != nil { + return nil, err + } + + count := len(result.Listens) + if count == 0 { + break + } + + for _, listen := range result.Listens { + if listen.ListenedAt > oldestTimestamp.Unix() { + listens = append(listens, Listen{}.FromMaloja(listen)) + } else { + break out + } + } + + page += 1 + } + + slices.Reverse(listens) + return listens, nil +} + +func (l Listen) FromMaloja(mlListen maloja.Listen) Listen { + track := mlListen.Track + listen := Listen{ + ListenedAt: time.Unix(mlListen.ListenedAt, 0), + PlaybackDuration: time.Duration(mlListen.Duration * int64(time.Second)), + Track: Track{ + TrackName: track.Title, + ReleaseName: track.Album.Title, + ArtistNames: track.Artists, + Duration: time.Duration(track.Length * int64(time.Second)), + }, + } + + client, found := strings.CutPrefix(mlListen.Origin, "client:") + if found { + listen.AdditionalInfo["media_player"] = client + } + + return listen +} diff --git a/backends/maloja/client.go b/backends/maloja/client.go new file mode 100644 index 0000000..6c3cf11 --- /dev/null +++ b/backends/maloja/client.go @@ -0,0 +1,58 @@ +/* +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 maloja + +import ( + "strconv" + + "github.com/go-resty/resty/v2" +) + +type Client struct { + resty *resty.Client + token string +} + +func New(serverUrl string, token string) Client { + resty := resty.New() + resty.SetBaseURL(serverUrl) + resty.SetHeader("Accept", "application/json") + client := Client{ + resty: resty, + token: token, + } + + return client +} + +func (c Client) GetListens(page int, perPage int) (GetListensResult, error) { + const path = "/apis/mlj_1/scrobbles" + result := &GetListensResult{} + _, err := c.resty.R(). + SetQueryParams(map[string]string{ + "page": strconv.Itoa(page), + "perpage": strconv.Itoa(perPage), + }). + SetResult(result). + Get(path) + return *result, err +} diff --git a/backends/maloja/models.go b/backends/maloja/models.go new file mode 100644 index 0000000..4a43c6c --- /dev/null +++ b/backends/maloja/models.go @@ -0,0 +1,56 @@ +/* +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 maloja + +type GetListensResult struct { + Status string `json:"status"` + Listens []Listen `json:"list"` + Pagination Pagination `json:"pagination"` +} + +type Listen struct { + ListenedAt int64 `json:"time"` + Duration int64 `json:"duration"` + // Maloja sets Origin to the name of the API key + // prefixed with "client:" + Origin string `json:"origin"` + Track Track `json:"track"` +} + +type Track struct { + Title string `json:"title"` + Artists []string `json:"artists"` + Album Album `json:"album"` + Length int64 `json:"length"` +} + +type Album struct { + Title string `json:"albumtitle"` + Artists []string `json:"artists"` +} + +type Pagination struct { + Page int `json:"page"` + PerPage int `json:"perpage"` + NextPage string `json:"next_page"` + PrevPage string `json:"prev_page"` +} diff --git a/backends/models.go b/backends/models.go index 2acc11e..9362b6d 100644 --- a/backends/models.go +++ b/backends/models.go @@ -52,7 +52,7 @@ func (t Track) ArtistName() string { type Listen struct { Track - ListenedAt time.Time - ListenDuration time.Duration - UserName string + ListenedAt time.Time + PlaybackDuration time.Duration + UserName string }