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
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:
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 ""
|
||||
}
|
||||
Reference in new issue
Block a user