Deezer export listens

This commit is contained in:
Philipp Wolfer 2023-11-23 17:34:11 +01:00
parent 3a364b6ae4
commit 1a06168039
No known key found for this signature in database
GPG key ID: 8FDF744D4919943B
8 changed files with 318 additions and 30 deletions

View file

@ -31,7 +31,7 @@ import (
)
const baseURL = "https://api.deezer.com/"
const MaxItemsPerGet = 50
const MaxItemsPerGet = 1000
const DefaultRateLimitWaitSeconds = 5
type Client struct {
@ -50,23 +50,14 @@ func NewClient(token oauth2.TokenSource) Client {
}
}
func (c Client) UserTracks(offset int, limit int) (result TracksResult, err error) {
const path = "/user/me/tracks"
request := c.HttpClient.R().
SetQueryParams(map[string]string{
"index": strconv.Itoa(offset),
"limit": strconv.Itoa(limit),
}).
SetResult(&result)
c.setToken(request)
response, err := request.Get(path)
func (c Client) UserHistory(offset int, limit int) (result HistoryResult, err error) {
const path = "/user/me/history"
return listRequest[HistoryResult](c, path, offset, limit)
}
if response.StatusCode() != 200 {
err = errors.New(response.String())
} else if result.Error != nil {
err = errors.New(result.Error.Message)
}
return
func (c Client) UserTracks(offset int, limit int) (TracksResult, error) {
const path = "/user/me/tracks"
return listRequest[TracksResult](c, path, offset, limit)
}
func (c Client) setToken(req *resty.Request) error {
@ -78,3 +69,21 @@ func (c Client) setToken(req *resty.Request) error {
req.SetQueryParam("access_token", tok.AccessToken)
return nil
}
func listRequest[T Result](c Client, path string, offset int, limit int) (result T, err error) {
request := c.HttpClient.R().
SetQueryParams(map[string]string{
"index": strconv.Itoa(offset),
"limit": strconv.Itoa(limit),
}).
SetResult(&result)
c.setToken(request)
response, err := request.Get(path)
if response.StatusCode() != 200 {
err = errors.New(response.String())
} else if result.Error() != nil {
err = errors.New(result.Error().Message)
}
return
}

View file

@ -39,6 +39,27 @@ func TestNewClient(t *testing.T) {
assert.IsType(t, deezer.Client{}, client)
}
func TestGetUserHistory(t *testing.T) {
defer httpmock.DeactivateAndReset()
token := oauth2.StaticTokenSource(&oauth2.Token{})
client := deezer.NewClient(token)
setupHttpMock(t, client.HttpClient.GetClient(),
"https://api.deezer.com/user/me/history",
"testdata/user-history.json")
result, err := client.UserHistory(0, 2)
require.NoError(t, err)
assert := assert.New(t)
assert.Equal(12, result.Total)
require.Len(t, result.Tracks, 2)
track1 := result.Tracks[0]
assert.Equal(int64(1700753817), track1.Timestamp)
assert.Equal("New Divide", track1.Track.Title)
assert.Equal("Linkin Park", track1.Track.Artist.Name)
}
func TestGetUserTracks(t *testing.T) {
defer httpmock.DeactivateAndReset()

View file

@ -64,6 +64,74 @@ func (b *DeezerApiBackend) OAuth2Setup(token oauth2.TokenSource) error {
return nil
}
func (b *DeezerApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, 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)}
var totalCount int
out:
for {
result, err := b.client.UserHistory(offset, perPage)
if err != nil {
progress <- p.Complete()
results <- models.ListensResult{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)
totalCount = result.Total
offset = result.Total - perPage
if offset < 0 {
offset = 0
}
continue
}
count := len(result.Tracks)
if count == 0 {
break out
}
listens := make(models.ListensList, 0, perPage)
for _, track := range result.Tracks {
listen := track.AsListen()
if listen.ListenedAt.Unix() > oldestTimestamp.Unix() {
listens = append(listens, listen)
} else {
totalCount -= 1
break
}
}
sort.Sort(listens)
results <- models.ListensResult{Listens: listens, Total: totalCount}
p.Elapsed += int64(count)
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 (b *DeezerApiBackend) 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.
@ -132,6 +200,15 @@ out:
progress <- p.Complete()
}
func (t Listen) AsListen() models.Listen {
love := models.Listen{
ListenedAt: time.Unix(t.Timestamp, 0),
Track: t.Track.AsTrack(),
}
return love
}
func (t LovedTrack) AsLove() models.Love {
love := models.Love{
Created: time.Unix(t.AddedAt, 0),

View file

@ -35,6 +35,23 @@ func TestFromConfig(t *testing.T) {
assert.IsType(t, &deezer.DeezerApiBackend{}, backend)
}
func TestListenAsListen(t *testing.T) {
data, err := os.ReadFile("testdata/listen.json")
require.NoError(t, err)
track := deezer.Listen{}
err = json.Unmarshal(data, &track)
require.NoError(t, err)
listen := track.AsListen()
assert.Equal(t, time.Unix(1700753817, 0), listen.ListenedAt)
assert.Equal(t, time.Duration(268*time.Second), listen.Duration)
assert.Equal(t, "New Divide", listen.TrackName)
assert.Equal(t, "New Divide (Int'l DMD Maxi)", listen.ReleaseName)
assert.Equal(t, "Linkin Park", listen.ArtistName())
assert.Equal(t, "deezer.com", listen.AdditionalInfo["music_service"])
assert.Equal(t, "https://www.deezer.com/track/14631511", listen.AdditionalInfo["origin_url"])
assert.Equal(t, "https://www.deezer.com/track/14631511", listen.AdditionalInfo["deezer_id"])
}
func TestLovedTrackAsLove(t *testing.T) {
data, err := os.ReadFile("testdata/track.json")
require.NoError(t, err)
@ -47,4 +64,7 @@ func TestLovedTrackAsLove(t *testing.T) {
assert.Equal(t, "Never Take Me Alive", love.TrackName)
assert.Equal(t, "Outland", love.ReleaseName)
assert.Equal(t, "Spear Of Destiny", love.ArtistName())
assert.Equal(t, "deezer.com", love.AdditionalInfo["music_service"])
assert.Equal(t, "https://www.deezer.com/track/3265090", love.AdditionalInfo["origin_url"])
assert.Equal(t, "https://www.deezer.com/track/3265090", love.AdditionalInfo["deezer_id"])
}

View file

@ -15,8 +15,16 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
package deezer
type Result struct {
Error *Error `json:"error,omitempty"`
type Result interface {
Error() *Error
}
type BaseResult struct {
Err *Error `json:"error,omitempty"`
}
func (r BaseResult) Error() *Error {
return r.Err
}
type Error struct {
@ -27,24 +35,38 @@ type Error struct {
}
type TracksResult struct {
Result
BaseResult
Next string `json:"next"`
Previous string `json:"prev"`
Total int `json:"total"`
Tracks []LovedTrack `json:"data"`
}
type HistoryResult struct {
BaseResult
Next string `json:"next"`
Previous string `json:"prev"`
Total int `json:"total"`
Tracks []Listen `json:"data"`
}
type Track struct {
Id int `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
Link string `json:"link"`
Duration int `json:"duration"`
Rank int `json:"rank"`
Readable bool `json:"readable"`
Explicit bool `json:"explicit_lyrics"`
Album Album `json:"album"`
Artist Artist `json:"artist"`
Id int `json:"id"`
Type string `json:"type"`
Link string `json:"link"`
Title string `json:"title"`
TitleVersion string `json:"title_version"`
Duration int `json:"duration"`
Rank int `json:"rank"`
Readable bool `json:"readable"`
Explicit bool `json:"explicit_lyrics"`
Album Album `json:"album"`
Artist Artist `json:"artist"`
}
type Listen struct {
Track
Timestamp int64 `json:"timestamp"`
}
type LovedTrack struct {
@ -55,6 +77,7 @@ type LovedTrack struct {
type Album struct {
Id int `json:"id"`
Type string `json:"type"`
Link string `json:"link"`
Title string `json:"title"`
TrackList string `json:"tracklist"`
}
@ -62,5 +85,6 @@ type Album struct {
type Artist struct {
Id int `json:"id"`
Type string `json:"type"`
Link string `json:"link"`
Name string `json:"name"`
}

View file

@ -43,3 +43,23 @@ func TestUserTracksResult(t *testing.T) {
assert.Equal("Outland", track1.Album.Title)
assert.Equal("Spear Of Destiny", track1.Artist.Name)
}
func TestUserHistoryResult(t *testing.T) {
data, err := os.ReadFile("testdata/user-history.json")
require.NoError(t, err)
result := deezer.HistoryResult{}
err = json.Unmarshal(data, &result)
require.NoError(t, err)
assert := assert.New(t)
assert.Equal(12, result.Total)
assert.Equal("https://api.deezer.com/user/me/history?limit=2&index=2",
result.Next)
require.Len(t, result.Tracks, 2)
track1 := result.Tracks[0]
assert.Equal(int64(1700753817), track1.Timestamp)
assert.Equal("New Divide", track1.Title)
assert.Equal("https://www.deezer.com/album/1346960", track1.Album.Link)
assert.Equal("Linkin Park", track1.Artist.Name)
assert.Equal("https://www.deezer.com/artist/92", track1.Artist.Link)
}

37
backends/deezer/testdata/listen.json vendored Normal file
View file

@ -0,0 +1,37 @@
{
"id": 14631511,
"readable": true,
"title": "New Divide",
"title_short": "New Divide",
"title_version": "",
"link": "https:\/\/www.deezer.com\/track\/14631511",
"duration": 268,
"rank": 530579,
"explicit_lyrics": false,
"explicit_content_lyrics": 6,
"explicit_content_cover": 0,
"preview": "https:\/\/cdns-preview-7.dzcdn.net\/stream\/c-7174e274381557670dda8d4851f4c854-6.mp3",
"md5_image": "cb21d305beb247a8f7c79998a96779d4",
"timestamp": 1700753817,
"artist": {
"id": 92,
"name": "Linkin Park",
"link": "https:\/\/www.deezer.com\/artist\/92",
"tracklist": "https:\/\/api.deezer.com\/artist\/92\/top?limit=50",
"type": "artist"
},
"album": {
"id": 1346960,
"title": "New Divide (Int'l DMD Maxi)",
"link": "https:\/\/www.deezer.com\/album\/1346960",
"cover": "https:\/\/api.deezer.com\/album\/1346960\/image",
"cover_small": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/cb21d305beb247a8f7c79998a96779d4\/56x56-000000-80-0-0.jpg",
"cover_medium": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/cb21d305beb247a8f7c79998a96779d4\/250x250-000000-80-0-0.jpg",
"cover_big": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/cb21d305beb247a8f7c79998a96779d4\/500x500-000000-80-0-0.jpg",
"cover_xl": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/cb21d305beb247a8f7c79998a96779d4\/1000x1000-000000-80-0-0.jpg",
"md5_image": "cb21d305beb247a8f7c79998a96779d4",
"tracklist": "https:\/\/api.deezer.com\/album\/1346960\/tracks",
"type": "album"
},
"type": "track"
}

View file

@ -0,0 +1,80 @@
{
"data": [
{
"id": 14631511,
"readable": true,
"title": "New Divide",
"title_short": "New Divide",
"title_version": "",
"link": "https:\/\/www.deezer.com\/track\/14631511",
"duration": 268,
"rank": 530579,
"explicit_lyrics": false,
"explicit_content_lyrics": 6,
"explicit_content_cover": 0,
"preview": "https:\/\/cdns-preview-7.dzcdn.net\/stream\/c-7174e274381557670dda8d4851f4c854-6.mp3",
"md5_image": "cb21d305beb247a8f7c79998a96779d4",
"timestamp": 1700753817,
"artist": {
"id": 92,
"name": "Linkin Park",
"link": "https:\/\/www.deezer.com\/artist\/92",
"tracklist": "https:\/\/api.deezer.com\/artist\/92\/top?limit=50",
"type": "artist"
},
"album": {
"id": 1346960,
"title": "New Divide (Int'l DMD Maxi)",
"link": "https:\/\/www.deezer.com\/album\/1346960",
"cover": "https:\/\/api.deezer.com\/album\/1346960\/image",
"cover_small": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/cb21d305beb247a8f7c79998a96779d4\/56x56-000000-80-0-0.jpg",
"cover_medium": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/cb21d305beb247a8f7c79998a96779d4\/250x250-000000-80-0-0.jpg",
"cover_big": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/cb21d305beb247a8f7c79998a96779d4\/500x500-000000-80-0-0.jpg",
"cover_xl": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/cb21d305beb247a8f7c79998a96779d4\/1000x1000-000000-80-0-0.jpg",
"md5_image": "cb21d305beb247a8f7c79998a96779d4",
"tracklist": "https:\/\/api.deezer.com\/album\/1346960\/tracks",
"type": "album"
},
"type": "track"
},
{
"id": 3819908,
"readable": true,
"title": "Duality",
"title_short": "Duality",
"title_version": "",
"link": "https:\/\/www.deezer.com\/track\/3819908",
"duration": 252,
"rank": 811964,
"explicit_lyrics": false,
"explicit_content_lyrics": 6,
"explicit_content_cover": 0,
"preview": "https:\/\/cdns-preview-e.dzcdn.net\/stream\/c-eaebaf19e890ee649f519c1b47f551b8-12.mp3",
"md5_image": "35b093d22fe1539003d5d18dd8f309eb",
"timestamp": 1700753544,
"artist": {
"id": 117,
"name": "Slipknot",
"link": "https:\/\/www.deezer.com\/artist\/117",
"tracklist": "https:\/\/api.deezer.com\/artist\/117\/top?limit=50",
"type": "artist"
},
"album": {
"id": 356130,
"title": "Vol. 3: The Subliminal Verses",
"link": "https:\/\/www.deezer.com\/album\/356130",
"cover": "https:\/\/api.deezer.com\/album\/356130\/image",
"cover_small": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/35b093d22fe1539003d5d18dd8f309eb\/56x56-000000-80-0-0.jpg",
"cover_medium": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/35b093d22fe1539003d5d18dd8f309eb\/250x250-000000-80-0-0.jpg",
"cover_big": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/35b093d22fe1539003d5d18dd8f309eb\/500x500-000000-80-0-0.jpg",
"cover_xl": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/35b093d22fe1539003d5d18dd8f309eb\/1000x1000-000000-80-0-0.jpg",
"md5_image": "35b093d22fe1539003d5d18dd8f309eb",
"tracklist": "https:\/\/api.deezer.com\/album\/356130\/tracks",
"type": "album"
},
"type": "track"
}
],
"total": 12,
"next": "https:\/\/api.deezer.com\/user\/me\/history?limit=2&index=2"
}