You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
382 lines
16 KiB
382 lines
16 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using NLog;
|
|
using NzbDrone.Common.Extensions;
|
|
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<Album> remoteAlbums, bool forceUpdateFileTags);
|
|
bool RefreshAlbumInfo(List<Album> albums, List<Album> remoteAlbums, bool forceAlbumRefresh, bool forceUpdateFileTags, DateTime? lastUpdate);
|
|
}
|
|
|
|
public class RefreshAlbumService : RefreshEntityServiceBase<Album, AlbumRelease>, IRefreshAlbumService, IExecute<RefreshAlbumCommand>
|
|
{
|
|
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<Album> 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<string, Album, List<ArtistMetadata>> 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<AlbumRelease>();
|
|
|
|
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<Album> { 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<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))
|
|
{
|
|
files.AddRange(_mediaFileService.GetFilesByRelease(release.Id));
|
|
}
|
|
|
|
files.ForEach(x => x.AlbumId = entity.Id);
|
|
_mediaFileService.Update(files);
|
|
|
|
return children;
|
|
}
|
|
|
|
protected override Tuple<AlbumRelease, List<AlbumRelease>> GetMatchingExistingChildren(List<AlbumRelease> 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<AlbumRelease> children)
|
|
{
|
|
_releaseService.InsertMany(children);
|
|
}
|
|
|
|
private void MonitorSingleRelease(List<AlbumRelease> 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<AlbumRelease> 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<Album> albums, List<Album> remoteAlbums, bool forceAlbumRefresh, bool forceUpdateFileTags, DateTime? lastUpdate)
|
|
{
|
|
bool updated = false;
|
|
|
|
HashSet<string> 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<Album> 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<string> { artist.Path };
|
|
_commandQueueManager.Push(new RescanFoldersCommand(folders, FilterFilesType.Matched, false, null));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|