refactor: isolate playback and upload use cases
ci / module boundaries / boundaries (push) Successful in 6s
ci / api / api (push) Successful in 47s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / db / db (push) Successful in 1m4s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m30s
ci / ts / ts (packages + web) (push) Successful in 2m54s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m14s

This commit is contained in:
mrleo1nid committed 2026-10-22 13:52:16 +03:00
1 parent c84f90e2cc
commit e158558da3
56 files changed
+3337 -2196

No files matched your search

+6 -49
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/fs"
@@ -17,7 +16,6 @@ import (
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"pipu/api/internal/library"
"pipu/api/internal/library/meta"
@@ -114,7 +112,7 @@ type outcome struct {
func (s *Service) process(ctx context.Context, f File) error {
abs := s.stagingAbs(f.StagingPath)
o := s.analyze(ctx, f, abs)
if err := s.saveOutcome(ctx, f, o); err != nil {
if err := s.store.SaveOutcome(ctx, f, o); err != nil {
return err
}
if o.status != StatusPendingReview {
@@ -256,38 +254,6 @@ func (s *Service) sourcePatterns(ctx context.Context, id uuid.UUID) []meta.Patte
return patterns
}
func (s *Service) saveOutcome(ctx context.Context, f File, o outcome) error {
return s.store.Transition(ctx, f.ID, []Status{StatusProcessing}, o.status, func(tx pgx.Tx) error {
var (
tags, metaJSON []byte
points []int32
)
if o.tags != nil {
tags, _ = json.Marshal(o.tags)
}
if o.meta != nil {
metaJSON, _ = json.Marshal(o.meta)
}
if o.print != nil {
points = make([]int32, len(o.print.Points))
for i, p := range o.print.Points {
points[i] = int32(p)
}
}
p := o.probe
_, err := tx.Exec(ctx, `
UPDATE upload_files SET sha256 = coalesce($2, sha256), format = NULLIF($3, ''), codec = NULLIF($4, ''),
bitrate = NULLIF($5, 0), sample_rate = NULLIF($6, 0), bit_depth = NULLIF($7, 0), duration_ms = NULLIF($8, 0),
tags = $9, metadata = $10, fingerprint = $11, duplicate_of_track_id = $12, duplicate_level = NULLIF($13, ''),
possible_duplicate = $14, quality_upgrade = $20, target_source_id = $15, target_rel_path = $16, status_reason = NULLIF($17, ''),
matched_artist_id = $18, matched_album_id = $19
WHERE id = $1`,
f.ID, o.sha, p.Format, p.Codec, p.Bitrate, p.SampleRate, p.BitDepth, p.DurationMs, tags, metaJSON, points,
o.dupTrack, o.dupLevel, o.possible, o.place.SourceID, o.place.RelPath, o.reason, o.place.ArtistID, o.place.AlbumID, o.quality)
return err
})
}
// --- Импорт ---
// importNext импортирует один одобренный файл; false — работы нет.
@@ -300,17 +266,14 @@ func (s *Service) importNext(ctx context.Context) bool {
s.log.Error("uploads: next to import", "err", err)
return false
}
if err := s.store.Transition(ctx, f.ID, []Status{StatusApproved}, StatusImporting, nil); err != nil {
if err := s.store.ChangeStatus(ctx, f.ID, []Status{StatusApproved}, StatusImporting); err != nil {
// Файл успели отклонить — берём следующий; сбой БД — ждём тика.
return errors.Is(err, ErrWrongStatus)
}
if err := s.importFile(ctx, f); err != nil {
s.log.Warn("upload import failed", "upload_id", f.ID, "err", err)
msg := err.Error()
if ferr := s.store.Transition(ctx, f.ID, []Status{StatusImporting}, StatusImportFailed, func(tx pgx.Tx) error {
_, err := tx.Exec(ctx, `UPDATE upload_files SET status_reason = $2 WHERE id = $1`, f.ID, CodeImportFailed+": "+msg)
return err
}); ferr != nil {
if ferr := s.store.FailImport(ctx, f.ID, CodeImportFailed+": "+msg); ferr != nil {
s.log.Error("uploads: mark import failed", "upload_id", f.ID, "err", ferr)
}
}
@@ -360,13 +323,7 @@ func (s *Service) importFile(ctx context.Context, f File) error {
if err != nil {
return fmt.Errorf("scan: %w", err)
}
if err := s.store.Transition(ctx, f.ID, []Status{StatusImporting}, StatusImported, func(tx pgx.Tx) error {
if _, err := tx.Exec(ctx, `UPDATE upload_files SET target_source_id = $2, target_rel_path = $3 WHERE id = $1`,
f.ID, pl.src.ID, rel); err != nil {
return err
}
return MarkImported(ctx, tx, f.ID, fileID)
}); err != nil {
if err := s.store.CompleteImport(ctx, f.ID, pl.src.ID, rel, fileID); err != nil {
return err
}
s.removeStaging(f)
@@ -439,7 +396,7 @@ func (s *Service) expire(ctx context.Context) {
if err != nil {
continue
}
if err := s.store.Transition(ctx, id, []Status{StatusUploading}, StatusExpired, nil); err == nil {
if err := s.store.ChangeStatus(ctx, id, []Status{StatusUploading}, StatusExpired); err == nil {
s.removeStaging(f)
}
}
@@ -447,7 +404,7 @@ func (s *Service) expire(ctx context.Context) {
// reconcile при старте: прерванный импорт — снова в approved; осиротевшие файлы staging удаляются.
func (s *Service) reconcile(ctx context.Context) {
if _, err := s.store.pool.Exec(ctx, `UPDATE upload_files SET status = 'approved', updated_at = now() WHERE status = 'importing'`); err != nil {
if err := s.store.ResetImporting(ctx); err != nil {
s.log.Error("uploads: reset importing", "err", err)
}
inUse, err := s.store.StagingInUse(ctx)