2 files changed,
65 insertions(+),
14 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-06-26 11:35:25 +0300
Authored at:
2026-06-26 11:26:58 +0300
Change ID:
xxwqyqrxpklruoswtoyykvsxmpyltwlo
Parent:
58cc1bd
M
sources/musicfeed/musicbrainz.go
··· 32 32 CoverArtArchive struct { 33 33 Artwork bool `json:"artwork"` 34 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"` 35 45 } 36 46 37 47 type mbReleaseResponse struct { ··· 150 160 151 161 func (a *musicbrainzAPI) fetchReleases(ctx context.Context, mbid string) ([]mbRelease, error) { 152 162 u := fmt.Sprintf( 153 - "%s/release?artist=%s&inc=artist-credits+release-groups&limit=100&fmt=json", 163 + "%s/release?artist=%s&inc=artist-credits+release-groups+url-rels&limit=100&fmt=json", 154 164 mbBaseURL, mbid, 155 165 ) 156 166 resp, err := a.doRequest(ctx, u)
M
sources/musicfeed/musicfeed.go
··· 31 31 artistName string 32 32 label string 33 33 hasArtwork bool 34 + spotifyURL string 35 + youtubeURL string 34 36 } 35 37 36 38 type releaseFetcher interface { ··· 188 190 sem := make(chan struct{}, 5) 189 191 190 192 for _, raw := range mf.artists { 191 - raw := raw 192 193 wg.Add(1) 193 194 sem <- struct{}{} 194 195 ··· 225 226 if len(r.ArtistCredit) > 0 { 226 227 artistName = r.ArtistCredit[0].Name 227 228 } 229 + spotifyURL, youtubeURL := extractExtURLs(r.Relations) 228 230 artistReleases = append(artistReleases, release{ 229 231 id: r.ID, 230 232 releaseGroupID: r.ReleaseGroup.ID, ··· 234 236 artistName: artistName, 235 237 label: label, 236 238 hasArtwork: r.CoverArtArchive.Artwork, 239 + spotifyURL: spotifyURL, 240 + youtubeURL: youtubeURL, 237 241 }) 238 242 } 239 243 ··· 282 286 } 283 287 284 288 if isMBID(entry.label) { 285 - name, err := mf.api.fetchArtist(ctx, entry.label) 286 - if err != nil { 287 - mf.logger.Warn("failed to fetch artist name", "mbid", entry.label, "err", err) 289 + name, ferr := mf.api.fetchArtist(ctx, entry.label) 290 + if ferr != nil { 291 + mf.logger.Warn("failed to fetch artist name", "mbid", entry.label, "err", ferr) 288 292 return entry.label, entry.label 289 293 } 290 - if err := mf.bucket.Set([]byte("mapping:"+name), []byte(entry.label)); err != nil { 291 - mf.logger.Warn("failed to cache artist mapping", "err", err) 294 + if berr := mf.bucket.Set([]byte("mapping:"+name), []byte(entry.label)); berr != nil { 295 + mf.logger.Warn("failed to cache artist mapping", "err", berr) 292 296 } 293 297 return entry.label, name 294 298 } ··· 363 367 return time.Time{} 364 368 } 365 369 370 +func extractExtURLs(rels []mbRelation) (spotifyURL, youtubeURL string) { 371 + for _, rel := range rels { 372 + if rel.Direction != "forward" { 373 + continue 374 + } 375 + resource := rel.URL.Resource 376 + switch { 377 + case strings.Contains(resource, "open.spotify.com"): 378 + spotifyURL = resource 379 + case strings.Contains(resource, "youtube.com") || strings.Contains(resource, "youtu.be"): 380 + youtubeURL = resource 381 + } 382 + } 383 + return 384 +} 385 + 366 386 func generateFeed(releases []release) *atom.Feed { 367 387 feed := atom.NewFeed("New Music Releases", "musicfeed") 368 388 for _, r := range releases { ··· 371 391 displayName = r.artistName 372 392 } 373 393 374 - links := []atom.Link{ 375 - { 376 - Rel: "alternate", 377 - Href: fmt.Sprintf("https://musicbrainz.org/release/%s", r.id), 378 - }, 379 - } 394 + links := []atom.Link{{ 395 + Rel: "alternate", 396 + Href: fmt.Sprintf("https://musicbrainz.org/release/%s", r.id), 397 + }} 380 398 381 399 content, contentType := releaseContent(r, displayName) 382 400 ··· 388 406 }) 389 407 } 390 408 409 + if r.spotifyURL != "" { 410 + links = append(links, atom.Link{ 411 + Rel: "related", 412 + Href: r.spotifyURL, 413 + Title: "Spotify", 414 + }) 415 + } 416 + if r.youtubeURL != "" { 417 + links = append(links, atom.Link{ 418 + Rel: "related", 419 + Href: r.youtubeURL, 420 + Title: "YouTube", 421 + }) 422 + } 423 + 391 424 releaseType := strings.TrimSpace(r.releaseType) 392 425 title := fmt.Sprintf("%s — %s", displayName, r.title) 393 426 if releaseType != "" { ··· 415 448 } 416 449 417 450 imageURL := fmt.Sprintf("%s/release/%s/front-250.jpg", caaBaseURL, r.id) 418 - parts := make([]string, 0, 4) 451 + parts := make([]string, 0, 6) 419 452 parts = append(parts, "<body>") 420 453 421 454 releaseType := strings.TrimSpace(r.releaseType) ··· 428 461 parts = append(parts, "<p>"+html.EscapeString(text)+"</p>") 429 462 parts = append(parts, 430 463 fmt.Sprintf(`<p><img src="%s" alt="%s"/></p>`, html.EscapeString(imageURL), html.EscapeString(r.title))) 464 + 465 + if r.spotifyURL != "" { 466 + parts = append(parts, fmt.Sprintf(`<p><a href="%s">Spotify</a></p>`, html.EscapeString(r.spotifyURL))) 467 + } 468 + if r.youtubeURL != "" { 469 + parts = append(parts, fmt.Sprintf(`<p><a href="%s">YouTube</a></p>`, html.EscapeString(r.youtubeURL))) 470 + } 471 + 431 472 parts = append(parts, "</body>") 432 473 433 474 return strings.Join(parts, ""), "xhtml"