ci / module boundaries / boundaries (push) Successful in 10s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m30s
ci / api / api (push) Successful in 58s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m1s
ci / db / db (push) Failing after 25s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m36s
ci / ts / ts (packages + web) (push) Failing after 6m3s
141 lines
4.9 KiB
Go
141 lines
4.9 KiB
Go
package uploads
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// EnsureReleaseGroups assigns a stable membership once the complete batch has been processed.
|
|
// Metadata confirmation cannot accidentally merge two groups or change another group's job.
|
|
func (st *Store) EnsureReleaseGroups(ctx context.Context, batch uuid.UUID) error {
|
|
tx, err := st.pool.Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback(ctx) }()
|
|
var cover *string
|
|
if err = tx.QueryRow(ctx, `SELECT cover_path FROM upload_batches WHERE id=$1 FOR UPDATE`, batch).Scan(&cover); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ErrNotFound
|
|
}
|
|
return err
|
|
}
|
|
var pending bool
|
|
if err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM upload_files WHERE batch_id=$1 AND status IN ('uploading','processing'))`, batch).Scan(&pending); err != nil {
|
|
return err
|
|
}
|
|
if pending {
|
|
return tx.Commit(ctx)
|
|
}
|
|
rows, err := tx.Query(ctx, `SELECT `+fileColumns+fileFrom+` WHERE f.batch_id=$1 AND f.release_group_id IS NULL ORDER BY f.id FOR UPDATE OF f`, batch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files := []File{}
|
|
for rows.Next() {
|
|
f, e := scanFile(rows)
|
|
if e != nil {
|
|
rows.Close()
|
|
return e
|
|
}
|
|
files = append(files, f)
|
|
}
|
|
err = rows.Err()
|
|
rows.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
drafts := groupReleaseFiles(files)
|
|
var existing int
|
|
if err = tx.QueryRow(ctx, `SELECT count(*) FROM upload_release_groups WHERE batch_id=$1`, batch).Scan(&existing); err != nil {
|
|
return err
|
|
}
|
|
for _, draft := range drafts {
|
|
group := newID()
|
|
var groupCover *string
|
|
if existing == 0 && len(drafts) == 1 {
|
|
groupCover = cover
|
|
}
|
|
err = tx.QueryRow(ctx, `INSERT INTO upload_release_groups(id,batch_id,grouping_key,artist,album,cover_path) VALUES($1,$2,$3,$4,$5,$6)
|
|
ON CONFLICT(batch_id,grouping_key) DO UPDATE SET grouping_key=EXCLUDED.grouping_key RETURNING id`, group, batch, draft.Key, draft.Artist, draft.Album, groupCover).Scan(&group)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err = tx.Exec(ctx, `UPDATE upload_files SET release_group_id=$2 WHERE batch_id=$1 AND id=ANY($3::uuid[]) AND release_group_id IS NULL`, batch, group, draft.Files); err != nil {
|
|
return err
|
|
}
|
|
if _, err = tx.Exec(ctx, `INSERT INTO upload_release_jobs(batch_id,group_key) VALUES($1,$2) ON CONFLICT DO NOTHING`, batch, group.String()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, err = tx.Exec(ctx, `DELETE FROM upload_release_jobs WHERE batch_id=$1 AND group_key=''`, batch); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit(ctx)
|
|
}
|
|
func (st *Store) ReleaseGroups(ctx context.Context, batch uuid.UUID) ([]ReleaseGroup, error) {
|
|
rows, err := st.pool.Query(ctx, `SELECT g.id::text,g.artist,g.album,g.cover_path IS NOT NULL,coalesce(j.state,'queued'),array_agg(f.id ORDER BY f.id)
|
|
FROM upload_release_groups g JOIN upload_files f ON f.release_group_id=g.id
|
|
LEFT JOIN upload_release_jobs j ON j.batch_id=g.batch_id AND j.group_key=g.id::text
|
|
WHERE g.batch_id=$1 AND f.status NOT IN ('withdrawn','rejected','expired','invalid')
|
|
GROUP BY g.id,j.state ORDER BY min(f.id::text)`, batch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := []ReleaseGroup{}
|
|
for rows.Next() {
|
|
var g ReleaseGroup
|
|
if err = rows.Scan(&g.ID, &g.Artist, &g.Album, &g.HasCover, &g.State, &g.FileIDs); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, g)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
func (st *Store) resolveGroupKey(ctx context.Context, batch uuid.UUID, key string) (string, error) {
|
|
if key != "" {
|
|
return key, nil
|
|
}
|
|
err := st.pool.QueryRow(ctx, `SELECT CASE WHEN count(*)=1 THEN min(id::text) ELSE '' END FROM upload_release_groups WHERE batch_id=$1`, batch).Scan(&key)
|
|
return key, err
|
|
}
|
|
func (st *Store) AllInReleaseGroup(ctx context.Context, batch uuid.UUID, group string) ([]File, error) {
|
|
rows, err := st.pool.Query(ctx, `SELECT `+fileColumns+fileFrom+` WHERE f.batch_id=$1 AND f.release_group_id::text=$2 ORDER BY f.id`, batch, group)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := []File{}
|
|
for rows.Next() {
|
|
f, e := scanFile(rows)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
out = append(out, f)
|
|
}
|
|
if err = rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(out) == 0 {
|
|
return nil, ErrNotFound
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (st *Store) UnusedReleaseCovers(ctx context.Context, batch uuid.UUID) ([]string, error) {
|
|
rows, err := st.pool.Query(ctx, `SELECT DISTINCT cover_path FROM upload_release_groups g WHERE g.batch_id=$1 AND g.cover_path IS NOT NULL
|
|
AND NOT EXISTS(SELECT 1 FROM upload_files f WHERE f.release_group_id=g.id AND f.status::text=ANY($2))
|
|
AND NOT EXISTS(SELECT 1 FROM upload_files active LEFT JOIN upload_release_groups ag ON ag.id=active.release_group_id
|
|
JOIN upload_batches ab ON ab.id=active.batch_id WHERE active.status::text=ANY($2)
|
|
AND (CASE WHEN active.release_group_id IS NULL THEN ab.cover_path ELSE ag.cover_path END)=g.cover_path)`, batch, occupyingStatuses)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
return pgx.CollectRows(rows, pgx.RowTo[string])
|
|
}
|