From 112b90968a5996bee0167486dcf3bcf26d4700e7 Mon Sep 17 00:00:00 2001 From: KaNaDaAT Date: Thu, 15 May 2025 20:04:18 +0200 Subject: [PATCH 1/8] update: Versions Xoutube.Client() did no longer work --- server/go.mod | 26 ++++++++++++++------------ server/go.sum | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/server/go.mod b/server/go.mod index 7478c63..34d6e2c 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,14 +1,17 @@ module song-recognition -go 1.21.6 +go 1.23.0 + +toolchain go1.24.3 require ( github.com/buger/jsonparser v1.1.1 github.com/fatih/color v1.16.0 github.com/googollee/go-socket.io v1.7.0 - github.com/kkdai/youtube/v2 v2.10.1 + github.com/kkdai/youtube/v2 v2.10.4 + github.com/mattn/go-sqlite3 v1.14.22 github.com/mdobak/go-xerrors v0.3.1 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/tidwall/gjson v1.17.1 go.mongodb.org/mongo-driver v1.14.0 gonum.org/v1/gonum v0.14.0 @@ -20,8 +23,8 @@ require ( cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/bitly/go-simplejson v0.5.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect - github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/dop251/goja v0.0.0-20250125213203-5ef83b82af17 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -31,7 +34,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gomodule/redigo v1.8.4 // indirect - github.com/google/pprof v0.0.0-20240320155624-b11c3daa6f07 // indirect + github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect @@ -40,7 +43,6 @@ require ( github.com/klauspost/compress v1.17.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -55,12 +57,12 @@ require ( go.opentelemetry.io/otel v1.23.0 // indirect go.opentelemetry.io/otel/metric v1.23.0 // indirect go.opentelemetry.io/otel/trace v1.23.0 // indirect - golang.org/x/crypto v0.23.0 // indirect - golang.org/x/net v0.22.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect golang.org/x/oauth2 v0.17.0 // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 // indirect google.golang.org/grpc v1.61.1 // indirect diff --git a/server/go.sum b/server/go.sum index 0dd0434..e51b386 100644 --- a/server/go.sum +++ b/server/go.sum @@ -23,9 +23,13 @@ github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwu github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 h1:O7I1iuzEA7SG+dK8ocOBSlYAA9jBUmCYl/Qa7ey7JAM= github.com/dop251/goja v0.0.0-20240220182346-e401ed450204/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja v0.0.0-20250125213203-5ef83b82af17 h1:spJaibPy2sZNwo6Q0HjBVufq7hBUj5jNFOKRoogCBow= +github.com/dop251/goja v0.0.0-20250125213203-5ef83b82af17/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -80,6 +84,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/pprof v0.0.0-20240320155624-b11c3daa6f07 h1:57oOH2Mu5Nw16KnZAVLdlUjmPH/TSYCKTJgG0OVfX0Y= github.com/google/pprof v0.0.0-20240320155624-b11c3daa6f07/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc= +github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -96,6 +102,8 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/kkdai/youtube/v2 v2.10.1 h1:jdPho4R7VxWoRi9Wx4ULMq4+hlzSVOXxh4Zh83f2F9M= github.com/kkdai/youtube/v2 v2.10.1/go.mod h1:qL8JZv7Q1IoDs4nnaL51o/hmITXEIvyCIXopB0oqgVM= +github.com/kkdai/youtube/v2 v2.10.4 h1:T3VAQ65EB4eHptwcQIigpFvUJlV9EcKRGJJdSVUy3aU= +github.com/kkdai/youtube/v2 v2.10.4/go.mod h1:pm4RuJ2tRIIaOvz4YMIpCY8Ls4Fm7IVtnZQyule61MU= github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -135,6 +143,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -168,6 +177,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= @@ -186,6 +197,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= @@ -195,6 +208,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -208,6 +223,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -217,6 +234,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= From 98e4d46a316b203ac6e463d0733a3a17d97e5e72 Mon Sep 17 00:00:00 2001 From: KaNaDaAT Date: Thu, 15 May 2025 20:05:35 +0200 Subject: [PATCH 2/8] style: Some tidy up / logging --- server/spotify/downloader.go | 4 ++-- server/spotify/youtube.go | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/spotify/downloader.go b/server/spotify/downloader.go index 1e32841..3838561 100644 --- a/server/spotify/downloader.go +++ b/server/spotify/downloader.go @@ -28,13 +28,12 @@ const DELETE_SONG_FILE = false var yellow = color.New(color.FgYellow) func DlSingleTrack(url, savePath string) (int, error) { + fmt.Println("Getting track info (" + url + ")...") trackInfo, err := TrackInfo(url) if err != nil { return 0, err } - fmt.Println("Getting track info...") - time.Sleep(500 * time.Millisecond) track := []Track{*trackInfo} fmt.Println("Now, downloading track...") @@ -198,6 +197,7 @@ func downloadYTaudio(id, path, filePath string) error { client := youtube.Client{} video, err := client.GetVideo(id) if err != nil { + fmt.Println("Error: ", err) return err } diff --git a/server/spotify/youtube.go b/server/spotify/youtube.go index b1d4e7a..51d5d04 100644 --- a/server/spotify/youtube.go +++ b/server/spotify/youtube.go @@ -96,6 +96,7 @@ func GetYoutubeId(track Track) (string, error) { allowedDurationRangeEnd := songDurationInSeconds + durationMatchThreshold resultSongDuration := convertStringDurationToSeconds(result.Duration) if resultSongDuration >= allowedDurationRangeStart && resultSongDuration <= allowedDurationRangeEnd { + fmt.Println("INFO: ", fmt.Sprintf("Found song with id '%s'", result.ID)) return result.ID, nil } } From fbaf3298723a495edbd873802b68fa1e6244b479 Mon Sep 17 00:00:00 2001 From: KaNaDaAT Date: Thu, 15 May 2025 20:23:49 +0200 Subject: [PATCH 3/8] refactor: switch to official Spotify developer API with client credentials flow - Replaced deprecated web token endpoint with POST to accounts.spotify.com/api/token - Integrated client credentials OAuth flow using client ID and secret - Adjusted request headers to use Bearer token --- server/credentials.json | 4 + server/spotify/spotify.go | 309 +++++++++++++++++++++++++++++--------- 2 files changed, 245 insertions(+), 68 deletions(-) create mode 100644 server/credentials.json diff --git a/server/credentials.json b/server/credentials.json new file mode 100644 index 0000000..4c7b62c --- /dev/null +++ b/server/credentials.json @@ -0,0 +1,4 @@ +{ + "client_id": "", + "client_secret": "" +} diff --git a/server/spotify/spotify.go b/server/spotify/spotify.go index 4729b27..f9f021a 100644 --- a/server/spotify/spotify.go +++ b/server/spotify/spotify.go @@ -1,14 +1,18 @@ package spotify import ( + "bytes" + "encoding/json" "errors" "fmt" "io" "math" "net/http" + "net/url" "regexp" "strings" "time" + "os" "github.com/tidwall/gjson" ) @@ -24,30 +28,120 @@ type Track struct { Duration int } + const ( - tokenEndpoint = "https://open.spotify.com/get_access_token?reason=transport&productType=web-player" - trackInitialPath = "https://api-partner.spotify.com/pathfinder/v1/query?operationName=getTrack&variables=" - playlistInitialPath = "https://api-partner.spotify.com/pathfinder/v1/query?operationName=fetchPlaylist&variables=" - albumInitialPath = "https://api-partner.spotify.com/pathfinder/v1/query?operationName=getAlbum&variables=" - trackEndPath = `{"persistedQuery":{"version":1,"sha256Hash":"e101aead6d78faa11d75bec5e36385a07b2f1c4a0420932d374d89ee17c70dd6"}}` - playlistEndPath = `{"persistedQuery":{"version":1,"sha256Hash":"b39f62e9b566aa849b1780927de1450f47e02c54abf1e66e513f96e849591e41"}}` - albumEndPath = `{"persistedQuery":{"version":1,"sha256Hash":"46ae954ef2d2fe7732b4b2b4022157b2e18b7ea84f70591ceb164e4de1b5d5d3"}}` + tokenURL = "https://accounts.spotify.com/api/token" + credentialsPath = "credentials.json" + cachedTokenPath = "token.json" ) +type credentials struct { + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` +} + +type tokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` +} + +type cachedToken struct { + Token string `json:"token"` + ExpiresAt time.Time `json:"expires_at"` +} + +func loadCredentials() (*credentials, error) { + file, err := os.Open(credentialsPath) + if err != nil { + if os.IsNotExist(err) { + absPath, _ := os.Getwd() + return nil, fmt.Errorf("credentials.json not found. Please create it in the same directory:\n%s/%s", absPath, credentialsPath) + } + return nil, err + } + defer file.Close() + + var creds credentials + if err := json.NewDecoder(file).Decode(&creds); err != nil { + return nil, fmt.Errorf("failed to parse credentials.json: %w", err) + } + return &creds, nil +} + + +func saveToken(token string, expiresIn int) error { + ct := cachedToken{ + Token: token, + ExpiresAt: time.Now().Add(time.Duration(expiresIn) * time.Second), + } + data, err := json.MarshalIndent(ct, "", " ") + if err != nil { + return err + } + return os.WriteFile(cachedTokenPath, data, 0644) +} + +func loadCachedToken() (string, error) { + data, err := os.ReadFile(cachedTokenPath) + if err != nil { + return "", err + } + var ct cachedToken + if err := json.Unmarshal(data, &ct); err != nil { + return "", err + } + if time.Now().After(ct.ExpiresAt) { + return "", errors.New("token expired") + } + return ct.Token, nil +} + func accessToken() (string, error) { - resp, err := http.Get(tokenEndpoint) + // Try using cached token + token, err := loadCachedToken() + if err == nil { + return token, nil + } + + // Fallback: request a new token + creds, err := loadCredentials() + if err != nil { + return "", err + } + + data := url.Values{} + data.Set("grant_type", "client_credentials") + data.Set("client_id", creds.ClientID) + data.Set("client_secret", creds.ClientSecret) + + req, err := http.NewRequest("POST", tokenURL, bytes.NewBufferString(data.Encode())) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return "", errors.New("token request failed (have a look at credentials.json): " + string(body)) + } + + var tr tokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil { return "", err } - accessToken := gjson.Get(string(body), "accessToken") - return accessToken.String(), nil + if err := saveToken(tr.AccessToken, tr.ExpiresIn); err != nil { + return "", err + } + + return tr.AccessToken, nil } /* requests to playlist/track endpoints */ @@ -89,86 +183,165 @@ func isValidPattern(url, pattern string) bool { } func TrackInfo(url string) (*Track, error) { - trackPattern := `^https:\/\/open\.spotify\.com\/track\/[a-zA-Z0-9]{22}\?si=[a-zA-Z0-9]{16}$` - if !isValidPattern(url, trackPattern) { - return nil, errors.New("invalid track url") + re := regexp.MustCompile(`open\.spotify\.com\/track\/([a-zA-Z0-9]{22})`) + matches := re.FindStringSubmatch(url) + if len(matches) != 2 { + return nil, errors.New("invalid track URL") } + id := matches[1] - id := getID(url) - endpointQuery := EncodeParam(fmt.Sprintf(`{"uri":"spotify:track:%s"}`, id)) - endpoint := trackInitialPath + endpointQuery + "&extensions=" + EncodeParam(trackEndPath) - + endpoint := fmt.Sprintf("https://api.spotify.com/v1/tracks/%s", id) statusCode, jsonResponse, err := request(endpoint) if err != nil { - return nil, fmt.Errorf("error on getting track info: %w", err) + return nil, fmt.Errorf("error getting track info: %w", err) + } + if statusCode != 200 { + return nil, fmt.Errorf("non-200 status code: %d", statusCode) } - if statusCode != 200 { - return nil, fmt.Errorf("received non-200 status code: %d", statusCode) + var result struct { + Name string `json:"name"` + Duration int `json:"duration_ms"` + Album struct { + Name string `json:"name"` + } `json:"album"` + Artists []struct { + Name string `json:"name"` + } `json:"artists"` + } + if err := json.Unmarshal([]byte(jsonResponse), &result); err != nil { + return nil, err } var allArtists []string - - if firstArtist := gjson.Get(jsonResponse, "data.trackUnion.firstArtist.items.0.profile.name").String(); firstArtist != "" { - allArtists = append(allArtists, firstArtist) + for _, a := range result.Artists { + allArtists = append(allArtists, a.Name) } - if artists := gjson.Get(jsonResponse, "data.trackUnion.otherArtists.items").Array(); len(artists) > 0 { - for _, artist := range artists { - if profile := artist.Get("profile").Map(); len(profile) > 0 { - if name := profile["name"].String(); name != "" { - allArtists = append(allArtists, name) - } + return (&Track{ + Title: result.Name, + Artist: allArtists[0], + Artists: allArtists, + Album: result.Album.Name, + Duration: result.Duration / 1000, + }).buildTrack(), nil +} + + +func PlaylistInfo(url string) ([]Track, error) { + re := regexp.MustCompile(`open\.spotify\.com\/playlist\/([a-zA-Z0-9]{22})`) + matches := re.FindStringSubmatch(url) + if len(matches) != 2 { + return nil, errors.New("invalid playlist URL") + } + id := matches[1] + + var allTracks []Track + offset := 0 + limit := 100 + + for { + endpoint := fmt.Sprintf("https://api.spotify.com/v1/playlists/%s/tracks?offset=%d&limit=%d", id, offset, limit) + statusCode, jsonResponse, err := request(endpoint) + if err != nil { + return nil, fmt.Errorf("request error: %w", err) + } + if statusCode != 200 { + return nil, fmt.Errorf("non-200 status: %d", statusCode) + } + + var result struct { + Items []struct { + Track struct { + Name string `json:"name"` + Duration int `json:"duration_ms"` + Album struct { + Name string `json:"name"` + } `json:"album"` + Artists []struct { + Name string `json:"name"` + } `json:"artists"` + } `json:"track"` + } `json:"items"` + Total int `json:"total"` + } + if err := json.Unmarshal([]byte(jsonResponse), &result); err != nil { + return nil, err + } + + for _, item := range result.Items { + track := item.Track + var artists []string + for _, a := range track.Artists { + artists = append(artists, a.Name) } + allTracks = append(allTracks, *(&Track{ + Title: track.Name, + Artist: artists[0], + Artists: artists, + Duration: track.Duration / 1000, + Album: track.Album.Name, + }).buildTrack()) + } + + offset += limit + if offset >= result.Total { + break } } - durationInSeconds := int(gjson.Get(jsonResponse, "data.trackUnion.duration.totalMilliseconds").Int()) - durationInSeconds = durationInSeconds / 1000 - - track := &Track{ - Title: gjson.Get(jsonResponse, "data.trackUnion.name").String(), - Artist: gjson.Get(jsonResponse, "data.trackUnion.firstArtist.items.0.profile.name").String(), - Artists: allArtists, - Duration: durationInSeconds, - Album: gjson.Get(jsonResponse, "data.trackUnion.albumOfTrack.name").String(), - } - - return track.buildTrack(), nil -} - -func PlaylistInfo(url string) ([]Track, error) { - playlistPattern := `^https:\/\/open\.spotify\.com\/playlist\/[a-zA-Z0-9]{22}\?si=[a-zA-Z0-9]{16}$` - if !isValidPattern(url, playlistPattern) { - return nil, errors.New("invalid playlist url") - } - - totalCount := "data.playlistV2.content.totalCount" - itemsArray := "data.playlistV2.content.items" - tracks, err := resourceInfo(url, "playlist", totalCount, itemsArray) - if err != nil { - return nil, err - } - - return tracks, nil + return allTracks, nil } func AlbumInfo(url string) ([]Track, error) { - albumPattern := `^https:\/\/open\.spotify\.com\/album\/[a-zA-Z0-9-]{22}\?si=[a-zA-Z0-9_-]{22}$` - if !isValidPattern(url, albumPattern) { - return nil, errors.New("invalid album url") + re := regexp.MustCompile(`open\.spotify\.com\/album\/([a-zA-Z0-9]{22})`) + matches := re.FindStringSubmatch(url) + if len(matches) != 2 { + return nil, errors.New("invalid album URL") + } + id := matches[1] + + endpoint := fmt.Sprintf("https://api.spotify.com/v1/albums/%s/tracks?limit=50", id) + statusCode, jsonResponse, err := request(endpoint) + if err != nil { + return nil, fmt.Errorf("error getting album info: %w", err) + } + if statusCode != 200 { + return nil, fmt.Errorf("non-200 status: %d", statusCode) } - totalCount := "data.albumUnion.discs.items.0.tracks.totalCount" - itemsArray := "data.albumUnion.discs.items" - tracks, err := resourceInfo(url, "album", totalCount, itemsArray) - if err != nil { + var result struct { + Items []struct { + Name string `json:"name"` + Duration int `json:"duration_ms"` + Artists []struct { + Name string `json:"name"` + } `json:"artists"` + } `json:"items"` + } + if err := json.Unmarshal([]byte(jsonResponse), &result); err != nil { return nil, err } + var tracks []Track + for _, item := range result.Items { + var artists []string + for _, a := range item.Artists { + artists = append(artists, a.Name) + } + tracks = append(tracks, *(&Track{ + Title: item.Name, + Artist: artists[0], + Artists: artists, + Duration: item.Duration / 1000, + Album: "", // You can fetch full album info if needed + }).buildTrack()) + } + return tracks, nil } + /* returns playlist/album slice of tracks */ func resourceInfo(url, resourceType, totalCount, itemList string) ([]Track, error) { id := getID(url) @@ -212,10 +385,10 @@ func jsonList(resourceType, id string, offset, limit int64) (string, error) { var endpoint string if resourceType == "playlist" { endpointQuery = EncodeParam(fmt.Sprintf(`{"uri":"spotify:playlist:%s","offset":%d,"limit":%d}`, id, offset, limit)) - endpoint = playlistInitialPath + endpointQuery + "&extensions=" + EncodeParam(playlistEndPath) + endpoint = endpointQuery } else { endpointQuery = EncodeParam(fmt.Sprintf(`{"uri":"spotify:album:%s","locale":"","offset":%d,"limit":%d}`, id, offset, limit)) - endpoint = albumInitialPath + endpointQuery + "&extensions=" + EncodeParam(albumEndPath) + endpoint = endpointQuery } statusCode, jsonResponse, err := request(endpoint) From 8f1ab855a26e7610796f8316aed26acbf7dd7253 Mon Sep 17 00:00:00 2001 From: KaNaDaAT Date: Thu, 15 May 2025 20:28:14 +0200 Subject: [PATCH 4/8] Update README.md Infos about the Spotify API --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 0c257f6..cfe6a68 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,26 @@ Prerequisites: [Docker](https://docs.docker.com/get-docker/) and [Docker Compose ```Bash docker-compose down ``` + +#### 🎧 Spotify API + +To access Spotify metadata, the project now uses the official [Spotify Web API](https://developer.spotify.com/documentation/web-api/). This requires creating a developer application and retrieving a client ID and client secret. + +Follow the [official getting started guide](https://developer.spotify.com/documentation/web-api/tutorials/getting-started#request-an-access-token) to: + +1. Create a Spotify developer app. +2. Copy your **Client ID** and **Client Secret**. + +Create a file named `credentials.json` in the `server/` directory with the following structure: + +```json +{ + "client_id": "your-client-id", + "client_secret": "your-client-secret" +} +``` +The application will automatically read this file to fetch and cache access tokens. If the token is expired or missing, a new one will be requested. + #### 💻 Set Up Natively Install dependencies for the backend ``` @@ -77,6 +97,8 @@ go run *.go download go run *.go save [-f|--force] ``` The `-f` or `--force` flag allows saving the song even if a YouTube ID is not found. Note that the frontend will not display matches without a YouTube ID. + +Note: if `*.go` does not work try to use `./...` instead. #### ▸ Find matches for a song/recording 🔎 ``` From 1daf68206203d4a61062fba8fcbe22c668f8fba9 Mon Sep 17 00:00:00 2001 From: KaNaDaAT Date: Thu, 15 May 2025 20:48:11 +0200 Subject: [PATCH 5/8] fix: spotify regex pattern Fix regex pattern to use non-capturing group for optional intl prefix in Spotify track URLs --- server/spotify/spotify.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/spotify/spotify.go b/server/spotify/spotify.go index f9f021a..204a5c6 100644 --- a/server/spotify/spotify.go +++ b/server/spotify/spotify.go @@ -183,9 +183,9 @@ func isValidPattern(url, pattern string) bool { } func TrackInfo(url string) (*Track, error) { - re := regexp.MustCompile(`open\.spotify\.com\/track\/([a-zA-Z0-9]{22})`) + re := regexp.MustCompile(`open\.spotify\.com\/(?:intl-.+\/)?track\/([a-zA-Z0-9]{22})(\?si=[a-zA-Z0-9]{16})?`) matches := re.FindStringSubmatch(url) - if len(matches) != 2 { + if len(matches) <= 2 { return nil, errors.New("invalid track URL") } id := matches[1] From ab5be2f50e28b254c9807bc43d7d14093c7d5064 Mon Sep 17 00:00:00 2001 From: KaNaDaAT Date: Thu, 15 May 2025 20:53:16 +0200 Subject: [PATCH 6/8] fix(db): prevent "database is locked" errors by improving SQLite usage - Add explicit rows.Close() calls inside loops to avoid holding locks too long - Add SQLite busy timeout (5s) to connection string to wait for locks instead of failing immediately --- server/db/sqlite.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/server/db/sqlite.go b/server/db/sqlite.go index 03691a6..6cffcf7 100644 --- a/server/db/sqlite.go +++ b/server/db/sqlite.go @@ -15,6 +15,15 @@ type SQLiteClient struct { } func NewSQLiteClient(dataSourceName string) (*SQLiteClient, error) { + // Add busy timeout param to DSN (milliseconds) + if !strings.Contains(dataSourceName, "_busy_timeout") { + if strings.Contains(dataSourceName, "?") { + dataSourceName += "&_busy_timeout=5000" // 5 seconds + } else { + dataSourceName += "?_busy_timeout=5000" + } + } + db, err := sql.Open("sqlite3", dataSourceName) if err != nil { return nil, fmt.Errorf("error connecting to SQLite: %s", err) @@ -28,6 +37,7 @@ func NewSQLiteClient(dataSourceName string) (*SQLiteClient, error) { return &SQLiteClient{db: db}, nil } + // createTables creates the required tables if they don't exist func createTables(db *sql.DB) error { createSongsTable := ` @@ -100,22 +110,26 @@ func (db *SQLiteClient) GetCouples(addresses []uint32) (map[uint32][]models.Coup if err != nil { return nil, fmt.Errorf("error querying database: %s", err) } - defer rows.Close() var docCouples []models.Couple for rows.Next() { var couple models.Couple if err := rows.Scan(&couple.AnchorTimeMs, &couple.SongID); err != nil { + rows.Close() // close before returning error return nil, fmt.Errorf("error scanning row: %s", err) } docCouples = append(docCouples, couple) } + + rows.Close() // close explicitly after reading + couples[address] = docCouples } return couples, nil } + func (db *SQLiteClient) TotalSongs() (int, error) { var count int err := db.db.QueryRow("SELECT COUNT(*) FROM songs").Scan(&count) From d6bea0a568292d238c62239a4a15d40cc6f4e438 Mon Sep 17 00:00:00 2001 From: KaNaDaAT Date: Sat, 7 Jun 2025 15:09:39 +0200 Subject: [PATCH 7/8] feat: Logging for downloader.go --- server/spotify/downloader.go | 51 +++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/server/spotify/downloader.go b/server/spotify/downloader.go index 3838561..b9a95fb 100644 --- a/server/spotify/downloader.go +++ b/server/spotify/downloader.go @@ -28,7 +28,8 @@ const DELETE_SONG_FILE = false var yellow = color.New(color.FgYellow) func DlSingleTrack(url, savePath string) (int, error) { - fmt.Println("Getting track info (" + url + ")...") + logger := utils.GetLogger() + logger.Info("Getting track info", slog.String("url", url)) trackInfo, err := TrackInfo(url) if err != nil { return 0, err @@ -36,7 +37,7 @@ func DlSingleTrack(url, savePath string) (int, error) { track := []Track{*trackInfo} - fmt.Println("Now, downloading track...") + logger.Info("Now downloading track") totalTracksDownloaded, err := dlTrack(track, savePath) if err != nil { return 0, err @@ -46,13 +47,14 @@ func DlSingleTrack(url, savePath string) (int, error) { } func DlPlaylist(url, savePath string) (int, error) { + logger := utils.GetLogger() tracks, err := PlaylistInfo(url) if err != nil { return 0, err } time.Sleep(1 * time.Second) - fmt.Println("Now, downloading playlist...") + logger.Info("Now downloading playlist") totalTracksDownloaded, err := dlTrack(tracks, savePath) if err != nil { return 0, err @@ -62,13 +64,14 @@ func DlPlaylist(url, savePath string) (int, error) { } func DlAlbum(url, savePath string) (int, error) { + logger := utils.GetLogger() tracks, err := AlbumInfo(url) if err != nil { return 0, err } time.Sleep(1 * time.Second) - fmt.Println("Now, downloading album...") + logger.Info("Now downloading album") totalTracksDownloaded, err := dlTrack(tracks, savePath) if err != nil { return 0, err @@ -118,7 +121,7 @@ func dlTrack(tracks []Track, path string) (int, error) { logger.ErrorContext(ctx, "error checking song existence", slog.Any("error", err)) } if keyExists { - logMessage := fmt.Sprintf("'%s' by '%s' already exits.", trackCopy.Title, trackCopy.Artist) + logMessage := fmt.Sprintf("'%s' by '%s' already exists.", trackCopy.Title, trackCopy.Artist) logger.Info(logMessage) return } @@ -163,7 +166,7 @@ func dlTrack(tracks []Track, path string) (int, error) { utils.DeleteFile(wavFilePath) } - fmt.Printf("'%s' by '%s' was downloaded\n", track.Title, track.Artist) + logger.Info(fmt.Sprintf("'%s' by '%s' was downloaded", track.Title, track.Artist)) downloadedTracks = append(downloadedTracks, fmt.Sprintf("%s, %s", track.Title, track.Artist)) results <- 1 }(t) @@ -178,26 +181,30 @@ func dlTrack(tracks []Track, path string) (int, error) { totalTracks++ } - fmt.Println("Total tracks downloaded:", totalTracks) + logger.Info(fmt.Sprintf("Total tracks downloaded: %d", totalTracks)) return totalTracks, nil } /* github.com/kkdai/youtube */ func downloadYTaudio(id, path, filePath string) error { + logger := utils.GetLogger() dir, err := os.Stat(path) if err != nil { - panic(err) + logger.Error("Error accessing path", slog.Any("error", err)) + return err } if !dir.IsDir() { - return errors.New("the path is not valid (not a dir)") + err := errors.New("the path is not valid (not a dir)") + logger.Error("Invalid directory path", slog.Any("error", err)) + return err } client := youtube.Client{} video, err := client.GetVideo(id) if err != nil { - fmt.Println("Error: ", err) + logger.Error("Error getting YouTube video", slog.Any("error", err)) return err } @@ -215,16 +222,19 @@ func downloadYTaudio(id, path, filePath string) error { var fileSize int64 file, err := os.Create(filePath) if err != nil { + logger.Error("Error creating file", slog.Any("error", err)) return err } for fileSize == 0 { stream, _, err := client.GetStream(video, &formats[0]) if err != nil { + logger.Error("Error getting stream", slog.Any("error", err)) return err } if _, err = io.Copy(file, stream); err != nil { + logger.Error("Error copying stream to file", slog.Any("error", err)) return err } @@ -236,6 +246,7 @@ func downloadYTaudio(id, path, filePath string) error { } func addTags(file string, track Track) error { + logger := utils.GetLogger() // Create a temporary file name by appending "2" before the extension tempFile := file index := strings.Index(file, ".wav") @@ -258,11 +269,13 @@ func addTags(file string, track Track) error { out, err := cmd.CombinedOutput() if err != nil { + logger.Error("Failed to add tags", slog.Any("error", err), slog.String("output", string(out))) return fmt.Errorf("failed to add tags: %v, output: %s", err, string(out)) } // Rename the temporary file to the original filename if err := os.Rename(tempFile, file); err != nil { + logger.Error("Failed to rename file", slog.Any("error", err)) return fmt.Errorf("failed to rename file: %v", err) } @@ -270,34 +283,41 @@ func addTags(file string, track Track) error { } func ProcessAndSaveSong(songFilePath, songTitle, songArtist, ytID string) error { + logger := utils.GetLogger() dbclient, err := db.NewDBClient() if err != nil { + logger.Error("Failed to create DB client", slog.Any("error", err)) return err } defer dbclient.Close() wavFilePath, err := wav.ConvertToWAV(songFilePath, 1) if err != nil { + logger.Error("Failed to convert to WAV", slog.Any("error", err)) return err } wavInfo, err := wav.ReadWavInfo(wavFilePath) if err != nil { + logger.Error("Failed to read WAV info", slog.Any("error", err)) return err } samples, err := wav.WavBytesToSamples(wavInfo.Data) if err != nil { + logger.Error("Error converting WAV bytes to samples", slog.Any("error", err)) return fmt.Errorf("error converting wav bytes to float64: %v", err) } spectro, err := shazam.Spectrogram(samples, wavInfo.SampleRate) if err != nil { + logger.Error("Error creating spectrogram", slog.Any("error", err)) return fmt.Errorf("error creating spectrogram: %v", err) } songID, err := dbclient.RegisterSong(songTitle, songArtist, ytID) if err != nil { + logger.Error("Failed to register song", slog.Any("error", err)) return err } @@ -307,14 +327,16 @@ func ProcessAndSaveSong(songFilePath, songTitle, songArtist, ytID string) error err = dbclient.StoreFingerprints(fingerprints) if err != nil { dbclient.DeleteSongByID(songID) - return fmt.Errorf("error to storing fingerprint: %v", err) + logger.Error("Failed to store fingerprints", slog.Any("error", err)) + return fmt.Errorf("error storing fingerprint: %v", err) } - fmt.Printf("Fingerprint for %v by %v saved in DB successfully\n", songTitle, songArtist) + logger.Info(fmt.Sprintf("Fingerprint for %v by %v saved in DB successfully", songTitle, songArtist)) return nil } func getYTID(trackCopy *Track) (string, error) { + logger := utils.GetLogger() ytID, err := GetYoutubeId(*trackCopy) if ytID == "" || err != nil { return "", err @@ -327,9 +349,8 @@ func getYTID(trackCopy *Track) (string, error) { } if ytidExists { // try to get the YouTube ID again - logMessage := fmt.Sprintf("YouTube ID (%s) exists. Trying again...\n", ytID) - fmt.Println("WARN: ", logMessage) - slog.Warn(logMessage) + logMessage := fmt.Sprintf("YouTube ID (%s) exists. Trying again...", ytID) + logger.Warn(logMessage) ytID, err = GetYoutubeId(*trackCopy) if ytID == "" || err != nil { From c4b7f5a14a6de58bc7f017015bf3678980ac9198 Mon Sep 17 00:00:00 2001 From: KaNaDaAT Date: Sat, 7 Jun 2025 16:15:38 +0200 Subject: [PATCH 8/8] feat: Use env for credentials --- .env.example | 4 +++- README.md | 15 +++++++++------ server/credentials.json | 4 ---- server/go.mod | 1 + server/go.sum | 2 ++ server/main.go | 4 +++- server/spotify/spotify.go | 29 ++++++++++++----------------- 7 files changed, 30 insertions(+), 29 deletions(-) delete mode 100644 server/credentials.json diff --git a/.env.example b/.env.example index bc65741..e30fdc5 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,6 @@ DB_PASS=password DB_NAME=seek-tune DB_HOST=192.168.0.1 DB_PORT=27017 -REACT_APP_BACKEND_URL=http://localhost:5000 \ No newline at end of file +REACT_APP_BACKEND_URL=http://localhost:5000 +SPOTIFY_CLIENT_ID=yourclientid +SPOTIFY_CLIENT_SECRET=yoursecret \ No newline at end of file diff --git a/README.md b/README.md index cfe6a68..3c66eb4 100644 --- a/README.md +++ b/README.md @@ -51,14 +51,17 @@ Follow the [official getting started guide](https://developer.spotify.com/docume 1. Create a Spotify developer app. 2. Copy your **Client ID** and **Client Secret**. -Create a file named `credentials.json` in the `server/` directory with the following structure: +##### Setting up Credentials +Instead of using a credentials.json file, the application now reads these values from environment variables. + +Create a .env file in the server directory with the following content: -```json -{ - "client_id": "your-client-id", - "client_secret": "your-client-secret" -} ``` +SPOTIFY_CLIENT_ID=your-client-id +SPOTIFY_CLIENT_SECRET=your-client-secret +``` + +Make sure this .env file is loaded into your environment before running the server. The application will automatically read this file to fetch and cache access tokens. If the token is expired or missing, a new one will be requested. #### 💻 Set Up Natively diff --git a/server/credentials.json b/server/credentials.json deleted file mode 100644 index 4c7b62c..0000000 --- a/server/credentials.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "client_id": "", - "client_secret": "" -} diff --git a/server/go.mod b/server/go.mod index 34d6e2c..fd5a569 100644 --- a/server/go.mod +++ b/server/go.mod @@ -40,6 +40,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.1 // indirect github.com/gorilla/websocket v1.4.2 // indirect + github.com/joho/godotenv v1.4.0 // indirect github.com/klauspost/compress v1.17.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/server/go.sum b/server/go.sum index e51b386..5aa3bf8 100644 --- a/server/go.sum +++ b/server/go.sum @@ -100,6 +100,8 @@ github.com/googollee/go-socket.io v1.7.0/go.mod h1:0vGP8/dXR9SZUMMD4+xxaGo/lohOw github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kkdai/youtube/v2 v2.10.1 h1:jdPho4R7VxWoRi9Wx4ULMq4+hlzSVOXxh4Zh83f2F9M= github.com/kkdai/youtube/v2 v2.10.1/go.mod h1:qL8JZv7Q1IoDs4nnaL51o/hmITXEIvyCIXopB0oqgVM= github.com/kkdai/youtube/v2 v2.10.4 h1:T3VAQ65EB4eHptwcQIigpFvUJlV9EcKRGJJdSVUy3aU= diff --git a/server/main.go b/server/main.go index 7836995..5fba59c 100644 --- a/server/main.go +++ b/server/main.go @@ -9,6 +9,7 @@ import ( "song-recognition/utils" "github.com/mdobak/go-xerrors" + "github.com/joho/godotenv" ) func main() { @@ -33,7 +34,8 @@ func main() { fmt.Println("Expected 'find', 'download', 'erase', 'save', or 'serve' subcommands") os.Exit(1) } - + _ = godotenv.Load() + switch os.Args[1] { case "find": if len(os.Args) < 3 { diff --git a/server/spotify/spotify.go b/server/spotify/spotify.go index 204a5c6..096558a 100644 --- a/server/spotify/spotify.go +++ b/server/spotify/spotify.go @@ -13,6 +13,7 @@ import ( "strings" "time" "os" + "song-recognition/utils" "github.com/tidwall/gjson" ) @@ -28,16 +29,14 @@ type Track struct { Duration int } - const ( tokenURL = "https://accounts.spotify.com/api/token" - credentialsPath = "credentials.json" cachedTokenPath = "token.json" ) type credentials struct { - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` + ClientID string + ClientSecret string } type tokenResponse struct { @@ -52,21 +51,17 @@ type cachedToken struct { } func loadCredentials() (*credentials, error) { - file, err := os.Open(credentialsPath) - if err != nil { - if os.IsNotExist(err) { - absPath, _ := os.Getwd() - return nil, fmt.Errorf("credentials.json not found. Please create it in the same directory:\n%s/%s", absPath, credentialsPath) - } - return nil, err - } - defer file.Close() + clientID := utils.GetEnv("SPOTIFY_CLIENT_ID", "") + clientSecret := utils.GetEnv("SPOTIFY_CLIENT_SECRET", "") - var creds credentials - if err := json.NewDecoder(file).Decode(&creds); err != nil { - return nil, fmt.Errorf("failed to parse credentials.json: %w", err) + if clientID == "" || clientSecret == "" { + return nil, fmt.Errorf("SPOTIFY_CLIENT_ID or SPOTIFY_CLIENT_SECRET environment variables not set") } - return &creds, nil + + return &credentials{ + ClientID: clientID, + ClientSecret: clientSecret, + }, nil }