Spotify: Loves export

This commit is contained in:
Philipp Wolfer 2023-11-22 11:52:28 +01:00
parent ed9debc127
commit 2e6319d296
No known key found for this signature in database
GPG key ID: 8FDF744D4919943B
3 changed files with 212 additions and 38 deletions

View file

@ -18,6 +18,7 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
package spotify
import (
"math"
"net/url"
"sort"
"strconv"
@ -128,51 +129,131 @@ func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results cha
progress <- p.Complete()
}
func (b *SpotifyApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) {
// Choose a high offset, we attempt to search the loves backwards starting
// at the oldest one.
offset := math.MaxInt32
perPage := MaxItemsPerGet
defer close(results)
defer close(progress)
p := models.Progress{Total: int64(perPage)}
out:
for {
result, err := b.client.UserTracks(offset, perPage)
if err != nil {
progress <- p.Complete()
results <- models.LovesResult{Error: err}
return
}
// The offset was higher then the actual number of tracks. Adjust the offset
// and continue.
if offset >= result.Total {
p.Total = int64(result.Total)
offset = result.Total - perPage
if offset < 0 {
offset = 0
}
continue
}
count := len(result.Items)
if count == 0 {
break out
}
loves := make(models.LovesList, 0, perPage)
for _, track := range result.Items {
love := track.ToLove()
if love.Created.Unix() > oldestTimestamp.Unix() {
p.Elapsed += 1
loves = append(loves, love)
} else {
break
}
}
sort.Sort(loves)
results <- models.LovesResult{Loves: loves}
progress <- p
if offset <= 0 {
// This was the last request, no further results
break out
}
offset -= perPage
if offset < 0 {
offset = 0
}
}
progress <- p.Complete()
}
func (l Listen) ToListen() models.Listen {
track := l.Track
listenedAt, _ := time.Parse(time.RFC3339, l.PlayedAt)
listen := models.Listen{
Track: models.Track{
TrackName: track.Name,
ReleaseName: track.Album.Name,
ArtistNames: make([]string, 0, len(track.Artists)),
Duration: time.Duration(track.DurationMs * int(time.Millisecond)),
TrackNumber: track.TrackNumber,
Isrc: track.ExternalIds.ISRC,
AdditionalInfo: map[string]any{},
},
}
listen.ListenedAt, _ = time.Parse(time.RFC3339, l.PlayedAt)
for _, artist := range track.Artists {
listen.Track.ArtistNames = append(listen.Track.ArtistNames, artist.Name)
}
info := listen.AdditionalInfo
if !l.Track.IsLocal {
info["music_service"] = "spotify.com"
}
if track.ExternalUrls.Spotify != "" {
info["origin_url"] = track.ExternalUrls.Spotify
info["spotify_id"] = track.ExternalUrls.Spotify
}
if track.Album.ExternalUrls.Spotify != "" {
info["spotify_album_id"] = track.Album.ExternalUrls.Spotify
}
if len(track.Artists) > 0 {
info["spotify_artist_ids"] = extractArtistIds(track.Artists)
}
if len(track.Album.Artists) > 0 {
info["spotify_album_artist_ids"] = extractArtistIds(track.Album.Artists)
ListenedAt: listenedAt,
Track: l.Track.ToTrack(),
}
return listen
}
func (t SavedTrack) ToLove() models.Love {
addedAt, _ := time.Parse(time.RFC3339, t.AddedAt)
love := models.Love{
Created: addedAt,
Track: t.Track.ToTrack(),
}
return love
}
func (t Track) ToTrack() models.Track {
track := models.Track{
TrackName: t.Name,
ReleaseName: t.Album.Name,
ArtistNames: make([]string, 0, len(t.Artists)),
Duration: time.Duration(t.DurationMs * int(time.Millisecond)),
TrackNumber: t.TrackNumber,
Isrc: t.ExternalIds.ISRC,
AdditionalInfo: map[string]any{},
}
for _, artist := range t.Artists {
track.ArtistNames = append(track.ArtistNames, artist.Name)
}
info := track.AdditionalInfo
if !t.IsLocal {
info["music_service"] = "spotify.com"
}
if t.ExternalUrls.Spotify != "" {
info["origin_url"] = t.ExternalUrls.Spotify
info["spotify_id"] = t.ExternalUrls.Spotify
}
if t.Album.ExternalUrls.Spotify != "" {
info["spotify_album_id"] = t.Album.ExternalUrls.Spotify
}
if len(t.Artists) > 0 {
info["spotify_artist_ids"] = extractArtistIds(t.Artists)
}
if len(t.Album.Artists) > 0 {
info["spotify_album_artist_ids"] = extractArtistIds(t.Album.Artists)
}
return track
}
func extractArtistIds(artists []Artist) []string {
artistIds := make([]string, len(artists))
for i, artist := range artists {

View file

@ -51,3 +51,16 @@ func TestSpotifyListenToListen(t *testing.T) {
assert.Equal(t, []string{"https://open.spotify.com/artist/101HSR6JTJqe3DBh6rb8kz"}, info["spotify_artist_ids"])
assert.Equal(t, []string{"https://open.spotify.com/artist/101HSR6JTJqe3DBh6rb8kz"}, info["spotify_album_artist_ids"])
}
func TestSavedTrackToLove(t *testing.T) {
data, err := os.ReadFile("testdata/track.json")
require.NoError(t, err)
track := spotify.SavedTrack{}
err = json.Unmarshal(data, &track)
require.NoError(t, err)
love := track.ToLove()
created, _ := time.Parse(time.RFC3339, "2022-02-13T21:46:08Z")
assert.Equal(t, created, love.Created)
assert.Equal(t, time.Duration(187680*time.Millisecond), love.Duration)
assert.Equal(t, "Death to the Holy", love.TrackName)
}

80
backends/spotify/testdata/track.json vendored Normal file
View file

@ -0,0 +1,80 @@
{
"added_at": "2022-02-13T21:46:08Z",
"track": {
"album": {
"album_type": "album",
"artists": [
{
"external_urls": {
"spotify": "https://open.spotify.com/artist/6yCjbLFZ9qAnWfsy9ujm5Y"
},
"href": "https://api.spotify.com/v1/artists/6yCjbLFZ9qAnWfsy9ujm5Y",
"id": "6yCjbLFZ9qAnWfsy9ujm5Y",
"name": "Zeal & Ardor",
"type": "artist",
"uri": "spotify:artist:6yCjbLFZ9qAnWfsy9ujm5Y"
}
],
"available_markets": [],
"external_urls": {
"spotify": "https://open.spotify.com/album/34u4cq27YP6837IcmzTTgX"
},
"href": "https://api.spotify.com/v1/albums/34u4cq27YP6837IcmzTTgX",
"id": "34u4cq27YP6837IcmzTTgX",
"images": [
{
"height": 640,
"url": "https://i.scdn.co/image/ab67616d0000b2731fedf861c139b6d8bebdc840",
"width": 640
},
{
"height": 300,
"url": "https://i.scdn.co/image/ab67616d00001e021fedf861c139b6d8bebdc840",
"width": 300
},
{
"height": 64,
"url": "https://i.scdn.co/image/ab67616d000048511fedf861c139b6d8bebdc840",
"width": 64
}
],
"name": "Zeal & Ardor",
"release_date": "2022-02-11",
"release_date_precision": "day",
"total_tracks": 14,
"type": "album",
"uri": "spotify:album:34u4cq27YP6837IcmzTTgX"
},
"artists": [
{
"external_urls": {
"spotify": "https://open.spotify.com/artist/6yCjbLFZ9qAnWfsy9ujm5Y"
},
"href": "https://api.spotify.com/v1/artists/6yCjbLFZ9qAnWfsy9ujm5Y",
"id": "6yCjbLFZ9qAnWfsy9ujm5Y",
"name": "Zeal & Ardor",
"type": "artist",
"uri": "spotify:artist:6yCjbLFZ9qAnWfsy9ujm5Y"
}
],
"available_markets": [],
"disc_number": 1,
"duration_ms": 187680,
"explicit": false,
"external_ids": {
"isrc": "CH8092101707"
},
"external_urls": {
"spotify": "https://open.spotify.com/track/07K2e1PXNra3Zd5SGCsLuZ"
},
"href": "https://api.spotify.com/v1/tracks/07K2e1PXNra3Zd5SGCsLuZ",
"id": "07K2e1PXNra3Zd5SGCsLuZ",
"is_local": false,
"name": "Death to the Holy",
"popularity": 0,
"preview_url": null,
"track_number": 3,
"type": "track",
"uri": "spotify:track:07K2e1PXNra3Zd5SGCsLuZ"
}
}