all repos

rss-tools @ 77475b60d1ffffea61016d65bfa79f6aaa52b20b

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
add musicfeed, 14 days 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
}
36
37
type mbReleaseResponse struct {
38
	Releases []mbRelease `json:"releases"`
39
}
40
41
type mbArtistSearchResponse struct {
42
	Artists []struct {
43
		ID             string `json:"id"`
44
		Name           string `json:"name"`
45
		Disambiguation string `json:"disambiguation,omitempty"`
46
		Score          int    `json:"score"`
47
	} `json:"artists"`
48
}
49
50
type mbArtistResponse struct {
51
	ID   string `json:"id"`
52
	Name string `json:"name"`
53
}
54
55
const mbRequestGap = 200 * time.Millisecond
56
57
type musicbrainzAPI struct {
58
	client  *http.Client
59
	mu      sync.Mutex
60
	lastReq time.Time
61
}
62
63
func newMusicBrainzAPI(client *http.Client) *musicbrainzAPI {
64
	return &musicbrainzAPI{
65
		client: client,
66
	}
67
}
68
69
func (a *musicbrainzAPI) throttle(ctx context.Context) error {
70
	a.mu.Lock()
71
	if a.lastReq.IsZero() {
72
		a.lastReq = time.Now()
73
		a.mu.Unlock()
74
		return nil
75
	}
76
77
	scheduled := a.lastReq.Add(mbRequestGap)
78
	a.lastReq = scheduled
79
	a.mu.Unlock()
80
81
	wait := time.Until(scheduled)
82
	if wait > 0 {
83
		select {
84
		case <-time.After(wait):
85
		case <-ctx.Done():
86
			return ctx.Err()
87
		}
88
	}
89
	return nil
90
}
91
92
func (a *musicbrainzAPI) doRequest(ctx context.Context, urlStr string) (*http.Response, error) {
93
	if err := a.throttle(ctx); err != nil {
94
		return nil, err
95
	}
96
97
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
98
	if err != nil {
99
		return nil, err
100
	}
101
	req.Header.Set("User-Agent", mbUserAgent)
102
103
	slog.Info("musicbrainz API request", "url", urlStr)
104
	return a.client.Do(req)
105
}
106
107
func (a *musicbrainzAPI) searchArtist(ctx context.Context, name string) (string, string, error) {
108
	q := url.QueryEscape(name)
109
	u := fmt.Sprintf("%s/artist?query=artist:%s&limit=5&fmt=json", mbBaseURL, q)
110
	resp, err := a.doRequest(ctx, u)
111
	if err != nil {
112
		return "", "", fmt.Errorf("search artist %q: %w", name, err)
113
	}
114
	defer resp.Body.Close()
115
116
	var result mbArtistSearchResponse
117
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
118
		return "", "", fmt.Errorf("decode artist search: %w", err)
119
	}
120
121
	if len(result.Artists) == 0 {
122
		return "", "", fmt.Errorf("no artist found for %q", name)
123
	}
124
125
	best := result.Artists[0]
126
	slog.Info("resolved artist",
127
		"query", name,
128
		"match", best.Name,
129
		"disambiguation", best.Disambiguation,
130
		"score", best.Score,
131
		"mbid", best.ID,
132
	)
133
	return best.ID, best.Name, nil
134
}
135
136
func (a *musicbrainzAPI) fetchArtist(ctx context.Context, mbid string) (string, error) {
137
	u := fmt.Sprintf("%s/artist/%s?fmt=json", mbBaseURL, mbid)
138
	resp, err := a.doRequest(ctx, u)
139
	if err != nil {
140
		return "", fmt.Errorf("fetch artist %s: %w", mbid, err)
141
	}
142
	defer resp.Body.Close()
143
144
	var result mbArtistResponse
145
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
146
		return "", fmt.Errorf("decode artist: %w", err)
147
	}
148
	return result.Name, nil
149
}
150
151
func (a *musicbrainzAPI) fetchReleases(ctx context.Context, mbid string) ([]mbRelease, error) {
152
	u := fmt.Sprintf(
153
		"%s/release?artist=%s&inc=artist-credits+release-groups&limit=100&fmt=json",
154
		mbBaseURL, mbid,
155
	)
156
	resp, err := a.doRequest(ctx, u)
157
	if err != nil {
158
		return nil, fmt.Errorf("fetch releases for %s: %w", mbid, err)
159
	}
160
	defer resp.Body.Close()
161
162
	var result mbReleaseResponse
163
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
164
		return nil, fmt.Errorf("decode releases: %w", err)
165
	}
166
	return result.Releases, nil
167
}