feat: finish the upload, playback, and test gaps from the docs
ci / api / api (push) Successful in 40s
ci / db / db (push) Successful in 1m3s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Failing after 1m48s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 46s
ci / ts / ts (packages + web) (push) Failing after 4m57s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 5m7s
ci / api / api (push) Successful in 40s
ci / db / db (push) Successful in 1m3s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Failing after 1m48s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 46s
ci / ts / ts (packages + web) (push) Failing after 4m57s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 5m7s
Quality upgrades, batch covers, staging preview, and resume after reload are in the queue. Loudness and crossfade work on web and Android, and the documented checks now have suites. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
2b76060ea3
commit
066ba24834
61 files changed
+2818
-463
No files matched your search
@@ -41,7 +41,16 @@ func (s *Service) Run(ctx context.Context) {
|
||||
s.reconcile(ctx)
|
||||
go s.loop(ctx, s.wakeProcess, pollEvery, func(ctx context.Context) bool { return s.processNext(ctx) })
|
||||
go s.loop(ctx, s.wakeImport, pollEvery, func(ctx context.Context) bool { return s.importNext(ctx) })
|
||||
go s.loop(ctx, nil, cleanupEvery, func(ctx context.Context) bool { s.expire(ctx); return false })
|
||||
go s.loop(ctx, nil, cleanupEvery, func(ctx context.Context) bool {
|
||||
s.expire(ctx)
|
||||
if err := s.store.ReconcileQuota(ctx); err != nil {
|
||||
s.log.Error("uploads: reconcile quota", "err", err)
|
||||
}
|
||||
return false
|
||||
})
|
||||
if err := s.store.ReconcileQuota(ctx); err != nil {
|
||||
s.log.Error("uploads: reconcile quota", "err", err)
|
||||
}
|
||||
wake(s.wakeProcess)
|
||||
wake(s.wakeImport)
|
||||
}
|
||||
@@ -98,6 +107,7 @@ type outcome struct {
|
||||
dupTrack *uuid.UUID
|
||||
dupLevel string
|
||||
possible bool
|
||||
quality bool
|
||||
place placement
|
||||
}
|
||||
|
||||
@@ -144,8 +154,12 @@ func (s *Service) analyze(ctx context.Context, f File, abs string) outcome {
|
||||
cands, err := s.store.FingerprintCandidates(ctx, probe.DurationMs, FingerprintTolerance, f.ID)
|
||||
if err == nil {
|
||||
if c, ok := bestMatch(fp.Points, cands); ok {
|
||||
o.status, o.reason, o.dupTrack, o.dupLevel = StatusDuplicate, CodeDuplicate, c.TrackID, DupFingerprint
|
||||
return o
|
||||
if s.keepAsUpgrade(ctx, &o, c.TrackID, probe) {
|
||||
// лучшее качество — дальше как обычный файл на рассмотрение
|
||||
} else {
|
||||
o.status, o.reason, o.dupTrack, o.dupLevel = StatusDuplicate, CodeDuplicate, c.TrackID, DupFingerprint
|
||||
return o
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !errors.Is(err, library.ErrNoFPCalc) {
|
||||
@@ -183,6 +197,26 @@ func (s *Service) analyze(ctx context.Context, f File, abs string) outcome {
|
||||
return o
|
||||
}
|
||||
|
||||
// keepAsUpgrade — совпадение по отпечатку, но файл лучше библиотечного и режим замены включён.
|
||||
// true — не считать дублем, оставить на рассмотрение.
|
||||
func (s *Service) keepAsUpgrade(ctx context.Context, o *outcome, track *uuid.UUID, probe library.Probe) bool {
|
||||
if track == nil {
|
||||
return false
|
||||
}
|
||||
st, err := s.store.Settings(ctx)
|
||||
if err != nil || !st.AllowQualityUpgrade {
|
||||
return false
|
||||
}
|
||||
codec, format, bitrate, err := s.store.BestAudio(ctx, *track)
|
||||
if err != nil || !BetterQuality(probe.Codec, probe.Format, probe.Bitrate, codec, format, bitrate) {
|
||||
return false
|
||||
}
|
||||
o.quality = true
|
||||
o.dupTrack = track
|
||||
o.dupLevel = DupFingerprint
|
||||
return true
|
||||
}
|
||||
|
||||
// bestMatch — отпечаток, похожий не меньше порога library.SameRecording.
|
||||
func bestMatch(points []uint32, cands []PrintCandidate) (PrintCandidate, bool) {
|
||||
best, score := PrintCandidate{}, 0.0
|
||||
@@ -245,11 +279,11 @@ func (s *Service) saveOutcome(ctx context.Context, f File, o outcome) error {
|
||||
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, target_source_id = $15, target_rel_path = $16, status_reason = NULLIF($17, ''),
|
||||
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.dupTrack, o.dupLevel, o.possible, o.place.SourceID, o.place.RelPath, o.reason, o.place.ArtistID, o.place.AlbumID, o.quality)
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -295,7 +329,7 @@ func (s *Service) importFile(ctx context.Context, f File) error {
|
||||
}
|
||||
rel := *pl.RelPath
|
||||
stagingRel, sum := f.StagingPath, f.SHA256
|
||||
if f.MetadataEdited {
|
||||
if f.MetadataEdited || (f.CoverPath != nil && *f.CoverPath != "") {
|
||||
tagged, tsum, err := s.writeTags(ctx, f, *f.Meta, path.Ext(rel))
|
||||
if err != nil {
|
||||
return fmt.Errorf("write tags: %w", err)
|
||||
@@ -352,7 +386,12 @@ func (s *Service) writeTags(ctx context.Context, f File, m Meta, ext string) (st
|
||||
}
|
||||
rel := f.StagingPath + ".tagged" + strings.ToLower(ext)
|
||||
out := s.stagingAbs(rel)
|
||||
args := []string{"-v", "error", "-y", "-i", s.stagingAbs(f.StagingPath), "-map", "0", "-c", "copy", "-map_metadata", "0"}
|
||||
args := []string{"-v", "error", "-y", "-i", s.stagingAbs(f.StagingPath)}
|
||||
if f.CoverPath != nil && *f.CoverPath != "" {
|
||||
args = append(args, "-i", s.stagingAbs(*f.CoverPath), "-map", "0:a", "-map", "1", "-c", "copy", "-disposition:v:0", "attached_pic", "-map_metadata", "0")
|
||||
} else {
|
||||
args = append(args, "-map", "0", "-c", "copy", "-map_metadata", "0")
|
||||
}
|
||||
set := func(k, v string) { args = append(args, "-metadata", k+"="+v) }
|
||||
set("title", m.Title)
|
||||
set("artist", m.Artist)
|
||||
|
||||
Reference in new issue
Block a user