all repos

rss-tools @ 05391f1ceeba6d6f8ff2adc080b7e254f645cd6d

get rss feed from sources that(i need and) dont provide one

rss-tools/sources/musicfeed/musicbrainz.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
musicfeed: include spotify and youtube links, 1 month ago
1
package musicfeed
2
3
import (
4
	"context"
5
	"encoding/json"
6
	"fmt"
7
	"log/slog"
8
	"net/http"
9
	"net/url"
10
	"sync"
11
	"time"
12
)
13
14
const (
15
	mbBaseURL   = "https://musicbrainz.org/ws/2"
16
	caaBaseURL  = "https://coverartarchive.org"
17
	mbUserAgent = "rss-tools/1.0 ( https://github.com/olexsmir/rss-tools )"
18
)
19
20
type mbRelease struct {
21
	ID           string `json:"id"`
22
	Title        string `json:"title"`
23
	Date         string `json:"date"`
24
	Status       string `json:"status"`
25
	ArtistCredit []struct {
26
		Name string `json:"name"`
27
	} `json:"artist-credit"`
28
	ReleaseGroup struct {
29
		ID          string `json:"id"`
30
		PrimaryType string `json:"primary-type"`
31
	} `json:"release-group"`
32
	CoverArtArchive struct {
33
		Artwork bool `json:"artwork"`
34
	} `json:"cover-art-archive"`
35
	Relations []mbRelation `json:"relations,omitempty"`
36
}
37
38
type mbRelation struct {
39
	Type      string `json:"type"`
40
	Direction string `json:"direction"`
41
	URL       struct {
42
		ID       string `json:"id"`
43
		Resource string `json:"resource"`
44
	} `json:"url"`
45
}
46
47
type mbReleaseResponse struct {
48
	Releases []mbRelease `json:"releases"`
49
}
50
51
type mbArtistSearchResponse struct {
52
	Artists []struct {
53
		ID             string `json:"id"`
54
		Name           string `json:"name"`
55
		Disambiguation string `json:"disambiguation,omitempty"`
56
		Score          int    `json:"score"`
57
	} `json:"artists"`
58
}
59
60
type mbArtistResponse struct {
61
	ID   string `json:"id"`
62
	Name string `json:"name"`
63
}
64
65
const mbRequestGap = 200 * time.Millisecond
66
67
type musicbrainzAPI struct {
68
	client  *http.Client
69
	mu      sync.Mutex
70
	lastReq time.Time
71
}
72
73
func newMusicBrainzAPI(client *http.Client) *musicbrainzAPI {
74
	return &musicbrainzAPI{
75
		client: client,
76
	}
77
}
78
79
func (a *musicbrainzAPI) throttle(ctx context.Context) error {
80
	a.mu.Lock()
81
	if a.lastReq.IsZero() {
82
		a.lastReq = time.Now()
83
		a.mu.Unlock()
84
		return nil
85
	}
86
87
	scheduled := a.lastReq.Add(mbRequestGap)
88
	a.lastReq = scheduled
89
	a.mu.Unlock()
90
91
	wait := time.Until(scheduled)
92
	if wait > 0 {
93
		select {
94
		case <-time.After(wait):
95
		case <-ctx.Done():
96
			return ctx.Err()
97
		}
98
	}
99
	return nil
100
}
101
102
func (a *musicbrainzAPI) doRequest(ctx context.Context, urlStr string) (*http.Response, error) {
103
	if err := a.throttle(ctx); err != nil {
104
		return nil, err
105
	}
106
107
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
108
	if err != nil {
109
		return nil, err
110
	}
111
	req.Header.Set("User-Agent", mbUserAgent)
112
113
	slog.Info("musicbrainz API request", "url", urlStr)
114
	return a.client.Do(req)
115
}
116
117
func (a *musicbrainzAPI) searchArtist(ctx context.Context, name string) (string, string, error) {
118
	q := url.QueryEscape(name)
119
	u := fmt.Sprintf("%s/artist?query=artist:%s&limit=5&fmt=json", mbBaseURL, q)
120
	resp, err := a.doRequest(ctx, u)
121
	if err != nil {
122
		return "", "", fmt.Errorf("search artist %q: %w", name, err)
123
	}
124
	defer resp.Body.Close()
125
126
	var result mbArtistSearchResponse
127
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
128
		return "", "", fmt.Errorf("decode artist search: %w", err)
129
	}
130
131
	if len(result.Artists) == 0 {
132
		return "", "", fmt.Errorf("no artist found for %q", name)
133
	}
134
135
	best := result.Artists[0]
136
	slog.Info("resolved artist",
137
		"query", name,
138
		"match", best.Name,
139
		"disambiguation", best.Disambiguation,
140
		"score", best.Score,
141
		"mbid", best.ID,
142
	)
143
	return best.ID, best.Name, nil
144
}
145
146
func (a *musicbrainzAPI) fetchArtist(ctx context.Context, mbid string) (string, error) {
147
	u := fmt.Sprintf("%s/artist/%s?fmt=json", mbBaseURL, mbid)
148
	resp, err := a.doRequest(ctx, u)
149
	if err != nil {
150
		return "", fmt.Errorf("fetch artist %s: %w", mbid, err)
151
	}
152
	defer resp.Body.Close()
153
154
	var result mbArtistResponse
155
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
156
		return "", fmt.Errorf("decode artist: %w", err)
157
	}
158
	return result.Name, nil
159
}
160
161
func (a *musicbrainzAPI) fetchReleases(ctx context.Context, mbid string) ([]mbRelease, error) {
162
	u := fmt.Sprintf(
163
		"%s/release?artist=%s&inc=artist-credits+release-groups+url-rels&limit=100&fmt=json",
164
		mbBaseURL, mbid,
165
	)
166
	resp, err := a.doRequest(ctx, u)
167
	if err != nil {
168
		return nil, fmt.Errorf("fetch releases for %s: %w", mbid, err)
169
	}
170
	defer resp.Body.Close()
171
172
	var result mbReleaseResponse
173
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
174
		return nil, fmt.Errorf("decode releases: %w", err)
175
	}
176
	return result.Releases, nil
177
}