diff --git a/src/NzbDrone.Api/Music/AlbumResource.cs b/src/NzbDrone.Api/Music/AlbumResource.cs new file mode 100644 index 000000000..a6d49d3bd --- /dev/null +++ b/src/NzbDrone.Api/Music/AlbumResource.cs @@ -0,0 +1,62 @@ +using NzbDrone.Core.Music; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Api.Music +{ + public class AlbumResource + { + public int AlbumId { get; set; } + public string AlbumName { get; set; } + public bool Monitored { get; set; } + public int Year { get; set; } + public List Genres { get; set; } + public string ArtworkUrl { get; set; } + + } + + public static class AlbumResourceMapper + { + public static AlbumResource ToResource(this Album model) + { + if (model == null) return null; + + return new AlbumResource + { + AlbumId = model.AlbumId, + Monitored = model.Monitored, + Year = model.Year, + AlbumName = model.Title, + Genres = model.Genres, + ArtworkUrl = model.ArtworkUrl + }; + } + + public static Album ToModel(this AlbumResource resource) + { + if (resource == null) return null; + + return new Album + { + AlbumId = resource.AlbumId, + Monitored = resource.Monitored, + Year = resource.Year, + Title = resource.AlbumName, + Genres = resource.Genres, + ArtworkUrl = resource.ArtworkUrl + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + + public static List ToModel(this IEnumerable resources) + { + return resources?.Select(ToModel).ToList() ?? new List(); + } + } +} diff --git a/src/NzbDrone.Api/Music/ArtistLookupModule.cs b/src/NzbDrone.Api/Music/ArtistLookupModule.cs new file mode 100644 index 000000000..4f6d5e030 --- /dev/null +++ b/src/NzbDrone.Api/Music/ArtistLookupModule.cs @@ -0,0 +1,46 @@ +using Nancy; +using NzbDrone.Api.Extensions; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MetadataSource; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Api.Music +{ + public class ArtistLookupModule : NzbDroneRestModule + { + private readonly ISearchForNewSeries _searchProxy; //TODO: Switch out for Music varriant + + public ArtistLookupModule(ISearchForNewSeries searchProxy) + : base("/artist/lookup") + { + _searchProxy = searchProxy; + Get["/"] = x => Search(); + } + + + private Response Search() + { + var iTunesResults = _searchProxy.SearchForNewArtist((string)Request.Query.term); + return MapToResource(iTunesResults).AsResponse(); + } + + + private static IEnumerable MapToResource(IEnumerable artists) + { + foreach (var currentArtist in artists) + { + var resource = currentArtist.ToResource(); + var poster = currentArtist.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); + if (poster != null) + { + resource.RemotePoster = poster.Url; + } + + yield return resource; + } + } + } +} 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 new file mode 100644 index 000000000..c42c8f4d7 --- /dev/null +++ b/src/NzbDrone.Api/Music/ArtistResource.cs @@ -0,0 +1,188 @@ +using NzbDrone.Api.REST; +using NzbDrone.Api.Series; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Api.Music +{ + public class ArtistResource : RestResource + { + public ArtistResource() + { + Monitored = true; + } + + + //View Only + public string ArtistName { get; set; } + public int ItunesId { get; set; } + //public List AlternateTitles { get; set; } + //public string SortTitle { get; set; } + + public int AlbumCount + { + get + { + if (Albums == null) return 0; + + return Albums.Where(s => s.AlbumId > 0).Count(); // TODO: CHeck this condition + } + } + + public int? TotalTrackCount { get; set; } + public int? TrackCount { get; set; } + public int? TrackFileCount { get; set; } + public long? SizeOnDisk { get; set; } + //public SeriesStatusType Status { get; set; } + + public List Images { get; set; } + + public string RemotePoster { get; set; } + public List Albums { get; set; } + + + //View & Edit + public string Path { get; set; } + public int ProfileId { get; set; } + + //Editing Only + public bool ArtistFolder { get; set; } + public bool Monitored { get; set; } + + public string RootFolderPath { get; set; } + public string Certification { get; set; } + public List Genres { get; set; } + public HashSet Tags { get; set; } + public DateTime Added { get; set; } + public AddSeriesOptions AddOptions { get; set; } + public Ratings Ratings { get; set; } + public string ArtistSlug { get; internal set; } + } + + public static class ArtistResourceMapper + { + public static ArtistResource ToResource(this Core.Music.Artist model) + { + if (model == null) return null; + + return new ArtistResource + { + Id = model.Id, + + ArtistName = model.ArtistName, + //AlternateTitles + //SortTitle = resource.SortTitle, + + //TotalEpisodeCount + //EpisodeCount + //EpisodeFileCount + //SizeOnDisk + //Status = resource.Status, + //Overview = resource.Overview, + //NextAiring + //PreviousAiring + //Network = resource.Network, + //AirTime = resource.AirTime, + Images = model.Images, + + Albums = model.Albums.ToResource(), + //Year = resource.Year, + + Path = model.Path, + ProfileId = model.ProfileId, + + ArtistFolder = model.ArtistFolder, + Monitored = model.Monitored, + + //UseSceneNumbering = resource.UseSceneNumbering, + //Runtime = resource.Runtime, + //TvdbId = resource.TvdbId, + //TvRageId = resource.TvRageId, + //TvMazeId = resource.TvMazeId, + //FirstAired = resource.FirstAired, + //LastInfoSync = resource.LastInfoSync, + //SeriesType = resource.SeriesType, + ItunesId = model.ItunesId, + ArtistSlug = model.ArtistSlug, + + RootFolderPath = model.RootFolderPath, + Genres = model.Genres, + Tags = model.Tags, + Added = model.Added, + AddOptions = model.AddOptions, + //Ratings = resource.Ratings + }; + } + + public static Core.Music.Artist ToModel(this ArtistResource resource) + { + if (resource == null) return null; + + return new Core.Music.Artist + { + Id = resource.Id, + + ArtistName = resource.ArtistName, + //AlternateTitles + //SortTitle = resource.SortTitle, + + //TotalEpisodeCount + //EpisodeCount + //EpisodeFileCount + //SizeOnDisk + //Status = resource.Status, + //Overview = resource.Overview, + //NextAiring + //PreviousAiring + //Network = resource.Network, + //AirTime = resource.AirTime, + Images = resource.Images, + + Albums = resource.Albums.ToModel(), + //Year = resource.Year, + + Path = resource.Path, + ProfileId = resource.ProfileId, + + ArtistFolder = resource.ArtistFolder, + Monitored = resource.Monitored, + + //UseSceneNumbering = resource.UseSceneNumbering, + //Runtime = resource.Runtime, + //TvdbId = resource.TvdbId, + //TvRageId = resource.TvRageId, + //TvMazeId = resource.TvMazeId, + //FirstAired = resource.FirstAired, + //LastInfoSync = resource.LastInfoSync, + //SeriesType = resource.SeriesType, + ItunesId = resource.ItunesId, + ArtistSlug = resource.ArtistSlug, + + RootFolderPath = resource.RootFolderPath, + Genres = resource.Genres, + Tags = resource.Tags, + Added = resource.Added, + AddOptions = resource.AddOptions, + //Ratings = resource.Ratings + }; + } + + public static Core.Music.Artist ToModel(this ArtistResource resource, Core.Music.Artist artist) + { + var updatedArtist = resource.ToModel(); + + artist.ApplyChanges(updatedArtist); + + return artist; + } + + public static List ToResource(this IEnumerable artist) + { + return artist.Select(ToResource).ToList(); + } + } +} diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index cce77e637..80572e068 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -111,6 +111,10 @@ + + + + diff --git a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs index 322bfbedf..29f0e9d78 100644 --- a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs +++ b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs @@ -5,6 +5,7 @@ namespace NzbDrone.Common.Cloud public interface ILidarrCloudRequestBuilder { IHttpRequestBuilderFactory Services { get; } + IHttpRequestBuilderFactory Search { get; } IHttpRequestBuilderFactory SkyHookTvdb { get; } } @@ -12,16 +13,21 @@ namespace NzbDrone.Common.Cloud { public LidarrCloudRequestBuilder() { - Services = new HttpRequestBuilder("http://services.lidarr.audio/v1/") + Services = new HttpRequestBuilder("http://services.lidarr.tv/v1/") .CreateFactory(); - SkyHookTvdb = new HttpRequestBuilder("http://skyhook.lidarr.audio/v1/tvdb/{route}/{language}/") + Search = new HttpRequestBuilder("https://itunes.apple.com/{route}/") + .CreateFactory(); + + SkyHookTvdb = new HttpRequestBuilder("http://skyhook.lidarr.tv/v1/tvdb/{route}/{language}/") .SetSegment("language", "en") .CreateFactory(); } public IHttpRequestBuilderFactory Services { get; } + public IHttpRequestBuilderFactory Search { get; } + public IHttpRequestBuilderFactory SkyHookTvdb { get; } } } diff --git a/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs b/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs index b94578c32..7953de12d 100644 --- a/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs +++ b/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common; using FluentAssertions; using NzbDrone.Common.Extensions; +using NzbDrone.Core.DataAugmentation; namespace NzbDrone.Core.Test.DataAugmentation.Scene { diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs index 14ec8922d..1afaf456a 100644 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs +++ b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs @@ -11,243 +11,243 @@ using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.DataAugmentation.Scene { - //public interface ISceneMappingService - //{ - // List GetSceneNames(int tvdbId, List seasonNumbers, List sceneSeasonNumbers); - // int? FindTvdbId(string title); - // List FindByTvdbId(int tvdbId); - // SceneMapping FindSceneMapping(string title); - // int? GetSceneSeasonNumber(string title); - // int? GetTvdbSeasonNumber(string title); - // int? GetSceneSeasonNumber(int tvdbId, int seasonNumber); - //} - - //public class SceneMappingService : ISceneMappingService, - // IHandle, - // IExecute - //{ - // private readonly ISceneMappingRepository _repository; - // private readonly IEnumerable _sceneMappingProviders; - // private readonly IEventAggregator _eventAggregator; - // private readonly Logger _logger; - // private readonly ICachedDictionary> _getTvdbIdCache; - // private readonly ICachedDictionary> _findByTvdbIdCache; - - //public SceneMappingService(ISceneMappingRepository repository, - // ICacheManager cacheManager, - // IEnumerable sceneMappingProviders, - // IEventAggregator eventAggregator, - // Logger logger) - //{ - // _repository = repository; - // _sceneMappingProviders = sceneMappingProviders; - // _eventAggregator = eventAggregator; - // _logger = logger; - - // _getTvdbIdCache = cacheManager.GetCacheDictionary>(GetType(), "tvdb_id"); - // _findByTvdbIdCache = cacheManager.GetCacheDictionary>(GetType(), "find_tvdb_id"); - //} - - // public List GetSceneNames(int tvdbId, List seasonNumbers, List sceneSeasonNumbers) - // { - // var mappings = FindByTvdbId(tvdbId); - - // if (mappings == null) - // { - // return new List(); - // } - - // var names = mappings.Where(n => n.SeasonNumber.HasValue && seasonNumbers.Contains(n.SeasonNumber.Value) || - // n.SceneSeasonNumber.HasValue && sceneSeasonNumbers.Contains(n.SceneSeasonNumber.Value) || - // (n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1) - // .Select(n => n.SearchTerm).Distinct().ToList(); - - // return FilterNonEnglish(names); - // } - - // public int? FindTvdbId(string title) - // { - // var mapping = FindMapping(title); - - // if (mapping == null) - // return null; - - // return mapping.TvdbId; - // } - - // public List FindByTvdbId(int tvdbId) - // { - // if (_findByTvdbIdCache.Count == 0) - // { - // RefreshCache(); - // } - - // var mappings = _findByTvdbIdCache.Find(tvdbId.ToString()); - - // if (mappings == null) - // { - // return new List(); - // } - - // return mappings; - // } - - // public SceneMapping FindSceneMapping(string title) - // { - // return FindMapping(title); - // } - - // public int? GetSceneSeasonNumber(string title) - // { - // var mapping = FindMapping(title); - - // if (mapping == null) - // { - // return null; - // } - - // return mapping.SceneSeasonNumber; - // } - - // public int? GetTvdbSeasonNumber(string title) - // { - // var mapping = FindMapping(title); - - // if (mapping == null) - // { - // return null; - // } - - // return mapping.SeasonNumber; - // } - - // public int? GetSceneSeasonNumber(int tvdbId, int seasonNumber) - // { - // var mappings = FindByTvdbId(tvdbId); - - // if (mappings == null) - // { - // return null; - // } - - // var mapping = mappings.FirstOrDefault(e => e.SeasonNumber == seasonNumber && e.SceneSeasonNumber.HasValue); - - // if (mapping == null) - // { - // return null; - // } - - // return mapping.SceneSeasonNumber; - // } - - // private void UpdateMappings() - // { - // _logger.Info("Updating Scene mappings"); - - // foreach (var sceneMappingProvider in _sceneMappingProviders) - // { - // try - // { - // var mappings = sceneMappingProvider.GetSceneMappings(); - - // if (mappings.Any()) - // { - // _repository.Clear(sceneMappingProvider.GetType().Name); - - // mappings.RemoveAll(sceneMapping => - // { - // if (sceneMapping.Title.IsNullOrWhiteSpace() || - // sceneMapping.SearchTerm.IsNullOrWhiteSpace()) - // { - // _logger.Warn("Invalid scene mapping found for: {0}, skipping", sceneMapping.TvdbId); - // return true; - // } - - // return false; - // }); - - // foreach (var sceneMapping in mappings) - // { - // sceneMapping.ParseTerm = sceneMapping.Title.CleanSeriesTitle(); - // sceneMapping.Type = sceneMappingProvider.GetType().Name; - // } - - // _repository.InsertMany(mappings.ToList()); - // } - // else - // { - // _logger.Warn("Received empty list of mapping. will not update."); - // } - // } - // catch (Exception ex) - // { - // _logger.Error(ex, "Failed to Update Scene Mappings."); - // } - // } - - // RefreshCache(); - - // _eventAggregator.PublishEvent(new SceneMappingsUpdatedEvent()); - // } - - // private SceneMapping FindMapping(string title) - // { - // if (_getTvdbIdCache.Count == 0) - // { - // RefreshCache(); - // } - - // var candidates = _getTvdbIdCache.Find(title.CleanSeriesTitle()); - - // if (candidates == null) - // { - // return null; - // } - - // if (candidates.Count == 1) - // { - // return candidates.First(); - // } - - // var exactMatch = candidates.OrderByDescending(v => v.SeasonNumber) - // .FirstOrDefault(v => v.Title == title); - - // if (exactMatch != null) - // { - // return exactMatch; - // } - - // var closestMatch = candidates.OrderBy(v => title.LevenshteinDistance(v.Title, 10, 1, 10)) - // .ThenByDescending(v => v.SeasonNumber) - // .First(); - - // return closestMatch; - // } - - // private void RefreshCache() - // { - // var mappings = _repository.All().ToList(); - - // _getTvdbIdCache.Update(mappings.GroupBy(v => v.ParseTerm).ToDictionary(v => v.Key, v => v.ToList())); - // _findByTvdbIdCache.Update(mappings.GroupBy(v => v.TvdbId).ToDictionary(v => v.Key.ToString(), v => v.ToList())); - // } - - // private List FilterNonEnglish(List titles) - // { - // return titles.Where(title => title.All(c => c <= 255)).ToList(); - // } - - // public void Handle(SeriesRefreshStartingEvent message) - // { - // if (message.ManualTrigger && _findByTvdbIdCache.IsExpired(TimeSpan.FromMinutes(1))) - // { - // UpdateMappings(); - // } - // } - - // public void Execute(UpdateSceneMappingCommand message) - // { - // UpdateMappings(); - // } - //} + public interface ISceneMappingService + { + List GetSceneNames(int tvdbId, List seasonNumbers, List sceneSeasonNumbers); + int? FindTvdbId(string title); + List FindByTvdbId(int tvdbId); + SceneMapping FindSceneMapping(string title); + int? GetSceneSeasonNumber(string title); + int? GetTvdbSeasonNumber(string title); + int? GetSceneSeasonNumber(int tvdbId, int seasonNumber); + } + + public class SceneMappingService : ISceneMappingService, + IHandle, + IExecute + { + private readonly ISceneMappingRepository _repository; + private readonly IEnumerable _sceneMappingProviders; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + private readonly ICachedDictionary> _getTvdbIdCache; + private readonly ICachedDictionary> _findByTvdbIdCache; + + public SceneMappingService(ISceneMappingRepository repository, + ICacheManager cacheManager, + IEnumerable sceneMappingProviders, + IEventAggregator eventAggregator, + Logger logger) + { + _repository = repository; + _sceneMappingProviders = sceneMappingProviders; + _eventAggregator = eventAggregator; + _logger = logger; + + _getTvdbIdCache = cacheManager.GetCacheDictionary>(GetType(), "tvdb_id"); + _findByTvdbIdCache = cacheManager.GetCacheDictionary>(GetType(), "find_tvdb_id"); + } + + public List GetSceneNames(int tvdbId, List seasonNumbers, List sceneSeasonNumbers) + { + var mappings = FindByTvdbId(tvdbId); + + if (mappings == null) + { + return new List(); + } + + var names = mappings.Where(n => n.SeasonNumber.HasValue && seasonNumbers.Contains(n.SeasonNumber.Value) || + n.SceneSeasonNumber.HasValue && sceneSeasonNumbers.Contains(n.SceneSeasonNumber.Value) || + (n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1) + .Select(n => n.SearchTerm).Distinct().ToList(); + + return FilterNonEnglish(names); + } + + public int? FindTvdbId(string title) + { + var mapping = FindMapping(title); + + if (mapping == null) + return null; + + return mapping.TvdbId; + } + + public List FindByTvdbId(int tvdbId) + { + if (_findByTvdbIdCache.Count == 0) + { + RefreshCache(); + } + + var mappings = _findByTvdbIdCache.Find(tvdbId.ToString()); + + if (mappings == null) + { + return new List(); + } + + return mappings; + } + + public SceneMapping FindSceneMapping(string title) + { + return FindMapping(title); + } + + public int? GetSceneSeasonNumber(string title) + { + var mapping = FindMapping(title); + + if (mapping == null) + { + return null; + } + + return mapping.SceneSeasonNumber; + } + + public int? GetTvdbSeasonNumber(string title) + { + var mapping = FindMapping(title); + + if (mapping == null) + { + return null; + } + + return mapping.SeasonNumber; + } + + public int? GetSceneSeasonNumber(int tvdbId, int seasonNumber) + { + var mappings = FindByTvdbId(tvdbId); + + if (mappings == null) + { + return null; + } + + var mapping = mappings.FirstOrDefault(e => e.SeasonNumber == seasonNumber && e.SceneSeasonNumber.HasValue); + + if (mapping == null) + { + return null; + } + + return mapping.SceneSeasonNumber; + } + + private void UpdateMappings() + { + _logger.Info("Updating Scene mappings"); + + foreach (var sceneMappingProvider in _sceneMappingProviders) + { + try + { + var mappings = sceneMappingProvider.GetSceneMappings(); + + if (mappings.Any()) + { + _repository.Clear(sceneMappingProvider.GetType().Name); + + mappings.RemoveAll(sceneMapping => + { + if (sceneMapping.Title.IsNullOrWhiteSpace() || + sceneMapping.SearchTerm.IsNullOrWhiteSpace()) + { + _logger.Warn("Invalid scene mapping found for: {0}, skipping", sceneMapping.TvdbId); + return true; + } + + return false; + }); + + foreach (var sceneMapping in mappings) + { + sceneMapping.ParseTerm = sceneMapping.Title.CleanSeriesTitle(); + sceneMapping.Type = sceneMappingProvider.GetType().Name; + } + + _repository.InsertMany(mappings.ToList()); + } + else + { + _logger.Warn("Received empty list of mapping. will not update."); + } + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to Update Scene Mappings."); + } + } + + RefreshCache(); + + _eventAggregator.PublishEvent(new SceneMappingsUpdatedEvent()); + } + + private SceneMapping FindMapping(string title) + { + if (_getTvdbIdCache.Count == 0) + { + RefreshCache(); + } + + var candidates = _getTvdbIdCache.Find(title.CleanSeriesTitle()); + + if (candidates == null) + { + return null; + } + + if (candidates.Count == 1) + { + return candidates.First(); + } + + var exactMatch = candidates.OrderByDescending(v => v.SeasonNumber) + .FirstOrDefault(v => v.Title == title); + + if (exactMatch != null) + { + return exactMatch; + } + + var closestMatch = candidates.OrderBy(v => title.LevenshteinDistance(v.Title, 10, 1, 10)) + .ThenByDescending(v => v.SeasonNumber) + .First(); + + return closestMatch; + } + + private void RefreshCache() + { + var mappings = _repository.All().ToList(); + + _getTvdbIdCache.Update(mappings.GroupBy(v => v.ParseTerm).ToDictionary(v => v.Key, v => v.ToList())); + _findByTvdbIdCache.Update(mappings.GroupBy(v => v.TvdbId).ToDictionary(v => v.Key.ToString(), v => v.ToList())); + } + + private List FilterNonEnglish(List titles) + { + return titles.Where(title => title.All(c => c <= 255)).ToList(); + } + + public void Handle(SeriesRefreshStartingEvent message) + { + if (message.ManualTrigger && _findByTvdbIdCache.IsExpired(TimeSpan.FromMinutes(1))) + { + UpdateMappings(); + } + } + + public void Execute(UpdateSceneMappingCommand message) + { + UpdateMappings(); + } + } } 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 new file mode 100644 index 000000000..faa998a0a --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs @@ -0,0 +1,74 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(111)] + public class setup_music : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("Artist") + .WithColumn("ItunesId").AsInt32().Unique() + .WithColumn("ArtistName").AsString().Unique() + .WithColumn("ArtistSlug").AsString().Nullable() //.Unique() + .WithColumn("CleanTitle").AsString().Nullable() // Do we need this? + .WithColumn("Monitored").AsBoolean() + .WithColumn("AlbumFolder").AsBoolean().Nullable() + .WithColumn("ArtistFolder").AsBoolean().Nullable() + .WithColumn("LastInfoSync").AsDateTime().Nullable() + .WithColumn("LastDiskSync").AsDateTime().Nullable() + .WithColumn("Status").AsInt32().Nullable() + .WithColumn("Path").AsString() + .WithColumn("Images").AsString().Nullable() + .WithColumn("QualityProfileId").AsInt32().Nullable() + .WithColumn("RootFolderPath").AsString().Nullable() + .WithColumn("Added").AsDateTime().Nullable() + .WithColumn("ProfileId").AsInt32().Nullable() // This is either ProfileId or Profile + .WithColumn("Genres").AsString().Nullable() + .WithColumn("Albums").AsString().Nullable() + .WithColumn("Tags").AsString().Nullable() + .WithColumn("AddOptions").AsString().Nullable() + ; + + Create.TableForModel("Albums") + .WithColumn("AlbumId").AsInt32() + .WithColumn("ArtistId").AsInt32() + .WithColumn("Title").AsString() + .WithColumn("Year").AsInt32() + .WithColumn("Image").AsInt32() + .WithColumn("TrackCount").AsInt32() + .WithColumn("DiscCount").AsInt32() + .WithColumn("Monitored").AsBoolean() + .WithColumn("Overview").AsString(); + + Create.TableForModel("Tracks") + .WithColumn("ItunesTrackId").AsInt32().Unique() + .WithColumn("AlbumId").AsInt32() + .WithColumn("ArtistsId").AsString().Nullable() + .WithColumn("TrackNumber").AsInt32() + .WithColumn("Title").AsString().Nullable() + .WithColumn("Ignored").AsBoolean().Nullable() + .WithColumn("Explict").AsBoolean() + .WithColumn("TrackExplicitName").AsString().Nullable() + .WithColumn("TrackCensoredName").AsString().Nullable() + .WithColumn("TrackFileId").AsInt32().Nullable() + .WithColumn("ReleaseDate").AsDateTime().Nullable(); + + + Create.TableForModel("TrackFiles") + .WithColumn("ArtistId").AsInt32() + .WithColumn("Path").AsString().Unique() + .WithColumn("Quality").AsString() + .WithColumn("Size").AsInt64() + .WithColumn("DateAdded").AsDateTime() + .WithColumn("AlbumId").AsInt32(); // How does this impact stand alone tracks? + + } + + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 62f6aeb8b..3d2594bef 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -34,6 +34,7 @@ using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Others; using NzbDrone.Core.Extras.Subtitles; using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Music; namespace NzbDrone.Core.Datastore { @@ -91,6 +92,26 @@ namespace NzbDrone.Core.Datastore .Relationship() .HasOne(episode => episode.EpisodeFile, episode => episode.EpisodeFileId); + Mapper.Entity().RegisterModel("Artist") + .Ignore(s => s.RootFolderPath) + .Relationship() + .HasOne(a => a.Profile, a => a.ProfileId); + + Mapper.Entity().RegisterModel("TrackFiles") + .Ignore(f => f.Path) + .Relationships.AutoMapICollectionOrComplexProperties() + .For("Tracks") + .LazyLoad(condition: parent => parent.Id > 0, + query: (db, parent) => db.Query().Where(c => c.ItunesTrackId == parent.Id).ToList()) + .HasOne(file => file.Artist, file => file.AlbumId); + + Mapper.Entity().RegisterModel("Tracks") + //.Ignore(e => e.SeriesTitle) + .Ignore(e => e.Album) + .Ignore(e => e.HasFile) + .Relationship() + .HasOne(track => track.TrackFile, track => track.TrackFileId); // TODO: Check lazy load for artists + Mapper.Entity().RegisterModel("QualityDefinitions") .Ignore(d => d.Weight); 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/MediaFiles/TrackFile.cs b/src/NzbDrone.Core/MediaFiles/TrackFile.cs new file mode 100644 index 000000000..9c2df9875 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/TrackFile.cs @@ -0,0 +1,34 @@ +using Marr.Data; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Music; +using NzbDrone.Core.Qualities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles +{ + public class TrackFile : ModelBase + { + public int ItunesTrackId { get; set; } + public int AlbumId { get; set; } + public string RelativePath { get; set; } + public string Path { get; set; } + public long Size { get; set; } + public DateTime DateAdded { get; set; } + public string SceneName { get; set; } + public string ReleaseGroup { get; set; } + public QualityModel Quality { get; set; } + public MediaInfoModel MediaInfo { get; set; } + public LazyLoaded> Episodes { get; set; } + public LazyLoaded Artist { get; set; } + public LazyLoaded> Tracks { get; set; } + + public override string ToString() + { + return string.Format("[{0}] {1}", Id, RelativePath); + } + } +} 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/ISearchForNewSeries.cs b/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs index 5abd02bcc..01b096254 100644 --- a/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs +++ b/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; namespace NzbDrone.Core.MetadataSource { public interface ISearchForNewSeries { List SearchForNewSeries(string title); + List SearchForNewArtist(string title); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs new file mode 100644 index 000000000..88ad6eae6 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class AlbumResource + { + public AlbumResource() + { + + } + + public string ArtistName { get; set; } + public int ArtistId { get; set; } + public string CollectionName { get; set; } + public int CollectionId { get; set; } + public string PrimaryGenreName { get; set; } + public string ArtworkUrl100 { get; set; } + public string Country { get; set; } + public string CollectionExplicitness { get; set; } + public int TrackCount { get; set; } + public string Copyright { get; set; } + public DateTime ReleaseDate { get; set; } + + } + + public class ArtistResource + { + public ArtistResource() + { + + } + + public int ResultCount { get; set; } + public List Results { get; set; } + //public string ArtistName { get; set; } + //public List Albums { get; set; } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 2dd0612d6..e7b97350b 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -10,10 +10,13 @@ using NzbDrone.Core.Exceptions; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource.SkyHook.Resource; using NzbDrone.Core.Tv; +using Newtonsoft.Json.Linq; +using NzbDrone.Core.Music; +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; @@ -23,12 +26,13 @@ namespace NzbDrone.Core.MetadataSource.SkyHook public SkyHookProxy(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder, Logger logger) { _httpClient = httpClient; - _requestBuilder = requestBuilder.SkyHookTvdb; + _requestBuilder = requestBuilder.Search; _logger = logger; } public Tuple> GetSeriesInfo(int tvdbSeriesId) { + Console.WriteLine("[GetSeriesInfo] id:" + tvdbSeriesId); var httpRequest = _requestBuilder.Create() .SetSegment("route", "shows") .Resource(tvdbSeriesId.ToString()) @@ -62,36 +66,136 @@ namespace NzbDrone.Core.MetadataSource.SkyHook try { var lowerTitle = title.ToLowerInvariant(); + Console.WriteLine("Searching for " + lowerTitle); + + //if (lowerTitle.StartsWith("tvdb:") || lowerTitle.StartsWith("tvdbid:")) + //{ + // var slug = lowerTitle.Split(':')[1].Trim(); + + // int tvdbId; + + // if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace) || !int.TryParse(slug, out tvdbId) || tvdbId <= 0) + // { + // return new List(); + // } + + // try + // { + // return new List { GetSeriesInfo(tvdbId).Item1 }; + // } + // catch (SeriesNotFoundException) + // { + // return new List(); + // } + //} + + // Majora: Temporarily, use iTunes to test. + var httpRequest = _requestBuilder.Create() + .AddQueryParam("entity", "album") + .AddQueryParam("term", title.ToLower().Trim()) + .Build(); + + - if (lowerTitle.StartsWith("tvdb:") || lowerTitle.StartsWith("tvdbid:")) + Console.WriteLine("httpRequest: ", httpRequest); + + var httpResponse = _httpClient.Get>(httpRequest); + + //Console.WriteLine("Response: ", httpResponse.GetType()); + //_logger.Info("Response: ", httpResponse.Resource.ResultCount); + + //_logger.Info("HTTP Response: ", httpResponse.Resource.ResultCount); + var tempList = new List(); + var tempSeries = new Series(); + tempSeries.Title = "AFI"; + tempList.Add(tempSeries); + return tempList; + + return httpResponse.Resource.SelectList(MapSeries); + } + catch (HttpException) + { + throw new SkyHookException("Search for '{0}' failed. Unable to communicate with SkyHook.", title); + } + catch (Exception ex) + { + _logger.Warn(ex, ex.Message); + throw new SkyHookException("Search for '{0}' failed. Invalid response received from SkyHook.", title); + } + } + + 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 + { + var lowerTitle = title.ToLowerInvariant(); + Console.WriteLine("Searching for " + lowerTitle); + + if (lowerTitle.StartsWith("itunes:") || lowerTitle.StartsWith("itunesid:")) { var slug = lowerTitle.Split(':')[1].Trim(); - int tvdbId; + int itunesId; - if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace) || !int.TryParse(slug, out tvdbId) || tvdbId <= 0) + if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace) || !int.TryParse(slug, out itunesId) || itunesId <= 0) { - return new List(); + return new List(); } try { - return new List { GetSeriesInfo(tvdbId).Item1 }; + return new List { GetArtistInfo(itunesId).Item1 }; } - catch (SeriesNotFoundException) + catch (ArtistNotFoundException) { - return new List(); + return new List(); } } var httpRequest = _requestBuilder.Create() - .SetSegment("route", "search") - .AddQueryParam("term", title.ToLower().Trim()) - .Build(); + .SetSegment("route", "search") + .AddQueryParam("entity", "album") + .AddQueryParam("term", title.ToLower().Trim()) + .Build(); - var httpResponse = _httpClient.Get>(httpRequest); - return httpResponse.Resource.SelectList(MapSeries); + + var httpResponse = _httpClient.Get(httpRequest); + + return MapArtists(httpResponse.Resource); } catch (HttpException) { @@ -104,6 +208,46 @@ 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(); + + album.AlbumId = albumQuery.CollectionId; + album.Title = albumQuery.CollectionName; + album.Year = albumQuery.ReleaseDate.Year; + album.ArtworkUrl = albumQuery.ArtworkUrl100; + album.Explicitness = albumQuery.CollectionExplicitness; + return album; + } + private static Series MapSeries(ShowResource show) { var series = new Series(); 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..072a67754 --- /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); // There is no Sort Title + 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/Album.cs b/src/NzbDrone.Core/Music/Album.cs new file mode 100644 index 000000000..c0c7fc19e --- /dev/null +++ b/src/NzbDrone.Core/Music/Album.cs @@ -0,0 +1,29 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public class Album : IEmbeddedDocument + { + public Album() + { + Images = new List(); + } + + public int AlbumId { get; set; } + public string Title { get; set; } // NOTE: This should be CollectionName in API + public int Year { get; set; } + public int TrackCount { get; set; } + public int DiscCount { get; set; } + public bool Monitored { get; set; } + public List Images { get; set; } + public List Actors { get; set; } // These are band members. TODO: Refactor + public List Genres { get; set; } + public string ArtworkUrl { get; set; } + public string Explicitness { get; set; } + } +} diff --git a/src/NzbDrone.Core/Music/Artist.cs b/src/NzbDrone.Core/Music/Artist.cs new file mode 100644 index 000000000..2fde7c0ac --- /dev/null +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -0,0 +1,108 @@ +using Marr.Data; +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; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public class Artist : ModelBase + { + public Artist() + { + Images = new List(); + Genres = new List(); + //Members = new List(); // Artist Band Member? (NOTE: This should be per album) + Albums = new List(); + Tags = new HashSet(); + + } + + public int ItunesId { get; set; } + public string ArtistName { get; set; } + public string ArtistSlug { get; set; } + public string CleanTitle { get; set; } + public bool Monitored { get; set; } + public bool AlbumFolder { get; set; } + 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()); + } + + public void ApplyChanges(Artist otherArtist) + { + + ItunesId = otherArtist.ItunesId; + ArtistName = otherArtist.ArtistName; + ArtistSlug = otherArtist.ArtistSlug; + CleanTitle = otherArtist.CleanTitle; + Monitored = otherArtist.Monitored; + AlbumFolder = otherArtist.AlbumFolder; + LastInfoSync = otherArtist.LastInfoSync; + Images = otherArtist.Images; + Path = otherArtist.Path; + Genres = otherArtist.Genres; + RootFolderPath = otherArtist.RootFolderPath; + Added = otherArtist.Added; + Profile = otherArtist.Profile; + ProfileId = otherArtist.ProfileId; + Albums = otherArtist.Albums; + Tags = otherArtist.Tags; + 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/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 new file mode 100644 index 000000000..f9e7f9da4 --- /dev/null +++ b/src/NzbDrone.Core/Music/ArtistRepository.cs @@ -0,0 +1,40 @@ +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 FindByName(string cleanTitle); + Artist FindByItunesId(int iTunesId); + } + + public class ArtistRepository : BasicRepository, IArtistRepository + { + public ArtistRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + + public bool ArtistPathExists(string path) + { + return Query.Where(c => c.Path == path).Any(); + } + + public Artist FindByItunesId(int iTunesId) + { + return Query.Where(s => s.ItunesId == iTunesId).SingleOrDefault(); + } + + public Artist FindByName(string cleanName) + { + cleanName = cleanName.ToLowerInvariant(); + + return Query.Where(s => s.CleanTitle == cleanName) + .SingleOrDefault(); + } + } +} diff --git a/src/NzbDrone.Core/Music/ArtistService.cs b/src/NzbDrone.Core/Music/ArtistService.cs new file mode 100644 index 000000000..85779b414 --- /dev/null +++ b/src/NzbDrone.Core/Music/ArtistService.cs @@ -0,0 +1,155 @@ +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 +{ + public interface IArtistService + { + Artist GetArtist(int artistId); + List GetArtists(IEnumerable artistIds); + Artist AddArtist(Artist newArtist); + Artist FindByItunesId(int itunesId); + Artist FindByName(string title); + Artist FindByTitleInexact(string title); + void DeleteArtist(int artistId, bool deleteFiles); + List GetAllArtists(); + Artist UpdateArtist(Artist artist); + List UpdateArtists(List artist); + bool ArtistPathExists(string folder); + void RemoveAddOptions(Artist artist); + } + + public class ArtistService : IArtistService + { + private readonly IArtistRepository _artistRepository; + private readonly IEventAggregator _eventAggregator; + private readonly ITrackService _trackService; + 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) + { + _artistRepository.Insert(newArtist); + _eventAggregator.PublishEvent(new ArtistAddedEvent(GetArtist(newArtist.Id))); + + return newArtist; + } + + public bool ArtistPathExists(string folder) + { + return _artistRepository.ArtistPathExists(folder); + } + + public void DeleteArtist(int artistId, bool deleteFiles) + { + var artist = _artistRepository.Get(artistId); + _artistRepository.Delete(artistId); + _eventAggregator.PublishEvent(new ArtistDeletedEvent(artist, deleteFiles)); + } + + public Artist FindByItunesId(int itunesId) + { + return _artistRepository.FindByItunesId(itunesId); + } + + public Artist FindByName(string title) + { + return _artistRepository.FindByName(title.CleanArtistTitle()); + } + + public Artist FindByTitleInexact(string title) + { + throw new NotImplementedException(); + } + + public List GetAllArtists() + { + _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) + { + return _artistRepository.Get(artistId); + } + + public List GetArtists(IEnumerable artistIds) + { + return _artistRepository.Get(artistIds).ToList(); + } + + public void RemoveAddOptions(Artist artist) + { + _artistRepository.SetFields(artist, s => s.AddOptions); + } + + public Artist UpdateArtist(Artist artist) + { + 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) + { + _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/Music/Track.cs b/src/NzbDrone.Core/Music/Track.cs new file mode 100644 index 000000000..65e28231b --- /dev/null +++ b/src/NzbDrone.Core/Music/Track.cs @@ -0,0 +1,52 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaFiles; +using Marr.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Core.Music +{ + public class Track : ModelBase + { + public Track() + { + } + + public const string RELEASE_DATE_FORMAT = "yyyy-MM-dd"; + + public int ItunesTrackId { get; set; } + public int AlbumId { get; set; } + public LazyLoaded ArtistsId { get; set; } + public int CompilationId { get; set; } + public bool Compilation { get; set; } + public int TrackNumber { get; set; } + public string Title { get; set; } + public bool Ignored { get; set; } + 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 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; }*/ + + //public string SeriesTitle { get; private set; } + + public LazyLoaded TrackFile { get; set; } + + public Album Album { get; set; } + + public bool HasFile => TrackFileId > 0; + + public override string ToString() + { + return string.Format("[{0}]{1}", ItunesTrackId, Title.NullSafe()); + } + } +} diff --git a/src/NzbDrone.Core/Music/TrackService.cs b/src/NzbDrone.Core/Music/TrackService.cs new file mode 100644 index 000000000..91bdeb5f7 --- /dev/null +++ b/src/NzbDrone.Core/Music/TrackService.cs @@ -0,0 +1,117 @@ +using NzbDrone.Core.Datastore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public interface ITrackService + { + Track GetTrack(int id); + List GetTracks(IEnumerable ids); + Track FindTrack(int artistId, int albumId, int trackNumber); + Track FindTrackByTitle(int artistId, int albumId, string releaseTitle); + List GetTrackByArtist(int artistId); + List GetTracksByAblum(int artistId, int albumId); + List GetTracksByAblumTitle(int artistId, string albumTitle); + List TracksWithFiles(int artistId); + PagingSpec TracksWithoutFiles(PagingSpec pagingSpec); + List GeTracksByFileId(int trackFileId); + void UpdateTrack(Track track); + void SetTrackMonitored(int trackId, bool monitored); + void UpdateTracks(List tracks); + void InsertMany(List tracks); + void UpdateMany(List tracks); + void DeleteMany(List tracks); + void SetTrackMonitoredByAlbum(int artistId, int albumId, bool monitored); + } + + public class TrackService : ITrackService + { + public void DeleteMany(List tracks) + { + throw new NotImplementedException(); + } + + public Track FindTrack(int artistId, int albumId, int trackNumber) + { + throw new NotImplementedException(); + } + + public Track FindTrackByTitle(int artistId, int albumId, string releaseTitle) + { + throw new NotImplementedException(); + } + + public List GeTracksByFileId(int trackFileId) + { + throw new NotImplementedException(); + } + + public Track GetTrack(int id) + { + throw new NotImplementedException(); + } + + public List GetTrackByArtist(int artistId) + { + throw new NotImplementedException(); + } + + public List GetTracks(IEnumerable ids) + { + throw new NotImplementedException(); + } + + public List GetTracksByAblum(int artistId, int albumId) + { + throw new NotImplementedException(); + } + + public List GetTracksByAblumTitle(int artistId, string albumTitle) + { + throw new NotImplementedException(); + } + + public void InsertMany(List tracks) + { + throw new NotImplementedException(); + } + + public void SetTrackMonitored(int trackId, bool monitored) + { + throw new NotImplementedException(); + } + + public void SetTrackMonitoredByAlbum(int artistId, int albumId, bool monitored) + { + throw new NotImplementedException(); + } + + public List TracksWithFiles(int artistId) + { + throw new NotImplementedException(); + } + + public PagingSpec TracksWithoutFiles(PagingSpec pagingSpec) + { + throw new NotImplementedException(); + } + + public void UpdateMany(List tracks) + { + throw new NotImplementedException(); + } + + public void UpdateTrack(Track track) + { + throw new NotImplementedException(); + } + + public void UpdateTracks(List tracks) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 9e238632b..606293015 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -287,6 +287,7 @@ Code + @@ -517,6 +518,7 @@ + @@ -757,6 +759,7 @@ + @@ -765,6 +768,8 @@ + + @@ -781,6 +786,7 @@ + @@ -806,7 +812,9 @@ + + @@ -837,6 +845,21 @@ + + + + + + + + + + + + + + + @@ -892,6 +915,8 @@ + + @@ -1130,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/Model/LocalTrack.cs b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs new file mode 100644 index 000000000..e3577527d --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs @@ -0,0 +1,42 @@ +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Music; +using NzbDrone.Core.Qualities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Parser.Model +{ + public class LocalTrack + { + public LocalTrack() + { + Tracks = new List(); + } + + public string Path { get; set; } + public long Size { get; set; } + public ParsedTrackInfo ParsedTrackInfo { get; set; } + public Artist Artist { get; set; } + public List Tracks { get; set; } + public QualityModel Quality { get; set; } + public MediaInfoModel MediaInfo { get; set; } + public bool ExistingFile { get; set; } + + public int Album + { + get + { + return Tracks.Select(c => c.AlbumId).Distinct().Single(); + } + } + + public bool IsSpecial => Album == 0; + + public override string ToString() + { + return Path; + } + } +} diff --git a/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs new file mode 100644 index 000000000..53cb470c9 --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs @@ -0,0 +1,97 @@ +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Qualities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Parser.Model +{ + public class ParsedTrackInfo + { + // [TODO]: Properly fill this out + public string ArtistTitle { get; set; } + public string AlbumTitle { get; set; } + public SeriesTitleInfo SeriesTitleInfo { get; set; } + public QualityModel Quality { get; set; } + public int SeasonNumber { get; set; } + public int[] EpisodeNumbers { get; set; } + public int[] AbsoluteEpisodeNumbers { get; set; } + public string AirDate { get; set; } + public Language Language { get; set; } + public bool FullSeason { get; set; } + public bool Special { get; set; } + public string ReleaseGroup { get; set; } + public string ReleaseHash { get; set; } + + public ParsedTrackInfo() + { + EpisodeNumbers = new int[0]; + AbsoluteEpisodeNumbers = new int[0]; + } + + public bool IsDaily + { + get + { + return !string.IsNullOrWhiteSpace(AirDate); + } + + //This prevents manually downloading a release from blowing up in mono + //TODO: Is there a better way? + private set { } + } + + public bool IsAbsoluteNumbering + { + get + { + return AbsoluteEpisodeNumbers.Any(); + } + + //This prevents manually downloading a release from blowing up in mono + //TODO: Is there a better way? + private set { } + } + + public bool IsPossibleSpecialEpisode + { + get + { + // if we don't have eny episode numbers we are likely a special episode and need to do a search by episode title + return (AirDate.IsNullOrWhiteSpace() && + ArtistTitle.IsNullOrWhiteSpace() && + (EpisodeNumbers.Length == 0 || SeasonNumber == 0) || + !ArtistTitle.IsNullOrWhiteSpace() && Special); + } + + //This prevents manually downloading a release from blowing up in mono + //TODO: Is there a better way? + private set { } + } + + public override string ToString() + { + string episodeString = "[Unknown Episode]"; + + if (IsDaily && EpisodeNumbers.Empty()) + { + episodeString = string.Format("{0}", AirDate); + } + else if (FullSeason) + { + episodeString = string.Format("Season {0:00}", SeasonNumber); + } + else if (EpisodeNumbers != null && EpisodeNumbers.Any()) + { + episodeString = string.Format("S{0:00}E{1}", SeasonNumber, string.Join("-", EpisodeNumbers.Select(c => c.ToString("00")))); + } + else if (AbsoluteEpisodeNumbers != null && AbsoluteEpisodeNumbers.Any()) + { + episodeString = string.Format("{0}", string.Join("-", AbsoluteEpisodeNumbers.Select(c => c.ToString("000")))); + } + + return string.Format("{0} - {1} {2}", ArtistTitle, episodeString, Quality); + } + } +} diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 4855926a9..be6ed1da3 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -277,6 +277,27 @@ namespace NzbDrone.Core.Parser private static readonly string[] Numbers = new[] { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" }; + public static ParsedTrackInfo ParseMusicPath(string path) + { + var fileInfo = new FileInfo(path); + + var result = ParseMusicTitle(fileInfo.Name); + + if (result == null) + { + Logger.Debug("Attempting to parse track info using directory and file names. {0}", fileInfo.Directory.Name); + result = ParseMusicTitle(fileInfo.Directory.Name + " " + fileInfo.Name); + } + + if (result == null) + { + Logger.Debug("Attempting to parse track info using directory name. {0}", fileInfo.Directory.Name); + result = ParseMusicTitle(fileInfo.Directory.Name + fileInfo.Extension); + } + + return result; + } + public static ParsedEpisodeInfo ParsePath(string path) { var fileInfo = new FileInfo(path); @@ -298,6 +319,116 @@ namespace NzbDrone.Core.Parser return result; } + public static ParsedTrackInfo ParseMusicTitle(string title) + { + try + { + if (!ValidateBeforeParsing(title)) return null; + + Logger.Debug("Parsing string '{0}'", title); + + if (ReversedTitleRegex.IsMatch(title)) + { + var titleWithoutExtension = RemoveFileExtension(title).ToCharArray(); + Array.Reverse(titleWithoutExtension); + + title = new string(titleWithoutExtension) + title.Substring(titleWithoutExtension.Length); + + Logger.Debug("Reversed name detected. Converted to '{0}'", title); + } + + var simpleTitle = SimpleTitleRegex.Replace(title, string.Empty); + + simpleTitle = RemoveFileExtension(simpleTitle); + + // TODO: Quick fix stripping [url] - prefixes. + simpleTitle = WebsitePrefixRegex.Replace(simpleTitle, string.Empty); + + simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle, string.Empty); + + var airDateMatch = AirDateRegex.Match(simpleTitle); + if (airDateMatch.Success) + { + simpleTitle = airDateMatch.Groups[1].Value + airDateMatch.Groups["airyear"].Value + "." + airDateMatch.Groups["airmonth"].Value + "." + airDateMatch.Groups["airday"].Value; + } + + var sixDigitAirDateMatch = SixDigitAirDateRegex.Match(simpleTitle); + if (sixDigitAirDateMatch.Success) + { + var airYear = sixDigitAirDateMatch.Groups["airyear"].Value; + var airMonth = sixDigitAirDateMatch.Groups["airmonth"].Value; + var airDay = sixDigitAirDateMatch.Groups["airday"].Value; + + if (airMonth != "00" || airDay != "00") + { + var fixedDate = string.Format("20{0}.{1}.{2}", airYear, airMonth, airDay); + + simpleTitle = simpleTitle.Replace(sixDigitAirDateMatch.Groups["airdate"].Value, fixedDate); + } + } + + foreach (var regex in ReportTitleRegex) + { + var match = regex.Matches(simpleTitle); + + if (match.Count != 0) + { + Logger.Trace(regex); + try + { + var result = ParseMatchMusicCollection(match); + + if (result != null) + { + if (result.FullSeason && title.ContainsIgnoreCase("Special")) + { + result.FullSeason = false; + result.Special = true; + } + + result.Language = LanguageParser.ParseLanguage(title); + Logger.Debug("Language parsed: {0}", result.Language); + + result.Quality = QualityParser.ParseQuality(title); + Logger.Debug("Quality parsed: {0}", result.Quality); + + result.ReleaseGroup = ParseReleaseGroup(title); + + var subGroup = GetSubGroup(match); + if (!subGroup.IsNullOrWhiteSpace()) + { + result.ReleaseGroup = subGroup; + } + + Logger.Debug("Release Group parsed: {0}", result.ReleaseGroup); + + result.ReleaseHash = GetReleaseHash(match); + if (!result.ReleaseHash.IsNullOrWhiteSpace()) + { + Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash); + } + + return result; + } + } + catch (InvalidDateException ex) + { + Logger.Debug(ex, ex.Message); + break; + } + } + } + } + catch (Exception e) + { + if (!title.ToLower().Contains("password") && !title.ToLower().Contains("yenc")) + Logger.Error(e, "An error has occurred while trying to parse {0}", title); + } + + Logger.Debug("Unable to parse {0}", title); + return null; + } + public static ParsedEpisodeInfo ParseTitle(string title) { try @@ -433,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); @@ -522,6 +664,10 @@ namespace NzbDrone.Core.Parser return seriesTitleInfo; } + private static ParsedTrackInfo ParseMatchMusicCollection(MatchCollection matchCollection) + { + throw new NotImplementedException(); + } private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchCollection) { var seriesName = matchCollection[0].Groups["title"].Value.Replace('.', ' ').Replace('_', ' '); diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index c47c5f37a..627bd6df1 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -8,6 +8,8 @@ using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; +using NzbDrone.Core.Music; +using System; namespace NzbDrone.Core.Parser { @@ -20,13 +22,20 @@ namespace NzbDrone.Core.Parser RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable episodeIds); List GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null); ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); + + // Music stuff here + LocalTrack GetLocalTrack(string filename, Artist artist); + LocalTrack GetLocalTrack(string filename, Artist artist, ParsedTrackInfo folderInfo, bool sceneSource); + } public class ParsingService : IParsingService { private readonly IEpisodeService _episodeService; private readonly ISeriesService _seriesService; - // private readonly ISceneMappingService _sceneMappingService; + + private readonly IArtistService _artistService; + private readonly ITrackService _trackService; private readonly Logger _logger; public ParsingService(IEpisodeService episodeService, @@ -474,5 +483,89 @@ namespace NzbDrone.Core.Parser return result; } + + public LocalTrack GetLocalTrack(string filename, Artist artist) + { + return GetLocalTrack(filename, artist, null, false); + } + + public LocalTrack GetLocalTrack(string filename, Artist artist, ParsedTrackInfo folderInfo, bool sceneSource) + { + ParsedTrackInfo parsedTrackInfo; + + if (folderInfo != null) + { + parsedTrackInfo = folderInfo.JsonClone(); + parsedTrackInfo.Quality = QualityParser.ParseQuality(Path.GetFileName(filename)); + } + + else + { + parsedTrackInfo = Parser.ParseMusicPath(filename); + } + + if (parsedTrackInfo == null || parsedTrackInfo.IsPossibleSpecialEpisode) + { + var title = Path.GetFileNameWithoutExtension(filename); + //var specialEpisodeInfo = ParseSpecialEpisodeTitle(title, series); + + //if (specialEpisodeInfo != null) + //{ + // parsedTrackInfo = specialEpisodeInfo; + //} + } + + if (parsedTrackInfo == null) + { + if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(filename))) + { + _logger.Warn("Unable to parse track info from path {0}", filename); + } + + return null; + } + + var tracks = GetTracks(parsedTrackInfo, artist, sceneSource); + + return new LocalTrack + { + Artist = artist, + Quality = parsedTrackInfo.Quality, + Tracks = tracks, + Path = filename, + ParsedTrackInfo = parsedTrackInfo, + ExistingFile = artist.Path.IsParentPath(filename) + }; + } + + private List GetTracks(ParsedTrackInfo parsedTrackInfo, Artist artist, bool sceneSource) + { + throw new NotImplementedException(); + + /*if (parsedTrackInfo.FullSeason) // IF Album + { + return _trackService.GetTracksByAlbumTitle(artist.Id, parsedTrackInfo.AlbumTitle); + } + + if (parsedTrackInfo.IsDaily) + { + if (artist.SeriesType == SeriesTypes.Standard) + { + _logger.Warn("Found daily-style episode for non-daily series: {0}.", series); + return new List(); + } + + var episodeInfo = GetDailyEpisode(artist, parsedTrackInfo.AirDate, searchCriteria); + + if (episodeInfo != null) + { + return new List { episodeInfo }; + } + + return new List(); + } + + return GetStandardEpisodes(artist, parsedTrackInfo, sceneSource, searchCriteria);*/ + } } } \ No newline at end of file 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/AddSeriesCollection.js b/src/UI/AddSeries/AddSeriesCollection.js index 5be24d3a7..a243649f4 100644 --- a/src/UI/AddSeries/AddSeriesCollection.js +++ b/src/UI/AddSeries/AddSeriesCollection.js @@ -1,10 +1,10 @@ var Backbone = require('backbone'); -var SeriesModel = require('../Series/SeriesModel'); +var ArtistModel = require('../Artist/ArtistModel'); var _ = require('underscore'); module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/series/lookup', - model : SeriesModel, + url : window.NzbDrone.ApiRoot + '/artist/lookup', + model : ArtistModel, parse : function(response) { var self = this; @@ -16,6 +16,7 @@ module.exports = Backbone.Collection.extend({ model.path = self.unmappedFolderModel.get('folder').path; } }); + console.log('response: ', response); return response; } diff --git a/src/UI/AddSeries/AddSeriesLayoutTemplate.hbs b/src/UI/AddSeries/AddSeriesLayoutTemplate.hbs index ab6e5e6c0..69b78aedb 100644 --- a/src/UI/AddSeries/AddSeriesLayoutTemplate.hbs +++ b/src/UI/AddSeries/AddSeriesLayoutTemplate.hbs @@ -3,9 +3,9 @@
- +
diff --git a/src/UI/AddSeries/AddSeriesView.js b/src/UI/AddSeries/AddSeriesView.js index 3cda1db63..0c128eb40 100644 --- a/src/UI/AddSeries/AddSeriesView.js +++ b/src/UI/AddSeries/AddSeriesView.js @@ -28,6 +28,7 @@ module.exports = Marionette.Layout.extend({ initialize : function(options) { this.isExisting = options.isExisting; this.collection = new AddSeriesCollection(); + console.log('this.collection:', this.collection); if (this.isExisting) { this.collection.unmappedFolderModel = this.model; diff --git a/src/UI/AddSeries/AddSeriesViewTemplate.hbs b/src/UI/AddSeries/AddSeriesViewTemplate.hbs index 18ed2ffb3..56d69b616 100644 --- a/src/UI/AddSeries/AddSeriesViewTemplate.hbs +++ b/src/UI/AddSeries/AddSeriesViewTemplate.hbs @@ -11,7 +11,7 @@ {{#if folder}} {{else}} - + {{/if}} diff --git a/src/UI/AddSeries/EmptyViewTemplate.hbs b/src/UI/AddSeries/EmptyViewTemplate.hbs index 60346f0c0..8c2b29e22 100644 --- a/src/UI/AddSeries/EmptyViewTemplate.hbs +++ b/src/UI/AddSeries/EmptyViewTemplate.hbs @@ -1,3 +1,3 @@
- You can also search by tvdbid using the tvdb: prefixes. + You can also search by iTunes using the itunes: prefixes.
diff --git a/src/UI/AddSeries/ErrorViewTemplate.hbs b/src/UI/AddSeries/ErrorViewTemplate.hbs index 163779c26..c0b1e3673 100644 --- a/src/UI/AddSeries/ErrorViewTemplate.hbs +++ b/src/UI/AddSeries/ErrorViewTemplate.hbs @@ -3,5 +3,5 @@ There was an error searching for '{{term}}'. - If the series title contains non-alphanumeric characters try removing them, otherwise try your search again later. + If the artist name contains non-alphanumeric characters try removing them, otherwise try your search again later. diff --git a/src/UI/AddSeries/Existing/AddExistingSeriesCollectionViewTemplate.hbs b/src/UI/AddSeries/Existing/AddExistingSeriesCollectionViewTemplate.hbs index d613a52d4..6dcb1ecc2 100644 --- a/src/UI/AddSeries/Existing/AddExistingSeriesCollectionViewTemplate.hbs +++ b/src/UI/AddSeries/Existing/AddExistingSeriesCollectionViewTemplate.hbs @@ -1,5 +1,5 @@
- Loading search results from TheTVDB for your series, this may take a few minutes. + Loading search results from iTunes for your artists, this may take a few minutes.
\ No newline at end of file diff --git a/src/UI/AddSeries/SearchResultView.js b/src/UI/AddSeries/SearchResultView.js index 817ab78ea..aaef92a1f 100644 --- a/src/UI/AddSeries/SearchResultView.js +++ b/src/UI/AddSeries/SearchResultView.js @@ -6,7 +6,7 @@ var Marionette = require('marionette'); var Profiles = require('../Profile/ProfileCollection'); var RootFolders = require('./RootFolders/RootFolderCollection'); var RootFolderLayout = require('./RootFolders/RootFolderLayout'); -var SeriesCollection = require('../Series/SeriesCollection'); +var ArtistCollection = require('../Artist/ArtistCollection'); var Config = require('../Config'); var Messenger = require('../Shared/Messenger'); var AsValidatedView = require('../Mixins/AsValidatedView'); @@ -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() { @@ -93,7 +97,7 @@ var view = Marionette.ItemView.extend({ }, _configureTemplateHelpers : function() { - var existingSeries = SeriesCollection.where({ tvdbId : this.model.get('tvdbId') }); + var existingSeries = ArtistCollection.where({ iTunesId : this.model.get('itunesId') }); if (existingSeries.length > 0) { this.templateHelpers.existing = existingSeries[0].toJSON(); @@ -161,7 +165,8 @@ var view = Marionette.ItemView.extend({ this._rootFolderChanged(); }, - _addWithoutSearch : function() { + _addWithoutSearch : function(evt) { + console.log(evt); this._addSeries(false); }, @@ -169,20 +174,22 @@ var view = Marionette.ItemView.extend({ this._addSeries(true); }, - _addSeries : function(searchForMissingEpisodes) { + _addSeries : function(searchForMissing) { + // TODO: Refactor to handle multiple add buttons/albums var addButton = this.ui.addButton; var addSearchButton = this.ui.addSearchButton; + console.log('_addSeries, searchForMissing=', searchForMissing); addButton.addClass('disabled'); addSearchButton.addClass('disabled'); var profile = this.ui.profile.val(); var rootFolderPath = this.ui.rootFolder.children(':selected').text(); - var seriesType = this.ui.seriesType.val(); + var seriesType = this.ui.seriesType.val(); // Perhaps make this a differnitator between artist or Album? var seasonFolder = this.ui.seasonFolder.prop('checked'); var options = this._getAddSeriesOptions(); - options.searchForMissingEpisodes = searchForMissingEpisodes; + options.searchForMissing = searchForMissing; this.model.set({ profileId : profile, @@ -196,7 +203,7 @@ var view = Marionette.ItemView.extend({ var self = this; var promise = this.model.save(); - if (searchForMissingEpisodes) { + if (searchForMissing) { this.ui.addSearchButton.spinForPromise(promise); } @@ -210,7 +217,8 @@ var view = Marionette.ItemView.extend({ }); promise.done(function() { - SeriesCollection.add(self.model); + console.log('[SearchResultView] _addSeries promise resolve:', self.model); + ArtistCollection.add(self.model); self.close(); @@ -218,9 +226,9 @@ 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('/series/' + self.model.get('titleSlug'), { trigger : true }); + Backbone.history.navigate('/artist/' + self.model.get('titleSlug'), { trigger : true }); } } }, @@ -239,10 +247,11 @@ var view = Marionette.ItemView.extend({ _getAddSeriesOptions : function() { var monitor = this.ui.monitor.val(); + //[TODO]: Refactor for albums 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, @@ -258,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; @@ -275,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 2eafdf2b0..23cee51e9 100644 --- a/src/UI/AddSeries/SearchResultViewTemplate.hbs +++ b/src/UI/AddSeries/SearchResultViewTemplate.hbs @@ -1,30 +1,26 @@
-

- {{titleWithYear}} + + {{artistName}} - +

-
+
{{#unless existing}} {{#unless path}} @@ -41,8 +37,6 @@ - -
@@ -52,13 +46,12 @@ {{> ProfileSelectionPartial profiles}}
-
- - {{> SeriesTypeSelectionPartial}} -
+
- +
{{#unless existing}} - {{#if title}} + {{#if artistName}}
- + + +
+
+ {{else}} +
+ +
+ {{/if}} + {{else}} + + {{/unless}} +
+
+
+
+ {{#each albums}} +
+ +
+

{{albumName}} ({{year}})

+ {{#unless existing}} + {{#if albumName}} +
+ + +
+ -
{{else}} -
+
@@ -105,5 +141,6 @@ {{/unless}}
+ {{/each}}
diff --git a/src/UI/AddSeries/addSeries.less b/src/UI/AddSeries/addSeries.less index 2ca8090f9..f958e01a3 100644 --- a/src/UI/AddSeries/addSeries.less +++ b/src/UI/AddSeries/addSeries.less @@ -75,6 +75,14 @@ margin : 10px; } + .album-poster { + min-width : 100px; + min-height : 100px; + max-width : 138px; + max-height : 203px; + margin : 10px; + } + a { color : #343434; } diff --git a/src/UI/Artist/ArtistCollection.js b/src/UI/Artist/ArtistCollection.js new file mode 100644 index 000000000..f9908032d --- /dev/null +++ b/src/UI/Artist/ArtistCollection.js @@ -0,0 +1,124 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var PageableCollection = require('backbone.pageable'); +var ArtistModel = require('./ArtistModel'); +var ApiData = require('../Shared/ApiData'); +var AsFilteredCollection = require('../Mixins/AsFilteredCollection'); +var AsSortedCollection = require('../Mixins/AsSortedCollection'); +var AsPersistedStateCollection = require('../Mixins/AsPersistedStateCollection'); +var moment = require('moment'); +require('../Mixins/backbone.signalr.mixin'); + +var Collection = PageableCollection.extend({ + url : window.NzbDrone.ApiRoot + '/artist', + model : ArtistModel, + tableName : 'artist', + + state : { + sortKey : 'sortTitle', + order : -1, + pageSize : 100000, + secondarySortKey : 'sortTitle', + secondarySortOrder : -1 + }, + + mode : 'client', + + save : function() { + var self = this; + + var proxy = _.extend(new Backbone.Model(), { + id : '', + + url : self.url + '/editor', + + toJSON : function() { + return self.filter(function(model) { + return model.edited; + }); + } + }); + + this.listenTo(proxy, 'sync', function(proxyModel, models) { + this.add(models, { merge : true }); + this.trigger('save', this); + }); + + return proxy.save(); + }, + + filterModes : { + 'all' : [ + null, + null + ], + 'continuing' : [ + 'status', + 'continuing' + ], + 'ended' : [ + 'status', + 'ended' + ], + 'monitored' : [ + 'monitored', + true + ], + 'missing' : [ + null, + null, + function(model) { return model.get('episodeCount') !== model.get('episodeFileCount'); } + ] + }, + + sortMappings : { + title : { + sortKey : 'sortTitle' + }, + + artistName: { + sortKey : 'artistName' + }, + + nextAiring : { + sortValue : function(model, attr, order) { + var nextAiring = model.get(attr); + + if (nextAiring) { + return moment(nextAiring).unix(); + } + + if (order === 1) { + return 0; + } + + return Number.MAX_VALUE; + } + }, + + percentOfEpisodes : { + sortValue : function(model, attr) { + var percentOfEpisodes = model.get(attr); + var episodeCount = model.get('episodeCount'); + + return percentOfEpisodes + episodeCount / 1000000; + } + }, + + path : { + sortValue : function(model) { + var path = model.get('path'); + + return path.toLowerCase(); + } + } + } +}); + +Collection = AsFilteredCollection.call(Collection); +Collection = AsSortedCollection.call(Collection); +Collection = AsPersistedStateCollection.call(Collection); + +var data = ApiData.get('series'); // TOOD: Build backend for artist + +module.exports = new Collection(data, { full : true }).bindSignalR(); diff --git a/src/UI/Artist/ArtistController.js b/src/UI/Artist/ArtistController.js new file mode 100644 index 000000000..838018670 --- /dev/null +++ b/src/UI/Artist/ArtistController.js @@ -0,0 +1,37 @@ +var NzbDroneController = require('../Shared/NzbDroneController'); +var AppLayout = require('../AppLayout'); +var ArtistCollection = require('./ArtistCollection'); +var SeriesIndexLayout = require('./Index/SeriesIndexLayout'); +var SeriesDetailsLayout = require('../Series/Details/SeriesDetailsLayout'); + +module.exports = NzbDroneController.extend({ + _originalInit : NzbDroneController.prototype.initialize, + + initialize : function() { + this.route('', this.series); + this.route('artist', this.series); + this.route('artist/:query', this.seriesDetails); + + this._originalInit.apply(this, arguments); + }, + + artist : function() { + this.setTitle('Lidarr'); + this.setArtistName('Lidarr'); + this.showMainRegion(new SeriesIndexLayout()); + }, + + seriesDetails : function(query) { + var artists = ArtistCollection.where({ artistNameSlug : query }); + console.log('seriesDetails, artists: ', artists); + if (artists.length !== 0) { + var targetSeries = artists[0]; + console.log("[ArtistController] targetSeries: ", targetSeries); + this.setTitle(targetSeries.get('title')); + this.setArtistName(targetSeries.get('artistName')); + this.showMainRegion(new SeriesDetailsLayout({ model : targetSeries })); + } else { + this.showNotFound(); + } + } +}); \ No newline at end of file diff --git a/src/UI/Artist/ArtistModel.js b/src/UI/Artist/ArtistModel.js new file mode 100644 index 000000000..209ebc1fa --- /dev/null +++ b/src/UI/Artist/ArtistModel.js @@ -0,0 +1,31 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); + +module.exports = Backbone.Model.extend({ + urlRoot : window.NzbDrone.ApiRoot + '/artist', + + defaults : { + episodeFileCount : 0, + episodeCount : 0, + isExisting : false, + status : 0 + }, + + setAlbumsMonitored : function(albumName) { + _.each(this.get('albums'), function(album) { + if (season.albumName === albumName) { + album.monitored = !album.monitored; + } + }); + }, + + setAlbumPass : function(seasonNumber) { + _.each(this.get('albums'), function(album) { + if (album.seasonNumber >= seasonNumber) { + album.monitored = true; + } else { + album.monitored = false; + } + }); + } +}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Series.js b/src/UI/Handlebars/Helpers/Series.js index 2c8a96bed..ff3ffd7f1 100644 --- a/src/UI/Handlebars/Helpers/Series.js +++ b/src/UI/Handlebars/Helpers/Series.js @@ -71,7 +71,7 @@ Handlebars.registerHelper('seasonCountHelper', function() { return new Handlebars.SafeString('{0} Seasons'.format(seasonCount)); }); -Handlebars.registerHelper('titleWithYear', function() { +/*Handlebars.registerHelper('titleWithYear', function() { if (this.title.endsWith(' ({0})'.format(this.year))) { return this.title; } @@ -81,4 +81,4 @@ Handlebars.registerHelper('titleWithYear', function() { } return new Handlebars.SafeString('{0} ({1})'.format(this.title, this.year)); -}); +});*/ diff --git a/src/UI/Series/Details/SeriesDetailsLayout.js b/src/UI/Series/Details/SeriesDetailsLayout.js index f33cb0414..e3a0294e9 100644 --- a/src/UI/Series/Details/SeriesDetailsLayout.js +++ b/src/UI/Series/Details/SeriesDetailsLayout.js @@ -4,7 +4,7 @@ var vent = require('vent'); var reqres = require('../../reqres'); var Marionette = require('marionette'); var Backbone = require('backbone'); -var SeriesCollection = require('../SeriesCollection'); +var ArtistCollection = require('../../Artist/ArtistCollection'); var EpisodeCollection = require('../EpisodeCollection'); var EpisodeFileCollection = require('../EpisodeFileCollection'); var SeasonCollection = require('../SeasonCollection'); @@ -45,7 +45,7 @@ module.exports = Marionette.Layout.extend({ }, initialize : function() { - this.seriesCollection = SeriesCollection.clone(); + this.seriesCollection = ArtistCollection.clone(); this.seriesCollection.shadowCollection.bindSignalR(); this.listenTo(this.model, 'change:monitored', this._setMonitoredState); @@ -155,6 +155,7 @@ module.exports = Marionette.Layout.extend({ }, _seriesSearch : function() { + console.log('_seriesSearch:', this.model); CommandController.Execute('seriesSearch', { name : 'seriesSearch', seriesId : this.model.id diff --git a/src/UI/Series/Index/EmptyTemplate.hbs b/src/UI/Series/Index/EmptyTemplate.hbs index abca7f764..16c5258ab 100644 --- a/src/UI/Series/Index/EmptyTemplate.hbs +++ b/src/UI/Series/Index/EmptyTemplate.hbs @@ -2,14 +2,14 @@
- You must be new around here, You should add some series. + You must be new around here, You should add some music.