diff --git a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs index 8a342e20c..52067b23b 100644 --- a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs +++ b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs @@ -55,6 +55,7 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Title").AsString().Nullable() .WithColumn("Ignored").AsBoolean().Nullable() .WithColumn("Explict").AsBoolean() + .WithColumn("Monitored").AsBoolean() .WithColumn("TrackExplicitName").AsString().Nullable() .WithColumn("TrackCensoredName").AsString().Nullable() .WithColumn("TrackFileId").AsInt32().Nullable() diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 3ad7b909a..ef974f612 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -16,6 +16,7 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv.Commands; using NzbDrone.Core.Update.Commands; +using NzbDrone.Core.Music.Commands; namespace NzbDrone.Core.Jobs { @@ -64,9 +65,10 @@ namespace NzbDrone.Core.Jobs new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFinishedDownloadCommand).FullName}, new ScheduledTask{ Interval = 5, TypeName = typeof(MessagingCleanupCommand).FullName}, new ScheduledTask{ Interval = 6*60, TypeName = typeof(ApplicationUpdateCommand).FullName}, - new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName}, + //new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName}, new ScheduledTask{ Interval = 6*60, TypeName = typeof(CheckHealthCommand).FullName}, - new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName}, + new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshArtistCommand).FullName}, + new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName}, // TODO: Remove new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName}, new ScheduledTask{ Interval = 7*24*60, TypeName = typeof(BackupCommand).FullName}, diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 84600ae18..a35e33835 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -32,6 +32,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook _logger = logger; } + + public Tuple> GetSeriesInfo(int tvdbSeriesId) { Console.WriteLine("[GetSeriesInfo] id:" + tvdbSeriesId); @@ -165,6 +167,11 @@ namespace NzbDrone.Core.MetadataSource.SkyHook public Tuple> GetArtistInfo(int itunesId) { + // TODO: [GetArtistInfo]: This needs to return a set of tracks from iTunes. + // This call is expected to return information about an artist and the tracks that make up said artist. + // To do this, we need 2-3 API calls. 1st is to gather information about the artist and the albums the artist has. This is https://itunes.apple.com/search?entity=album&id=itunesId + // Next call is to populate the overview field and calls the internal API + // Finally, we need to, for each album, get all tracks, which means calling this N times: https://itunes.apple.com/search?entity=musicTrack&term=artistName (id will not work) _logger.Debug("Getting Artist with iTunesID of {0}", itunesId); var httpRequest1 = _requestBuilder.Create() .SetSegment("route", "lookup") diff --git a/src/NzbDrone.Core/Music/AddArtistValidator.cs b/src/NzbDrone.Core/Music/AddArtistValidator.cs index a21e3bac5..ab789c2fc 100644 --- a/src/NzbDrone.Core/Music/AddArtistValidator.cs +++ b/src/NzbDrone.Core/Music/AddArtistValidator.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Music SeriesPathValidator seriesPathValidator, DroneFactoryValidator droneFactoryValidator, SeriesAncestorValidator seriesAncestorValidator, - ArtistSlugValidator seriesTitleSlugValidator) + ArtistSlugValidator artistTitleSlugValidator) { RuleFor(c => c.Path).Cascade(CascadeMode.StopOnFirstFailure) .IsValidPath() @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Music .SetValidator(droneFactoryValidator) .SetValidator(seriesAncestorValidator); - RuleFor(c => c.ArtistSlug).SetValidator(seriesTitleSlugValidator);// TODO: Check if we are going to use a slug or artistName + RuleFor(c => c.ArtistSlug).SetValidator(artistTitleSlugValidator);// TODO: Check if we are going to use a slug or artistName } } } diff --git a/src/NzbDrone.Core/Music/Artist.cs b/src/NzbDrone.Core/Music/Artist.cs index 457176bab..ef89b2998 100644 --- a/src/NzbDrone.Core/Music/Artist.cs +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -32,35 +32,19 @@ namespace NzbDrone.Core.Music public bool ArtistFolder { get; set; } public DateTime? LastInfoSync { get; set; } public DateTime? LastDiskSync { get; set; } - public int Status { get; set; } // TODO: Figure out what this is, do we need it? public string Path { get; set; } public List Images { get; set; } public List Genres { get; set; } public int QualityProfileId { get; set; } - public string RootFolderPath { get; set; } public DateTime Added { get; set; } public LazyLoaded Profile { get; set; } public int ProfileId { get; set; } public List Albums { get; set; } public HashSet Tags { get; set; } - public AddSeriesOptions AddOptions { get; set; } - //public string SortTitle { get; set; } - //public SeriesStatusType Status { get; set; } - //public int Runtime { get; set; } - //public SeriesTypes SeriesType { get; set; } - //public string Network { get; set; } - //public bool UseSceneNumbering { get; set; } - //public string TitleSlug { get; set; } - //public int Year { get; set; } - //public Ratings Ratings { get; set; } - //public List Actors { get; set; } // MOve to album? - //public string Certification { get; set; } - //public DateTime? FirstAired { get; set; } - public override string ToString() { return string.Format("[{0}][{1}]", ItunesId, ArtistName.NullSafe()); @@ -88,18 +72,11 @@ namespace NzbDrone.Core.Music ArtistFolder = otherArtist.ArtistFolder; AddOptions = otherArtist.AddOptions; - - //TODO: Implement - ItunesId = otherArtist.ItunesId; - Albums = otherArtist.Albums; Path = otherArtist.Path; ProfileId = otherArtist.ProfileId; - AlbumFolder = otherArtist.AlbumFolder; Monitored = otherArtist.Monitored; - - //SeriesType = otherArtist.SeriesType; RootFolderPath = otherArtist.RootFolderPath; Tags = otherArtist.Tags; AddOptions = otherArtist.AddOptions; diff --git a/src/NzbDrone.Core/Music/ArtistAddedHandler.cs b/src/NzbDrone.Core/Music/ArtistAddedHandler.cs new file mode 100644 index 000000000..b2da66db9 --- /dev/null +++ b/src/NzbDrone.Core/Music/ArtistAddedHandler.cs @@ -0,0 +1,26 @@ +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music.Commands; +using NzbDrone.Core.Music.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public class ArtistAddedHandler : IHandle + { + private readonly IManageCommandQueue _commandQueueManager; + + public ArtistAddedHandler(IManageCommandQueue commandQueueManager) + { + _commandQueueManager = commandQueueManager; + } + + public void Handle(ArtistAddedEvent message) + { + _commandQueueManager.Push(new RefreshArtistCommand(message.Artist.Id)); + } + } +} diff --git a/src/NzbDrone.Core/Music/Commands/RefreshArtistCommand.cs b/src/NzbDrone.Core/Music/Commands/RefreshArtistCommand.cs new file mode 100644 index 000000000..fdf3e56d6 --- /dev/null +++ b/src/NzbDrone.Core/Music/Commands/RefreshArtistCommand.cs @@ -0,0 +1,26 @@ +using NzbDrone.Core.Messaging.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music.Commands +{ + public class RefreshArtistCommand : Command + { + public int? ArtistId { get; set; } + + public RefreshArtistCommand() + { + } + + public RefreshArtistCommand(int? artistId) + { + ArtistId = artistId; + } + + public override bool SendUpdatesToClient => true; + + public override bool UpdateScheduledTask => !ArtistId.HasValue; + } +} diff --git a/src/NzbDrone.Core/Music/Events/ArtistRefreshStartingEvent.cs b/src/NzbDrone.Core/Music/Events/ArtistRefreshStartingEvent.cs new file mode 100644 index 000000000..45d9a50c8 --- /dev/null +++ b/src/NzbDrone.Core/Music/Events/ArtistRefreshStartingEvent.cs @@ -0,0 +1,18 @@ +using NzbDrone.Common.Messaging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music.Events +{ + public class ArtistRefreshStartingEvent : IEvent + { + public bool ManualTrigger { get; set; } + + public ArtistRefreshStartingEvent(bool manualTrigger) + { + ManualTrigger = manualTrigger; + } + } +} diff --git a/src/NzbDrone.Core/Music/Events/TrackInfoRefreshedEvent.cs b/src/NzbDrone.Core/Music/Events/TrackInfoRefreshedEvent.cs new file mode 100644 index 000000000..99661c480 --- /dev/null +++ b/src/NzbDrone.Core/Music/Events/TrackInfoRefreshedEvent.cs @@ -0,0 +1,23 @@ +using NzbDrone.Common.Messaging; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music.Events +{ + public class TrackInfoRefreshedEvent : IEvent + { + public Artist Artist { get; set; } + public ReadOnlyCollection Added { get; private set; } + public ReadOnlyCollection Updated { get; private set; } + + public TrackInfoRefreshedEvent(Artist artist, IList added, IList updated) + { + Artist = artist; + Added = new ReadOnlyCollection(added); + Updated = new ReadOnlyCollection(updated); + } + } +} diff --git a/src/NzbDrone.Core/Music/RefreshArtistService.cs b/src/NzbDrone.Core/Music/RefreshArtistService.cs new file mode 100644 index 000000000..401753ef8 --- /dev/null +++ b/src/NzbDrone.Core/Music/RefreshArtistService.cs @@ -0,0 +1,173 @@ +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MetadataSource.SkyHook; +using NzbDrone.Core.Music.Commands; +using NzbDrone.Core.Music.Events; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public class RefreshArtistService : IExecute + { + private readonly IProvideArtistInfo _artistInfo; + private readonly IArtistService _artistService; + private readonly IRefreshTrackService _refreshTrackService; + private readonly IEventAggregator _eventAggregator; + //private readonly IDailySeriesService _dailySeriesService; + private readonly IDiskScanService _diskScanService; + //private readonly ICheckIfArtistShouldBeRefreshed _checkIfArtistShouldBeRefreshed; + private readonly Logger _logger; + + public RefreshArtistService(IProvideArtistInfo artistInfo, + IArtistService artistService, + IRefreshTrackService refreshTrackService, + IEventAggregator eventAggregator, + IDiskScanService diskScanService, + //ICheckIfArtistShouldBeRefreshed checkIfArtistShouldBeRefreshed, + Logger logger) + { + _artistInfo = artistInfo; + _artistService = artistService; + _refreshTrackService = refreshTrackService; + _eventAggregator = eventAggregator; + _diskScanService = diskScanService; + //_checkIfArtistShouldBeRefreshed = checkIfArtistShouldBeRefreshed; + _logger = logger; + } + + private void RefreshArtistInfo(Artist artist) + { + _logger.ProgressInfo("Updating Info for {0}", artist.ArtistName); + + Tuple> tuple; + + try + { + tuple = _artistInfo.GetArtistInfo(artist.ItunesId); + } + catch (ArtistNotFoundException) + { + _logger.Error("Artist '{0}' (itunesid {1}) was not found, it may have been removed from iTunes.", artist.ArtistName, artist.ItunesId); + return; + } + + var artistInfo = tuple.Item1; + + if (artist.ItunesId != artistInfo.ItunesId) + { + _logger.Warn("Artist '{0}' (itunes {1}) was replaced with '{2}' (itunes {3}), because the original was a duplicate.", artist.ArtistName, artist.ItunesId, artistInfo.ArtistName, artistInfo.ItunesId); + artist.ItunesId = artistInfo.ItunesId; + } + + artist.ArtistName = artistInfo.ArtistName; + artist.ArtistSlug = artistInfo.ArtistSlug; + artist.Overview = artistInfo.Overview; + artist.Status = artistInfo.Status; + artist.CleanTitle = artistInfo.CleanTitle; + artist.LastInfoSync = DateTime.UtcNow; + artist.Images = artistInfo.Images; + //artist.Actors = artistInfo.Actors; + artist.Genres = artistInfo.Genres; + + try + { + artist.Path = new DirectoryInfo(artist.Path).FullName; + artist.Path = artist.Path.GetActualCasing(); + } + catch (Exception e) + { + _logger.Warn(e, "Couldn't update artist path for " + artist.Path); + } + + artist.Albums = UpdateAlbums(artist, artistInfo); + + _artistService.UpdateArtist(artist); + _refreshTrackService.RefreshTrackInfo(artist, tuple.Item2); + + _logger.Debug("Finished artist refresh for {0}", artist.ArtistName); + _eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist)); + } + + private List UpdateAlbums(Artist artist, Artist artistInfo) + { + var albums = artistInfo.Albums.DistinctBy(s => s.AlbumId).ToList(); + + foreach (var album in albums) + { + var existingAlbum = artist.Albums.FirstOrDefault(s => s.AlbumId == album.AlbumId); + + //Todo: Should this should use the previous season's monitored state? + if (existingAlbum == null) + { + //if (album.SeasonNumber == 0) + //{ + // album.Monitored = false; + // continue; + //} + + _logger.Debug("New album ({0}) for artist: [{1}] {2}, setting monitored to true", album.Title, artist.ItunesId, artist.ArtistName); + album.Monitored = true; + } + + else + { + album.Monitored = existingAlbum.Monitored; + } + } + + return albums; + } + + public void Execute(RefreshArtistCommand message) + { + _eventAggregator.PublishEvent(new ArtistRefreshStartingEvent(message.Trigger == CommandTrigger.Manual)); + + if (message.ArtistId.HasValue) + { + var artist = _artistService.GetArtist(message.ArtistId.Value); + RefreshArtistInfo(artist); + } + else + { + var allArtists = _artistService.GetAllArtists().OrderBy(c => c.ArtistName).ToList(); + + foreach (var artist in allArtists) + { + if (message.Trigger == CommandTrigger.Manual /*|| _checkIfArtistShouldBeRefreshed.ShouldRefresh(artist)*/) + { + try + { + RefreshArtistInfo(artist); + } + catch (Exception e) + { + _logger.Error(e, "Couldn't refresh info for {0}", artist); + } + } + + else + { + try + { + _logger.Info("Skipping refresh of artist: {0}", artist.ArtistName); + //TODO: _diskScanService.Scan(artist); + } + catch (Exception e) + { + _logger.Error(e, "Couldn't rescan artist {0}", artist); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Music/RefreshTrackService.cs b/src/NzbDrone.Core/Music/RefreshTrackService.cs new file mode 100644 index 000000000..76499545b --- /dev/null +++ b/src/NzbDrone.Core/Music/RefreshTrackService.cs @@ -0,0 +1,125 @@ +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public interface IRefreshTrackService + { + void RefreshTrackInfo(Artist artist, IEnumerable remoteTracks); + } + + public class RefreshTrackService : IRefreshTrackService + { + private readonly ITrackService _trackService; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + public RefreshTrackService(ITrackService trackService, IEventAggregator eventAggregator, Logger logger) + { + _trackService = trackService; + _eventAggregator = eventAggregator; + _logger = logger; + } + + public void RefreshTrackInfo(Artist artist, IEnumerable remoteTracks) + { + _logger.Info("Starting track info refresh for: {0}", artist); + var successCount = 0; + var failCount = 0; + + var existingTracks = _trackService.GetTrackByArtist(artist.ItunesId); + var albums = artist.Albums; + + var updateList = new List(); + var newList = new List(); + var dupeFreeRemoteTracks = remoteTracks.DistinctBy(m => new { m.AlbumId, m.TrackNumber }).ToList(); + + foreach (var track in OrderTracks(artist, dupeFreeRemoteTracks)) + { + try + { + var trackToUpdate = GetTrackToUpdate(artist, track, existingTracks); + + if (trackToUpdate != null) + { + existingTracks.Remove(trackToUpdate); + updateList.Add(trackToUpdate); + } + else + { + trackToUpdate = new Track(); + trackToUpdate.Monitored = GetMonitoredStatus(track, albums); + newList.Add(trackToUpdate); + } + trackToUpdate.ArtistId = artist.ItunesId; // TODO: Ensure LazyLoaded field gets updated. + trackToUpdate.TrackNumber = track.TrackNumber; + trackToUpdate.Title = track.Title ?? "Unknown"; + + // TODO: Implement rest of [RefreshTrackService] fields + + + + successCount++; + } + catch (Exception e) + { + _logger.Fatal(e, "An error has occurred while updating track info for artist {0}. {1}", artist, track); + failCount++; + } + } + + var allTracks = new List(); + allTracks.AddRange(newList); + allTracks.AddRange(updateList); + + // TODO: See if anything needs to be done here + //AdjustMultiEpisodeAirTime(artist, allTracks); + //AdjustDirectToDvdAirDate(artist, allTracks); + + _trackService.DeleteMany(existingTracks); + _trackService.UpdateMany(updateList); + _trackService.InsertMany(newList); + + _eventAggregator.PublishEvent(new TrackInfoRefreshedEvent(artist, newList, updateList)); + + if (failCount != 0) + { + _logger.Info("Finished track refresh for artist: {0}. Successful: {1} - Failed: {2} ", + artist.ArtistName, successCount, failCount); + } + else + { + _logger.Info("Finished track refresh for artist: {0}.", artist); + } + } + + private bool GetMonitoredStatus(Track track, IEnumerable albums) + { + if (track.TrackNumber == 0 /*&& track.AlbumId != 1*/) + { + return false; + } + + var album = albums.SingleOrDefault(c => c.AlbumId == track.AlbumId); + return album == null || album.Monitored; + } + + + private Track GetTrackToUpdate(Artist artist, Track track, List existingTracks) + { + return existingTracks.FirstOrDefault(e => e.AlbumId == track.AlbumId && e.TrackNumber == track.TrackNumber); + } + + private IEnumerable OrderTracks(Artist artist, List tracks) + { + return tracks.OrderBy(e => e.AlbumId).ThenBy(e => e.TrackNumber); + } + } +} + diff --git a/src/NzbDrone.Core/Music/Track.cs b/src/NzbDrone.Core/Music/Track.cs index 65e28231b..fb437193b 100644 --- a/src/NzbDrone.Core/Music/Track.cs +++ b/src/NzbDrone.Core/Music/Track.cs @@ -19,7 +19,8 @@ namespace NzbDrone.Core.Music public int ItunesTrackId { get; set; } public int AlbumId { get; set; } - public LazyLoaded ArtistsId { get; set; } + public LazyLoaded Artist { get; set; } + public int ArtistId { get; set; } public int CompilationId { get; set; } public bool Compilation { get; set; } public int TrackNumber { get; set; } @@ -28,11 +29,10 @@ namespace NzbDrone.Core.Music public bool Explict { get; set; } public string TrackExplicitName { get; set; } public string TrackCensoredName { get; set; } - public string Monitored { get; set; } - public int TrackFileId { get; set; } // JVM: Is this needed with TrackFile reference? + public bool Monitored { get; set; } + public int TrackFileId { get; set; } public DateTime? ReleaseDate { get; set; } - /*public int? SceneEpisodeNumber { get; set; } - public bool UnverifiedSceneNumbering { get; set; } + /* public Ratings Ratings { get; set; } // This might be aplicable as can be pulled from IDv3 tags public List Images { get; set; }*/ diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 678b2e8da..5dcadb8e0 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -851,14 +851,20 @@ + + + + + + diff --git a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs index f562800fc..19da3e335 100644 --- a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs +++ b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs @@ -8,7 +8,7 @@
@@ -50,6 +50,9 @@
{{> EpisodeProgressPartial }}
+
+ Path {{path}} +