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
145 lines
4.1 KiB
Go
145 lines
4.1 KiB
Go
package uploads
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"path"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"pipu/api/internal/library"
|
|
"pipu/api/internal/library/meta"
|
|
"pipu/api/internal/platform/apperr"
|
|
)
|
|
|
|
type ReleaseGroup struct {
|
|
ID string `json:"id"`
|
|
Artist string `json:"artist"`
|
|
Album string `json:"album"`
|
|
FileIDs []uuid.UUID `json:"file_ids"`
|
|
HasCover bool `json:"has_cover"`
|
|
State string `json:"state"`
|
|
}
|
|
type releaseGroupDraft struct {
|
|
Key, Artist, Album string
|
|
Files []uuid.UUID
|
|
}
|
|
|
|
var discDirectory = regexp.MustCompile(`(?i)^(?:cd|disc|disk|диск)[ ._-]*0*\d+(?:\s*[-(].*)?$`)
|
|
|
|
func releaseFolder(f File) string {
|
|
name := f.OriginalName
|
|
if f.ClientRelPath != nil {
|
|
name = *f.ClientRelPath
|
|
}
|
|
dir := path.Dir(strings.ReplaceAll(name, `\`, "/"))
|
|
for dir != "." && dir != "/" && discDirectory.MatchString(path.Base(dir)) {
|
|
dir = path.Dir(dir)
|
|
}
|
|
if dir == "." || dir == "/" {
|
|
return ""
|
|
}
|
|
return strings.ToLower(dir)
|
|
}
|
|
func releaseBase(f File) string {
|
|
m := f.Meta
|
|
year := ""
|
|
if m.Year != nil {
|
|
year = strconv.Itoa(*m.Year)
|
|
}
|
|
if m.Album != "" && m.Album != library.UnknownAlbum {
|
|
if m.AlbumArtist != "" && m.AlbumArtist != meta.UnknownArtist {
|
|
return "tags\x00" + meta.Key(m.AlbumArtist) + "\x00" + meta.Key(m.Album) + "\x00" + year
|
|
}
|
|
if folder := releaseFolder(f); folder != "" {
|
|
return "album-folder\x00" + folder + "\x00" + meta.Key(m.Album) + "\x00" + year
|
|
}
|
|
return "tags\x00" + meta.Key(m.Artist) + "\x00" + meta.Key(m.Album) + "\x00" + year
|
|
}
|
|
return "folder\x00" + releaseFolder(f)
|
|
}
|
|
func groupReleaseFiles(files []File) []releaseGroupDraft {
|
|
files = releaseFiles(files)
|
|
known := map[string]map[string]bool{}
|
|
for _, f := range files {
|
|
base := releaseBase(f)
|
|
if known[base] == nil {
|
|
known[base] = map[string]bool{}
|
|
}
|
|
if id := tagID(f.Tags, "musicbrainz_albumid", "musicbrainz album id", "musicbrainz release id", "musicbrainz_releaseid"); id != "" {
|
|
known[base][id] = true
|
|
}
|
|
}
|
|
groups := map[string]*releaseGroupDraft{}
|
|
for _, f := range files {
|
|
base := releaseBase(f)
|
|
id := tagID(f.Tags, "musicbrainz_albumid", "musicbrainz album id", "musicbrainz release id", "musicbrainz_releaseid")
|
|
if id == "" && len(known[base]) == 1 {
|
|
for value := range known[base] {
|
|
id = value
|
|
}
|
|
}
|
|
key := base
|
|
if id != "" {
|
|
key = "mbid\x00" + id
|
|
}
|
|
sum := sha256.Sum256([]byte(key))
|
|
key = hex.EncodeToString(sum[:])
|
|
if groups[key] == nil {
|
|
artist := f.Meta.AlbumArtist
|
|
if artist == "" {
|
|
artist = f.Meta.Artist
|
|
}
|
|
groups[key] = &releaseGroupDraft{Key: key, Artist: artist, Album: f.Meta.Album, Files: []uuid.UUID{}}
|
|
}
|
|
groups[key].Files = append(groups[key].Files, f.ID)
|
|
}
|
|
out := []releaseGroupDraft{}
|
|
for _, g := range groups {
|
|
sort.Slice(g.Files, func(i, j int) bool { return g.Files[i].String() < g.Files[j].String() })
|
|
out = append(out, *g)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Files[0].String() < out[j].Files[0].String() })
|
|
return out
|
|
}
|
|
|
|
// releaseScope resolves only a group belonging to this batch. A mixed batch requires explicit selection.
|
|
func (s *Service) releaseScope(ctx context.Context, batch uuid.UUID, requested string) ([]File, []ReleaseGroup, string, error) {
|
|
if err := s.store.EnsureReleaseGroups(ctx, batch); err != nil {
|
|
return nil, nil, "", notFound(err)
|
|
}
|
|
groups, err := s.store.ReleaseGroups(ctx, batch)
|
|
if err != nil {
|
|
return nil, nil, "", err
|
|
}
|
|
if requested == "" && len(groups) == 1 {
|
|
requested = groups[0].ID
|
|
}
|
|
if requested != "" {
|
|
found := false
|
|
for _, g := range groups {
|
|
found = found || g.ID == requested
|
|
}
|
|
if !found {
|
|
return nil, groups, "", apperr.NotFound("uploads.release_group_not_found", "release group does not belong to batch")
|
|
}
|
|
files, err := s.store.AllInReleaseGroup(ctx, batch, requested)
|
|
return files, groups, requested, err
|
|
}
|
|
if len(groups) > 1 {
|
|
return nil, groups, "", nil
|
|
}
|
|
files, err := s.store.AllInBatch(ctx, batch)
|
|
return files, groups, "", err
|
|
}
|
|
func firstGroup(values []string) string {
|
|
if len(values) > 0 {
|
|
return values[0]
|
|
}
|
|
return ""
|
|
}
|