mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-16 10:09:28 +02:00
Spotify: Loves export
This commit is contained in:
parent
ed9debc127
commit
2e6319d296
3 changed files with 212 additions and 38 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
80
backends/spotify/testdata/track.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue