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
132 lines
3.7 KiB
Go
132 lines
3.7 KiB
Go
package uploads
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"pipu/api/internal/library"
|
|
"pipu/api/internal/library/meta"
|
|
)
|
|
|
|
// Ports are owned by the upload use cases; implementations remain in library.
|
|
type SourceCatalog interface {
|
|
GetSource(context.Context, uuid.UUID) (library.Source, error)
|
|
ListSources(context.Context) ([]library.Source, error)
|
|
Patterns(context.Context, uuid.UUID) ([]library.PatternRow, error)
|
|
}
|
|
type RootResolver interface {
|
|
Root(library.Source) (string, error)
|
|
}
|
|
type ImportedScanner interface {
|
|
ScanFile(context.Context, library.Source, string, string) (uuid.UUID, error)
|
|
}
|
|
type PlacementStore interface {
|
|
Settings(context.Context) (Settings, error)
|
|
MatchCatalog(context.Context, string, string) (*uuid.UUID, *uuid.UUID, int, error)
|
|
BatchMaxDisc(context.Context, uuid.UUID, string) (int, error)
|
|
}
|
|
|
|
type PlacementPlanner struct {
|
|
store PlacementStore
|
|
lib SourceCatalog
|
|
roots RootResolver
|
|
}
|
|
|
|
// --- Раскладка в хранилище ---
|
|
|
|
// placement — куда ляжет файл; Reason — почему не получится (код), путь тогда nil.
|
|
type placement struct {
|
|
SourceID *uuid.UUID
|
|
RelPath *string
|
|
Reason string
|
|
ArtistID *uuid.UUID
|
|
AlbumID *uuid.UUID
|
|
src library.Source
|
|
root string
|
|
}
|
|
|
|
// pickSource — хранилище: выбранное админом → по умолчанию → единственное принимающее загрузки.
|
|
func (s *PlacementPlanner) pickSource(ctx context.Context, chosen *uuid.UUID) (library.Source, string) {
|
|
id := chosen
|
|
if id == nil {
|
|
st, err := s.store.Settings(ctx)
|
|
if err == nil {
|
|
id = st.DefaultSourceID
|
|
}
|
|
}
|
|
if id != nil {
|
|
src, err := s.lib.GetSource(ctx, *id)
|
|
if err != nil || !src.AcceptsUploads {
|
|
return library.Source{}, CodeNoTarget
|
|
}
|
|
return src, ""
|
|
}
|
|
sources, err := s.lib.ListSources(ctx)
|
|
if err != nil {
|
|
return library.Source{}, CodeNoTarget
|
|
}
|
|
var found []library.Source
|
|
for _, src := range sources {
|
|
if src.AcceptsUploads {
|
|
found = append(found, src)
|
|
}
|
|
}
|
|
if len(found) != 1 {
|
|
return library.Source{}, CodeNoTarget
|
|
}
|
|
return found[0], ""
|
|
}
|
|
|
|
// plan считает целевой путь по write_template хранилища; существующие папки исполнителя / альбома
|
|
// переиспользуются в их написании (docs/uploads.md#очистка-имён).
|
|
func (s *PlacementPlanner) plan(ctx context.Context, f File, m Meta, chosen *uuid.UUID) placement {
|
|
var (
|
|
pl placement
|
|
discs int // дисков в альбоме: в каталоге, в пачке, у самого файла
|
|
)
|
|
if artist, album, maxDisc, err := s.store.MatchCatalog(ctx, meta.Key(m.AlbumArtist), meta.Key(m.Album)); err == nil {
|
|
pl.ArtistID, pl.AlbumID, discs = artist, album, maxDisc
|
|
}
|
|
src, reason := s.pickSource(ctx, chosen)
|
|
if reason != "" {
|
|
pl.Reason = reason
|
|
return pl
|
|
}
|
|
pl.SourceID, pl.src = &src.ID, src
|
|
if src.WriteTemplate == nil {
|
|
pl.Reason = CodeTemplate
|
|
return pl
|
|
}
|
|
tpl, err := meta.ParseTemplate(*src.WriteTemplate)
|
|
if err != nil {
|
|
pl.Reason = CodeTemplate
|
|
return pl
|
|
}
|
|
if n, err := s.store.BatchMaxDisc(ctx, f.BatchID, m.Album); err == nil {
|
|
discs = max(discs, n)
|
|
}
|
|
if m.Disc != nil {
|
|
discs = max(discs, *m.Disc)
|
|
}
|
|
rel, err := tpl.Render(meta.TemplateInput{Fields: m.Fields(), DiscCount: discs, Ext: extFor(f)})
|
|
if err != nil {
|
|
var mf *meta.MissingFieldError
|
|
if errors.As(err, &mf) {
|
|
pl.Reason = CodeMissingField
|
|
} else {
|
|
pl.Reason = CodeTemplate
|
|
}
|
|
return pl
|
|
}
|
|
root, err := s.roots.Root(src)
|
|
if err != nil {
|
|
pl.Reason = CodeNoTarget
|
|
return pl
|
|
}
|
|
rel = reuseExisting(root, rel)
|
|
pl.RelPath, pl.root = &rel, root
|
|
return pl
|
|
}
|