Lidarr/src/NzbDrone.Core/Music/Services/RefreshAlbumService.cs

382 lines
16 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Linq;
2019-12-16 21:21:32 +00:00
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Exceptions;
2019-07-09 20:14:05 +00:00
using NzbDrone.Core.History;
using NzbDrone.Core.MediaCover;
2019-12-16 21:21:32 +00:00
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Commands;
2019-12-16 21:21:32 +00:00
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Music.Commands;
using NzbDrone.Core.Music.Events;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.Music
{
public interface IRefreshAlbumService
{
2019-07-09 20:14:05 +00:00
bool RefreshAlbumInfo(Album album, List<Album> remoteAlbums, bool forceUpdateFileTags);
bool RefreshAlbumInfo(List<Album> albums, List<Album> remoteAlbums, bool forceAlbumRefresh, bool forceUpdateFileTags, DateTime? lastUpdate);
}
2019-07-09 20:14:05 +00:00
public class RefreshAlbumService : RefreshEntityServiceBase<Album, AlbumRelease>, IRefreshAlbumService, IExecute<RefreshAlbumCommand>
{
private readonly IAlbumService _albumService;
private readonly IArtistService _artistService;
private readonly IRootFolderService _rootFolderService;
2019-07-09 20:14:05 +00:00
private readonly IAddArtistService _addArtistService;
private readonly IReleaseService _releaseService;
private readonly IProvideAlbumInfo _albumInfo;
2019-07-09 20:14:05 +00:00
private readonly IRefreshAlbumReleaseService _refreshAlbumReleaseService;
private readonly IMediaFileService _mediaFileService;
private readonly IHistoryService _historyService;
private readonly IEventAggregator _eventAggregator;
private readonly IManageCommandQueue _commandQueueManager;
private readonly ICheckIfAlbumShouldBeRefreshed _checkIfAlbumShouldBeRefreshed;
private readonly IMapCoversToLocal _mediaCoverService;
private readonly Logger _logger;
public RefreshAlbumService(IAlbumService albumService,
IArtistService artistService,
IRootFolderService rootFolderService,
2019-07-09 20:14:05 +00:00
IAddArtistService addArtistService,
IArtistMetadataService artistMetadataService,
IReleaseService releaseService,
IProvideAlbumInfo albumInfo,
2019-07-09 20:14:05 +00:00
IRefreshAlbumReleaseService refreshAlbumReleaseService,
IMediaFileService mediaFileService,
IHistoryService historyService,
IEventAggregator eventAggregator,
IManageCommandQueue commandQueueManager,
ICheckIfAlbumShouldBeRefreshed checkIfAlbumShouldBeRefreshed,
IMapCoversToLocal mediaCoverService,
Logger logger)
: base(logger, artistMetadataService)
{
_albumService = albumService;
_artistService = artistService;
_rootFolderService = rootFolderService;
2019-07-09 20:14:05 +00:00
_addArtistService = addArtistService;
_releaseService = releaseService;
_albumInfo = albumInfo;
2019-07-09 20:14:05 +00:00
_refreshAlbumReleaseService = refreshAlbumReleaseService;
_mediaFileService = mediaFileService;
_historyService = historyService;
_eventAggregator = eventAggregator;
_commandQueueManager = commandQueueManager;
_checkIfAlbumShouldBeRefreshed = checkIfAlbumShouldBeRefreshed;
_mediaCoverService = mediaCoverService;
_logger = logger;
}
2019-07-09 20:14:05 +00:00
protected override RemoteData GetRemoteData(Album local, List<Album> remote)
{
2019-07-09 20:14:05 +00:00
var result = new RemoteData();
2019-07-09 20:14:05 +00:00
// remove not in remote list and ShouldDelete is true
if (remote != null &&
!remote.Any(x => x.ForeignAlbumId == local.ForeignAlbumId || x.OldForeignAlbumIds.Contains(local.ForeignAlbumId)) &&
ShouldDelete(local))
{
2019-07-09 20:14:05 +00:00
return result;
}
2019-07-09 20:14:05 +00:00
Tuple<string, Album, List<ArtistMetadata>> tuple = null;
try
{
2019-07-09 20:14:05 +00:00
tuple = _albumInfo.GetAlbumInfo(local.ForeignAlbumId);
}
catch (AlbumNotFoundException)
{
2019-07-09 20:14:05 +00:00
return result;
}
if (tuple.Item2.AlbumReleases.Value.Count == 0)
{
2019-07-09 20:14:05 +00:00
_logger.Debug($"{local} has no valid releases, removing.");
return result;
}
2019-07-09 20:14:05 +00:00
result.Entity = tuple.Item2;
result.Entity.Id = local.Id;
2019-07-09 20:14:05 +00:00
result.Metadata = tuple.Item3;
return result;
}
2019-07-09 20:14:05 +00:00
protected override void EnsureNewParent(Album local, Album remote)
{
// Make sure the appropriate artist exists (it could be that an album changes parent)
// The artistMetadata entry will be in the db but make sure a corresponding artist is too
// so that the album doesn't just disappear.
2019-07-09 20:14:05 +00:00
// TODO filter by metadata id before hitting database
_logger.Trace($"Ensuring parent artist exists [{remote.ArtistMetadata.Value.ForeignArtistId}]");
2019-07-09 20:14:05 +00:00
var newArtist = _artistService.FindById(remote.ArtistMetadata.Value.ForeignArtistId);
2019-07-09 20:14:05 +00:00
if (newArtist == null)
{
var oldArtist = local.Artist.Value;
var addArtist = new Artist
{
2019-07-09 20:14:05 +00:00
Metadata = remote.ArtistMetadata.Value,
MetadataProfileId = oldArtist.MetadataProfileId,
QualityProfileId = oldArtist.QualityProfileId,
RootFolderPath = _rootFolderService.GetBestRootFolderPath(oldArtist.Path),
2019-07-09 20:14:05 +00:00
Monitored = oldArtist.Monitored,
Tags = oldArtist.Tags
2019-07-09 20:14:05 +00:00
};
_logger.Debug($"Adding missing parent artist {addArtist}");
_addArtistService.AddArtist(addArtist);
}
}
protected override bool ShouldDelete(Album local)
{
2019-12-16 21:21:32 +00:00
// not manually added and has no files
return local.AddOptions.AddType != AlbumAddType.Manual &&
!_mediaFileService.GetFilesByAlbum(local.Id).Any();
2019-07-09 20:14:05 +00:00
}
2019-07-09 20:14:05 +00:00
protected override void LogProgress(Album local)
{
_logger.ProgressInfo("Updating Info for {0}", local.Title);
}
2019-07-09 20:14:05 +00:00
protected override bool IsMerge(Album local, Album remote)
{
return local.ForeignAlbumId != remote.ForeignAlbumId;
}
protected override UpdateResult UpdateEntity(Album local, Album remote)
{
UpdateResult result;
2019-12-16 21:21:32 +00:00
remote.UseDbFieldsFrom(local);
2019-07-09 20:14:05 +00:00
if (local.Title != (remote.Title ?? "Unknown") ||
local.ForeignAlbumId != remote.ForeignAlbumId ||
local.ArtistMetadata.Value.ForeignArtistId != remote.ArtistMetadata.Value.ForeignArtistId)
{
2019-07-09 20:14:05 +00:00
result = UpdateResult.UpdateTags;
}
else if (!local.Equals(remote))
{
result = UpdateResult.Standard;
}
else
{
result = UpdateResult.None;
}
// Force update and fetch covers if images have changed so that we can write them into tags
if (remote.Images.Any() && !local.Images.SequenceEqual(remote.Images))
{
if (_mediaCoverService.EnsureAlbumCovers(remote))
{
result = UpdateResult.UpdateTags;
}
}
local.UseMetadataFrom(remote);
2019-07-09 20:14:05 +00:00
local.ArtistMetadataId = remote.ArtistMetadata.Value.Id;
local.LastInfoSync = DateTime.UtcNow;
local.AlbumReleases = new List<AlbumRelease>();
return result;
}
2019-07-09 20:14:05 +00:00
protected override UpdateResult MergeEntity(Album local, Album target, Album remote)
{
_logger.Warn($"Album {local} was merged with {remote} because the original was a duplicate.");
2019-07-09 20:14:05 +00:00
// move releases over to the new album and delete
var localReleases = _releaseService.GetReleasesByAlbum(local.Id);
var allReleases = localReleases.Concat(_releaseService.GetReleasesByAlbum(target.Id)).ToList();
_logger.Trace($"Moving {localReleases.Count} releases from {local} to {remote}");
2019-07-09 20:14:05 +00:00
// Update album ID and unmonitor all releases from the old album
allReleases.ForEach(x => x.AlbumId = target.Id);
MonitorSingleRelease(allReleases);
_releaseService.UpdateMany(allReleases);
2019-07-09 20:14:05 +00:00
// Update album ids for trackfiles
var files = _mediaFileService.GetFilesByAlbum(local.Id);
files.ForEach(x => x.AlbumId = target.Id);
_mediaFileService.Update(files);
2019-07-09 20:14:05 +00:00
// Update album ids for history
var items = _historyService.GetByAlbum(local.Id, null);
items.ForEach(x => x.AlbumId = target.Id);
_historyService.UpdateMany(items);
2019-07-09 20:14:05 +00:00
// Finally delete the old album
_albumService.DeleteMany(new List<Album> { local });
2019-07-09 20:14:05 +00:00
return UpdateResult.UpdateTags;
}
2019-07-09 20:14:05 +00:00
protected override Album GetEntityByForeignId(Album local)
{
return _albumService.FindById(local.ForeignAlbumId);
}
2019-07-09 20:14:05 +00:00
protected override void SaveEntity(Album local)
{
// Use UpdateMany to avoid firing the album edited event
_albumService.UpdateMany(new List<Album> { local });
}
protected override void DeleteEntity(Album local, bool deleteFiles)
{
_albumService.DeleteAlbum(local.Id, true);
}
protected override List<AlbumRelease> GetRemoteChildren(Album remote)
{
return remote.AlbumReleases.Value.DistinctBy(m => m.ForeignReleaseId).ToList();
}
protected override List<AlbumRelease> GetLocalChildren(Album entity, List<AlbumRelease> remoteChildren)
{
var children = _releaseService.GetReleasesForRefresh(entity.Id,
remoteChildren.Select(x => x.ForeignReleaseId)
.Concat(remoteChildren.SelectMany(x => x.OldForeignReleaseIds)));
// Make sure trackfiles point to the new album where we are grabbing a release from another album
var files = new List<TrackFile>();
foreach (var release in children.Where(x => x.AlbumId != entity.Id))
{
2019-07-09 20:14:05 +00:00
files.AddRange(_mediaFileService.GetFilesByRelease(release.Id));
}
2019-07-09 20:14:05 +00:00
files.ForEach(x => x.AlbumId = entity.Id);
_mediaFileService.Update(files);
return children;
}
protected override Tuple<AlbumRelease, List<AlbumRelease>> GetMatchingExistingChildren(List<AlbumRelease> existingChildren, AlbumRelease remote)
2019-07-09 20:14:05 +00:00
{
var existingChild = existingChildren.SingleOrDefault(x => x.ForeignReleaseId == remote.ForeignReleaseId);
var mergeChildren = existingChildren.Where(x => remote.OldForeignReleaseIds.Contains(x.ForeignReleaseId)).ToList();
return Tuple.Create(existingChild, mergeChildren);
}
protected override void PrepareNewChild(AlbumRelease child, Album entity)
{
child.AlbumId = entity.Id;
child.Album = entity;
}
protected override void PrepareExistingChild(AlbumRelease local, AlbumRelease remote, Album entity)
{
local.AlbumId = entity.Id;
local.Album = entity;
remote.UseDbFieldsFrom(local);
2019-07-09 20:14:05 +00:00
}
protected override void AddChildren(List<AlbumRelease> children)
{
_releaseService.InsertMany(children);
}
private void MonitorSingleRelease(List<AlbumRelease> releases)
{
var monitored = releases.Where(x => x.Monitored).ToList();
if (!monitored.Any())
{
2019-07-09 20:14:05 +00:00
monitored = releases;
}
2019-07-09 20:14:05 +00:00
var toMonitor = monitored.OrderByDescending(x => _mediaFileService.GetFilesByRelease(x.Id).Count)
.ThenByDescending(x => x.TrackCount)
.First();
2019-07-09 20:14:05 +00:00
releases.ForEach(x => x.Monitored = false);
toMonitor.Monitored = true;
}
protected override bool RefreshChildren(SortedChildren localChildren, List<AlbumRelease> remoteChildren, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate)
2019-07-09 20:14:05 +00:00
{
var refreshList = localChildren.All;
2019-07-09 20:14:05 +00:00
// make sure only one of the releases ends up monitored
localChildren.Old.ForEach(x => x.Monitored = false);
MonitorSingleRelease(localChildren.Future);
2019-07-09 20:14:05 +00:00
refreshList.ForEach(x => _logger.Trace($"release: {x} monitored: {x.Monitored}"));
2019-07-09 20:14:05 +00:00
return _refreshAlbumReleaseService.RefreshEntityInfo(refreshList, remoteChildren, forceChildRefresh, forceUpdateFileTags);
}
2019-12-16 21:21:32 +00:00
protected override void PublishEntityUpdatedEvent(Album entity)
{
// Fetch fresh from DB so all lazy loads are available
_eventAggregator.PublishEvent(new AlbumUpdatedEvent(_albumService.GetAlbum(entity.Id)));
}
public bool RefreshAlbumInfo(List<Album> albums, List<Album> remoteAlbums, bool forceAlbumRefresh, bool forceUpdateFileTags, DateTime? lastUpdate)
2019-07-09 20:14:05 +00:00
{
bool updated = false;
HashSet<string> updatedMusicbrainzAlbums = null;
if (lastUpdate.HasValue && lastUpdate.Value.AddDays(14) > DateTime.UtcNow)
{
updatedMusicbrainzAlbums = _albumInfo.GetChangedAlbums(lastUpdate.Value);
}
2019-07-09 20:14:05 +00:00
foreach (var album in albums)
{
if (forceAlbumRefresh ||
(updatedMusicbrainzAlbums == null && _checkIfAlbumShouldBeRefreshed.ShouldRefresh(album)) ||
(updatedMusicbrainzAlbums != null && updatedMusicbrainzAlbums.Contains(album.ForeignAlbumId)))
2019-07-09 20:14:05 +00:00
{
updated |= RefreshAlbumInfo(album, remoteAlbums, forceUpdateFileTags);
}
else
{
_logger.Debug("Skipping refresh of album: {0}", album.Title);
}
2019-07-09 20:14:05 +00:00
}
return updated;
}
2019-07-09 20:14:05 +00:00
public bool RefreshAlbumInfo(Album album, List<Album> remoteAlbums, bool forceUpdateFileTags)
{
return RefreshEntityInfo(album, remoteAlbums, true, forceUpdateFileTags, null);
2019-07-09 20:14:05 +00:00
}
public void Execute(RefreshAlbumCommand message)
{
if (message.AlbumId.HasValue)
{
var album = _albumService.GetAlbum(message.AlbumId.Value);
var artist = _artistService.GetArtistByMetadataId(album.ArtistMetadataId);
2019-07-09 20:14:05 +00:00
var updated = RefreshAlbumInfo(album, null, false);
if (updated)
{
_eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist));
2019-12-16 21:21:32 +00:00
_eventAggregator.PublishEvent(new AlbumUpdatedEvent(album));
}
if (message.IsNewAlbum)
{
// Just scan the artist path - triggering a full rescan is too painful
var folders = new List<string> { artist.Path };
_commandQueueManager.Push(new RescanFoldersCommand(folders, FilterFilesType.Matched, false, null));
}
}
}
}
}