Start to replace Songwhip links

This commit is contained in:
JP Hastings-Spital 2025-02-13 20:43:33 +00:00
parent 17157e5bab
commit f336366dc5
10 changed files with 372 additions and 23 deletions

View file

@ -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" >}}

View file

@ -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" >}}

View file

@ -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"]}

View file

@ -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"]}

View file

@ -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
) )

View file

@ -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=

View 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
}
}

View 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))
}

View 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{}
}

View 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)
}