Skip to content

Commit

Permalink
cmd: add use_mbid command
Browse files Browse the repository at this point in the history
Converts the library to use MusicBrainz IDs for albums, artists, and
albumartists, thus fixing navidrome#489. All playlists, stars, etc. are kept.

This is an irreversible transformation; the user is given a warning
and may cancel within 10 seconds by pressing ^C.

A full rescan is trigged to un-merge the albums.
  • Loading branch information
vs49688 committed Oct 15, 2021
1 parent 5b9c022 commit 1862bb3
Showing 1 changed file with 224 additions and 0 deletions.
224 changes: 224 additions & 0 deletions cmd/mbzid_migration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package cmd

import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"strings"
"time"

"github.com/google/uuid"
"github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/persistence"
"github.com/navidrome/navidrome/utils"
"github.com/spf13/cobra"
)

var mbidCmd = &cobra.Command{
Use: "use_mbid",
Short: "Use MusicBrainz IDs",
Long: "Convert Navidrome's database to use MusicBrainz IDs",
Run: func(cmd *cobra.Command, args []string) {
db.EnsureLatestVersion()
if err := convertToMbzIDs(); err != nil {
log.Error("Error handling MusicBrainz cataloging. Aborting", err)
os.Exit(1)
return
}
},
}

func init() {
rootCmd.AddCommand(mbidCmd)
}

func warnMbzMigration(dur time.Duration) bool {
log.Warn("About to convert database to use MusicBrainz metadata. This CANNOT be undone.")
log.Warn(fmt.Sprintf("If this isn't intentional, press ^C NOW. Will begin in %s...", dur))

sc := make(chan os.Signal, 1)
signal.Notify(sc, os.Interrupt)

defer signal.Stop(sc)

select {
case <- sc:
return false
case <- time.After(dur):
return true
}
}

type mbidMap map[string][]string

func (m mbidMap) maybeGet(s string, idx uint) string {
if val, ok := m[s]; ok {
return val[idx]
}

return s
}

func migrateArtists(repo model.ArtistRepository) (mbidMap, error) {
artists, err := repo.GetAll()
if err != nil {
return nil, err
}

mbmap := make(mbidMap)
toRemove := []string{}

for _, a := range artists {
if a.MbzArtistID == "" {
continue
}

if _, err := uuid.Parse(a.MbzArtistID); err != nil {
log.Warn(fmt.Sprintf("Ignoring invalid MBID %s", a.MbzArtistID))
continue
}

toRemove = append(toRemove, a.ID)

mbmap[a.ID] = append(mbmap[a.ID], a.MbzArtistID)
aa := a
aa.ID = a.MbzArtistID
if err = repo.Put(&aa); err != nil {
return nil, err
}

if err = repo.MoveAnnotation(a.ID, aa.ID); err != nil {
return nil, err
}
}

log.Debug(fmt.Sprintf("Removing %d leftover artists", len(toRemove)))

return mbmap, utils.RangeByChunks(toRemove, 100, func(s []string) error {
return repo.DeleteMany(s...)
})
}

func migrateAlbums(repo model.AlbumRepository, artistMap mbidMap) (mbidMap, error) {
albums, err := repo.GetAll()
if err != nil {
return nil, err
}

mbmap := make(mbidMap)
toRemove := []string{}

for _, a := range albums {
if a.MbzAlbumID == "" {
continue
}

if _, err := uuid.Parse(a.MbzAlbumID); err != nil {
log.Warn(fmt.Sprintf("Ignoring invalid MBID %s", a.MbzAlbumID))
continue
}

toRemove = append(toRemove, a.ID)

mbmap[a.ID] = append(mbmap[a.ID], a.MbzAlbumID)

aa := a
aa.ID = a.MbzAlbumID
aa.ArtistID = artistMap.maybeGet(a.ArtistID, 0)
aa.AlbumArtistID = artistMap.maybeGet(a.AlbumArtistID, 0)

newArtists := []string{}
for _, a := range strings.Split(a.AllArtistIDs, " ") {
newArtists = append(newArtists, artistMap.maybeGet(a, 0))
}
aa.AllArtistIDs = utils.SanitizeStrings(newArtists...)

if err = repo.Put(&aa); err != nil {
return nil, err
}

if err = repo.MoveAnnotation(a.ID, aa.ID); err != nil {
return nil, err
}
}

log.Debug(fmt.Sprintf("Removing %d leftover albums", len(toRemove)))
return mbmap, utils.RangeByChunks(toRemove, 100, func(s []string) error {
return repo.DeleteMany(s...)
})
}

func migrateMediaFiles(repo model.MediaFileRepository, albumMap mbidMap, artistMap mbidMap) error {
mfs, err := repo.GetAll()
if err != nil {
return err
}

for _, t := range mfs {
tt := t

/*
* This won't actually split the merged albums, a full
* rescan is needed for that.
*/
tt.ArtistID = artistMap.maybeGet(t.ArtistID, 0)
tt.AlbumArtistID = artistMap.maybeGet(t.AlbumArtistID, 0)
tt.AlbumID = albumMap.maybeGet(t.AlbumID, 0)
tt.UpdatedAt = time.Time{} /* To force a rescan. */

if err = repo.Put(&tt); err != nil {
return err
}
}

return nil
}

func convertToMbzIDs() error {
db := db.Db()
ctx := context.Background()

ds := persistence.New(db)

return ds.WithTx(func(tx model.DataStore) error {
props := tx.Property(ctx)

useMbzIDs, err := props.DefaultGetBool(model.PropUsingMbzIDs, false)
if err != nil {
return err
}

// Nothing to do
if useMbzIDs {
return nil
}

if !warnMbzMigration(10 * time.Second) {
return errors.New("user aborted")
}

artistMap, err := migrateArtists(tx.Artist(ctx))
if err != nil {
return err
}

albumMap, err := migrateAlbums(tx.Album(ctx), artistMap)
if err != nil {
return err
}

if err = migrateMediaFiles(tx.MediaFile(ctx), albumMap, artistMap); err != nil {
return err
}

if err = props.Put(model.PropUsingMbzIDs, "true"); err != nil {
return err
}

return props.DeletePrefixed(model.PropLastScan)
})
}

0 comments on commit 1862bb3

Please sign in to comment.