mirror of
https://github.com/by-jp/www.byjp.me.git
synced 2025-08-09 01:35:56 +01:00
Start to replace Songwhip links
This commit is contained in:
parent
17157e5bab
commit
f336366dc5
10 changed files with 372 additions and 23 deletions
|
@ -5,4 +5,4 @@ slug: 18n24
|
||||||
---
|
---
|
||||||
I search for "Oasis" on Spotify and before even a whole album's worth of tracks is listed, sits <cite>Girls & Boys</cite> by Blur 😂 The "rivalry" continues!
|
I search for "Oasis" on Spotify and before even a whole album's worth of tracks is listed, sits <cite>Girls & Boys</cite> by Blur 😂 The "rivalry" continues!
|
||||||
|
|
||||||
{{< spotify path="/track/5CeL9C3bsoe4yzYS1Qz8cw" title="Girls & Boys" artist="Blur" url="https://songwhip.com/blur/girls-and-boys2000" >}}
|
{{< music "8a8909c0-291c-4643-977f-17d27f052154" >}}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
date: "2016-10-20T14:38:36Z"
|
date: 2016-10-20T14:38:36Z
|
||||||
tags:
|
tags:
|
||||||
- imported
|
- imported
|
||||||
- from-facebook
|
- from-facebook
|
||||||
|
@ -8,4 +8,4 @@ tags:
|
||||||
---
|
---
|
||||||
Today this is my jam! (Who am I kidding, Vulfpeck are always my jam)
|
Today this is my jam! (Who am I kidding, Vulfpeck are always my jam)
|
||||||
|
|
||||||
{{< spotify path="/track/1SHA4IJyiyNobDOrQzFFXy" title="Animal Spirits" artist="Vulfpeck" >}}
|
{{< music "8893154c-4e9d-44d3-8c77-3db17365ca76" >}}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{"artist":"Vulfpeck","title":"Animal Spirits","musicbrainz":"https://musicbrainz.org/recording/8893154c-4e9d-44d3-8c77-3db17365ca76","links":["https://open.spotify.com/track/1SHA4IJyiyNobDOrQzFFXy"]}
|
|
@ -0,0 +1 @@
|
||||||
|
{"artist":"Blur","title":"Girls and Boys","musicbrainz":"https://musicbrainz.org/recording/8a8909c0-291c-4643-977f-17d27f052154","links":["https://open.spotify.com/track/5CeL9C3bsoe4yzYS1Qz8cw"]}
|
18
tools/go.mod
18
tools/go.mod
|
@ -6,8 +6,7 @@ require (
|
||||||
github.com/BurntSushi/toml v1.3.2
|
github.com/BurntSushi/toml v1.3.2
|
||||||
github.com/Davincible/goinsta/v3 v3.2.6
|
github.com/Davincible/goinsta/v3 v3.2.6
|
||||||
github.com/PuerkitoBio/goquery v1.9.1
|
github.com/PuerkitoBio/goquery v1.9.1
|
||||||
github.com/aws/smithy-go v1.20.2
|
github.com/agnivade/levenshtein v1.2.1
|
||||||
github.com/bmatcuk/doublestar v1.3.4
|
|
||||||
github.com/bmatcuk/doublestar/v4 v4.6.1
|
github.com/bmatcuk/doublestar/v4 v4.6.1
|
||||||
github.com/grokify/html-strip-tags-go v0.1.0
|
github.com/grokify/html-strip-tags-go v0.1.0
|
||||||
github.com/h2non/filetype v1.1.3
|
github.com/h2non/filetype v1.1.3
|
||||||
|
@ -18,14 +17,12 @@ require (
|
||||||
github.com/mmcdole/gofeed v1.3.0
|
github.com/mmcdole/gofeed v1.3.0
|
||||||
github.com/stretchr/testify v1.8.2
|
github.com/stretchr/testify v1.8.2
|
||||||
golang.org/x/image v0.15.0
|
golang.org/x/image v0.15.0
|
||||||
golang.org/x/net v0.21.0
|
|
||||||
golang.org/x/text v0.14.0
|
golang.org/x/text v0.14.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
github.com/benbjohnson/clock v1.1.0 // indirect
|
|
||||||
github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319 // indirect
|
github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319 // indirect
|
||||||
github.com/chromedp/cdproto v0.0.0-20220901095120-1a01299a2163 // indirect
|
github.com/chromedp/cdproto v0.0.0-20220901095120-1a01299a2163 // indirect
|
||||||
github.com/chromedp/chromedp v0.8.5 // indirect
|
github.com/chromedp/chromedp v0.8.5 // indirect
|
||||||
|
@ -39,14 +36,12 @@ require (
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.0 // indirect
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-hclog v0.9.2 // indirect
|
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
|
||||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||||
github.com/ipfs/bbloom v0.0.4 // indirect
|
github.com/ipfs/bbloom v0.0.4 // indirect
|
||||||
github.com/ipfs/go-block-format v0.1.2 // indirect
|
github.com/ipfs/go-block-format v0.1.2 // indirect
|
||||||
github.com/ipfs/go-cid v0.4.0 // indirect
|
github.com/ipfs/go-cid v0.4.0 // indirect
|
||||||
github.com/ipfs/go-datastore v0.6.0 // indirect
|
github.com/ipfs/go-datastore v0.6.0 // indirect
|
||||||
github.com/ipfs/go-detect-race v0.0.1 // indirect
|
|
||||||
github.com/ipfs/go-ipfs-blockstore v1.3.0 // indirect
|
github.com/ipfs/go-ipfs-blockstore v1.3.0 // indirect
|
||||||
github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect
|
github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect
|
||||||
github.com/ipfs/go-ipfs-util v0.0.2 // indirect
|
github.com/ipfs/go-ipfs-util v0.0.2 // indirect
|
||||||
|
@ -58,11 +53,7 @@ require (
|
||||||
github.com/jbenet/goprocess v0.1.4 // indirect
|
github.com/jbenet/goprocess v0.1.4 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
github.com/kr/pretty v0.2.0 // indirect
|
|
||||||
github.com/kr/text v0.1.0 // indirect
|
|
||||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 // indirect
|
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||||
|
@ -76,24 +67,19 @@ require (
|
||||||
github.com/multiformats/go-multihash v0.2.1 // indirect
|
github.com/multiformats/go-multihash v0.2.1 // indirect
|
||||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
|
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
|
||||||
github.com/smartystreets/assertions v1.2.0 // indirect
|
|
||||||
github.com/smartystreets/goconvey v1.7.2 // indirect
|
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect
|
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect
|
||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 // indirect
|
|
||||||
github.com/whyrusleeping/cbor-gen v0.0.0-20230331140348-1f892b517e70 // indirect
|
github.com/whyrusleeping/cbor-gen v0.0.0-20230331140348-1f892b517e70 // indirect
|
||||||
go.uber.org/atomic v1.10.0 // indirect
|
go.uber.org/atomic v1.10.0 // indirect
|
||||||
go.uber.org/goleak v1.1.11 // indirect
|
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.24.0 // indirect
|
go.uber.org/zap v1.24.0 // indirect
|
||||||
golang.org/x/crypto v0.19.0 // indirect
|
golang.org/x/crypto v0.19.0 // indirect
|
||||||
|
golang.org/x/net v0.21.0 // indirect
|
||||||
golang.org/x/sys v0.17.0 // indirect
|
golang.org/x/sys v0.17.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lukechampine.com/blake3 v1.1.7 // indirect
|
lukechampine.com/blake3 v1.1.7 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,18 +6,17 @@ github.com/Davincible/goinsta/v3 v3.2.6 h1:+lNIWU6NABWd2VSGe83UQypnef+kzWwjmfgGi
|
||||||
github.com/Davincible/goinsta/v3 v3.2.6/go.mod h1:jIDhrWZmttL/gtXj/mkCaZyeNdAAqW3UYjasOUW0YEw=
|
github.com/Davincible/goinsta/v3 v3.2.6/go.mod h1:jIDhrWZmttL/gtXj/mkCaZyeNdAAqW3UYjasOUW0YEw=
|
||||||
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
|
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
|
||||||
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
|
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
|
||||||
|
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
||||||
|
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||||
github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5/go.mod h1:Y2QMoi1vgtOIfc+6DhrMOGkLoGzqSV2rKp4Sm+opsyA=
|
github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5/go.mod h1:Y2QMoi1vgtOIfc+6DhrMOGkLoGzqSV2rKp4Sm+opsyA=
|
||||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
|
||||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319 h1:VCNXRXpgyK3xkaQ8fzL5WzswerwLycke4B9ggLs1uOA=
|
github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319 h1:VCNXRXpgyK3xkaQ8fzL5WzswerwLycke4B9ggLs1uOA=
|
||||||
github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319/go.mod h1:Hc09SUJXAIujaAvq7JXxi8ZQQI887grzPkHgn4JyE1Q=
|
github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319/go.mod h1:Hc09SUJXAIujaAvq7JXxi8ZQQI887grzPkHgn4JyE1Q=
|
||||||
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
|
|
||||||
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
|
||||||
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
github.com/brianvoe/gofakeit/v6 v6.20.2/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
|
github.com/brianvoe/gofakeit/v6 v6.20.2/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
|
||||||
|
@ -34,6 +33,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
|
||||||
|
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||||
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
|
26
tools/music-normalizer/filter.go
Normal file
26
tools/music-normalizer/filter.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/agnivade/levenshtein"
|
||||||
|
)
|
||||||
|
|
||||||
|
func filter[T interface{}](items []T, filters ...func(T) bool) []T {
|
||||||
|
var allowed []T
|
||||||
|
for _, item := range items {
|
||||||
|
for _, filter := range filters {
|
||||||
|
if filter(item) {
|
||||||
|
allowed = append(allowed, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasCorrectDetails(trackName, artistName string) func(mbRecording) bool {
|
||||||
|
return func(mr mbRecording) bool {
|
||||||
|
dt := levenshtein.ComputeDistance(trackName, mr.Title)
|
||||||
|
da := levenshtein.ComputeDistance(artistName, mr.ArtistCredit[0].Name)
|
||||||
|
|
||||||
|
return dt+da < 6
|
||||||
|
}
|
||||||
|
}
|
137
tools/music-normalizer/main.go
Normal file
137
tools/music-normalizer/main.go
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Spotify API credentials (set as environment variables)
|
||||||
|
var spotifyClientID = os.Getenv("SPOTIFY_CLIENT_ID")
|
||||||
|
var spotifyClientSecret = os.Getenv("SPOTIFY_CLIENT_SECRET")
|
||||||
|
|
||||||
|
// Structs to parse Spotify API responses
|
||||||
|
type SpotifyAuthResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpotifyTrackResponse struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Artists []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"artists"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSpotifyAccessToken() (string, error) {
|
||||||
|
authURL := "https://accounts.spotify.com/api/token"
|
||||||
|
data := url.Values{}
|
||||||
|
data.Set("grant_type", "client_credentials")
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST", authURL, strings.NewReader(data.Encode()))
|
||||||
|
req.SetBasicAuth(spotifyClientID, spotifyClientSecret)
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
var authResp SpotifyAuthResponse
|
||||||
|
json.Unmarshal(body, &authResp)
|
||||||
|
|
||||||
|
return authResp.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSpotifyTrackMetadata(trackID string) (string, string, error) {
|
||||||
|
token, err := getSpotifyAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://api.spotify.com/v1/tracks/%s", trackID)
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
var trackResp SpotifyTrackResponse
|
||||||
|
json.Unmarshal(body, &trackResp)
|
||||||
|
|
||||||
|
if len(trackResp.Artists) == 0 {
|
||||||
|
return "", "", fmt.Errorf("no artist found for the track")
|
||||||
|
}
|
||||||
|
|
||||||
|
return trackResp.Name, trackResp.Artists[0].Name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func check(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type musicbrainzData struct {
|
||||||
|
ID string `json:"-"`
|
||||||
|
Composer string `json:"composer,omitempty"`
|
||||||
|
Artist string `json:"artist,omitempty"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
URL string `json:"musicbrainz"`
|
||||||
|
Links []string `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromSpotifyTrack(trackID string) (musicbrainzData, error) {
|
||||||
|
trackName, artistName, err := getSpotifyTrackMetadata(trackID)
|
||||||
|
if err != nil {
|
||||||
|
return musicbrainzData{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Searching for %s - %s\n", trackName, artistName)
|
||||||
|
mb, err := searchMusicbrainz(trackName, artistName)
|
||||||
|
if err != nil {
|
||||||
|
return mb, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mb.Links = []string{"https://open.spotify.com/track/" + trackID}
|
||||||
|
|
||||||
|
return mb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Printf("Usage: %s <streaming-music-url>\n", os.Args[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(os.Args[1])
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
var mb musicbrainzData
|
||||||
|
switch {
|
||||||
|
case u.Host == "open.spotify.com" && strings.HasPrefix(u.Path, "/track/"):
|
||||||
|
mb, err = fromSpotifyTrack(u.Path[7:])
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unknown URL: %s", u)
|
||||||
|
}
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
f, err := os.OpenFile("./data/music/musicbrainz/"+mb.ID+".json", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
check(json.NewEncoder(f).Encode(mb))
|
||||||
|
}
|
73
tools/music-normalizer/musicbrainz.go
Normal file
73
tools/music-normalizer/musicbrainz.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func searchMusicbrainz(trackName, artistName string) (musicbrainzData, error) {
|
||||||
|
var mb musicbrainzData
|
||||||
|
query := url.QueryEscape(fmt.Sprintf("title:\"%s\" AND artist:\"%s\"", trackName, artistName))
|
||||||
|
searchURL := fmt.Sprintf("https://musicbrainz.org/ws/2/recording/?query=%s&method=advanced&limit=50&fmt=json", query)
|
||||||
|
|
||||||
|
resp, err := http.Get(searchURL)
|
||||||
|
if err != nil {
|
||||||
|
return mb, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return mb, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res mbResponse
|
||||||
|
if err := json.Unmarshal(body, &res); err != nil {
|
||||||
|
return mb, err
|
||||||
|
}
|
||||||
|
if len(res.Recordings) == 0 {
|
||||||
|
return mb, fmt.Errorf("no results found on MusicBrainz")
|
||||||
|
}
|
||||||
|
|
||||||
|
recs := filter(res.Recordings,
|
||||||
|
hasCorrectDetails(trackName, artistName),
|
||||||
|
)
|
||||||
|
|
||||||
|
sort.Sort(ByDesirability(recs))
|
||||||
|
|
||||||
|
mb.ID = recs[0].ID
|
||||||
|
mb.URL = "https://musicbrainz.org/recording/" + recs[0].ID
|
||||||
|
mb.Title = recs[0].Title
|
||||||
|
mb.Artist = recs[0].ArtistCredit[0].Name
|
||||||
|
|
||||||
|
return mb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringIs(str string, has ...string) bool {
|
||||||
|
t := strings.ToLower(str)
|
||||||
|
for _, ha := range has {
|
||||||
|
if strings.Contains(t, ha) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseReleaseDate(str string) time.Time {
|
||||||
|
if t, err := time.Parse("2006-01-02", str); err == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, err := time.Parse("2006", str); err == nil {
|
||||||
|
// Go to the last day of the year, as we want more specific dates to take precedence
|
||||||
|
return t.AddDate(1, 0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Time{}
|
||||||
|
}
|
125
tools/music-normalizer/sort.go
Normal file
125
tools/music-normalizer/sort.go
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mbResponse struct {
|
||||||
|
Recordings []mbRecording
|
||||||
|
}
|
||||||
|
|
||||||
|
type mbRecording struct {
|
||||||
|
ID string
|
||||||
|
Score int
|
||||||
|
Title string
|
||||||
|
Disambiguation string
|
||||||
|
FirstRelease string `json:"first-release-date"`
|
||||||
|
ArtistCredit []mbArtist `json:"artist-credit"`
|
||||||
|
Releases []mbRelease `json:"releases"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type mbArtist struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type mbRelease struct {
|
||||||
|
ID string
|
||||||
|
Score int
|
||||||
|
Title string
|
||||||
|
Country string
|
||||||
|
Status string
|
||||||
|
Quality string
|
||||||
|
ArtistCredit []mbArtist `json:"artist-credit"`
|
||||||
|
Date string `json:"first-release-date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByDesirability []mbRecording
|
||||||
|
|
||||||
|
func (r ByDesirability) Len() int { return len(r) }
|
||||||
|
func (r ByDesirability) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||||
|
func (r ByDesirability) Less(i, j int) bool {
|
||||||
|
return sortInTurn(r[i], r[j],
|
||||||
|
// highScoreFirst,
|
||||||
|
preferGoodRelease,
|
||||||
|
preferNonLiveRecordings,
|
||||||
|
earliestRelease,
|
||||||
|
preferShorterName,
|
||||||
|
deterministicallyByID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortInTurn[T interface{}](a, b T, comparators ...func(T, T) int) bool {
|
||||||
|
for _, comparator := range comparators {
|
||||||
|
switch comparator(a, b) {
|
||||||
|
case -1:
|
||||||
|
return true
|
||||||
|
case 1:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
// Try the next comparator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Don't swap if they're identical
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ternary(first, last bool) int {
|
||||||
|
if first {
|
||||||
|
return -1
|
||||||
|
} else if last {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func highScoreFirst(a, b mbRecording) int {
|
||||||
|
as := a.Score / 20
|
||||||
|
bs := b.Score / 20
|
||||||
|
return ternary(as > bs, as < bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func preferNonLiveRecordings(a, b mbRecording) int {
|
||||||
|
al := stringIs(a.Disambiguation, "live")
|
||||||
|
bl := stringIs(b.Disambiguation, "live")
|
||||||
|
return ternary(!al && bl, al && !bl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func preferGoodRelease(a, b mbRecording) int {
|
||||||
|
filter := func(r mbRelease) bool {
|
||||||
|
if len(r.ArtistCredit) > 0 && stringIs(r.ArtistCredit[0].Name, "various") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !stringIs(r.Status, "official") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ar := filterReleases(a, filter)
|
||||||
|
br := filterReleases(a, filter)
|
||||||
|
return ternary(!ar && br, ar && !br)
|
||||||
|
}
|
||||||
|
|
||||||
|
func preferShorterName(a, b mbRecording) int {
|
||||||
|
return ternary(len(a.Title) < len(b.Title), len(a.Title) > len(b.Title))
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterReleases(r mbRecording, filter func(mbRelease) bool) bool {
|
||||||
|
for _, rel := range r.Releases {
|
||||||
|
if filter(rel) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func earliestRelease(a, b mbRecording) int {
|
||||||
|
ad := parseReleaseDate(a.FirstRelease)
|
||||||
|
bd := parseReleaseDate(b.FirstRelease)
|
||||||
|
return ternary(ad.Before(bd), ad.After(bd))
|
||||||
|
}
|
||||||
|
|
||||||
|
func deterministicallyByID(a, b mbRecording) int {
|
||||||
|
return strings.Compare(a.ID, b.ID)
|
||||||
|
}
|
Loading…
Reference in a new issue