using System; using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Exceptions; using NzbDrone.Core.History; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Commands; 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 { bool RefreshAlbumInfo(Album album, List remoteAlbums, bool forceUpdateFileTags); bool RefreshAlbumInfo(List albums, List remoteAlbums, bool forceAlbumRefresh, bool forceUpdateFileTags, DateTime? lastUpdate); } public class RefreshAlbumService : RefreshEntityServiceBase, IRefreshAlbumService, IExecute { private readonly IAlbumService _albumService; private readonly IArtistService _artistService; private readonly IRootFolderService _rootFolderService; private readonly IAddArtistService _addArtistService; private readonly IReleaseService _releaseService; private readonly IProvideAlbumInfo _albumInfo; 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, IAddArtistService addArtistService, IArtistMetadataService artistMetadataService, IReleaseService releaseService, IProvideAlbumInfo albumInfo, 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; _addArtistService = addArtistService; _releaseService = releaseService; _albumInfo = albumInfo; _refreshAlbumReleaseService = refreshAlbumReleaseService; _mediaFileService = mediaFileService; _historyService = historyService; _eventAggregator = eventAggregator; _commandQueueManager = commandQueueManager; _checkIfAlbumShouldBeRefreshed = checkIfAlbumShouldBeRefreshed; _mediaCoverService = mediaCoverService; _logger = logger; } protected override RemoteData GetRemoteData(Album local, List remote) { var result = new RemoteData(); // 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)) { return result; } Tuple> tuple = null; try { tuple = _albumInfo.GetAlbumInfo(local.ForeignAlbumId); } catch (AlbumNotFoundException) { return result; } if (tuple.Item2.AlbumReleases.Value.Count == 0) { _logger.Debug($"{local} has no valid releases, removing."); return result; } result.Entity = tuple.Item2; result.Entity.Id = local.Id; result.Metadata = tuple.Item3; return result; } 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. // TODO filter by metadata id before hitting database _logger.Trace($"Ensuring parent artist exists [{remote.ArtistMetadata.Value.ForeignArtistId}]"); var newArtist = _artistService.FindById(remote.ArtistMetadata.Value.ForeignArtistId); if (newArtist == null) { var oldArtist = local.Artist.Value; var addArtist = new Artist { Metadata = remote.ArtistMetadata.Value, MetadataProfileId = oldArtist.MetadataProfileId, QualityProfileId = oldArtist.QualityProfileId, RootFolderPath = _rootFolderService.GetBestRootFolderPath(oldArtist.Path), Monitored = oldArtist.Monitored, Tags = oldArtist.Tags }; _logger.Debug($"Adding missing parent artist {addArtist}"); _addArtistService.AddArtist(addArtist); } } protected override bool ShouldDelete(Album local) { // not manually added and has no files return local.AddOptions.AddType != AlbumAddType.Manual && !_mediaFileService.GetFilesByAlbum(local.Id).Any(); } protected override void LogProgress(Album local) { _logger.ProgressInfo("Updating Info for {0}", local.Title); } protected override bool IsMerge(Album local, Album remote) { return local.ForeignAlbumId != remote.ForeignAlbumId; } protected override UpdateResult UpdateEntity(Album local, Album remote) { UpdateResult result; remote.UseDbFieldsFrom(local); if (local.Title != (remote.Title ?? "Unknown") || local.ForeignAlbumId != remote.ForeignAlbumId || local.ArtistMetadata.Value.ForeignArtistId != remote.ArtistMetadata.Value.ForeignArtistId) { 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); local.ArtistMetadataId = remote.ArtistMetadata.Value.Id; local.LastInfoSync = DateTime.UtcNow; local.AlbumReleases = new List(); return result; } protected override UpdateResult MergeEntity(Album local, Album target, Album remote) { _logger.Warn($"Album {local} was merged with {remote} because the original was a duplicate."); // 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}"); // Update album ID and unmonitor all releases from the old album allReleases.ForEach(x => x.AlbumId = target.Id); MonitorSingleRelease(allReleases); _releaseService.UpdateMany(allReleases); // Update album ids for trackfiles var files = _mediaFileService.GetFilesByAlbum(local.Id); files.ForEach(x => x.AlbumId = target.Id); _mediaFileService.Update(files); // Update album ids for history var items = _historyService.GetByAlbum(local.Id, null); items.ForEach(x => x.AlbumId = target.Id); _historyService.UpdateMany(items); // Finally delete the old album _albumService.DeleteMany(new List { local }); return UpdateResult.UpdateTags; } protected override Album GetEntityByForeignId(Album local) { return _albumService.FindById(local.ForeignAlbumId); } protected override void SaveEntity(Album local) { // Use UpdateMany to avoid firing the album edited event _albumService.UpdateMany(new List { local }); } protected override void DeleteEntity(Album local, bool deleteFiles) { _albumService.DeleteAlbum(local.Id, true); } protected override List GetRemoteChildren(Album remote) { return remote.AlbumReleases.Value.DistinctBy(m => m.ForeignReleaseId).ToList(); } protected override List GetLocalChildren(Album entity, List remoteChildren) { var children = _releaseService.GetReleasesForRefresh(entity.Id, remoteChildren.Select(x => x.ForeignReleaseId) .Concat(remoteChildren.SelectMany(x => x.OldForeignReleaseIds)).ToList()); // Make sure trackfiles point to the new album where we are grabbing a release from another album var files = new List(); foreach (var release in children.Where(x => x.AlbumId != entity.Id)) { files.AddRange(_mediaFileService.GetFilesByRelease(release.Id)); } files.ForEach(x => x.AlbumId = entity.Id); _mediaFileService.Update(files); return children; } protected override Tuple> GetMatchingExistingChildren(List existingChildren, AlbumRelease remote) { 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); } protected override void AddChildren(List children) { _releaseService.InsertMany(children); } private void MonitorSingleRelease(List releases) { var monitored = releases.Where(x => x.Monitored).ToList(); if (!monitored.Any()) { monitored = releases; } var toMonitor = monitored.OrderByDescending(x => _mediaFileService.GetFilesByRelease(x.Id).Count) .ThenByDescending(x => x.TrackCount) .First(); releases.ForEach(x => x.Monitored = false); toMonitor.Monitored = true; } protected override bool RefreshChildren(SortedChildren localChildren, List remoteChildren, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) { var refreshList = localChildren.All; // make sure only one of the releases ends up monitored localChildren.Old.ForEach(x => x.Monitored = false); MonitorSingleRelease(localChildren.Future); refreshList.ForEach(x => _logger.Trace($"release: {x} monitored: {x.Monitored}")); return _refreshAlbumReleaseService.RefreshEntityInfo(refreshList, remoteChildren, forceChildRefresh, forceUpdateFileTags); } 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 albums, List remoteAlbums, bool forceAlbumRefresh, bool forceUpdateFileTags, DateTime? lastUpdate) { var updated = false; HashSet updatedMusicbrainzAlbums = null; if (lastUpdate.HasValue && lastUpdate.Value.AddDays(14) > DateTime.UtcNow) { updatedMusicbrainzAlbums = _albumInfo.GetChangedAlbums(lastUpdate.Value); } foreach (var album in albums) { if (forceAlbumRefresh || (updatedMusicbrainzAlbums == null && _checkIfAlbumShouldBeRefreshed.ShouldRefresh(album)) || (updatedMusicbrainzAlbums != null && updatedMusicbrainzAlbums.Contains(album.ForeignAlbumId))) { updated |= RefreshAlbumInfo(album, remoteAlbums, forceUpdateFileTags); } else { _logger.Debug("Skipping refresh of album: {0}", album.Title); } } return updated; } public bool RefreshAlbumInfo(Album album, List remoteAlbums, bool forceUpdateFileTags) { return RefreshEntityInfo(album, remoteAlbums, true, forceUpdateFileTags, null); } public void Execute(RefreshAlbumCommand message) { if (message.AlbumId.HasValue) { var album = _albumService.GetAlbum(message.AlbumId.Value); var artist = _artistService.GetArtistByMetadataId(album.ArtistMetadataId); var updated = RefreshAlbumInfo(album, null, false); if (updated) { _eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist)); _eventAggregator.PublishEvent(new AlbumUpdatedEvent(album)); } if (message.IsNewAlbum) { // Just scan the artist path - triggering a full rescan is too painful var folders = new List { artist.Path }; _commandQueueManager.Push(new RescanFoldersCommand(folders, FilterFilesType.Matched, false, null)); } } } } }