mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-18 19:19:28 +02:00
Deezer authentication and loves export
This commit is contained in:
parent
f447a259d4
commit
3a364b6ae4
11 changed files with 685 additions and 0 deletions
|
@ -23,6 +23,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"go.uploadedlobster.com/scotty/backends/deezer"
|
||||||
"go.uploadedlobster.com/scotty/backends/dump"
|
"go.uploadedlobster.com/scotty/backends/dump"
|
||||||
"go.uploadedlobster.com/scotty/backends/funkwhale"
|
"go.uploadedlobster.com/scotty/backends/funkwhale"
|
||||||
"go.uploadedlobster.com/scotty/backends/jspf"
|
"go.uploadedlobster.com/scotty/backends/jspf"
|
||||||
|
@ -75,6 +76,7 @@ func GetBackends() []BackendInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
var knownBackends = map[string]func() models.Backend{
|
var knownBackends = map[string]func() models.Backend{
|
||||||
|
"deezer": func() models.Backend { return &deezer.DeezerApiBackend{} },
|
||||||
"dump": func() models.Backend { return &dump.DumpBackend{} },
|
"dump": func() models.Backend { return &dump.DumpBackend{} },
|
||||||
"funkwhale": func() models.Backend { return &funkwhale.FunkwhaleApiBackend{} },
|
"funkwhale": func() models.Backend { return &funkwhale.FunkwhaleApiBackend{} },
|
||||||
"jspf": func() models.Backend { return &jspf.JSPFBackend{} },
|
"jspf": func() models.Backend { return &jspf.JSPFBackend{} },
|
||||||
|
|
83
backends/deezer/auth.go
Normal file
83
backends/deezer/auth.go
Normal 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
80
backends/deezer/client.go
Normal 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
|
||||||
|
}
|
71
backends/deezer/client_test.go
Normal file
71
backends/deezer/client_test.go
Normal 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
161
backends/deezer/deezer.go
Normal 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
|
||||||
|
}
|
50
backends/deezer/deezer_test.go
Normal file
50
backends/deezer/deezer_test.go
Normal 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
66
backends/deezer/models.go
Normal 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"`
|
||||||
|
}
|
45
backends/deezer/models_test.go
Normal file
45
backends/deezer/models_test.go
Normal 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
37
backends/deezer/testdata/track.json
vendored
Normal 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"
|
||||||
|
}
|
80
backends/deezer/testdata/user-tracks.json
vendored
Normal file
80
backends/deezer/testdata/user-tracks.json
vendored
Normal 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"
|
||||||
|
}
|
|
@ -76,6 +76,16 @@ backend = "spotify"
|
||||||
client-id = ""
|
client-id = ""
|
||||||
client-secret = ""
|
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]
|
[service.dump]
|
||||||
# This backend allows writing listens and loves as console output. Useful for
|
# This backend allows writing listens and loves as console output. Useful for
|
||||||
# debugging the export from other services.
|
# debugging the export from other services.
|
||||||
|
|
Loading…
Add table
Reference in a new issue