feat(genius): fetch artist avatars by name and match names with transliteration suffix
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
795bb7de46
commit
139dd226fa
5 files changed
+144
-1
No files matched your search
@@ -0,0 +1,102 @@
|
||||
package genius
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
artistImageBatch = 20
|
||||
artistImageRecheck = 30 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// artistSource — то, что нужно проходу по аватаркам от Genius-клиента.
|
||||
type artistSource interface {
|
||||
Search(ctx context.Context, query string) ([]Song, error)
|
||||
Artist(ctx context.Context, id int64) (Artist, error)
|
||||
}
|
||||
|
||||
// artistImage ищет исполнителя по имени: берёт песню, у которой primary_artist совпал с именем,
|
||||
// и аватарку из неё либо из отдельного запроса артиста. Пустая строка — не нашли.
|
||||
func artistImage(ctx context.Context, src artistSource, name string) (string, error) {
|
||||
songs, err := src.Search(ctx, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, song := range songs {
|
||||
if !sameArtist(song.Artist.Name, name) {
|
||||
continue
|
||||
}
|
||||
if img := imageURL(song.Artist.ImageURL); img != "" {
|
||||
return img, nil
|
||||
}
|
||||
if song.Artist.ID <= 0 {
|
||||
continue
|
||||
}
|
||||
a, err := src.Artist(ctx, song.Artist.ID)
|
||||
if err != nil {
|
||||
var he *HTTPError
|
||||
if errors.As(err, &he) && he.StatusCode == 404 {
|
||||
continue
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return imageURL(a.ImageURL), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// fillArtistImages проходит по исполнителям без аватарки; каждого проверяет раз в artistImageRecheck.
|
||||
func (s *Service) fillArtistImages(ctx context.Context) error {
|
||||
var active bool
|
||||
if err := s.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM genius_state WHERE NOT paused AND cooldown_until<=now())`).Scan(&active); err != nil || !active {
|
||||
return err
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `SELECT id, name FROM artists
|
||||
WHERE image_path IS NULL AND genius_image_url IS NULL
|
||||
AND (genius_image_checked_at IS NULL OR genius_image_checked_at < now()-$1::interval)
|
||||
ORDER BY genius_image_checked_at NULLS FIRST LIMIT $2`, artistImageRecheck.String(), artistImageBatch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type row struct {
|
||||
id uuid.UUID
|
||||
name string
|
||||
}
|
||||
var batch []row
|
||||
for rows.Next() {
|
||||
var r row
|
||||
if err := rows.Scan(&r.id, &r.name); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
batch = append(batch, r)
|
||||
}
|
||||
rows.Close()
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range batch {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
reqCtx, cancel := context.WithTimeout(ctx, time.Minute)
|
||||
img, err := artistImage(reqCtx, s.client, r.name)
|
||||
cancel()
|
||||
if err != nil {
|
||||
var he *HTTPError
|
||||
if errors.As(err, &he) && he.StatusCode == 429 {
|
||||
_, _ = s.pool.Exec(ctx, `UPDATE genius_state SET cooldown_until=GREATEST(cooldown_until,$1)`, time.Now().Add(retryDelay(he.RetryAfter)))
|
||||
}
|
||||
return err
|
||||
}
|
||||
if _, err := s.pool.Exec(ctx, `UPDATE artists SET genius_image_checked_at=now(),
|
||||
genius_image_url=COALESCE(genius_image_url, NULLIF($2,'')) WHERE id=$1`, r.id, img); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user