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 }