Deezer authentication and loves export

This commit is contained in:
Philipp Wolfer 2023-11-23 15:30:43 +01:00
parent f447a259d4
commit 3a364b6ae4
No known key found for this signature in database
GPG key ID: 8FDF744D4919943B
11 changed files with 685 additions and 0 deletions

View file

@ -23,6 +23,7 @@ import (
"strings"
"github.com/spf13/viper"
"go.uploadedlobster.com/scotty/backends/deezer"
"go.uploadedlobster.com/scotty/backends/dump"
"go.uploadedlobster.com/scotty/backends/funkwhale"
"go.uploadedlobster.com/scotty/backends/jspf"
@ -75,6 +76,7 @@ func GetBackends() []BackendInfo {
}
var knownBackends = map[string]func() models.Backend{
"deezer": func() models.Backend { return &deezer.DeezerApiBackend{} },
"dump": func() models.Backend { return &dump.DumpBackend{} },
"funkwhale": func() models.Backend { return &funkwhale.FunkwhaleApiBackend{} },
"jspf": func() models.Backend { return &jspf.JSPFBackend{} },

83
backends/deezer/auth.go Normal file
View file

@ -0,0 +1,83 @@
/*
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
Scotty is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.
Scotty is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
Scotty. If not, see <https://www.gnu.org/licenses/>.
*/
package deezer
import (
"encoding/json"
"io"
"net/http"
"time"
"go.uploadedlobster.com/scotty/internal/auth"
"golang.org/x/oauth2"
)
type deezerStrategy struct {
conf oauth2.Config
}
func (s deezerStrategy) Config() oauth2.Config {
return s.conf
}
func (s deezerStrategy) AuthCodeURL(verifier string, state string) string {
return s.conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier))
}
func (s deezerStrategy) ExchangeToken(code auth.CodeResponse, verifier string) (*oauth2.Token, error) {
// Deezer has a non-standard token exchange, expecting all parameters in the URL's query
req, err := http.NewRequest(http.MethodGet, s.conf.Endpoint.TokenURL, nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Add("app_id", s.conf.ClientID)
q.Add("secret", s.conf.ClientSecret)
q.Add("code", code.Code)
q.Add("output", "json")
req.URL.RawQuery = q.Encode()
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
reqBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
token := deezerToken{}
if err = json.Unmarshal(reqBody, &token); err != nil {
return nil, err
}
return token.Token(), nil
}
type deezerToken struct {
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires"`
}
func (t deezerToken) Token() *oauth2.Token {
token := &oauth2.Token{AccessToken: t.AccessToken}
if t.ExpiresIn > 0 {
token.Expiry = time.Now().Add(time.Duration(t.ExpiresIn * time.Second.Nanoseconds()))
}
return token
}

80
backends/deezer/client.go Normal file
View file

@ -0,0 +1,80 @@
/*
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
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 deezer
import (
"errors"
"strconv"
"github.com/go-resty/resty/v2"
"golang.org/x/oauth2"
)
const baseURL = "https://api.deezer.com/"
const MaxItemsPerGet = 50
const DefaultRateLimitWaitSeconds = 5
type Client struct {
HttpClient *resty.Client
token oauth2.TokenSource
}
func NewClient(token oauth2.TokenSource) Client {
client := resty.New()
client.SetBaseURL(baseURL)
client.SetHeader("Accept", "application/json")
client.SetRetryCount(5)
return Client{
HttpClient: client,
token: token,
}
}
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)
if response.StatusCode() != 200 {
err = errors.New(response.String())
} else if result.Error != nil {
err = errors.New(result.Error.Message)
}
return
}
func (c Client) setToken(req *resty.Request) error {
tok, err := c.token.Token()
if err != nil {
return err
}
req.SetQueryParam("access_token", tok.AccessToken)
return nil
}

View file

@ -0,0 +1,71 @@
/*
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
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 deezer_test
import (
"net/http"
"testing"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uploadedlobster.com/scotty/backends/deezer"
"golang.org/x/oauth2"
)
func TestNewClient(t *testing.T) {
token := oauth2.StaticTokenSource(&oauth2.Token{})
client := deezer.NewClient(token)
assert.IsType(t, deezer.Client{}, client)
}
func TestGetUserTracks(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/tracks",
"testdata/user-tracks.json")
result, err := client.UserTracks(0, 2)
require.NoError(t, err)
assert := assert.New(t)
assert.Equal(4, result.Total)
require.Len(t, result.Tracks, 2)
track1 := result.Tracks[0]
assert.Equal(int64(1700743848), track1.AddedAt)
assert.Equal("Never Take Me Alive", track1.Track.Title)
assert.Equal("Outland", track1.Track.Album.Title)
}
func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) {
httpmock.ActivateNonDefault(client)
responder, err := httpmock.NewJsonResponder(200, httpmock.File(testDataPath))
if err != nil {
t.Fatal(err)
}
httpmock.RegisterResponder("GET", url, responder)
}

161
backends/deezer/deezer.go Normal file
View file

@ -0,0 +1,161 @@
/*
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
Scotty is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.
Scotty is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
Scotty. If not, see <https://www.gnu.org/licenses/>.
*/
package deezer
import (
"fmt"
"math"
"net/url"
"sort"
"time"
"github.com/spf13/viper"
"go.uploadedlobster.com/scotty/internal/auth"
"go.uploadedlobster.com/scotty/models"
"golang.org/x/oauth2"
)
type DeezerApiBackend struct {
client Client
clientId string
clientSecret string
}
func (b *DeezerApiBackend) Name() string { return "deezer" }
func (b *DeezerApiBackend) FromConfig(config *viper.Viper) models.Backend {
b.clientId = config.GetString("client-id")
b.clientSecret = config.GetString("client-secret")
return b
}
func (b *DeezerApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy {
conf := oauth2.Config{
ClientID: b.clientId,
ClientSecret: b.clientSecret,
Scopes: []string{
"offline_access,basic_access,listening_history",
},
RedirectURL: redirectUrl.String(),
Endpoint: oauth2.Endpoint{
AuthURL: "https://connect.deezer.com/oauth/auth.php",
TokenURL: "https://connect.deezer.com/oauth/access_token.php",
},
}
return deezerStrategy{conf: conf}
}
func (b *DeezerApiBackend) OAuth2Setup(token oauth2.TokenSource) error {
b.client = NewClient(token)
return nil
}
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.
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.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)
totalCount = result.Total
offset = result.Total - perPage
if offset < 0 {
offset = 0
}
continue
}
count := len(result.Tracks)
if count == 0 {
break out
}
loves := make(models.LovesList, 0, perPage)
for _, track := range result.Tracks {
love := track.AsLove()
if love.Created.Unix() > oldestTimestamp.Unix() {
loves = append(loves, love)
} else {
totalCount -= 1
break
}
}
sort.Sort(loves)
results <- models.LovesResult{Loves: loves, 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 (t LovedTrack) AsLove() models.Love {
love := models.Love{
Created: time.Unix(t.AddedAt, 0),
Track: t.Track.AsTrack(),
}
return love
}
func (t Track) AsTrack() models.Track {
track := models.Track{
TrackName: t.Title,
ReleaseName: t.Album.Title,
ArtistNames: []string{t.Artist.Name},
Duration: time.Duration(t.Duration * int(time.Second)),
AdditionalInfo: map[string]any{},
}
info := track.AdditionalInfo
info["music_service"] = "deezer.com"
info["origin_url"] = t.Link
info["deezer_id"] = t.Link
info["deezer_album_id"] = fmt.Sprintf("https://www.deezer.com/track/%v", t.Album.Id)
info["deezer_artist_id"] = fmt.Sprintf("https://www.deezer.com/track/%v", t.Artist.Id)
return track
}

View file

@ -0,0 +1,50 @@
/*
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
Scotty is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.
Scotty is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
Scotty. If not, see <https://www.gnu.org/licenses/>.
*/
package deezer_test
import (
"encoding/json"
"os"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uploadedlobster.com/scotty/backends/deezer"
)
func TestFromConfig(t *testing.T) {
config := viper.New()
config.Set("client-id", "someclientid")
config.Set("client-secret", "someclientsecret")
backend := (&deezer.DeezerApiBackend{}).FromConfig(config)
assert.IsType(t, &deezer.DeezerApiBackend{}, backend)
}
func TestLovedTrackAsLove(t *testing.T) {
data, err := os.ReadFile("testdata/track.json")
require.NoError(t, err)
track := deezer.LovedTrack{}
err = json.Unmarshal(data, &track)
require.NoError(t, err)
love := track.AsLove()
assert.Equal(t, time.Unix(1700743848, 0), love.Created)
assert.Equal(t, time.Duration(255*time.Second), love.Duration)
assert.Equal(t, "Never Take Me Alive", love.TrackName)
assert.Equal(t, "Outland", love.ReleaseName)
assert.Equal(t, "Spear Of Destiny", love.ArtistName())
}

66
backends/deezer/models.go Normal file
View file

@ -0,0 +1,66 @@
/*
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
Scotty is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.
Scotty is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
Scotty. If not, see <https://www.gnu.org/licenses/>.
*/
package deezer
type Result struct {
Error *Error `json:"error,omitempty"`
}
type Error struct {
// {"error":{"type":"OAuthException","message":"Invalid OAuth access token.","code":300}}
Type string `json:"type"`
Message string `json:"message"`
Code int `json:"code"`
}
type TracksResult struct {
Result
Next string `json:"next"`
Previous string `json:"prev"`
Total int `json:"total"`
Tracks []LovedTrack `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"`
}
type LovedTrack struct {
Track
AddedAt int64 `json:"time_add"`
}
type Album struct {
Id int `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
TrackList string `json:"tracklist"`
}
type Artist struct {
Id int `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
}

View file

@ -0,0 +1,45 @@
/*
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
Scotty is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.
Scotty is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
Scotty. If not, see <https://www.gnu.org/licenses/>.
*/
package deezer_test
import (
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uploadedlobster.com/scotty/backends/deezer"
)
func TestUserTracksResult(t *testing.T) {
data, err := os.ReadFile("testdata/user-tracks.json")
require.NoError(t, err)
result := deezer.TracksResult{}
err = json.Unmarshal(data, &result)
require.NoError(t, err)
assert := assert.New(t)
assert.Equal(4, result.Total)
assert.Equal("https://api.deezer.com/user/me/tracks?limit=2&index=2",
result.Next)
require.Len(t, result.Tracks, 2)
track1 := result.Tracks[0]
assert.Equal(int64(1700743848), track1.AddedAt)
assert.Equal("Never Take Me Alive", track1.Title)
assert.Equal("Outland", track1.Album.Title)
assert.Equal("Spear Of Destiny", track1.Artist.Name)
}

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

@ -0,0 +1,37 @@
{
"id": 3265090,
"readable": true,
"title": "Never Take Me Alive",
"link": "https:\/\/www.deezer.com\/track\/3265090",
"duration": 255,
"rank": 72294,
"explicit_lyrics": false,
"explicit_content_lyrics": 0,
"explicit_content_cover": 0,
"md5_image": "193e4db0eb58117978059acbffe79e93",
"time_add": 1700743848,
"album": {
"id": 311576,
"title": "Outland",
"cover": "https:\/\/api.deezer.com\/album\/311576\/image",
"cover_small": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/193e4db0eb58117978059acbffe79e93\/56x56-000000-80-0-0.jpg",
"cover_medium": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/193e4db0eb58117978059acbffe79e93\/250x250-000000-80-0-0.jpg",
"cover_big": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/193e4db0eb58117978059acbffe79e93\/500x500-000000-80-0-0.jpg",
"cover_xl": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/193e4db0eb58117978059acbffe79e93\/1000x1000-000000-80-0-0.jpg",
"md5_image": "193e4db0eb58117978059acbffe79e93",
"tracklist": "https:\/\/api.deezer.com\/album\/311576\/tracks",
"type": "album"
},
"artist": {
"id": 94057,
"name": "Spear Of Destiny",
"picture": "https:\/\/api.deezer.com\/artist\/94057\/image",
"picture_small": "https:\/\/e-cdns-images.dzcdn.net\/images\/artist\/3fc9b790e88636a03d58f1fe5e74d37c\/56x56-000000-80-0-0.jpg",
"picture_medium": "https:\/\/e-cdns-images.dzcdn.net\/images\/artist\/3fc9b790e88636a03d58f1fe5e74d37c\/250x250-000000-80-0-0.jpg",
"picture_big": "https:\/\/e-cdns-images.dzcdn.net\/images\/artist\/3fc9b790e88636a03d58f1fe5e74d37c\/500x500-000000-80-0-0.jpg",
"picture_xl": "https:\/\/e-cdns-images.dzcdn.net\/images\/artist\/3fc9b790e88636a03d58f1fe5e74d37c\/1000x1000-000000-80-0-0.jpg",
"tracklist": "https:\/\/api.deezer.com\/artist\/94057\/top?limit=50",
"type": "artist"
},
"type": "track"
}

View file

@ -0,0 +1,80 @@
{
"data": [
{
"id": 3265090,
"readable": true,
"title": "Never Take Me Alive",
"link": "https:\/\/www.deezer.com\/track\/3265090",
"duration": 255,
"rank": 72294,
"explicit_lyrics": false,
"explicit_content_lyrics": 0,
"explicit_content_cover": 0,
"md5_image": "193e4db0eb58117978059acbffe79e93",
"time_add": 1700743848,
"album": {
"id": 311576,
"title": "Outland",
"cover": "https:\/\/api.deezer.com\/album\/311576\/image",
"cover_small": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/193e4db0eb58117978059acbffe79e93\/56x56-000000-80-0-0.jpg",
"cover_medium": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/193e4db0eb58117978059acbffe79e93\/250x250-000000-80-0-0.jpg",
"cover_big": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/193e4db0eb58117978059acbffe79e93\/500x500-000000-80-0-0.jpg",
"cover_xl": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/193e4db0eb58117978059acbffe79e93\/1000x1000-000000-80-0-0.jpg",
"md5_image": "193e4db0eb58117978059acbffe79e93",
"tracklist": "https:\/\/api.deezer.com\/album\/311576\/tracks",
"type": "album"
},
"artist": {
"id": 94057,
"name": "Spear Of Destiny",
"picture": "https:\/\/api.deezer.com\/artist\/94057\/image",
"picture_small": "https:\/\/e-cdns-images.dzcdn.net\/images\/artist\/3fc9b790e88636a03d58f1fe5e74d37c\/56x56-000000-80-0-0.jpg",
"picture_medium": "https:\/\/e-cdns-images.dzcdn.net\/images\/artist\/3fc9b790e88636a03d58f1fe5e74d37c\/250x250-000000-80-0-0.jpg",
"picture_big": "https:\/\/e-cdns-images.dzcdn.net\/images\/artist\/3fc9b790e88636a03d58f1fe5e74d37c\/500x500-000000-80-0-0.jpg",
"picture_xl": "https:\/\/e-cdns-images.dzcdn.net\/images\/artist\/3fc9b790e88636a03d58f1fe5e74d37c\/1000x1000-000000-80-0-0.jpg",
"tracklist": "https:\/\/api.deezer.com\/artist\/94057\/top?limit=50",
"type": "artist"
},
"type": "track"
},
{
"id": 2510418,
"readable": true,
"title": "Voodoo Lady",
"link": "https:\/\/www.deezer.com\/track\/2510418",
"duration": 259,
"rank": 196860,
"explicit_lyrics": true,
"explicit_content_lyrics": 1,
"explicit_content_cover": 2,
"md5_image": "ca459f264d682177d1c8f7620100a8bc",
"time_add": 1700747083,
"album": {
"id": 246602,
"title": "The Distance To Here",
"cover": "https:\/\/api.deezer.com\/album\/246602\/image",
"cover_small": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/ca459f264d682177d1c8f7620100a8bc\/56x56-000000-80-0-0.jpg",
"cover_medium": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/ca459f264d682177d1c8f7620100a8bc\/250x250-000000-80-0-0.jpg",
"cover_big": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/ca459f264d682177d1c8f7620100a8bc\/500x500-000000-80-0-0.jpg",
"cover_xl": "https:\/\/e-cdns-images.dzcdn.net\/images\/cover\/ca459f264d682177d1c8f7620100a8bc\/1000x1000-000000-80-0-0.jpg",
"md5_image": "ca459f264d682177d1c8f7620100a8bc",
"tracklist": "https:\/\/api.deezer.com\/album\/246602\/tracks",
"type": "album"
},
"artist": {
"id": 168,
"name": "LIVE",
"picture": "https:\/\/api.deezer.com\/artist\/168\/image",
"picture_small": "https:\/\/e-cdns-images.dzcdn.net\/images\/artist\/5d102eed174da4b807345125b3d955ef\/56x56-000000-80-0-0.jpg",
"picture_medium": "https:\/\/e-cdns-images.dzcdn.net\/images\/artist\/5d102eed174da4b807345125b3d955ef\/250x250-000000-80-0-0.jpg",
"picture_big": "https:\/\/e-cdns-images.dzcdn.net\/images\/artist\/5d102eed174da4b807345125b3d955ef\/500x500-000000-80-0-0.jpg",
"picture_xl": "https:\/\/e-cdns-images.dzcdn.net\/images\/artist\/5d102eed174da4b807345125b3d955ef\/1000x1000-000000-80-0-0.jpg",
"tracklist": "https:\/\/api.deezer.com\/artist\/168\/top?limit=50",
"type": "artist"
},
"type": "track"
}
],
"total": 4,
"next": "https:\/\/api.deezer.com\/user\/me\/tracks?limit=2&index=2"
}

View file

@ -76,6 +76,16 @@ backend = "spotify"
client-id = ""
client-secret = ""
[service.deezer]
# Read listens and loves from a Deezer account
backend = "deezer"
# You need to register an application on https://developers.deezer.com/myapps
# and set the client ID and client secret below.
# When registering use "http://127.0.0.1:2222/callback/deezer" as the
# callback URI.
client-id = ""
client-secret = ""
[service.dump]
# This backend allows writing listens and loves as console output. Useful for
# debugging the export from other services.