diff --git a/backends/funkwhale/client.go b/backends/funkwhale/client.go index 2b32453..c3d8f51 100644 --- a/backends/funkwhale/client.go +++ b/backends/funkwhale/client.go @@ -49,20 +49,39 @@ func NewClient(serverUrl string, token string) Client { return client } -func (c Client) GetHistoryListenings(user string, page int, perPage int) (ListeningsResult, error) { +func (c Client) GetHistoryListenings(user string, page int, perPage int) (result ListeningsResult, err error) { const path = "/api/v1/history/listenings" - result := &ListeningsResult{} response, err := c.HttpClient.R(). SetQueryParams(map[string]string{ "username": user, "page": strconv.Itoa(page), "page_size": strconv.Itoa(perPage), + "ordering": "-creation_date", }). - SetResult(result). + SetResult(&result). Get(path) if response.StatusCode() != 200 { - return *result, errors.New(response.String()) + err = errors.New(response.String()) + return } - return *result, err + return +} + +func (c Client) GetFavoriteTracks(page int, perPage int) (result FavoriteTracksResult, err error) { + const path = "/api/v1/favorites/tracks" + response, err := c.HttpClient.R(). + SetQueryParams(map[string]string{ + "page": strconv.Itoa(page), + "page_size": strconv.Itoa(perPage), + "ordering": "-creation_date", + }). + SetResult(&result). + Get(path) + + if response.StatusCode() != 200 { + err = errors.New(response.String()) + return + } + return } diff --git a/backends/funkwhale/client_test.go b/backends/funkwhale/client_test.go index c85317b..ee78d76 100644 --- a/backends/funkwhale/client_test.go +++ b/backends/funkwhale/client_test.go @@ -54,6 +54,7 @@ func TestGetHistoryListenings(t *testing.T) { assert := assert.New(t) assert.Equal(2204, result.Count) + require.Len(t, result.Results, 2) listen1 := result.Results[0] assert.Equal("2023-11-09T23:59:29.022005Z", listen1.CreationDate) assert.Equal("Way to Eden", listen1.Track.Title) @@ -62,6 +63,30 @@ func TestGetHistoryListenings(t *testing.T) { assert.Equal("phw", listen1.User.UserName) } +func TestGetFavoriteTracks(t *testing.T) { + defer httpmock.DeactivateAndReset() + + token := "thetoken" + serverUrl := "https://funkwhale.example.com" + client := funkwhale.NewClient(serverUrl, token) + setupHttpMock(t, client.HttpClient.GetClient(), + "https://funkwhale.example.com/api/v1/favorites/tracks", + "testdata/favorite-tracks.json") + + result, err := client.GetFavoriteTracks(0, 2) + require.NoError(t, err) + + assert := assert.New(t) + assert.Equal(76, result.Count) + require.Len(t, result.Results, 2) + fav1 := result.Results[0] + assert.Equal("2023-11-05T20:32:32.339738Z", fav1.CreationDate) + assert.Equal("Reign", fav1.Track.Title) + assert.Equal("Home Economics", fav1.Track.Album.Title) + assert.Equal("Prinzhorn Dance School", fav1.Track.Artist.Name) + assert.Equal("phw", fav1.User.UserName) +} + func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) { httpmock.ActivateNonDefault(client) diff --git a/backends/funkwhale/funkwhale.go b/backends/funkwhale/funkwhale.go index 7666e7d..ccac342 100644 --- a/backends/funkwhale/funkwhale.go +++ b/backends/funkwhale/funkwhale.go @@ -49,7 +49,7 @@ func (b FunkwhaleApiBackend) ExportListens(oldestTimestamp time.Time) ([]models. page := 1 perPage := MaxItemsPerGet - listens := make([]models.Listen, 0) + listens := make([]models.Listen, 0, 2*MaxItemsPerGet) out: for { @@ -64,7 +64,7 @@ out: } for _, fwListen := range result.Results { - listen := ListenFromFunkwhale(fwListen) + listen := fwListen.ToListen() if listen.ListenedAt.Unix() > oldestTimestamp.Unix() { listens = append(listens, listen) } else { @@ -84,10 +84,49 @@ out: return listens, nil } -func ListenFromFunkwhale(fwListen Listening) models.Listen { - track := fwListen.Track +func (b FunkwhaleApiBackend) ExportLoves(oldestTimestamp time.Time) ([]models.Love, error) { + page := 1 + perPage := MaxItemsPerGet + + loves := make([]models.Love, 0, 2*MaxItemsPerGet) + +out: + for { + result, err := b.client.GetFavoriteTracks(page, perPage) + if err != nil { + return nil, err + } + + count := len(result.Results) + if count == 0 { + break out + } + + for _, favorite := range result.Results { + love := favorite.ToLove() + if love.Created.Unix() > oldestTimestamp.Unix() { + loves = append(loves, love) + } else { + break out + } + } + + if result.Next == "" { + // No further results + break out + } + + page += 1 + } + + slices.Reverse(loves) + return loves, nil +} + +func (l Listening) ToListen() models.Listen { + track := l.Track listen := models.Listen{ - UserName: fwListen.User.UserName, + UserName: l.User.UserName, Track: models.Track{ TrackName: track.Title, ReleaseName: track.Album.Title, @@ -103,7 +142,7 @@ func ListenFromFunkwhale(fwListen Listening) models.Listen { }, } - listenedAt, err := time.Parse(time.RFC3339, fwListen.CreationDate) + listenedAt, err := time.Parse(time.RFC3339, l.CreationDate) if err == nil { listen.ListenedAt = listenedAt } @@ -114,3 +153,36 @@ func ListenFromFunkwhale(fwListen Listening) models.Listen { return listen } + +func (f FavoriteTrack) ToLove() models.Love { + track := f.Track + recordingMbid := models.MBID(track.RecordingMbid) + love := models.Love{ + UserName: f.User.UserName, + RecordingMbid: recordingMbid, + Track: models.Track{ + TrackName: track.Title, + ReleaseName: track.Album.Title, + ArtistNames: []string{track.Artist.Name}, + TrackNumber: track.Position, + RecordingMbid: recordingMbid, + ReleaseMbid: models.MBID(track.Album.ReleaseMbid), + ArtistMbids: []models.MBID{models.MBID(track.Artist.ArtistMbid)}, + Tags: track.Tags, + AdditionalInfo: map[string]any{ + "media_player": FunkwhaleClientName, + }, + }, + } + + created, err := time.Parse(time.RFC3339, f.CreationDate) + if err == nil { + love.Created = created + } + + if len(track.Uploads) > 0 { + love.Track.Duration = time.Duration(track.Uploads[0].Duration * int(time.Second)) + } + + return love +} diff --git a/backends/funkwhale/funkwhale_test.go b/backends/funkwhale/funkwhale_test.go index e8b1e63..e5789c3 100644 --- a/backends/funkwhale/funkwhale_test.go +++ b/backends/funkwhale/funkwhale_test.go @@ -30,7 +30,7 @@ import ( "go.uploadedlobster.com/scotty/models" ) -func TestListenFromFunkwhale(t *testing.T) { +func TestFunkwhaleListeningToListen(t *testing.T) { fwListen := funkwhale.Listening{ CreationDate: "2023-11-09T23:59:29.022005Z", User: funkwhale.User{ @@ -57,7 +57,7 @@ func TestListenFromFunkwhale(t *testing.T) { }, }, } - listen := funkwhale.ListenFromFunkwhale(fwListen) + listen := fwListen.ToListen() assert.Equal(t, time.Unix(1699574369, 0).Unix(), listen.ListenedAt.Unix()) assert.Equal(t, fwListen.User.UserName, listen.UserName) assert.Equal(t, time.Duration(414*time.Second), listen.Duration) @@ -72,3 +72,45 @@ func TestListenFromFunkwhale(t *testing.T) { assert.Equal(t, models.MBID(fwListen.Track.Artist.ArtistMbid), listen.ArtistMbids[0]) assert.Equal(t, funkwhale.FunkwhaleClientName, listen.AdditionalInfo["media_player"]) } + +func TestFunkwhaleFavoriteTrackToLove(t *testing.T) { + favorite := funkwhale.FavoriteTrack{ + CreationDate: "2023-11-09T23:59:29.022005Z", + User: funkwhale.User{ + UserName: "outsidecontext", + }, + Track: funkwhale.Track{ + Title: "Oweynagat", + RecordingMbid: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", + Position: 5, + DiscNumber: 1, + Tags: []string{"foo", "bar"}, + Artist: funkwhale.Artist{ + Name: "Dool", + ArtistMbid: "24412926-c7bd-48e8-afad-8a285b42e131", + }, + Album: funkwhale.Album{ + Title: "Here Now, There Then", + ReleaseMbid: "d7f22677-9803-4d21-ba42-081b633a6f68", + }, + Uploads: []funkwhale.Upload{ + { + Duration: 414, + }, + }, + }, + } + love := favorite.ToLove() + assert.Equal(t, time.Unix(1699574369, 0).Unix(), love.Created.Unix()) + assert.Equal(t, favorite.User.UserName, love.UserName) + assert.Equal(t, time.Duration(414*time.Second), love.Duration) + assert.Equal(t, favorite.Track.Title, love.TrackName) + assert.Equal(t, favorite.Track.Album.Title, love.ReleaseName) + assert.Equal(t, []string{favorite.Track.Artist.Name}, love.ArtistNames) + assert.Equal(t, favorite.Track.Position, love.Track.TrackNumber) + assert.Equal(t, favorite.Track.Tags, love.Track.Tags) + assert.Equal(t, models.MBID(favorite.Track.RecordingMbid), love.RecordingMbid) + assert.Equal(t, models.MBID(favorite.Track.Album.ReleaseMbid), love.ReleaseMbid) + assert.Equal(t, models.MBID(favorite.Track.Artist.ArtistMbid), love.ArtistMbids[0]) + assert.Equal(t, funkwhale.FunkwhaleClientName, love.AdditionalInfo["media_player"]) +} diff --git a/backends/funkwhale/models.go b/backends/funkwhale/models.go index aa0b071..6e0349e 100644 --- a/backends/funkwhale/models.go +++ b/backends/funkwhale/models.go @@ -35,6 +35,20 @@ type Listening struct { CreationDate string `json:"creation_date"` } +type FavoriteTracksResult struct { + Count int `json:"count"` + Previous string `json:"previous"` + Next string `json:"next"` + Results []FavoriteTrack `json:"results"` +} + +type FavoriteTrack struct { + Id int `json:"int"` + User User `json:"user"` + Track Track `json:"track"` + CreationDate string `json:"creation_date"` +} + type Track struct { Id int `json:"int"` Artist Artist `json:"artist"` diff --git a/backends/funkwhale/testdata/favorite-tracks.json b/backends/funkwhale/testdata/favorite-tracks.json new file mode 100644 index 0000000..378c7a0 --- /dev/null +++ b/backends/funkwhale/testdata/favorite-tracks.json @@ -0,0 +1,261 @@ +{ + "count": 76, + "next": "https://music.uploadedlobster.com/api/v1/favorites/tracks/?page=2&page_size=2", + "previous": null, + "results": [ + { + "id": 80, + "user": { + "id": 1, + "username": "phw", + "name": "", + "date_joined": "2020-11-13T16:22:52.464109Z", + "avatar": { + "uuid": "8f87ca98-fe9e-4f7e-aa54-aa8276c25fd4", + "size": 262868, + "mimetype": "image/png", + "creation_date": "2021-08-30T12:02:20.962405Z", + "urls": { + "source": null, + "original": "https://music.uploadedlobster.com/media/attachments/4b/e2/c3/canned-ape.png", + "medium_square_crop": "https://music.uploadedlobster.com/media/__sized__/attachments/4b/e2/c3/canned-ape-crop-c0-5__0-5-200x200.png", + "large_square_crop": "https://music.uploadedlobster.com/media/__sized__/attachments/4b/e2/c3/canned-ape-crop-c0-5__0-5-600x600.png" + } + } + }, + "track": { + "artist": { + "id": 211, + "fid": "https://music.uploadedlobster.com/federation/music/artists/92ab65ce-95d4-405f-a162-959e9a69ec3e", + "mbid": "a1f2450a-c076-4a0d-ac9c-764bfc4225f7", + "name": "Prinzhorn Dance School", + "creation_date": "2020-11-14T08:27:27.964479Z", + "modification_date": "2020-11-14T08:27:27.964602Z", + "is_local": true, + "content_category": "music", + "description": null, + "attachment_cover": null, + "channel": null + }, + "album": { + "id": 237, + "fid": "https://music.uploadedlobster.com/federation/music/albums/d301df18-c0b0-4608-8c68-bba86a65eabc", + "mbid": "7cb1093c-bfa5-4ffc-b3ac-943fb5c7f39f", + "title": "Home Economics", + "artist": { + "id": 211, + "fid": "https://music.uploadedlobster.com/federation/music/artists/92ab65ce-95d4-405f-a162-959e9a69ec3e", + "mbid": "a1f2450a-c076-4a0d-ac9c-764bfc4225f7", + "name": "Prinzhorn Dance School", + "creation_date": "2020-11-14T08:27:27.964479Z", + "modification_date": "2020-11-14T08:27:27.964602Z", + "is_local": true, + "content_category": "music", + "description": null, + "attachment_cover": null, + "channel": null + }, + "release_date": "2015-06-08", + "cover": { + "uuid": "e2022441-b120-431d-912c-aa930c668cd5", + "size": 279103, + "mimetype": "image/jpeg", + "creation_date": "2023-03-30T07:51:33.366860Z", + "urls": { + "source": null, + "original": "https://music.uploadedlobster.com/media/attachments/98/0a/af/attachment_cover-d301df18-c0b0-4608-8c68-bba86a65eabc.jpg", + "medium_square_crop": "https://music.uploadedlobster.com/media/__sized__/attachments/98/0a/af/attachment_cover-d301df18-c0b0-4608-8c68-bba86a65eabc-crop-c0-5__0-5-200x200-95.jpg", + "large_square_crop": "https://music.uploadedlobster.com/media/__sized__/attachments/98/0a/af/attachment_cover-d301df18-c0b0-4608-8c68-bba86a65eabc-crop-c0-5__0-5-600x600-95.jpg" + } + }, + "creation_date": "2020-11-14T08:27:28.805848Z", + "is_local": true, + "tracks_count": 6 + }, + "uploads": [ + { + "uuid": "6647a0d0-5119-4dd0-bf1f-dfc64a1d956c", + "listen_url": "/api/v1/listen/b4cb522f-ce1b-49fb-81b8-0cc0d1cb5495/?upload=6647a0d0-5119-4dd0-bf1f-dfc64a1d956c", + "size": 11147671, + "duration": 271, + "bitrate": 320000, + "mimetype": "audio/mpeg", + "extension": "mp3", + "is_local": true + } + ], + "listen_url": "/api/v1/listen/b4cb522f-ce1b-49fb-81b8-0cc0d1cb5495/", + "tags": [], + "attributed_to": { + "fid": "https://music.uploadedlobster.com/federation/actors/phw", + "url": null, + "creation_date": "2020-11-13T16:54:45.182645Z", + "summary": null, + "preferred_username": "phw", + "name": "phw", + "last_fetch_date": "2020-11-13T16:54:45.182661Z", + "domain": "music.uploadedlobster.com", + "type": "Person", + "manually_approves_followers": false, + "full_username": "phw@music.uploadedlobster.com", + "is_local": true + }, + "id": 2833, + "fid": "https://music.uploadedlobster.com/federation/music/tracks/b4cb522f-ce1b-49fb-81b8-0cc0d1cb5495", + "mbid": "b59cf4e7-caee-4019-a844-79d2c58d4dff", + "title": "Reign", + "creation_date": "2020-11-14T08:27:28.954529Z", + "is_local": true, + "position": 1, + "disc_number": 1, + "downloads_count": 3, + "copyright": null, + "license": null, + "cover": null, + "is_playable": true + }, + "creation_date": "2023-11-05T20:32:32.339738Z", + "actor": { + "fid": "https://music.uploadedlobster.com/federation/actors/phw", + "url": null, + "creation_date": "2020-11-13T16:54:45.182645Z", + "summary": null, + "preferred_username": "phw", + "name": "phw", + "last_fetch_date": "2020-11-13T16:54:45.182661Z", + "domain": "music.uploadedlobster.com", + "type": "Person", + "manually_approves_followers": false, + "full_username": "phw@music.uploadedlobster.com", + "is_local": true + } + }, + { + "id": 79, + "user": { + "id": 1, + "username": "phw", + "name": "", + "date_joined": "2020-11-13T16:22:52.464109Z", + "avatar": { + "uuid": "8f87ca98-fe9e-4f7e-aa54-aa8276c25fd4", + "size": 262868, + "mimetype": "image/png", + "creation_date": "2021-08-30T12:02:20.962405Z", + "urls": { + "source": null, + "original": "https://music.uploadedlobster.com/media/attachments/4b/e2/c3/canned-ape.png", + "medium_square_crop": "https://music.uploadedlobster.com/media/__sized__/attachments/4b/e2/c3/canned-ape-crop-c0-5__0-5-200x200.png", + "large_square_crop": "https://music.uploadedlobster.com/media/__sized__/attachments/4b/e2/c3/canned-ape-crop-c0-5__0-5-600x600.png" + } + } + }, + "track": { + "artist": { + "id": 3800, + "fid": "https://music.uploadedlobster.com/federation/music/artists/2ef92f34-f4bc-4bf5-85de-a6847ea51a62", + "mbid": "055b6082-b9cc-4688-85c4-8153c0ef2d70", + "name": "Crippled Black Phoenix", + "creation_date": "2023-07-19T06:39:22.078159Z", + "modification_date": "2023-07-19T06:39:22.078238Z", + "is_local": true, + "content_category": "music", + "description": null, + "attachment_cover": null, + "channel": null + }, + "album": { + "id": 2684, + "fid": "https://music.uploadedlobster.com/federation/music/albums/138bd960-8e94-4794-adaa-51de5fba33c3", + "mbid": "97509ec0-93cc-47ca-9033-1ac27678d799", + "title": "Ellengæst", + "artist": { + "id": 3800, + "fid": "https://music.uploadedlobster.com/federation/music/artists/2ef92f34-f4bc-4bf5-85de-a6847ea51a62", + "mbid": "055b6082-b9cc-4688-85c4-8153c0ef2d70", + "name": "Crippled Black Phoenix", + "creation_date": "2023-07-19T06:39:22.078159Z", + "modification_date": "2023-07-19T06:39:22.078238Z", + "is_local": true, + "content_category": "music", + "description": null, + "attachment_cover": null, + "channel": null + }, + "release_date": "2020-11-04", + "cover": { + "uuid": "2c191778-0c1c-467e-9bf1-9949e3d98507", + "size": 129633, + "mimetype": "image/jpeg", + "creation_date": "2023-07-19T06:39:22.102600Z", + "urls": { + "source": null, + "original": "https://music.uploadedlobster.com/media/attachments/e5/d6/bf/attachment_cover-138bd960-8e94-4794-adaa-51de5fba33c3.jpg", + "medium_square_crop": "https://music.uploadedlobster.com/media/__sized__/attachments/e5/d6/bf/attachment_cover-138bd960-8e94-4794-adaa-51de5fba33c3-crop-c0-5__0-5-200x200-95.jpg", + "large_square_crop": "https://music.uploadedlobster.com/media/__sized__/attachments/e5/d6/bf/attachment_cover-138bd960-8e94-4794-adaa-51de5fba33c3-crop-c0-5__0-5-600x600-95.jpg" + } + }, + "creation_date": "2023-07-19T06:39:22.094428Z", + "is_local": true, + "tracks_count": 8 + }, + "uploads": [ + { + "uuid": "f28de29b-6928-4879-a9da-32dcf9dd5ee4", + "listen_url": "/api/v1/listen/9263d5c0-dee3-4273-beef-7585e1d0041a/?upload=f28de29b-6928-4879-a9da-32dcf9dd5ee4", + "size": 12065231, + "duration": 491, + "bitrate": 0, + "mimetype": "audio/opus", + "extension": "opus", + "is_local": true + } + ], + "listen_url": "/api/v1/listen/9263d5c0-dee3-4273-beef-7585e1d0041a/", + "tags": [], + "attributed_to": { + "fid": "https://music.uploadedlobster.com/federation/actors/phw", + "url": null, + "creation_date": "2020-11-13T16:54:45.182645Z", + "summary": null, + "preferred_username": "phw", + "name": "phw", + "last_fetch_date": "2020-11-13T16:54:45.182661Z", + "domain": "music.uploadedlobster.com", + "type": "Person", + "manually_approves_followers": false, + "full_username": "phw@music.uploadedlobster.com", + "is_local": true + }, + "id": 28095, + "fid": "https://music.uploadedlobster.com/federation/music/tracks/9263d5c0-dee3-4273-beef-7585e1d0041a", + "mbid": "14d612f0-4022-4adc-8cef-87a569e2d65c", + "title": "Lost", + "creation_date": "2023-07-19T06:39:22.459072Z", + "is_local": true, + "position": 2, + "disc_number": 1, + "downloads_count": 2, + "copyright": null, + "license": null, + "cover": null, + "is_playable": true + }, + "creation_date": "2023-10-25T16:14:36.112517Z", + "actor": { + "fid": "https://music.uploadedlobster.com/federation/actors/phw", + "url": null, + "creation_date": "2020-11-13T16:54:45.182645Z", + "summary": null, + "preferred_username": "phw", + "name": "phw", + "last_fetch_date": "2020-11-13T16:54:45.182661Z", + "domain": "music.uploadedlobster.com", + "type": "Person", + "manually_approves_followers": false, + "full_username": "phw@music.uploadedlobster.com", + "is_local": true + } + } + ] +} \ No newline at end of file