Fold stale missing tracks into their on-disk replacement (M485) #141
@@ -118,82 +118,25 @@ func MergeDuplicateGroup(
|
||||
res := MergeResult{Tier: group.Tier, Survivor: mergedCopyOf(survivor)}
|
||||
likers := map[string]struct{}{}
|
||||
changes := mergeChanges{}
|
||||
survivorKey := syncpkg.FormatUUID(survivor.ID)
|
||||
|
||||
for _, l := range losers {
|
||||
ids := struct{ survivor, loser pgtype.UUID }{survivor.ID, l.ID}
|
||||
loserKey := syncpkg.FormatUUID(l.ID)
|
||||
|
||||
n, err := tq.MergeRepointPlayEvents(ctx, dbq.MergeRepointPlayEventsParams{SurvivorID: ids.survivor, LoserID: ids.loser})
|
||||
if err != nil {
|
||||
return MergeResult{}, fmt.Errorf("move play events: %w", err)
|
||||
}
|
||||
res.PlayEvents += n
|
||||
n, err = tq.MergeRepointSkipEvents(ctx, dbq.MergeRepointSkipEventsParams{SurvivorID: ids.survivor, LoserID: ids.loser})
|
||||
if err != nil {
|
||||
return MergeResult{}, fmt.Errorf("move skip events: %w", err)
|
||||
}
|
||||
res.SkipEvents += n
|
||||
if _, err := tq.MergeRepointContextualLikes(ctx, dbq.MergeRepointContextualLikesParams{SurvivorID: ids.survivor, LoserID: ids.loser}); err != nil {
|
||||
return MergeResult{}, fmt.Errorf("move contextual likes: %w", err)
|
||||
}
|
||||
if _, err := tq.MergeRepointPlaybackErrors(ctx, dbq.MergeRepointPlaybackErrorsParams{SurvivorID: ids.survivor, LoserID: ids.loser}); err != nil {
|
||||
return MergeResult{}, fmt.Errorf("move playback errors: %w", err)
|
||||
}
|
||||
if _, err := tq.MergeRepointLidarrRequests(ctx, dbq.MergeRepointLidarrRequestsParams{SurvivorID: ids.survivor, LoserID: ids.loser}); err != nil {
|
||||
return MergeResult{}, fmt.Errorf("move lidarr request matches: %w", err)
|
||||
}
|
||||
|
||||
playlists, err := tq.MergeRepointPlaylistTracks(ctx, dbq.MergeRepointPlaylistTracksParams{SurvivorID: ids.survivor, LoserID: ids.loser})
|
||||
if err != nil {
|
||||
return MergeResult{}, fmt.Errorf("move playlist entries: %w", err)
|
||||
}
|
||||
res.PlaylistEntries += len(playlists)
|
||||
for _, pl := range playlists {
|
||||
plKey := syncpkg.FormatUUID(pl)
|
||||
changes.playlistDelete(syncpkg.EncodePlaylistTrackID(plKey, loserKey))
|
||||
changes.playlistUpsert(syncpkg.EncodePlaylistTrackID(plKey, survivorKey))
|
||||
}
|
||||
|
||||
users, err := tq.MergeCopyGeneralLikes(ctx, dbq.MergeCopyGeneralLikesParams{SurvivorID: ids.survivor, LoserID: ids.loser})
|
||||
if err != nil {
|
||||
return MergeResult{}, fmt.Errorf("move likes: %w", err)
|
||||
}
|
||||
for _, u := range users {
|
||||
userKey := syncpkg.FormatUUID(u)
|
||||
likers[userKey] = struct{}{}
|
||||
changes.likeDelete(syncpkg.EncodeLikeID(userKey, loserKey))
|
||||
changes.likeUpsert(syncpkg.EncodeLikeID(userKey, survivorKey))
|
||||
}
|
||||
|
||||
if _, err := tq.MergeCopyTrackTags(ctx, dbq.MergeCopyTrackTagsParams{SurvivorID: ids.survivor, LoserID: ids.loser}); err != nil {
|
||||
return MergeResult{}, fmt.Errorf("merge tags: %w", err)
|
||||
}
|
||||
if _, err := tq.MergeCopyTrackSimilarity(ctx, dbq.MergeCopyTrackSimilarityParams{SurvivorID: ids.survivor, LoserID: ids.loser}); err != nil {
|
||||
return MergeResult{}, fmt.Errorf("merge similarity: %w", err)
|
||||
}
|
||||
if err := tq.MergeInheritTrackMbid(ctx, dbq.MergeInheritTrackMbidParams{SurvivorID: ids.survivor, LoserID: ids.loser}); err != nil {
|
||||
return MergeResult{}, fmt.Errorf("inherit recording mbid: %w", err)
|
||||
}
|
||||
|
||||
// Everything the loser carried now sits on the survivor, so the CASCADE
|
||||
// this delete sets off has nothing left to destroy.
|
||||
deleted, err := tq.DeleteTrack(ctx, l.ID)
|
||||
if err != nil {
|
||||
return MergeResult{}, fmt.Errorf("delete merged copy: %w", err)
|
||||
}
|
||||
tidied, err := tidyEmptiedAlbum(ctx, tq, deleted.AlbumID)
|
||||
f, err := foldTrackInto(ctx, tq, survivor.ID, l.ID, &changes)
|
||||
if err != nil {
|
||||
return MergeResult{}, err
|
||||
}
|
||||
if tidied.AlbumID != nil {
|
||||
res.DeletedAlbumIDs = append(res.DeletedAlbumIDs, *tidied.AlbumID)
|
||||
res.PlayEvents += f.PlayEvents
|
||||
res.SkipEvents += f.SkipEvents
|
||||
res.PlaylistEntries += f.PlaylistEntries
|
||||
for _, u := range f.Likers {
|
||||
likers[u] = struct{}{}
|
||||
}
|
||||
if tidied.ArtistID != nil {
|
||||
res.DeletedArtistIDs = append(res.DeletedArtistIDs, *tidied.ArtistID)
|
||||
if f.DeletedAlbumID != nil {
|
||||
res.DeletedAlbumIDs = append(res.DeletedAlbumIDs, *f.DeletedAlbumID)
|
||||
}
|
||||
if f.DeletedArtistID != nil {
|
||||
res.DeletedArtistIDs = append(res.DeletedArtistIDs, *f.DeletedArtistID)
|
||||
}
|
||||
res.Removed = append(res.Removed, mergedCopyOf(l))
|
||||
changes.trackDelete(loserKey)
|
||||
}
|
||||
res.Likes = len(likers)
|
||||
|
||||
@@ -223,6 +166,98 @@ func MergeDuplicateGroup(
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// foldResult is what one fold moved onto the survivor.
|
||||
type foldResult struct {
|
||||
PlayEvents int64
|
||||
SkipEvents int64
|
||||
PlaylistEntries int
|
||||
Likers []string // users whose like now sits on the survivor
|
||||
|
||||
DeletedAlbumID *pgtype.UUID
|
||||
DeletedArtistID *pgtype.UUID
|
||||
}
|
||||
|
||||
// foldTrackInto moves everything the loser row carries onto the survivor, then
|
||||
// deletes the loser row and tidies an album or artist it leaves empty. It is the
|
||||
// row half of a merge, shared by the duplicate merge and the missing-pair pass
|
||||
// (M485): neither touches a file here, and both run it inside their own
|
||||
// transaction. The sync changes it owes clients go into changes, which the
|
||||
// caller logs once before committing.
|
||||
func foldTrackInto(
|
||||
ctx context.Context, tq *dbq.Queries, survivorID, loserID pgtype.UUID, changes *mergeChanges,
|
||||
) (foldResult, error) {
|
||||
var f foldResult
|
||||
survivorKey := syncpkg.FormatUUID(survivorID)
|
||||
loserKey := syncpkg.FormatUUID(loserID)
|
||||
|
||||
n, err := tq.MergeRepointPlayEvents(ctx, dbq.MergeRepointPlayEventsParams{SurvivorID: survivorID, LoserID: loserID})
|
||||
if err != nil {
|
||||
return f, fmt.Errorf("move play events: %w", err)
|
||||
}
|
||||
f.PlayEvents = n
|
||||
n, err = tq.MergeRepointSkipEvents(ctx, dbq.MergeRepointSkipEventsParams{SurvivorID: survivorID, LoserID: loserID})
|
||||
if err != nil {
|
||||
return f, fmt.Errorf("move skip events: %w", err)
|
||||
}
|
||||
f.SkipEvents = n
|
||||
if _, err := tq.MergeRepointContextualLikes(ctx, dbq.MergeRepointContextualLikesParams{SurvivorID: survivorID, LoserID: loserID}); err != nil {
|
||||
return f, fmt.Errorf("move contextual likes: %w", err)
|
||||
}
|
||||
if _, err := tq.MergeRepointPlaybackErrors(ctx, dbq.MergeRepointPlaybackErrorsParams{SurvivorID: survivorID, LoserID: loserID}); err != nil {
|
||||
return f, fmt.Errorf("move playback errors: %w", err)
|
||||
}
|
||||
if _, err := tq.MergeRepointLidarrRequests(ctx, dbq.MergeRepointLidarrRequestsParams{SurvivorID: survivorID, LoserID: loserID}); err != nil {
|
||||
return f, fmt.Errorf("move lidarr request matches: %w", err)
|
||||
}
|
||||
|
||||
playlists, err := tq.MergeRepointPlaylistTracks(ctx, dbq.MergeRepointPlaylistTracksParams{SurvivorID: survivorID, LoserID: loserID})
|
||||
if err != nil {
|
||||
return f, fmt.Errorf("move playlist entries: %w", err)
|
||||
}
|
||||
f.PlaylistEntries = len(playlists)
|
||||
for _, pl := range playlists {
|
||||
plKey := syncpkg.FormatUUID(pl)
|
||||
changes.playlistDelete(syncpkg.EncodePlaylistTrackID(plKey, loserKey))
|
||||
changes.playlistUpsert(syncpkg.EncodePlaylistTrackID(plKey, survivorKey))
|
||||
}
|
||||
|
||||
users, err := tq.MergeCopyGeneralLikes(ctx, dbq.MergeCopyGeneralLikesParams{SurvivorID: survivorID, LoserID: loserID})
|
||||
if err != nil {
|
||||
return f, fmt.Errorf("move likes: %w", err)
|
||||
}
|
||||
for _, u := range users {
|
||||
userKey := syncpkg.FormatUUID(u)
|
||||
f.Likers = append(f.Likers, userKey)
|
||||
changes.likeDelete(syncpkg.EncodeLikeID(userKey, loserKey))
|
||||
changes.likeUpsert(syncpkg.EncodeLikeID(userKey, survivorKey))
|
||||
}
|
||||
|
||||
if _, err := tq.MergeCopyTrackTags(ctx, dbq.MergeCopyTrackTagsParams{SurvivorID: survivorID, LoserID: loserID}); err != nil {
|
||||
return f, fmt.Errorf("merge tags: %w", err)
|
||||
}
|
||||
if _, err := tq.MergeCopyTrackSimilarity(ctx, dbq.MergeCopyTrackSimilarityParams{SurvivorID: survivorID, LoserID: loserID}); err != nil {
|
||||
return f, fmt.Errorf("merge similarity: %w", err)
|
||||
}
|
||||
if err := tq.MergeInheritTrackMbid(ctx, dbq.MergeInheritTrackMbidParams{SurvivorID: survivorID, LoserID: loserID}); err != nil {
|
||||
return f, fmt.Errorf("inherit recording mbid: %w", err)
|
||||
}
|
||||
|
||||
// Everything the loser carried now sits on the survivor, so the CASCADE
|
||||
// this delete sets off has nothing left to destroy.
|
||||
deleted, err := tq.DeleteTrack(ctx, loserID)
|
||||
if err != nil {
|
||||
return f, fmt.Errorf("delete merged copy: %w", err)
|
||||
}
|
||||
tidied, err := tidyEmptiedAlbum(ctx, tq, deleted.AlbumID)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
f.DeletedAlbumID = tidied.AlbumID
|
||||
f.DeletedArtistID = tidied.ArtistID
|
||||
changes.trackDelete(loserKey)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// splitSurvivor separates the copy to keep from the copies to remove. An
|
||||
// invalid survivorID takes ProposeSurvivor's choice.
|
||||
func splitSurvivor(
|
||||
|
||||
Reference in New Issue
Block a user