forked from navidrome/navidrome
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
1 changed file
with
224 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |