Import missing artist portraits from uploaded album folders
ci / module boundaries / boundaries (push) Successful in 6s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 2m1s
ci / ts / ts (packages + web) (push) Failing after 5m13s
ci / api / api (push) Successful in 41s
ci / db / db (push) Failing after 1m33s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 2m25s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 2m19s
ci / module boundaries / boundaries (push) Successful in 6s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 2m1s
ci / ts / ts (packages + web) (push) Failing after 5m13s
ci / api / api (push) Successful in 41s
ci / db / db (push) Failing after 1m33s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 2m25s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 2m19s
This commit is contained in:
1 parent
fd17c06397
commit
82bf015283
23 files changed
+829
-317
No files matched your search
@@ -0,0 +1,74 @@
|
||||
package library
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type artistImageStore interface {
|
||||
ArtistWithoutImageForFile(context.Context, uuid.UUID) (uuid.UUID, error)
|
||||
SetArtistImageIfMissing(context.Context, uuid.UUID, string) error
|
||||
}
|
||||
|
||||
type imageRenderer interface {
|
||||
StoreImage(context.Context, []byte) (string, error)
|
||||
}
|
||||
|
||||
// ArtistImages applies approved portraits to the album artist, never guests.
|
||||
type ArtistImages struct {
|
||||
store artistImageStore
|
||||
images imageRenderer
|
||||
}
|
||||
|
||||
func NewArtistImages(store *Store, covers *Covers) *ArtistImages {
|
||||
return &ArtistImages{store: store, images: covers}
|
||||
}
|
||||
|
||||
func (a *ArtistImages) ApplyArtistImage(ctx context.Context, file uuid.UUID, data []byte) error {
|
||||
id, err := a.store.ArtistWithoutImageForFile(ctx, file)
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := a.images.StoreImage(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.store.SetArtistImageIfMissing(ctx, id, key)
|
||||
}
|
||||
|
||||
func (c *Covers) StoreImage(ctx context.Context, data []byte) (string, error) {
|
||||
if err := os.MkdirAll(c.Dir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
sum := sha256.Sum256(data)
|
||||
key := hex.EncodeToString(sum[:8])
|
||||
_, err := c.render(ctx, data, key)
|
||||
return key, err
|
||||
}
|
||||
|
||||
func (st *Store) ArtistWithoutImageForFile(ctx context.Context, file uuid.UUID) (uuid.UUID, error) {
|
||||
var id uuid.UUID
|
||||
err := st.pool.QueryRow(ctx, `SELECT a.id FROM media_files f JOIN tracks t ON t.id=f.track_id
|
||||
JOIN albums al ON al.id=t.album_id JOIN artists a ON a.id=al.album_artist_id
|
||||
WHERE f.id=$1 AND a.image_path IS NULL AND a.genius_image_url IS NULL`, file).Scan(&id)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return id, ErrNotFound
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (st *Store) SetArtistImageIfMissing(ctx context.Context, id uuid.UUID, key string) error {
|
||||
// Another import or Genius may have supplied a portrait during rendering.
|
||||
_, err := st.pool.Exec(ctx, `UPDATE artists SET image_path=$2 WHERE id=$1
|
||||
AND image_path IS NULL AND genius_image_url IS NULL`, id, key)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package library
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type portraitStore struct {
|
||||
id uuid.UUID
|
||||
err error
|
||||
saved uuid.UUID
|
||||
key string
|
||||
}
|
||||
|
||||
func (s *portraitStore) ArtistWithoutImageForFile(context.Context, uuid.UUID) (uuid.UUID, error) {
|
||||
return s.id, s.err
|
||||
}
|
||||
func (s *portraitStore) SetArtistImageIfMissing(_ context.Context, id uuid.UUID, key string) error {
|
||||
s.saved, s.key = id, key
|
||||
return nil
|
||||
}
|
||||
|
||||
type portraitRenderer struct {
|
||||
data []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *portraitRenderer) StoreImage(_ context.Context, data []byte) (string, error) {
|
||||
r.data = data
|
||||
return "portrait-key", r.err
|
||||
}
|
||||
|
||||
func TestArtistPortraitOnlyRendersForAnArtistWithoutImage(t *testing.T) {
|
||||
for _, scenario := range []string{"new portrait", "already has image", "lookup failure", "render failure"} {
|
||||
t.Run(scenario, func(t *testing.T) {
|
||||
st := &portraitStore{id: uuid.New()}
|
||||
renderer := &portraitRenderer{}
|
||||
switch scenario {
|
||||
case "already has image":
|
||||
st.err = ErrNotFound
|
||||
case "lookup failure":
|
||||
st.err = errors.New("database unavailable")
|
||||
case "render failure":
|
||||
renderer.err = errors.New("invalid image")
|
||||
}
|
||||
images := &ArtistImages{store: st, images: renderer}
|
||||
err := images.ApplyArtistImage(context.Background(), uuid.New(), []byte("portrait"))
|
||||
if scenario == "lookup failure" || scenario == "render failure" {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
if scenario == "new portrait" {
|
||||
require.Equal(t, st.id, st.saved)
|
||||
require.Equal(t, "portrait-key", st.key)
|
||||
require.Equal(t, []byte("portrait"), renderer.data)
|
||||
} else {
|
||||
require.Equal(t, uuid.Nil, st.saved)
|
||||
}
|
||||
if scenario == "already has image" || scenario == "lookup failure" {
|
||||
require.Nil(t, renderer.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ type TrackRow struct {
|
||||
// ArtistRow — исполнитель со счётчиками.
|
||||
type ArtistRow struct {
|
||||
ImageURL *string
|
||||
ImagePath *string
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
AlbumCount int
|
||||
@@ -196,10 +197,10 @@ func (st *Store) ArtistAlbums(ctx context.Context, artistID uuid.UUID) ([]AlbumR
|
||||
func (st *Store) Artist(ctx context.Context, id uuid.UUID) (ArtistRow, error) {
|
||||
var a ArtistRow
|
||||
err := st.pool.QueryRow(ctx, `
|
||||
SELECT a.id, a.name, a.genius_image_url,
|
||||
SELECT a.id, a.name, a.genius_image_url, a.image_path,
|
||||
(SELECT count(*) FROM albums al WHERE al.album_artist_id = a.id),
|
||||
(SELECT count(DISTINCT ta.track_id) FROM track_artists ta WHERE ta.artist_id = a.id)
|
||||
FROM artists a WHERE a.id = $1`, id).Scan(&a.ID, &a.Name, &a.ImageURL, &a.AlbumCount, &a.TrackCount)
|
||||
FROM artists a WHERE a.id = $1`, id).Scan(&a.ID, &a.Name, &a.ImageURL, &a.ImagePath, &a.AlbumCount, &a.TrackCount)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ArtistRow{}, ErrNotFound
|
||||
}
|
||||
|
||||
@@ -485,7 +485,11 @@ func (h *Handler) GetArtist(ctx context.Context, req gen.GetArtistRequestObject)
|
||||
if err != nil {
|
||||
return nil, catalogErr(err)
|
||||
}
|
||||
return gen.GetArtist200JSONResponse{Id: a.ID, Name: a.Name, ImageUrl: a.ImageURL, AlbumCount: a.AlbumCount, TrackCount: a.TrackCount}, nil
|
||||
image := a.ImageURL
|
||||
if a.ImagePath != nil {
|
||||
image = h.coverURL(AlbumRow{ID: a.ID, CoverPath: a.ImagePath})
|
||||
}
|
||||
return gen.GetArtist200JSONResponse{Id: a.ID, Name: a.Name, ImageUrl: image, AlbumCount: a.AlbumCount, TrackCount: a.TrackCount}, nil
|
||||
}
|
||||
|
||||
// ListArtistAlbums реализует gen.StrictServerInterface.
|
||||
|
||||
Reference in new issue
Block a user