From fa52eabb79b03d52494c1ac1ef40fe6e5ac7e583 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 30 Apr 2017 16:54:01 -0500 Subject: [PATCH] Almost finished linking frontend to backend. A few issues with DB mapping to work out. --- src/NzbDrone.Api/Music/ArtistModule.cs | 194 ++++++++++++++++++ src/NzbDrone.Api/Music/ArtistResource.cs | 4 +- src/NzbDrone.Api/NzbDrone.Api.csproj | 1 + .../Cloud/SonarrCloudRequestBuilder.cs | 2 +- .../DataAugmentation/Xem/XemService.cs | 148 ++++++------- .../Datastore/Migration/111_setup_music.cs | 15 +- .../Exceptions/ArtistNotFoundException.cs | 31 +++ .../MediaFiles/Events/ArtistRenamedEvent.cs | 19 ++ .../Events/TrackFileDeletedEvent.cs | 20 ++ .../MediaFiles/Events/TrackImportedEvent.cs | 36 ++++ .../MetadataSource/IProvideArtistInfo.cs | 11 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 108 ++++++---- src/NzbDrone.Core/Music/AddArtistOptions.cs | 13 ++ src/NzbDrone.Core/Music/AddArtistService.cs | 101 +++++++++ src/NzbDrone.Core/Music/AddArtistValidator.cs | 34 +++ src/NzbDrone.Core/Music/Artist.cs | 5 +- .../Music/ArtistNameNormalizer.cs | 28 +++ src/NzbDrone.Core/Music/ArtistRepository.cs | 117 ++--------- src/NzbDrone.Core/Music/ArtistService.cs | 86 ++++++-- .../Music/ArtistSlugValidator.cs | 29 +++ .../Music/Events/ArtistAddedEvent.cs | 18 ++ .../Music/Events/ArtistDeletedEvent.cs | 20 ++ .../Music/Events/ArtistEditedEvent.cs | 20 ++ .../Music/Events/ArtistUpdatedEvent.cs | 14 ++ src/NzbDrone.Core/NzbDrone.Core.csproj | 16 ++ .../Organizer/FileNameBuilder.cs | 40 ++++ src/NzbDrone.Core/Organizer/NamingConfig.cs | 6 +- src/NzbDrone.Core/Parser/Parser.cs | 11 + src/NzbDrone.Core/Tv/AddSeriesOptions.cs | 1 + src/NzbDrone.Core/Tv/MonitoringOptions.cs | 3 + .../Validation/Paths/ArtistExistsValidator.cs | 29 +++ .../Validation/Paths/ArtistPathValidator.cs | 31 +++ src/UI/AddSeries/SearchResultView.js | 55 ++--- src/UI/AddSeries/SearchResultViewTemplate.hbs | 8 +- src/UI/Artist/ArtistModel.js | 4 +- 35 files changed, 1009 insertions(+), 269 deletions(-) create mode 100644 src/NzbDrone.Api/Music/ArtistModule.cs create mode 100644 src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs create mode 100644 src/NzbDrone.Core/MediaFiles/Events/ArtistRenamedEvent.cs create mode 100644 src/NzbDrone.Core/MediaFiles/Events/TrackFileDeletedEvent.cs create mode 100644 src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs create mode 100644 src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs create mode 100644 src/NzbDrone.Core/Music/AddArtistOptions.cs create mode 100644 src/NzbDrone.Core/Music/AddArtistService.cs create mode 100644 src/NzbDrone.Core/Music/AddArtistValidator.cs create mode 100644 src/NzbDrone.Core/Music/ArtistNameNormalizer.cs create mode 100644 src/NzbDrone.Core/Music/ArtistSlugValidator.cs create mode 100644 src/NzbDrone.Core/Music/Events/ArtistAddedEvent.cs create mode 100644 src/NzbDrone.Core/Music/Events/ArtistDeletedEvent.cs create mode 100644 src/NzbDrone.Core/Music/Events/ArtistEditedEvent.cs create mode 100644 src/NzbDrone.Core/Music/Events/ArtistUpdatedEvent.cs create mode 100644 src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs create mode 100644 src/NzbDrone.Core/Validation/Paths/ArtistPathValidator.cs diff --git a/src/NzbDrone.Api/Music/ArtistModule.cs b/src/NzbDrone.Api/Music/ArtistModule.cs new file mode 100644 index 000000000..d616becfb --- /dev/null +++ b/src/NzbDrone.Api/Music/ArtistModule.cs @@ -0,0 +1,194 @@ +using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music; +using NzbDrone.Core.Music.Events; +using NzbDrone.Core.SeriesStats; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; +using NzbDrone.SignalR; +using System; +using System.Collections.Generic; + +namespace NzbDrone.Api.Music +{ + public class ArtistModule : NzbDroneRestModuleWithSignalR, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle + //IHandle + { + private readonly IArtistService _artistService; + private readonly IAddArtistService _addSeriesService; + private readonly ISeriesStatisticsService _seriesStatisticsService; + private readonly IMapCoversToLocal _coverMapper; + + public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster, + IArtistService artistService, + IAddArtistService addSeriesService, + ISeriesStatisticsService seriesStatisticsService, + IMapCoversToLocal coverMapper, + RootFolderValidator rootFolderValidator, + ArtistPathValidator seriesPathValidator, + ArtistExistsValidator artistExistsValidator, + DroneFactoryValidator droneFactoryValidator, + SeriesAncestorValidator seriesAncestorValidator, + ProfileExistsValidator profileExistsValidator + ) + : base(signalRBroadcaster) + { + _artistService = artistService; + _addSeriesService = addSeriesService; + _seriesStatisticsService = seriesStatisticsService; + + _coverMapper = coverMapper; + + GetResourceAll = AllArtist; + GetResourceById = GetArtist; + CreateResource = AddArtist; + UpdateResource = UpdatArtist; + DeleteResource = DeleteArtist; + + Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.ProfileId)); + + SharedValidator.RuleFor(s => s.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(seriesPathValidator) + .SetValidator(droneFactoryValidator) + .SetValidator(seriesAncestorValidator) + .When(s => !s.Path.IsNullOrWhiteSpace()); + + SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator); + + PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.ItunesId).GreaterThan(0).SetValidator(artistExistsValidator); + + PutValidator.RuleFor(s => s.Path).IsValidPath(); + } + + private ArtistResource GetArtist(int id) + { + var artist = _artistService.GetArtist(id); + return MapToResource(artist); + } + + private ArtistResource MapToResource(Artist artist) + { + if (artist == null) return null; + + var resource = artist.ToResource(); + MapCoversToLocal(resource); + //FetchAndLinkSeriesStatistics(resource); + //PopulateAlternateTitles(resource); + + return resource; + } + + private List AllArtist() + { + //var seriesStats = _seriesStatisticsService.SeriesStatistics(); + var artistResources = _artistService.GetAllArtists().ToResource(); + + MapCoversToLocal(artistResources.ToArray()); + //LinkSeriesStatistics(seriesResources, seriesStats); + //PopulateAlternateTitles(seriesResources); + + return artistResources; + } + + private int AddArtist(ArtistResource seriesResource) + { + var model = seriesResource.ToModel(); + + return _addSeriesService.AddArtist(model).Id; + } + + private void UpdatArtist(ArtistResource artistResource) + { + var model = artistResource.ToModel(_artistService.GetArtist(artistResource.Id)); + + _artistService.UpdateArtist(model); + + BroadcastResourceChange(ModelAction.Updated, artistResource.Id); + } + + private void DeleteArtist(int id) + { + var deleteFiles = false; + var deleteFilesQuery = Request.Query.deleteFiles; + + if (deleteFilesQuery.HasValue) + { + deleteFiles = Convert.ToBoolean(deleteFilesQuery.Value); + } + + _artistService.DeleteArtist(id, deleteFiles); + } + + private void MapCoversToLocal(params ArtistResource[] artists) + { + foreach (var artistResource in artists) + { + _coverMapper.ConvertToLocalUrls(artistResource.Id, artistResource.Images); + } + } + + public void Handle(TrackImportedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.ImportedTrack.ItunesTrackId); + } + + public void Handle(TrackFileDeletedEvent message) + { + if (message.Reason == DeleteMediaFileReason.Upgrade) return; + + BroadcastResourceChange(ModelAction.Updated, message.TrackFile.ItunesTrackId); + } + + public void Handle(ArtistUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); + } + + public void Handle(ArtistEditedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); + } + + public void Handle(ArtistDeletedEvent message) + { + BroadcastResourceChange(ModelAction.Deleted, message.Artist.ToResource()); + } + + public void Handle(ArtistRenamedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); + } + + //public void Handle(ArtistDeletedEvent message) + //{ + // BroadcastResourceChange(ModelAction.Deleted, message.Artist.ToResource()); + //} + + //public void Handle(ArtistRenamedEvent message) + //{ + // BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); + //} + + //public void Handle(MediaCoversUpdatedEvent message) + //{ + // BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); + //} + + } +} diff --git a/src/NzbDrone.Api/Music/ArtistResource.cs b/src/NzbDrone.Api/Music/ArtistResource.cs index a59df1798..92590c381 100644 --- a/src/NzbDrone.Api/Music/ArtistResource.cs +++ b/src/NzbDrone.Api/Music/ArtistResource.cs @@ -114,7 +114,7 @@ namespace NzbDrone.Api.Music Genres = model.Genres, Tags = model.Tags, Added = model.Added, - //AddOptions = resource.AddOptions, + AddOptions = model.AddOptions, //Ratings = resource.Ratings }; } @@ -168,7 +168,7 @@ namespace NzbDrone.Api.Music Genres = resource.Genres, Tags = resource.Tags, Added = resource.Added, - //AddOptions = resource.AddOptions, + AddOptions = resource.AddOptions, //Ratings = resource.Ratings }; } diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 89f43a55a..80572e068 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -113,6 +113,7 @@ + diff --git a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs index 23674eaec..cf86a5791 100644 --- a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs +++ b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Common.Cloud Services = new HttpRequestBuilder("http://services.lidarr.tv/v1/") .CreateFactory(); - Search = new HttpRequestBuilder("https://itunes.apple.com/search/") + Search = new HttpRequestBuilder("https://itunes.apple.com/{route}/") .CreateFactory(); SkyHookTvdb = new HttpRequestBuilder("http://skyhook.lidarr.tv/v1/tvdb/{route}/{language}/") diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs b/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs index c80cd8c92..5e06431c4 100644 --- a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs +++ b/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs @@ -33,58 +33,58 @@ namespace NzbDrone.Core.DataAugmentation.Xem { _logger.Debug("Updating scene numbering mapping for: {0}", series); - try - { - var mappings = _xemProxy.GetSceneTvdbMappings(series.TvdbId); - - if (!mappings.Any() && !series.UseSceneNumbering) - { - _logger.Debug("Mappings for: {0} are empty, skipping", series); - return; - } - - var episodes = _episodeService.GetEpisodeBySeries(series.Id); - - foreach (var episode in episodes) - { - episode.SceneAbsoluteEpisodeNumber = null; - episode.SceneSeasonNumber = null; - episode.SceneEpisodeNumber = null; - episode.UnverifiedSceneNumbering = false; - } - - foreach (var mapping in mappings) - { - _logger.Debug("Setting scene numbering mappings for {0} S{1:00}E{2:00}", series, mapping.Tvdb.Season, mapping.Tvdb.Episode); - - var episode = episodes.SingleOrDefault(e => e.SeasonNumber == mapping.Tvdb.Season && e.EpisodeNumber == mapping.Tvdb.Episode); - - if (episode == null) - { - _logger.Debug("Information hasn't been added to TheTVDB yet, skipping."); - continue; - } - - episode.SceneAbsoluteEpisodeNumber = mapping.Scene.Absolute; - episode.SceneSeasonNumber = mapping.Scene.Season; - episode.SceneEpisodeNumber = mapping.Scene.Episode; - } - - if (episodes.Any(v => v.SceneEpisodeNumber.HasValue && v.SceneSeasonNumber != 0)) - { - ExtrapolateMappings(series, episodes, mappings); - } - - _episodeService.UpdateEpisodes(episodes); - series.UseSceneNumbering = mappings.Any(); - _seriesService.UpdateSeries(series); - - _logger.Debug("XEM mapping updated for {0}", series); - } - catch (Exception ex) - { - _logger.Error(ex, "Error updating scene numbering mappings for {0}", series); - } + //try + //{ + // var mappings = _xemProxy.GetSceneTvdbMappings(series.TvdbId); + + // if (!mappings.Any() && !series.UseSceneNumbering) + // { + // _logger.Debug("Mappings for: {0} are empty, skipping", series); + // return; + // } + + // var episodes = _episodeService.GetEpisodeBySeries(series.Id); + + // foreach (var episode in episodes) + // { + // episode.SceneAbsoluteEpisodeNumber = null; + // episode.SceneSeasonNumber = null; + // episode.SceneEpisodeNumber = null; + // episode.UnverifiedSceneNumbering = false; + // } + + // foreach (var mapping in mappings) + // { + // _logger.Debug("Setting scene numbering mappings for {0} S{1:00}E{2:00}", series, mapping.Tvdb.Season, mapping.Tvdb.Episode); + + // var episode = episodes.SingleOrDefault(e => e.SeasonNumber == mapping.Tvdb.Season && e.EpisodeNumber == mapping.Tvdb.Episode); + + // if (episode == null) + // { + // _logger.Debug("Information hasn't been added to TheTVDB yet, skipping."); + // continue; + // } + + // episode.SceneAbsoluteEpisodeNumber = mapping.Scene.Absolute; + // episode.SceneSeasonNumber = mapping.Scene.Season; + // episode.SceneEpisodeNumber = mapping.Scene.Episode; + // } + + // if (episodes.Any(v => v.SceneEpisodeNumber.HasValue && v.SceneSeasonNumber != 0)) + // { + // ExtrapolateMappings(series, episodes, mappings); + // } + + // _episodeService.UpdateEpisodes(episodes); + // series.UseSceneNumbering = mappings.Any(); + // _seriesService.UpdateSeries(series); + + // _logger.Debug("XEM mapping updated for {0}", series); + //} + //catch (Exception ex) + //{ + // _logger.Error(ex, "Error updating scene numbering mappings for {0}", series); + //} } private void ExtrapolateMappings(Series series, List episodes, List mappings) @@ -212,32 +212,32 @@ namespace NzbDrone.Core.DataAugmentation.Xem public void Handle(SeriesUpdatedEvent message) { - if (_cache.IsExpired(TimeSpan.FromHours(3))) - { - UpdateXemSeriesIds(); - } - - if (_cache.Count == 0) - { - _logger.Debug("Scene numbering is not available"); - return; - } - - if (!_cache.Find(message.Series.TvdbId.ToString()) && !message.Series.UseSceneNumbering) - { - _logger.Debug("Scene numbering is not available for {0} [{1}]", message.Series.Title, message.Series.TvdbId); - return; - } - - PerformUpdate(message.Series); + //if (_cache.IsExpired(TimeSpan.FromHours(3))) + //{ + // UpdateXemSeriesIds(); + //} + + //if (_cache.Count == 0) + //{ + // _logger.Debug("Scene numbering is not available"); + // return; + //} + + //if (!_cache.Find(message.Series.TvdbId.ToString()) && !message.Series.UseSceneNumbering) + //{ + // _logger.Debug("Scene numbering is not available for {0} [{1}]", message.Series.Title, message.Series.TvdbId); + // return; + //} + + //PerformUpdate(message.Series); } public void Handle(SeriesRefreshStartingEvent message) { - if (message.ManualTrigger && _cache.IsExpired(TimeSpan.FromMinutes(1))) - { - UpdateXemSeriesIds(); - } + //if (message.ManualTrigger && _cache.IsExpired(TimeSpan.FromMinutes(1))) + //{ + // UpdateXemSeriesIds(); + //} } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs index 48f921f30..48676acb4 100644 --- a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs +++ b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs @@ -12,21 +12,23 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Create.TableForModel("Artists") + Create.TableForModel("Artist") .WithColumn("ItunesId").AsInt32().Unique() .WithColumn("ArtistName").AsString().Unique() .WithColumn("ArtistSlug").AsString().Unique() - .WithColumn("CleanTitle").AsString() + .WithColumn("CleanTitle").AsString() // Do we need this? .WithColumn("Monitored").AsBoolean() .WithColumn("LastInfoSync").AsDateTime().Nullable() .WithColumn("LastDiskSync").AsDateTime().Nullable() - .WithColumn("Overview").AsString() .WithColumn("Status").AsInt32() .WithColumn("Path").AsString() .WithColumn("Images").AsString() .WithColumn("QualityProfileId").AsInt32() - .WithColumn("AirTime").AsString().Nullable() // JVM: This might be DropDate instead - //.WithColumn("BacklogSetting").AsInt32() + .WithColumn("Added").AsDateTime() + .WithColumn("AddOptions").AsString() + .WithColumn("AlbumFolder").AsInt32() + .WithColumn("Genre").AsString() + .WithColumn("Albums").AsString() ; Create.TableForModel("Albums") @@ -37,7 +39,8 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Image").AsInt32() .WithColumn("TrackCount").AsInt32() .WithColumn("DiscCount").AsInt32() - .WithColumn("Monitored").AsBoolean(); + .WithColumn("Monitored").AsBoolean() + .WithColumn("Overview").AsString(); Create.TableForModel("Tracks") .WithColumn("ItunesTrackId").AsInt32().Unique() diff --git a/src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs b/src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs new file mode 100644 index 000000000..60a05febd --- /dev/null +++ b/src/NzbDrone.Core/Exceptions/ArtistNotFoundException.cs @@ -0,0 +1,31 @@ +using NzbDrone.Common.Exceptions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Exceptions +{ + public class ArtistNotFoundException : NzbDroneException + { + public int ItunesId { get; set; } + + public ArtistNotFoundException(int itunesId) + : base(string.Format("Series with iTunesId {0} was not found, it may have been removed from iTunes.", itunesId)) + { + ItunesId = itunesId; + } + + public ArtistNotFoundException(int itunesId, string message, params object[] args) + : base(message, args) + { + ItunesId = itunesId; + } + + public ArtistNotFoundException(int itunesId, string message) + : base(message) + { + ItunesId = itunesId; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Events/ArtistRenamedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/ArtistRenamedEvent.cs new file mode 100644 index 000000000..f20f4f280 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/ArtistRenamedEvent.cs @@ -0,0 +1,19 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Music; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class ArtistRenamedEvent : IEvent + { + public Artist Artist { get; private set; } + + public ArtistRenamedEvent(Artist artist) + { + Artist = artist; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Events/TrackFileDeletedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/TrackFileDeletedEvent.cs new file mode 100644 index 000000000..19017e686 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/TrackFileDeletedEvent.cs @@ -0,0 +1,20 @@ +using NzbDrone.Common.Messaging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class TrackFileDeletedEvent : IEvent + { + public TrackFile TrackFile { get; private set; } + public DeleteMediaFileReason Reason { get; private set; } + + public TrackFileDeletedEvent(TrackFile trackFile, DeleteMediaFileReason reason) + { + TrackFile = trackFile; + Reason = reason; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs new file mode 100644 index 000000000..812b2ae78 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/TrackImportedEvent.cs @@ -0,0 +1,36 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Parser.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class TrackImportedEvent : IEvent + { + public LocalTrack TrackInfo { get; private set; } + public TrackFile ImportedTrack { get; private set; } + public bool NewDownload { get; private set; } + public string DownloadClient { get; private set; } + public string DownloadId { get; private set; } + public bool IsReadOnly { get; set; } + + public TrackImportedEvent(LocalTrack trackInfo, TrackFile importedTrack, bool newDownload) + { + TrackInfo = trackInfo; + ImportedTrack = importedTrack; + NewDownload = newDownload; + } + + public TrackImportedEvent(LocalTrack trackInfo, TrackFile importedTrack, bool newDownload, string downloadClient, string downloadId, bool isReadOnly) + { + TrackInfo = trackInfo; + ImportedTrack = importedTrack; + NewDownload = newDownload; + DownloadClient = downloadClient; + DownloadId = downloadId; + IsReadOnly = isReadOnly; + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs b/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs new file mode 100644 index 000000000..4ae5a3420 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/IProvideArtistInfo.cs @@ -0,0 +1,11 @@ +using NzbDrone.Core.Music; +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.MetadataSource.SkyHook +{ + public interface IProvideArtistInfo + { + Tuple> GetArtistInfo(int itunesId); + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index d11b60107..e7b97350b 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -16,7 +16,7 @@ using Newtonsoft.Json; namespace NzbDrone.Core.MetadataSource.SkyHook { - public class SkyHookProxy : IProvideSeriesInfo, ISearchForNewSeries + public class SkyHookProxy : IProvideSeriesInfo, IProvideArtistInfo, ISearchForNewSeries { private readonly IHttpClient _httpClient; private readonly Logger _logger; @@ -124,6 +124,39 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } + public Tuple> GetArtistInfo(int itunesId) + { + Console.WriteLine("[GetArtistInfo] id:" + itunesId); + //https://itunes.apple.com/lookup?id=909253 + var httpRequest = _requestBuilder.Create() + .SetSegment("route", "lookup") + .AddQueryParam("id", itunesId.ToString()) + .Build(); + + httpRequest.AllowAutoRedirect = true; + httpRequest.SuppressHttpError = true; + + var httpResponse = _httpClient.Get(httpRequest); + + if (httpResponse.HasHttpError) + { + if (httpResponse.StatusCode == HttpStatusCode.NotFound) + { + throw new ArtistNotFoundException(itunesId); + } + else + { + throw new HttpException(httpRequest, httpResponse); + } + } + + Console.WriteLine("GetArtistInfo, GetArtistInfo"); + //var tracks = httpResponse.Resource.Episodes.Select(MapEpisode); + //var artist = MapArtist(httpResponse.Resource); + // I don't know how we are getting tracks from iTunes yet. + return new Tuple>(MapArtists(httpResponse.Resource)[0], new List()); + //return new Tuple>(artist, tracks.ToList()); + } public List SearchForNewArtist(string title) { try @@ -142,52 +175,27 @@ namespace NzbDrone.Core.MetadataSource.SkyHook return new List(); } - //try - //{ - // return new List { GetArtistInfo(itunesId).Item1 }; - //} - //catch (ArtistNotFoundException) - //{ - // return new List(); - //} + try + { + return new List { GetArtistInfo(itunesId).Item1 }; + } + catch (ArtistNotFoundException) + { + return new List(); + } } var httpRequest = _requestBuilder.Create() + .SetSegment("route", "search") .AddQueryParam("entity", "album") .AddQueryParam("term", title.ToLower().Trim()) .Build(); - Console.WriteLine("httpRequest: ", httpRequest); - var httpResponse = _httpClient.Get(httpRequest); - Album tempAlbum; - List artists = new List(); - foreach (var album in httpResponse.Resource.Results) - { - int index = artists.FindIndex(a => a.ItunesId == album.ArtistId); - tempAlbum = MapAlbum(album); - - if (index >= 0) - { - artists[index].Albums.Add(tempAlbum); - } - else - { - Artist tempArtist = new Artist(); - // TODO: Perform the MapArtist call here - tempArtist.ItunesId = album.ArtistId; - tempArtist.ArtistName = album.ArtistName; - tempArtist.Genres.Add(album.PrimaryGenreName); - tempArtist.Albums.Add(tempAlbum); - artists.Add(tempArtist); - } - - } - - return artists; + return MapArtists(httpResponse.Resource); } catch (HttpException) { @@ -200,6 +208,34 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } + private List MapArtists(ArtistResource resource) + { + Album tempAlbum; + List artists = new List(); + foreach (var album in resource.Results) + { + int index = artists.FindIndex(a => a.ItunesId == album.ArtistId); + tempAlbum = MapAlbum(album); + + if (index >= 0) + { + artists[index].Albums.Add(tempAlbum); + } + else + { + Artist tempArtist = new Artist(); + tempArtist.ItunesId = album.ArtistId; + tempArtist.ArtistName = album.ArtistName; + tempArtist.Genres.Add(album.PrimaryGenreName); + tempArtist.Albums.Add(tempAlbum); + artists.Add(tempArtist); + } + + } + + return artists; + } + private Album MapAlbum(AlbumResource albumQuery) { Album album = new Album(); diff --git a/src/NzbDrone.Core/Music/AddArtistOptions.cs b/src/NzbDrone.Core/Music/AddArtistOptions.cs new file mode 100644 index 000000000..5f83e1c72 --- /dev/null +++ b/src/NzbDrone.Core/Music/AddArtistOptions.cs @@ -0,0 +1,13 @@ +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public class AddArtistOptions : MonitoringOptions + { + public bool SearchForMissingTracks { get; set; } + } +} diff --git a/src/NzbDrone.Core/Music/AddArtistService.cs b/src/NzbDrone.Core/Music/AddArtistService.cs new file mode 100644 index 000000000..c6dcf8946 --- /dev/null +++ b/src/NzbDrone.Core/Music/AddArtistService.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FluentValidation; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser; +using NzbDrone.Core.MetadataSource.SkyHook; + +namespace NzbDrone.Core.Music +{ + public interface IAddArtistService + { + Artist AddArtist(Artist newArtist); + } + + public class AddSeriesService : IAddArtistService + { + private readonly IArtistService _artistService; + private readonly IProvideArtistInfo _artistInfo; + private readonly IBuildFileNames _fileNameBuilder; + private readonly IAddArtistValidator _addArtistValidator; + private readonly Logger _logger; + + public AddSeriesService(IArtistService artistService, + IProvideArtistInfo artistInfo, + IBuildFileNames fileNameBuilder, + IAddArtistValidator addArtistValidator, + Logger logger) + { + _artistService = artistService; + _artistInfo = artistInfo; + _fileNameBuilder = fileNameBuilder; + _addArtistValidator = addArtistValidator; + _logger = logger; + } + + public Artist AddArtist(Artist newArtist) + { + Ensure.That(newArtist, () => newArtist).IsNotNull(); + + newArtist = AddSkyhookData(newArtist); + + if (string.IsNullOrWhiteSpace(newArtist.Path)) + { + var folderName = newArtist.ArtistName;// _fileNameBuilder.GetArtistFolder(newArtist); + newArtist.Path = Path.Combine(newArtist.RootFolderPath, folderName); + } + + newArtist.CleanTitle = newArtist.ArtistName.CleanSeriesTitle(); + newArtist.SortTitle = ArtistNameNormalizer.Normalize(newArtist.ArtistName, newArtist.ItunesId); + newArtist.Added = DateTime.UtcNow; + + var validationResult = _addArtistValidator.Validate(newArtist); + + if (!validationResult.IsValid) + { + throw new ValidationException(validationResult.Errors); + } + + _logger.Info("Adding Series {0} Path: [{1}]", newArtist, newArtist.Path); + _artistService.AddArtist(newArtist); + + return newArtist; + } + + private Artist AddSkyhookData(Artist newArtist) + { + Tuple> tuple; + + try + { + tuple = _artistInfo.GetArtistInfo(newArtist.ItunesId); + } + catch (SeriesNotFoundException) + { + _logger.Error("tvdbid {1} was not found, it may have been removed from TheTVDB.", newArtist.ItunesId); + + throw new ValidationException(new List + { + new ValidationFailure("TvdbId", "A series with this ID was not found", newArtist.ItunesId) + }); + } + + var artist = tuple.Item1; + + // If seasons were passed in on the new series use them, otherwise use the seasons from Skyhook + // TODO: Refactor for albums + newArtist.Albums = newArtist.Albums != null && newArtist.Albums.Any() ? newArtist.Albums : artist.Albums; + + artist.ApplyChanges(newArtist); + + return artist; + } + } +} diff --git a/src/NzbDrone.Core/Music/AddArtistValidator.cs b/src/NzbDrone.Core/Music/AddArtistValidator.cs new file mode 100644 index 000000000..a21e3bac5 --- /dev/null +++ b/src/NzbDrone.Core/Music/AddArtistValidator.cs @@ -0,0 +1,34 @@ +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Core.Validation.Paths; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public interface IAddArtistValidator + { + ValidationResult Validate(Artist instance); + } + + public class AddArtistValidator : AbstractValidator, IAddArtistValidator + { + public AddArtistValidator(RootFolderValidator rootFolderValidator, + SeriesPathValidator seriesPathValidator, + DroneFactoryValidator droneFactoryValidator, + SeriesAncestorValidator seriesAncestorValidator, + ArtistSlugValidator seriesTitleSlugValidator) + { + RuleFor(c => c.Path).Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(seriesPathValidator) + .SetValidator(droneFactoryValidator) + .SetValidator(seriesAncestorValidator); + + RuleFor(c => c.ArtistSlug).SetValidator(seriesTitleSlugValidator);// 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 6a8ccf959..7c8385bfc 100644 --- a/src/NzbDrone.Core/Music/Artist.cs +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -2,6 +2,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Profiles; +using NzbDrone.Core.Tv; using System; using System.Collections.Generic; using System.Linq; @@ -56,7 +57,7 @@ namespace NzbDrone.Core.Music public HashSet Tags { get; set; } public bool ArtistFolder { get; set; } - //public AddSeriesOptions AddOptions { get; set; } // TODO: Learn what this does + public AddSeriesOptions AddOptions { get; set; } // TODO: Learn what this does public override string ToString() { @@ -78,7 +79,7 @@ namespace NzbDrone.Core.Music //SeriesType = otherArtist.SeriesType; RootFolderPath = otherArtist.RootFolderPath; Tags = otherArtist.Tags; - //AddOptions = otherArtist.AddOptions; + AddOptions = otherArtist.AddOptions; } } } diff --git a/src/NzbDrone.Core/Music/ArtistNameNormalizer.cs b/src/NzbDrone.Core/Music/ArtistNameNormalizer.cs new file mode 100644 index 000000000..64003d8ff --- /dev/null +++ b/src/NzbDrone.Core/Music/ArtistNameNormalizer.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + + public static class ArtistNameNormalizer + { + private readonly static Dictionary PreComputedTitles = new Dictionary + { + { 281588, "a to z" }, + { 266757, "ad trials triumph early church" }, + { 289260, "ad bible continues"} + }; + + public static string Normalize(string title, int iTunesId) + { + if (PreComputedTitles.ContainsKey(iTunesId)) + { + return PreComputedTitles[iTunesId]; + } + + return Parser.Parser.NormalizeTitle(title).ToLower(); + } + } +} diff --git a/src/NzbDrone.Core/Music/ArtistRepository.cs b/src/NzbDrone.Core/Music/ArtistRepository.cs index 39fcbb1db..f9e7f9da4 100644 --- a/src/NzbDrone.Core/Music/ArtistRepository.cs +++ b/src/NzbDrone.Core/Music/ArtistRepository.cs @@ -1,129 +1,40 @@ -using NzbDrone.Core.Datastore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Linq.Expressions; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Music { public interface IArtistRepository : IBasicRepository { bool ArtistPathExists(string path); - Artist FindByTitle(string cleanTitle); + Artist FindByName(string cleanTitle); Artist FindByItunesId(int iTunesId); } - public class ArtistRepository : IArtistRepository + public class ArtistRepository : BasicRepository, IArtistRepository { - public IEnumerable All() + public ArtistRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) { - throw new NotImplementedException(); } + public bool ArtistPathExists(string path) { - throw new NotImplementedException(); - } - - public int Count() - { - throw new NotImplementedException(); - } - - public void Delete(Artist model) - { - throw new NotImplementedException(); - } - - public void Delete(int id) - { - throw new NotImplementedException(); - } - - public void DeleteMany(IEnumerable ids) - { - throw new NotImplementedException(); - } - - public void DeleteMany(List model) - { - throw new NotImplementedException(); + return Query.Where(c => c.Path == path).Any(); } public Artist FindByItunesId(int iTunesId) { - throw new NotImplementedException(); + return Query.Where(s => s.ItunesId == iTunesId).SingleOrDefault(); } - public Artist FindByTitle(string cleanTitle) + public Artist FindByName(string cleanName) { - throw new NotImplementedException(); - } + cleanName = cleanName.ToLowerInvariant(); - public IEnumerable Get(IEnumerable ids) - { - throw new NotImplementedException(); - } - - public Artist Get(int id) - { - throw new NotImplementedException(); - } - - public PagingSpec GetPaged(PagingSpec pagingSpec) - { - throw new NotImplementedException(); - } - - public bool HasItems() - { - throw new NotImplementedException(); - } - - public Artist Insert(Artist model) - { - throw new NotImplementedException(); - } - - public void InsertMany(IList model) - { - throw new NotImplementedException(); - } - - public void Purge(bool vacuum = false) - { - throw new NotImplementedException(); - } - - public void SetFields(Artist model, params Expression>[] properties) - { - throw new NotImplementedException(); - } - - public Artist Single() - { - throw new NotImplementedException(); - } - - public Artist SingleOrDefault() - { - throw new NotImplementedException(); - } - - public Artist Update(Artist model) - { - throw new NotImplementedException(); - } - - public void UpdateMany(IList model) - { - throw new NotImplementedException(); - } - - public Artist Upsert(Artist model) - { - throw new NotImplementedException(); + return Query.Where(s => s.CleanTitle == cleanName) + .SingleOrDefault(); } } } diff --git a/src/NzbDrone.Core/Music/ArtistService.cs b/src/NzbDrone.Core/Music/ArtistService.cs index 4aecdb36d..85779b414 100644 --- a/src/NzbDrone.Core/Music/ArtistService.cs +++ b/src/NzbDrone.Core/Music/ArtistService.cs @@ -1,10 +1,14 @@ using NLog; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music.Events; using NzbDrone.Core.Organizer; using System; using System.Collections.Generic; using System.Linq; +using NzbDrone.Core.Parser; using System.Text; +using System.IO; +using NzbDrone.Common.Extensions; namespace NzbDrone.Core.Music { @@ -14,7 +18,7 @@ namespace NzbDrone.Core.Music List GetArtists(IEnumerable artistIds); Artist AddArtist(Artist newArtist); Artist FindByItunesId(int itunesId); - Artist FindByTitle(string title); + Artist FindByName(string title); Artist FindByTitleInexact(string title); void DeleteArtist(int artistId, bool deleteFiles); List GetAllArtists(); @@ -32,29 +36,47 @@ namespace NzbDrone.Core.Music private readonly IBuildFileNames _fileNameBuilder; private readonly Logger _logger; + public ArtistService(IArtistRepository artistRepository, + IEventAggregator eventAggregator, + ITrackService trackService, + IBuildFileNames fileNameBuilder, + Logger logger) + { + _artistRepository = artistRepository; + _eventAggregator = eventAggregator; + _trackService = trackService; + _fileNameBuilder = fileNameBuilder; + _logger = logger; + } + public Artist AddArtist(Artist newArtist) { - throw new NotImplementedException(); + _artistRepository.Insert(newArtist); + _eventAggregator.PublishEvent(new ArtistAddedEvent(GetArtist(newArtist.Id))); + + return newArtist; } public bool ArtistPathExists(string folder) { - throw new NotImplementedException(); + return _artistRepository.ArtistPathExists(folder); } public void DeleteArtist(int artistId, bool deleteFiles) { - throw new NotImplementedException(); + var artist = _artistRepository.Get(artistId); + _artistRepository.Delete(artistId); + _eventAggregator.PublishEvent(new ArtistDeletedEvent(artist, deleteFiles)); } public Artist FindByItunesId(int itunesId) { - throw new NotImplementedException(); + return _artistRepository.FindByItunesId(itunesId); } - public Artist FindByTitle(string title) + public Artist FindByName(string title) { - throw new NotImplementedException(); + return _artistRepository.FindByName(title.CleanArtistTitle()); } public Artist FindByTitleInexact(string title) @@ -64,32 +86,70 @@ namespace NzbDrone.Core.Music public List GetAllArtists() { - throw new NotImplementedException(); + _logger.Debug("Count of repository: " + _artistRepository.Count()); + // TEMP: Return empty list while we debug the DB error + return new List(); + //return _artistRepository.All().ToList(); } public Artist GetArtist(int artistId) { - throw new NotImplementedException(); + return _artistRepository.Get(artistId); } public List GetArtists(IEnumerable artistIds) { - throw new NotImplementedException(); + return _artistRepository.Get(artistIds).ToList(); } public void RemoveAddOptions(Artist artist) { - throw new NotImplementedException(); + _artistRepository.SetFields(artist, s => s.AddOptions); } public Artist UpdateArtist(Artist artist) { - throw new NotImplementedException(); + var storedArtist = GetArtist(artist.Id); // Is it Id or iTunesId? + + foreach (var album in artist.Albums) + { + var storedAlbum = storedArtist.Albums.SingleOrDefault(s => s.AlbumId == album.AlbumId); + + if (storedAlbum != null && album.Monitored != storedAlbum.Monitored) + { + _trackService.SetTrackMonitoredByAlbum(artist.Id, album.AlbumId, album.Monitored); + } + } + + var updatedArtist = _artistRepository.Update(artist); + _eventAggregator.PublishEvent(new ArtistEditedEvent(updatedArtist, storedArtist)); + + return updatedArtist; } public List UpdateArtists(List artist) { - throw new NotImplementedException(); + _logger.Debug("Updating {0} artist", artist.Count); + foreach (var s in artist) + { + _logger.Trace("Updating: {0}", s.ArtistName); + if (!s.RootFolderPath.IsNullOrWhiteSpace()) + { + var folderName = new DirectoryInfo(s.Path).Name; + s.Path = Path.Combine(s.RootFolderPath, folderName); + _logger.Trace("Changing path for {0} to {1}", s.ArtistName, s.Path); + } + + else + { + _logger.Trace("Not changing path for: {0}", s.ArtistName); + } + } + + _artistRepository.UpdateMany(artist); + _logger.Debug("{0} artists updated", artist.Count); + + return artist; } } } diff --git a/src/NzbDrone.Core/Music/ArtistSlugValidator.cs b/src/NzbDrone.Core/Music/ArtistSlugValidator.cs new file mode 100644 index 000000000..4d5626c89 --- /dev/null +++ b/src/NzbDrone.Core/Music/ArtistSlugValidator.cs @@ -0,0 +1,29 @@ +using FluentValidation.Validators; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public class ArtistSlugValidator : PropertyValidator + { + private readonly IArtistService _artistService; + + public ArtistSlugValidator(IArtistService artistService) + : base("Title slug is in use by another artist with a similar name") + { + _artistService = artistService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + dynamic instance = context.ParentContext.InstanceToValidate; + var instanceId = (int)instance.Id; + + return !_artistService.GetAllArtists().Exists(s => s.ArtistSlug.Equals(context.PropertyValue.ToString()) && s.Id != instanceId); + } + } +} diff --git a/src/NzbDrone.Core/Music/Events/ArtistAddedEvent.cs b/src/NzbDrone.Core/Music/Events/ArtistAddedEvent.cs new file mode 100644 index 000000000..d8b374ac3 --- /dev/null +++ b/src/NzbDrone.Core/Music/Events/ArtistAddedEvent.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 ArtistAddedEvent : IEvent + { + public Artist Artist { get; private set; } + + public ArtistAddedEvent(Artist artist) + { + Artist = artist; + } + } +} diff --git a/src/NzbDrone.Core/Music/Events/ArtistDeletedEvent.cs b/src/NzbDrone.Core/Music/Events/ArtistDeletedEvent.cs new file mode 100644 index 000000000..b889816a6 --- /dev/null +++ b/src/NzbDrone.Core/Music/Events/ArtistDeletedEvent.cs @@ -0,0 +1,20 @@ +using NzbDrone.Common.Messaging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music.Events +{ + public class ArtistDeletedEvent : IEvent + { + public Artist Artist { get; private set; } + public bool DeleteFiles { get; private set; } + + public ArtistDeletedEvent(Artist artist, bool deleteFiles) + { + Artist = artist; + DeleteFiles = deleteFiles; + } + } +} diff --git a/src/NzbDrone.Core/Music/Events/ArtistEditedEvent.cs b/src/NzbDrone.Core/Music/Events/ArtistEditedEvent.cs new file mode 100644 index 000000000..4511e8943 --- /dev/null +++ b/src/NzbDrone.Core/Music/Events/ArtistEditedEvent.cs @@ -0,0 +1,20 @@ +using NzbDrone.Common.Messaging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music.Events +{ + public class ArtistEditedEvent : IEvent + { + public Artist Artist { get; private set; } + public Artist OldArtist { get; private set; } + + public ArtistEditedEvent(Artist artist, Artist oldArtist) + { + Artist = artist; + OldArtist = oldArtist; + } + } +} diff --git a/src/NzbDrone.Core/Music/Events/ArtistUpdatedEvent.cs b/src/NzbDrone.Core/Music/Events/ArtistUpdatedEvent.cs new file mode 100644 index 000000000..8555eba80 --- /dev/null +++ b/src/NzbDrone.Core/Music/Events/ArtistUpdatedEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Music.Events +{ + public class ArtistUpdatedEvent : IEvent + { + public Artist Artist { get; private set; } + + public ArtistUpdatedEvent(Artist artist) + { + Artist = artist; + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 3b00c7cf0..606293015 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -518,6 +518,7 @@ + @@ -758,6 +759,7 @@ + @@ -766,6 +768,8 @@ + + @@ -808,6 +812,7 @@ + @@ -840,10 +845,19 @@ + + + + + + + + + @@ -1141,6 +1155,8 @@ + + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 1a9b1568b..c85e72927 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -11,6 +11,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Organizer { @@ -22,6 +23,9 @@ namespace NzbDrone.Core.Organizer BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); string GetSeriesFolder(Series series, NamingConfig namingConfig = null); string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); + + // TODO: Implement Music functions + //string GetArtistFolder(Artist artist, NamingConfig namingConfig = null); } public class FileNameBuilder : IBuildFileNames @@ -278,6 +282,12 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title); } + private void AddArtistTokens(Dictionary> tokenHandlers, Artist artist) + { + tokenHandlers["{Artist Name}"] = m => artist.ArtistName; + tokenHandlers["{Artist CleanTitle}"] = m => CleanTitle(artist.ArtistName); + } + private string AddSeasonEpisodeNumberingTokens(string pattern, Dictionary> tokenHandlers, List episodes, NamingConfig namingConfig) { var episodeFormats = GetEpisodeFormat(pattern).DistinctBy(v => v.SeasonEpisodePattern).ToList(); @@ -768,6 +778,36 @@ namespace NzbDrone.Core.Organizer return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); } + + //public string GetArtistFolder(Artist artist, NamingConfig namingConfig = null) + //{ + // if (namingConfig == null) + // { + // namingConfig = _namingConfigService.GetConfig(); + // } + + // var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + // AddArtistTokens(tokenHandlers, artist); + + // return CleanFolderName(ReplaceTokens("{Artist Name}", tokenHandlers, namingConfig)); //namingConfig.ArtistFolderFormat, + //} + + //public string GetAlbumFolder(Artist artist, string albumName, NamingConfig namingConfig = null) + //{ + // throw new NotImplementedException(); + // //if (namingConfig == null) + // //{ + // // namingConfig = _namingConfigService.GetConfig(); + // //} + + // //var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + // //AddSeriesTokens(tokenHandlers, artist); + // //AddSeasonTokens(tokenHandlers, seasonNumber); + + // //return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); + //} } internal sealed class TokenMatch diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs index 5de62a090..637cd15cd 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -13,7 +13,9 @@ namespace NzbDrone.Core.Organizer DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Full}", AnimeEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}", SeriesFolderFormat = "{Series Title}", - SeasonFolderFormat = "Season {season}" + SeasonFolderFormat = "Season {season}", + ArtistFolderFormat = "{Artist Name}", + AlbumFolderFormat = "{Album Name} ({Year})" }; public bool RenameEpisodes { get; set; } @@ -24,5 +26,7 @@ namespace NzbDrone.Core.Organizer public string AnimeEpisodeFormat { get; set; } public string SeriesFolderFormat { get; set; } public string SeasonFolderFormat { get; set; } + public string ArtistFolderFormat { get; set; } + public string AlbumFolderFormat { get; set; } } } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 1a541cd1c..be6ed1da3 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -564,6 +564,17 @@ namespace NzbDrone.Core.Parser return NormalizeRegex.Replace(title, string.Empty).ToLower().RemoveAccent(); } + public static string CleanArtistTitle(this string title) + { + long number = 0; + + //If Title only contains numbers return it as is. + if (long.TryParse(title, out number)) + return title; + + return NormalizeRegex.Replace(title, string.Empty).ToLower().RemoveAccent(); + } + public static string NormalizeEpisodeTitle(string title) { title = SpecialEpisodeWordRegex.Replace(title, string.Empty); diff --git a/src/NzbDrone.Core/Tv/AddSeriesOptions.cs b/src/NzbDrone.Core/Tv/AddSeriesOptions.cs index fceae6586..d325076d8 100644 --- a/src/NzbDrone.Core/Tv/AddSeriesOptions.cs +++ b/src/NzbDrone.Core/Tv/AddSeriesOptions.cs @@ -3,5 +3,6 @@ public class AddSeriesOptions : MonitoringOptions { public bool SearchForMissingEpisodes { get; set; } + } } diff --git a/src/NzbDrone.Core/Tv/MonitoringOptions.cs b/src/NzbDrone.Core/Tv/MonitoringOptions.cs index 2cda68b1c..760ec68ec 100644 --- a/src/NzbDrone.Core/Tv/MonitoringOptions.cs +++ b/src/NzbDrone.Core/Tv/MonitoringOptions.cs @@ -6,5 +6,8 @@ namespace NzbDrone.Core.Tv { public bool IgnoreEpisodesWithFiles { get; set; } public bool IgnoreEpisodesWithoutFiles { get; set; } + + public bool IgnoreTracksWithFiles { get; set; } + public bool IgnoreTracksWithoutFiles { get; set; } } } diff --git a/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs new file mode 100644 index 000000000..4a56bd072 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/ArtistExistsValidator.cs @@ -0,0 +1,29 @@ +using FluentValidation.Validators; +using NzbDrone.Core.Music; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Validation.Paths +{ + public class ArtistExistsValidator : PropertyValidator + { + private readonly IArtistService _artistService; + + public ArtistExistsValidator(IArtistService artistService) + : base("This artist has already been added") + { + _artistService = artistService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + var itunesId = Convert.ToInt32(context.PropertyValue.ToString()); + + return (!_artistService.GetAllArtists().Exists(s => s.ItunesId == itunesId)); + } + } +} diff --git a/src/NzbDrone.Core/Validation/Paths/ArtistPathValidator.cs b/src/NzbDrone.Core/Validation/Paths/ArtistPathValidator.cs new file mode 100644 index 000000000..f901127f3 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/ArtistPathValidator.cs @@ -0,0 +1,31 @@ +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Music; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Validation.Paths +{ + public class ArtistPathValidator : PropertyValidator + { + private readonly IArtistService _artistService; + + public ArtistPathValidator(IArtistService artistService) + : base("Path is already configured for another artist") + { + _artistService = artistService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + dynamic instance = context.ParentContext.InstanceToValidate; + var instanceId = (int)instance.Id; + + return (!_artistService.GetAllArtists().Exists(s => s.Path.PathEquals(context.PropertyValue.ToString()) && s.Id != instanceId)); + } + } +} diff --git a/src/UI/AddSeries/SearchResultView.js b/src/UI/AddSeries/SearchResultView.js index 7e7f60f47..aaef92a1f 100644 --- a/src/UI/AddSeries/SearchResultView.js +++ b/src/UI/AddSeries/SearchResultView.js @@ -18,25 +18,29 @@ var view = Marionette.ItemView.extend({ template : 'AddSeries/SearchResultViewTemplate', ui : { - profile : '.x-profile', - rootFolder : '.x-root-folder', - seasonFolder : '.x-season-folder', - seriesType : '.x-series-type', - monitor : '.x-monitor', - monitorTooltip : '.x-monitor-tooltip', - addButton : '.x-add', - addSearchButton : '.x-add-search', - overview : '.x-overview' + profile : '.x-profile', + rootFolder : '.x-root-folder', + seasonFolder : '.x-season-folder', + seriesType : '.x-series-type', + monitor : '.x-monitor', + monitorTooltip : '.x-monitor-tooltip', + addButton : '.x-add', + addAlbumButton : '.x-add-album', + addSearchButton : '.x-add-search', + addAlbumSearchButton : '.x-add-album-search', + overview : '.x-overview' }, events : { - 'click .x-add' : '_addWithoutSearch', - 'click .x-add-search' : '_addAndSearch', - 'change .x-profile' : '_profileChanged', - 'change .x-root-folder' : '_rootFolderChanged', - 'change .x-season-folder' : '_seasonFolderChanged', - 'change .x-series-type' : '_seriesTypeChanged', - 'change .x-monitor' : '_monitorChanged' + 'click .x-add' : '_addWithoutSearch', + 'click .x-add-album' : '_addWithoutSearch', + 'click .x-add-search' : '_addAndSearch', + 'click .x-add-album-search' : '_addAndSearch', + 'change .x-profile' : '_profileChanged', + 'change .x-root-folder' : '_rootFolderChanged', + 'change .x-season-folder' : '_seasonFolderChanged', + 'change .x-series-type' : '_seriesTypeChanged', + 'change .x-monitor' : '_monitorChanged' }, initialize : function() { @@ -161,7 +165,8 @@ var view = Marionette.ItemView.extend({ this._rootFolderChanged(); }, - _addWithoutSearch : function() { + _addWithoutSearch : function(evt) { + console.log(evt); this._addSeries(false); }, @@ -171,8 +176,8 @@ var view = Marionette.ItemView.extend({ _addSeries : function(searchForMissing) { // TODO: Refactor to handle multiple add buttons/albums - var addButton = this.ui.addButton[0]; - var addSearchButton = this.ui.addSearchButton[0]; + var addButton = this.ui.addButton; + var addSearchButton = this.ui.addSearchButton; console.log('_addSeries, searchForMissing=', searchForMissing); addButton.addClass('disabled'); @@ -221,7 +226,7 @@ var view = Marionette.ItemView.extend({ message : 'Added: ' + self.model.get('title'), actions : { goToSeries : { - label : 'Go to Series', + label : 'Go to Artist', action : function() { Backbone.history.navigate('/artist/' + self.model.get('titleSlug'), { trigger : true }); } @@ -246,7 +251,7 @@ var view = Marionette.ItemView.extend({ var lastSeason = _.max(this.model.get('seasons'), 'seasonNumber'); var firstSeason = _.min(_.reject(this.model.get('seasons'), { seasonNumber : 0 }), 'seasonNumber'); - this.model.setSeasonPass(firstSeason.seasonNumber); + //this.model.setSeasonPass(firstSeason.seasonNumber); // TODO var options = { ignoreEpisodesWithFiles : false, @@ -262,14 +267,14 @@ var view = Marionette.ItemView.extend({ options.ignoreEpisodesWithoutFiles = true; } - else if (monitor === 'latest') { + /*else if (monitor === 'latest') { this.model.setSeasonPass(lastSeason.seasonNumber); } else if (monitor === 'first') { this.model.setSeasonPass(lastSeason.seasonNumber + 1); this.model.setSeasonMonitored(firstSeason.seasonNumber); - } + }*/ else if (monitor === 'missing') { options.ignoreEpisodesWithFiles = true; @@ -279,9 +284,9 @@ var view = Marionette.ItemView.extend({ options.ignoreEpisodesWithoutFiles = true; } - else if (monitor === 'none') { + /*else if (monitor === 'none') { this.model.setSeasonPass(lastSeason.seasonNumber + 1); - } + }*/ return options; } diff --git a/src/UI/AddSeries/SearchResultViewTemplate.hbs b/src/UI/AddSeries/SearchResultViewTemplate.hbs index 12e4f9d44..23cee51e9 100644 --- a/src/UI/AddSeries/SearchResultViewTemplate.hbs +++ b/src/UI/AddSeries/SearchResultViewTemplate.hbs @@ -73,11 +73,11 @@
- -
@@ -116,11 +116,11 @@
- -
diff --git a/src/UI/Artist/ArtistModel.js b/src/UI/Artist/ArtistModel.js index 70763dac2..209ebc1fa 100644 --- a/src/UI/Artist/ArtistModel.js +++ b/src/UI/Artist/ArtistModel.js @@ -11,9 +11,9 @@ module.exports = Backbone.Model.extend({ status : 0 }, - setAlbumsMonitored : function(seasonNumber) { + setAlbumsMonitored : function(albumName) { _.each(this.get('albums'), function(album) { - if (season.seasonNumber === seasonNumber) { + if (season.albumName === albumName) { album.monitored = !album.monitored; } });