feat(uploads): match release groups with MusicBrainz and AcoustID
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

This commit is contained in:
mrleo1nid committed 2026-10-29 20:44:59 +03:00
1 parent 1e3a149aef
commit 39bf7d4b70
45 files changed
+4792 -417

No files matched your search

@@ -0,0 +1,144 @@
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 ""
}