From ad23e8ce9f8daad49aff5f8042fa3736eac69292 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Thu, 20 Apr 2017 18:19:47 -0500 Subject: [PATCH 01/12] Added Music models and basic database --- .../Scene/SceneMappingServiceFixture.cs | 1 + .../Scene/SceneMappingService.cs | 478 +++++++++--------- .../Datastore/Migration/111_setup_music.cs | 67 +++ src/NzbDrone.Core/Datastore/TableMapping.cs | 21 + src/NzbDrone.Core/MediaFiles/TrackFile.cs | 34 ++ src/NzbDrone.Core/Music/Album.cs | 26 + src/NzbDrone.Core/Music/Artist.cs | 83 +++ src/NzbDrone.Core/Music/Track.cs | 49 ++ src/NzbDrone.Core/NzbDrone.Core.csproj | 5 + 9 files changed, 525 insertions(+), 239 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs create mode 100644 src/NzbDrone.Core/MediaFiles/TrackFile.cs create mode 100644 src/NzbDrone.Core/Music/Album.cs create mode 100644 src/NzbDrone.Core/Music/Artist.cs create mode 100644 src/NzbDrone.Core/Music/Track.cs 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/Datastore/Migration/111_setup_music.cs b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs new file mode 100644 index 000000000..14f38294a --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs @@ -0,0 +1,67 @@ +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("Artists") + .WithColumn("ItunesId").AsInt32().Unique() + .WithColumn("ArtistName").AsString().Unique() + .WithColumn("ArtistSlug").AsString().Unique() + .WithColumn("CleanTitle").AsString() + .WithColumn("Monitored").AsBoolean() + .WithColumn("LastInfoSync").AsDateTime().Nullable() + .WithColumn("LastDiskSync").AsDateTime().Nullable() + .WithColumn("Overview").AsString() + .WithColumn("Status").AsInt32() + .WithColumn("Path").AsString() + .WithColumn("Images").AsString() + .WithColumn("QualityProfileId").AsInt32() + .WithColumn("AirTime").AsString().Nullable() // JVM: This might be DropDate instead + //.WithColumn("BacklogSetting").AsInt32() + ; + + Create.TableForModel("Albums") + .WithColumn("AlbumId").AsInt32() // Does this map to collectionId? + .WithColumn("Title").AsString() + .WithColumn("Year").AsInt32() + .WithColumn("Image").AsInt32() // Is this needed? + .WithColumn("TrackCount").AsInt32() + .WithColumn("DiscCount").AsInt32() + .WithColumn("Monitored").AsBoolean(); + + Create.TableForModel("Tracks") + .WithColumn("ItunesTrackId").AsInt32().Unique() + .WithColumn("AlbumId").AsInt32() + .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(); + //.WithColumn("AbsoluteEpisodeNumber").AsInt32().Nullable() + //.WithColumn("SceneAbsoluteEpisodeNumber").AsInt32().Nullable() + //.WithColumn("SceneSeasonNumber").AsInt32().Nullable() + //.WithColumn("SceneEpisodeNumber").AsInt32().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..b48d48803 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(s => s.Profile, s => s.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); + Mapper.Entity().RegisterModel("QualityDefinitions") .Ignore(d => d.Weight); 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/Music/Album.cs b/src/NzbDrone.Core/Music/Album.cs new file mode 100644 index 000000000..0466e479f --- /dev/null +++ b/src/NzbDrone.Core/Music/Album.cs @@ -0,0 +1,26 @@ +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; } + 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 + } +} diff --git a/src/NzbDrone.Core/Music/Artist.cs b/src/NzbDrone.Core/Music/Artist.cs new file mode 100644 index 000000000..f0faed6b1 --- /dev/null +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -0,0 +1,83 @@ +using Marr.Data; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Profiles; +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 int TvRageId { get; set; } + //public int TvMazeId { get; set; } + //public string ImdbId { get; set; } + public string ArtistName { get; set; } + public string ArtistSlug { get; set; } + public string CleanTitle { get; set; } + public string SortTitle { get; set; } + //public SeriesStatusType Status { get; set; } + public string Overview { get; set; } + public bool Monitored { get; set; } + //public int ProfileId { get; set; } + public bool AlbumFolder { get; set; } + public DateTime? LastInfoSync { get; set; } + //public int Runtime { get; set; } + public List Images { get; set; } + //public SeriesTypes SeriesType { get; set; } + //public string Network { get; set; } + //public bool UseSceneNumbering { get; set; } + //public string TitleSlug { get; set; } + public string Path { get; set; } + //public int Year { get; set; } + //public Ratings Ratings { get; set; } + public List Genres { get; set; } + //public List Actors { get; set; } // MOve to album? + public string Certification { get; set; } + public string RootFolderPath { get; set; } + public DateTime Added { get; set; } + public DateTime? FirstAired { 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; } // TODO: Learn what this does + + public override string ToString() + { + return string.Format("[{0}][{1}]", ItunesId, ArtistName.NullSafe()); + } + + public void ApplyChanges(Artist otherArtist) + { + //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/Track.cs b/src/NzbDrone.Core/Music/Track.cs new file mode 100644 index 000000000..27a61e58d --- /dev/null +++ b/src/NzbDrone.Core/Music/Track.cs @@ -0,0 +1,49 @@ +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 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/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 9e238632b..d82211ee6 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -287,6 +287,7 @@ Code + @@ -781,6 +782,7 @@ + @@ -837,6 +839,9 @@ + + + From 118e2dfe936bfa47c69a4b8d6d8b06ff1a8071d7 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 25 Apr 2017 07:47:14 -0500 Subject: [PATCH 02/12] Updated to support Album and Track level Compilations --- .../Datastore/Migration/111_setup_music.cs | 8 ++++++++ src/NzbDrone.Core/Datastore/TableMapping.cs | 3 +++ src/NzbDrone.Core/Music/Compilation.cs | 19 +++++++++++++++++++ src/NzbDrone.Core/Music/Track.cs | 2 ++ 4 files changed, 32 insertions(+) create mode 100644 src/NzbDrone.Core/Music/Compilation.cs diff --git a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs index 14f38294a..7fe64acfe 100644 --- a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs +++ b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs @@ -31,6 +31,8 @@ namespace NzbDrone.Core.Datastore.Migration Create.TableForModel("Albums") .WithColumn("AlbumId").AsInt32() // Does this map to collectionId? + .WithColumn("CompilationId").AsInt32() + .WithColumn("Compilation").AsBoolean() .WithColumn("Title").AsString() .WithColumn("Year").AsInt32() .WithColumn("Image").AsInt32() // Is this needed? @@ -41,6 +43,8 @@ namespace NzbDrone.Core.Datastore.Migration Create.TableForModel("Tracks") .WithColumn("ItunesTrackId").AsInt32().Unique() .WithColumn("AlbumId").AsInt32() + .WithColumn("CompilationId").AsInt32().Nullable() + .WithColumn("Compilation").AsBoolean().WithDefaultValue("False") .WithColumn("TrackNumber").AsInt32() .WithColumn("Title").AsString().Nullable() .WithColumn("Ignored").AsBoolean().Nullable() @@ -61,6 +65,10 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Size").AsInt64() .WithColumn("DateAdded").AsDateTime() .WithColumn("AlbumId").AsInt32(); // How does this impact stand alone tracks? + + Create.TableForModel("Compilation") + .WithColumn("CompilationId").AsInt32().Unique() + .WithColumn("ArtistId").AsString().Nullable(); } } diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index b48d48803..ce619b72b 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -112,6 +112,9 @@ namespace NzbDrone.Core.Datastore .Relationship() .HasOne(track => track.TrackFile, track => track.TrackFileId); + Mapper.Entity().RegisterModel("Compilation") + .Relationships.AutoMapICollectionOrComplexProperties(); //TODO: Figure out how to map this Table + Mapper.Entity().RegisterModel("QualityDefinitions") .Ignore(d => d.Weight); diff --git a/src/NzbDrone.Core/Music/Compilation.cs b/src/NzbDrone.Core/Music/Compilation.cs new file mode 100644 index 000000000..923e6d7f3 --- /dev/null +++ b/src/NzbDrone.Core/Music/Compilation.cs @@ -0,0 +1,19 @@ +using NzbDrone.Core.Datastore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public class Compilation : ModelBase + { + public Compilation() + { + + } + + public int CompilationId { get; set; } + public LazyList Artists { get; set; } + } +} diff --git a/src/NzbDrone.Core/Music/Track.cs b/src/NzbDrone.Core/Music/Track.cs index 27a61e58d..89793d514 100644 --- a/src/NzbDrone.Core/Music/Track.cs +++ b/src/NzbDrone.Core/Music/Track.cs @@ -19,6 +19,8 @@ namespace NzbDrone.Core.Music public int ItunesTrackId { get; set; } public int AlbumId { 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; } From 39b58ebda37af18c9cf29989f593b84cea8b8e06 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Thu, 27 Apr 2017 18:28:36 -0500 Subject: [PATCH 03/12] Changed the db to represent compilations. By default a track will have a single albumId and a list of artistIds. If the album is a compilation, we will use Various Artists to align with Plex, iTunes, and Amazon. --- .../Datastore/Migration/111_setup_music.cs | 18 +++++------------- src/NzbDrone.Core/Datastore/TableMapping.cs | 7 ++----- src/NzbDrone.Core/Music/Track.cs | 1 + 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs index 7fe64acfe..48f921f30 100644 --- a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs +++ b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs @@ -30,12 +30,11 @@ namespace NzbDrone.Core.Datastore.Migration ; Create.TableForModel("Albums") - .WithColumn("AlbumId").AsInt32() // Does this map to collectionId? - .WithColumn("CompilationId").AsInt32() - .WithColumn("Compilation").AsBoolean() + .WithColumn("AlbumId").AsInt32() + .WithColumn("ArtistId").AsInt32() .WithColumn("Title").AsString() .WithColumn("Year").AsInt32() - .WithColumn("Image").AsInt32() // Is this needed? + .WithColumn("Image").AsInt32() .WithColumn("TrackCount").AsInt32() .WithColumn("DiscCount").AsInt32() .WithColumn("Monitored").AsBoolean(); @@ -43,8 +42,7 @@ namespace NzbDrone.Core.Datastore.Migration Create.TableForModel("Tracks") .WithColumn("ItunesTrackId").AsInt32().Unique() .WithColumn("AlbumId").AsInt32() - .WithColumn("CompilationId").AsInt32().Nullable() - .WithColumn("Compilation").AsBoolean().WithDefaultValue("False") + .WithColumn("ArtistsId").AsString().Nullable() .WithColumn("TrackNumber").AsInt32() .WithColumn("Title").AsString().Nullable() .WithColumn("Ignored").AsBoolean().Nullable() @@ -53,10 +51,7 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("TrackCensoredName").AsString().Nullable() .WithColumn("TrackFileId").AsInt32().Nullable() .WithColumn("ReleaseDate").AsDateTime().Nullable(); - //.WithColumn("AbsoluteEpisodeNumber").AsInt32().Nullable() - //.WithColumn("SceneAbsoluteEpisodeNumber").AsInt32().Nullable() - //.WithColumn("SceneSeasonNumber").AsInt32().Nullable() - //.WithColumn("SceneEpisodeNumber").AsInt32().Nullable(); + Create.TableForModel("TrackFiles") .WithColumn("ArtistId").AsInt32() @@ -66,9 +61,6 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("DateAdded").AsDateTime() .WithColumn("AlbumId").AsInt32(); // How does this impact stand alone tracks? - Create.TableForModel("Compilation") - .WithColumn("CompilationId").AsInt32().Unique() - .WithColumn("ArtistId").AsString().Nullable(); } } diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index ce619b72b..dcf8c0924 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -103,17 +103,14 @@ namespace NzbDrone.Core.Datastore .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); + .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); - - Mapper.Entity().RegisterModel("Compilation") - .Relationships.AutoMapICollectionOrComplexProperties(); //TODO: Figure out how to map this Table + .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/Music/Track.cs b/src/NzbDrone.Core/Music/Track.cs index 89793d514..65e28231b 100644 --- a/src/NzbDrone.Core/Music/Track.cs +++ b/src/NzbDrone.Core/Music/Track.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Music 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; } From 9ce71ff69824d2990418c1f3a8f354098a4fd04c Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Thu, 20 Apr 2017 18:19:47 -0500 Subject: [PATCH 04/12] Added Music models and basic database --- .../Scene/SceneMappingServiceFixture.cs | 1 + .../Scene/SceneMappingService.cs | 478 +++++++++--------- .../Datastore/Migration/111_setup_music.cs | 75 +++ src/NzbDrone.Core/Datastore/TableMapping.cs | 24 + src/NzbDrone.Core/MediaFiles/TrackFile.cs | 34 ++ src/NzbDrone.Core/Music/Album.cs | 26 + src/NzbDrone.Core/Music/Artist.cs | 83 +++ src/NzbDrone.Core/Music/Compilation.cs | 19 + src/NzbDrone.Core/Music/Track.cs | 51 ++ src/NzbDrone.Core/NzbDrone.Core.csproj | 5 + 10 files changed, 557 insertions(+), 239 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs create mode 100644 src/NzbDrone.Core/MediaFiles/TrackFile.cs create mode 100644 src/NzbDrone.Core/Music/Album.cs create mode 100644 src/NzbDrone.Core/Music/Artist.cs create mode 100644 src/NzbDrone.Core/Music/Compilation.cs create mode 100644 src/NzbDrone.Core/Music/Track.cs 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/Datastore/Migration/111_setup_music.cs b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs new file mode 100644 index 000000000..7fe64acfe --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs @@ -0,0 +1,75 @@ +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("Artists") + .WithColumn("ItunesId").AsInt32().Unique() + .WithColumn("ArtistName").AsString().Unique() + .WithColumn("ArtistSlug").AsString().Unique() + .WithColumn("CleanTitle").AsString() + .WithColumn("Monitored").AsBoolean() + .WithColumn("LastInfoSync").AsDateTime().Nullable() + .WithColumn("LastDiskSync").AsDateTime().Nullable() + .WithColumn("Overview").AsString() + .WithColumn("Status").AsInt32() + .WithColumn("Path").AsString() + .WithColumn("Images").AsString() + .WithColumn("QualityProfileId").AsInt32() + .WithColumn("AirTime").AsString().Nullable() // JVM: This might be DropDate instead + //.WithColumn("BacklogSetting").AsInt32() + ; + + Create.TableForModel("Albums") + .WithColumn("AlbumId").AsInt32() // Does this map to collectionId? + .WithColumn("CompilationId").AsInt32() + .WithColumn("Compilation").AsBoolean() + .WithColumn("Title").AsString() + .WithColumn("Year").AsInt32() + .WithColumn("Image").AsInt32() // Is this needed? + .WithColumn("TrackCount").AsInt32() + .WithColumn("DiscCount").AsInt32() + .WithColumn("Monitored").AsBoolean(); + + Create.TableForModel("Tracks") + .WithColumn("ItunesTrackId").AsInt32().Unique() + .WithColumn("AlbumId").AsInt32() + .WithColumn("CompilationId").AsInt32().Nullable() + .WithColumn("Compilation").AsBoolean().WithDefaultValue("False") + .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(); + //.WithColumn("AbsoluteEpisodeNumber").AsInt32().Nullable() + //.WithColumn("SceneAbsoluteEpisodeNumber").AsInt32().Nullable() + //.WithColumn("SceneSeasonNumber").AsInt32().Nullable() + //.WithColumn("SceneEpisodeNumber").AsInt32().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? + + Create.TableForModel("Compilation") + .WithColumn("CompilationId").AsInt32().Unique() + .WithColumn("ArtistId").AsString().Nullable(); + } + + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 62f6aeb8b..ce619b72b 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,29 @@ namespace NzbDrone.Core.Datastore .Relationship() .HasOne(episode => episode.EpisodeFile, episode => episode.EpisodeFileId); + Mapper.Entity().RegisterModel("Artist") + .Ignore(s => s.RootFolderPath) + .Relationship() + .HasOne(s => s.Profile, s => s.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); + + Mapper.Entity().RegisterModel("Compilation") + .Relationships.AutoMapICollectionOrComplexProperties(); //TODO: Figure out how to map this Table + Mapper.Entity().RegisterModel("QualityDefinitions") .Ignore(d => d.Weight); 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/Music/Album.cs b/src/NzbDrone.Core/Music/Album.cs new file mode 100644 index 000000000..0466e479f --- /dev/null +++ b/src/NzbDrone.Core/Music/Album.cs @@ -0,0 +1,26 @@ +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; } + 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 + } +} diff --git a/src/NzbDrone.Core/Music/Artist.cs b/src/NzbDrone.Core/Music/Artist.cs new file mode 100644 index 000000000..f0faed6b1 --- /dev/null +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -0,0 +1,83 @@ +using Marr.Data; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Profiles; +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 int TvRageId { get; set; } + //public int TvMazeId { get; set; } + //public string ImdbId { get; set; } + public string ArtistName { get; set; } + public string ArtistSlug { get; set; } + public string CleanTitle { get; set; } + public string SortTitle { get; set; } + //public SeriesStatusType Status { get; set; } + public string Overview { get; set; } + public bool Monitored { get; set; } + //public int ProfileId { get; set; } + public bool AlbumFolder { get; set; } + public DateTime? LastInfoSync { get; set; } + //public int Runtime { get; set; } + public List Images { get; set; } + //public SeriesTypes SeriesType { get; set; } + //public string Network { get; set; } + //public bool UseSceneNumbering { get; set; } + //public string TitleSlug { get; set; } + public string Path { get; set; } + //public int Year { get; set; } + //public Ratings Ratings { get; set; } + public List Genres { get; set; } + //public List Actors { get; set; } // MOve to album? + public string Certification { get; set; } + public string RootFolderPath { get; set; } + public DateTime Added { get; set; } + public DateTime? FirstAired { 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; } // TODO: Learn what this does + + public override string ToString() + { + return string.Format("[{0}][{1}]", ItunesId, ArtistName.NullSafe()); + } + + public void ApplyChanges(Artist otherArtist) + { + //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/Compilation.cs b/src/NzbDrone.Core/Music/Compilation.cs new file mode 100644 index 000000000..923e6d7f3 --- /dev/null +++ b/src/NzbDrone.Core/Music/Compilation.cs @@ -0,0 +1,19 @@ +using NzbDrone.Core.Datastore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public class Compilation : ModelBase + { + public Compilation() + { + + } + + public int CompilationId { get; set; } + public LazyList Artists { get; set; } + } +} diff --git a/src/NzbDrone.Core/Music/Track.cs b/src/NzbDrone.Core/Music/Track.cs new file mode 100644 index 000000000..89793d514 --- /dev/null +++ b/src/NzbDrone.Core/Music/Track.cs @@ -0,0 +1,51 @@ +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 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/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 9e238632b..d82211ee6 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -287,6 +287,7 @@ Code + @@ -781,6 +782,7 @@ + @@ -837,6 +839,9 @@ + + + From ce62cff57de234a67b2e89c6afb7747758b6593e Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Thu, 27 Apr 2017 18:28:36 -0500 Subject: [PATCH 05/12] Changed the db to represent compilations. By default a track will have a single albumId and a list of artistIds. If the album is a compilation, we will use Various Artists to align with Plex, iTunes, and Amazon. --- .../Datastore/Migration/111_setup_music.cs | 18 +++++------------- src/NzbDrone.Core/Datastore/TableMapping.cs | 7 ++----- src/NzbDrone.Core/Music/Compilation.cs | 19 ------------------- src/NzbDrone.Core/Music/Track.cs | 1 + 4 files changed, 8 insertions(+), 37 deletions(-) delete mode 100644 src/NzbDrone.Core/Music/Compilation.cs diff --git a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs index 7fe64acfe..48f921f30 100644 --- a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs +++ b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs @@ -30,12 +30,11 @@ namespace NzbDrone.Core.Datastore.Migration ; Create.TableForModel("Albums") - .WithColumn("AlbumId").AsInt32() // Does this map to collectionId? - .WithColumn("CompilationId").AsInt32() - .WithColumn("Compilation").AsBoolean() + .WithColumn("AlbumId").AsInt32() + .WithColumn("ArtistId").AsInt32() .WithColumn("Title").AsString() .WithColumn("Year").AsInt32() - .WithColumn("Image").AsInt32() // Is this needed? + .WithColumn("Image").AsInt32() .WithColumn("TrackCount").AsInt32() .WithColumn("DiscCount").AsInt32() .WithColumn("Monitored").AsBoolean(); @@ -43,8 +42,7 @@ namespace NzbDrone.Core.Datastore.Migration Create.TableForModel("Tracks") .WithColumn("ItunesTrackId").AsInt32().Unique() .WithColumn("AlbumId").AsInt32() - .WithColumn("CompilationId").AsInt32().Nullable() - .WithColumn("Compilation").AsBoolean().WithDefaultValue("False") + .WithColumn("ArtistsId").AsString().Nullable() .WithColumn("TrackNumber").AsInt32() .WithColumn("Title").AsString().Nullable() .WithColumn("Ignored").AsBoolean().Nullable() @@ -53,10 +51,7 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("TrackCensoredName").AsString().Nullable() .WithColumn("TrackFileId").AsInt32().Nullable() .WithColumn("ReleaseDate").AsDateTime().Nullable(); - //.WithColumn("AbsoluteEpisodeNumber").AsInt32().Nullable() - //.WithColumn("SceneAbsoluteEpisodeNumber").AsInt32().Nullable() - //.WithColumn("SceneSeasonNumber").AsInt32().Nullable() - //.WithColumn("SceneEpisodeNumber").AsInt32().Nullable(); + Create.TableForModel("TrackFiles") .WithColumn("ArtistId").AsInt32() @@ -66,9 +61,6 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("DateAdded").AsDateTime() .WithColumn("AlbumId").AsInt32(); // How does this impact stand alone tracks? - Create.TableForModel("Compilation") - .WithColumn("CompilationId").AsInt32().Unique() - .WithColumn("ArtistId").AsString().Nullable(); } } diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index ce619b72b..dcf8c0924 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -103,17 +103,14 @@ namespace NzbDrone.Core.Datastore .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); + .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); - - Mapper.Entity().RegisterModel("Compilation") - .Relationships.AutoMapICollectionOrComplexProperties(); //TODO: Figure out how to map this Table + .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/Music/Compilation.cs b/src/NzbDrone.Core/Music/Compilation.cs deleted file mode 100644 index 923e6d7f3..000000000 --- a/src/NzbDrone.Core/Music/Compilation.cs +++ /dev/null @@ -1,19 +0,0 @@ -using NzbDrone.Core.Datastore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.Music -{ - public class Compilation : ModelBase - { - public Compilation() - { - - } - - public int CompilationId { get; set; } - public LazyList Artists { get; set; } - } -} diff --git a/src/NzbDrone.Core/Music/Track.cs b/src/NzbDrone.Core/Music/Track.cs index 89793d514..65e28231b 100644 --- a/src/NzbDrone.Core/Music/Track.cs +++ b/src/NzbDrone.Core/Music/Track.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Music 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; } From 2813fccc78b027cecc4aa01804fa94b4d612cd84 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Fri, 28 Apr 2017 17:05:35 -0500 Subject: [PATCH 06/12] Updated the code to allow a search to be made from UI to iTunes and return a mock result. --- src/NzbDrone.Api/Music/AlbumResource.cs | 55 +++++ src/NzbDrone.Api/Music/ArtistLookupModule.cs | 46 ++++ src/NzbDrone.Api/Music/ArtistResource.cs | 205 ++++++++++++++++++ src/NzbDrone.Api/NzbDrone.Api.csproj | 3 + .../Cloud/SonarrCloudRequestBuilder.cs | 11 +- .../MetadataSource/ISearchForNewSeries.cs | 2 + .../SkyHook/Resource/ArtistResource.cs | 32 +++ .../MetadataSource/SkyHook/SkyHookProxy.cs | 145 ++++++++++--- src/NzbDrone.Core/Music/Artist.cs | 2 + src/NzbDrone.Core/Music/ArtistRepository.cs | 129 +++++++++++ src/NzbDrone.Core/Music/ArtistService.cs | 95 ++++++++ src/NzbDrone.Core/Music/Compilation.cs | 19 -- src/NzbDrone.Core/Music/TrackService.cs | 117 ++++++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 6 + src/NzbDrone.Core/Parser/Model/LocalTrack.cs | 42 ++++ .../Parser/Model/ParsedTrackInfo.cs | 97 +++++++++ src/NzbDrone.Core/Parser/Parser.cs | 135 ++++++++++++ src/NzbDrone.Core/Parser/ParsingService.cs | 95 +++++++- src/UI/AddSeries/AddSeriesCollection.js | 8 +- src/UI/AddSeries/AddSeriesLayoutTemplate.hbs | 4 +- src/UI/AddSeries/AddSeriesView.js | 1 + src/UI/AddSeries/AddSeriesViewTemplate.hbs | 2 +- src/UI/AddSeries/EmptyViewTemplate.hbs | 2 +- src/UI/AddSeries/ErrorViewTemplate.hbs | 2 +- ...ddExistingSeriesCollectionViewTemplate.hbs | 2 +- src/UI/AddSeries/SearchResultView.js | 4 +- src/UI/AddSeries/SearchResultViewTemplate.hbs | 10 +- src/UI/Artist/ArtistCollection.js | 124 +++++++++++ src/UI/Artist/ArtistController.js | 37 ++++ src/UI/Artist/ArtistModel.js | 31 +++ src/UI/Handlebars/Helpers/Series.js | 4 +- src/UI/Series/Details/SeriesDetailsLayout.js | 5 +- 32 files changed, 1404 insertions(+), 68 deletions(-) create mode 100644 src/NzbDrone.Api/Music/AlbumResource.cs create mode 100644 src/NzbDrone.Api/Music/ArtistLookupModule.cs create mode 100644 src/NzbDrone.Api/Music/ArtistResource.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs create mode 100644 src/NzbDrone.Core/Music/ArtistRepository.cs create mode 100644 src/NzbDrone.Core/Music/ArtistService.cs delete mode 100644 src/NzbDrone.Core/Music/Compilation.cs create mode 100644 src/NzbDrone.Core/Music/TrackService.cs create mode 100644 src/NzbDrone.Core/Parser/Model/LocalTrack.cs create mode 100644 src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs create mode 100644 src/UI/Artist/ArtistCollection.js create mode 100644 src/UI/Artist/ArtistController.js create mode 100644 src/UI/Artist/ArtistModel.js diff --git a/src/NzbDrone.Api/Music/AlbumResource.cs b/src/NzbDrone.Api/Music/AlbumResource.cs new file mode 100644 index 000000000..c0235e48a --- /dev/null +++ b/src/NzbDrone.Api/Music/AlbumResource.cs @@ -0,0 +1,55 @@ +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 bool Monitored { get; set; } + //public string Overview { get; set; } + public int Year { get; set; } + //public SeasonStatisticsResource Statistics { 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, + //Overview = model.Overview; //TODO: Inspect if Album needs an overview + Year = model.Year + }; + } + + public static Album ToModel(this AlbumResource resource) + { + if (resource == null) return null; + + return new Album + { + AlbumId = resource.AlbumId, + Monitored = resource.Monitored, + Year = resource.Year + }; + } + + 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/ArtistResource.cs b/src/NzbDrone.Api/Music/ArtistResource.cs new file mode 100644 index 000000000..0921ea1b5 --- /dev/null +++ b/src/NzbDrone.Api/Music/ArtistResource.cs @@ -0,0 +1,205 @@ +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; + } + + //Todo: Sorters should be done completely on the client + //Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? + //Todo: We should get the entire Profile instead of ID and Name separately + + //View Only + public string ArtistName { 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 bool UseSceneNumbering { get; set; } + //public int Runtime { get; set; } + //public int TvdbId { get; set; } + //public int TvRageId { get; set; } + //public int TvMazeId { get; set; } + //public DateTime? FirstAired { get; set; } + //public DateTime? LastInfoSync { get; set; } + //public SeriesTypes SeriesType { get; set; } + public string CleanTitle { get; set; } + public int ItunesId { get; set; } + //public string TitleSlug { 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, + CleanTitle = model.CleanTitle, + ItunesId = model.ItunesId, + ArtistSlug = model.ArtistSlug, + + RootFolderPath = model.RootFolderPath, + Certification = model.Certification, + Genres = model.Genres, + Tags = model.Tags, + Added = model.Added, + //AddOptions = resource.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, + CleanTitle = resource.CleanTitle, + ItunesId = resource.ItunesId, + ArtistSlug = resource.ArtistSlug, + + RootFolderPath = resource.RootFolderPath, + Certification = resource.Certification, + 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..89f43a55a 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -111,6 +111,9 @@ + + + diff --git a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs index 668535986..23674eaec 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,22 @@ namespace NzbDrone.Common.Cloud { public LidarrCloudRequestBuilder() { - Services = new HttpRequestBuilder("http://services.Lidarr.tv/v1/") + + Services = new HttpRequestBuilder("http://services.lidarr.tv/v1/") .CreateFactory(); - SkyHookTvdb = new HttpRequestBuilder("http://skyhook.Lidarr.tv/v1/tvdb/{route}/{language}/") + Search = new HttpRequestBuilder("https://itunes.apple.com/search/") + .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/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..7e6040021 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs @@ -0,0 +1,32 @@ +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 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..0b68cb78f 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -10,6 +10,9 @@ 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 { @@ -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,114 @@ 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:")) - { - 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(); - } - } + + + 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.Results.SelectList(MapArtist); + } + 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 List SearchForNewArtist(string title) + { + 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(); + // } + //} var httpRequest = _requestBuilder.Create() - .SetSegment("route", "search") - .AddQueryParam("term", title.ToLower().Trim()) - .Build(); + .AddQueryParam("entity", "album") + .AddQueryParam("term", title.ToLower().Trim()) + .Build(); + + - var httpResponse = _httpClient.Get>(httpRequest); + Console.WriteLine("httpRequest: ", httpRequest); - return httpResponse.Resource.SelectList(MapSeries); + 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 Artist(); + tempSeries.ArtistName = "AFI"; + tempList.Add(tempSeries); + return tempList; + + //return httpResponse.Resource.Results.SelectList(MapArtist); } catch (HttpException) { @@ -104,6 +186,17 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } + private static Artist MapArtist(ArtistResource artistQuery) + { + var artist = new Artist(); + //artist.ItunesId = artistQuery.artistId; + + // artist.ArtistName = artistQuery.ArtistName; + + + return artist; + } + private static Series MapSeries(ShowResource show) { var series = new Series(); diff --git a/src/NzbDrone.Core/Music/Artist.cs b/src/NzbDrone.Core/Music/Artist.cs index f0faed6b1..50c297a56 100644 --- a/src/NzbDrone.Core/Music/Artist.cs +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -55,6 +55,8 @@ namespace NzbDrone.Core.Music public List Albums { get; set; } public HashSet Tags { get; set; } + public bool ArtistFolder { get; set; } + //public AddSeriesOptions AddOptions { get; set; } // TODO: Learn what this does public override string ToString() diff --git a/src/NzbDrone.Core/Music/ArtistRepository.cs b/src/NzbDrone.Core/Music/ArtistRepository.cs new file mode 100644 index 000000000..39fcbb1db --- /dev/null +++ b/src/NzbDrone.Core/Music/ArtistRepository.cs @@ -0,0 +1,129 @@ +using NzbDrone.Core.Datastore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Linq.Expressions; + +namespace NzbDrone.Core.Music +{ + public interface IArtistRepository : IBasicRepository + { + bool ArtistPathExists(string path); + Artist FindByTitle(string cleanTitle); + Artist FindByItunesId(int iTunesId); + } + + public class ArtistRepository : IArtistRepository + { + public IEnumerable All() + { + throw new NotImplementedException(); + } + + public bool ArtistPathExists(string path) + { + throw new NotImplementedException(); + } + + public int Count() + { + throw new NotImplementedException(); + } + + public void Delete(Artist model) + { + throw new NotImplementedException(); + } + + public void Delete(int id) + { + throw new NotImplementedException(); + } + + public void DeleteMany(IEnumerable ids) + { + throw new NotImplementedException(); + } + + public void DeleteMany(List model) + { + throw new NotImplementedException(); + } + + public Artist FindByItunesId(int iTunesId) + { + throw new NotImplementedException(); + } + + public Artist FindByTitle(string cleanTitle) + { + throw new NotImplementedException(); + } + + public IEnumerable Get(IEnumerable ids) + { + throw new NotImplementedException(); + } + + public Artist Get(int id) + { + throw new NotImplementedException(); + } + + public PagingSpec GetPaged(PagingSpec pagingSpec) + { + throw new NotImplementedException(); + } + + public bool HasItems() + { + throw new NotImplementedException(); + } + + public Artist Insert(Artist model) + { + throw new NotImplementedException(); + } + + public void InsertMany(IList model) + { + throw new NotImplementedException(); + } + + public void Purge(bool vacuum = false) + { + throw new NotImplementedException(); + } + + public void SetFields(Artist model, params Expression>[] properties) + { + throw new NotImplementedException(); + } + + public Artist Single() + { + throw new NotImplementedException(); + } + + public Artist SingleOrDefault() + { + throw new NotImplementedException(); + } + + public Artist Update(Artist model) + { + throw new NotImplementedException(); + } + + public void UpdateMany(IList model) + { + throw new NotImplementedException(); + } + + public Artist Upsert(Artist model) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/NzbDrone.Core/Music/ArtistService.cs b/src/NzbDrone.Core/Music/ArtistService.cs new file mode 100644 index 000000000..4aecdb36d --- /dev/null +++ b/src/NzbDrone.Core/Music/ArtistService.cs @@ -0,0 +1,95 @@ +using NLog; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Organizer; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Music +{ + public interface IArtistService + { + Artist GetArtist(int artistId); + List GetArtists(IEnumerable artistIds); + Artist AddArtist(Artist newArtist); + Artist FindByItunesId(int itunesId); + Artist FindByTitle(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 Artist AddArtist(Artist newArtist) + { + throw new NotImplementedException(); + } + + public bool ArtistPathExists(string folder) + { + throw new NotImplementedException(); + } + + public void DeleteArtist(int artistId, bool deleteFiles) + { + throw new NotImplementedException(); + } + + public Artist FindByItunesId(int itunesId) + { + throw new NotImplementedException(); + } + + public Artist FindByTitle(string title) + { + throw new NotImplementedException(); + } + + public Artist FindByTitleInexact(string title) + { + throw new NotImplementedException(); + } + + public List GetAllArtists() + { + throw new NotImplementedException(); + } + + public Artist GetArtist(int artistId) + { + throw new NotImplementedException(); + } + + public List GetArtists(IEnumerable artistIds) + { + throw new NotImplementedException(); + } + + public void RemoveAddOptions(Artist artist) + { + throw new NotImplementedException(); + } + + public Artist UpdateArtist(Artist artist) + { + throw new NotImplementedException(); + } + + public List UpdateArtists(List artist) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/NzbDrone.Core/Music/Compilation.cs b/src/NzbDrone.Core/Music/Compilation.cs deleted file mode 100644 index 923e6d7f3..000000000 --- a/src/NzbDrone.Core/Music/Compilation.cs +++ /dev/null @@ -1,19 +0,0 @@ -using NzbDrone.Core.Datastore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.Music -{ - public class Compilation : ModelBase - { - public Compilation() - { - - } - - public int CompilationId { get; set; } - public LazyList Artists { get; set; } - } -} 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 d82211ee6..3b00c7cf0 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -809,6 +809,7 @@ + @@ -841,7 +842,10 @@ + + + @@ -897,6 +901,8 @@ + + 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..1a541cd1c 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 @@ -522,6 +653,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/UI/AddSeries/AddSeriesCollection.js b/src/UI/AddSeries/AddSeriesCollection.js index 5be24d3a7..d81094eca 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; @@ -15,7 +15,9 @@ module.exports = Backbone.Collection.extend({ if (self.unmappedFolderModel) { model.path = self.unmappedFolderModel.get('folder').path; } + console.log('model: ', model); }); + console.log('response: ', response); // Note: this gets called after api responds with artist model 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..348413a35 100644 --- a/src/UI/AddSeries/SearchResultView.js +++ b/src/UI/AddSeries/SearchResultView.js @@ -6,7 +6,8 @@ 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 SeriesCollection = require('../Series/SeriesCollection'); +var SeriesCollection = require('../Artist/ArtistCollection'); var Config = require('../Config'); var Messenger = require('../Shared/Messenger'); var AsValidatedView = require('../Mixins/AsValidatedView'); @@ -210,6 +211,7 @@ var view = Marionette.ItemView.extend({ }); promise.done(function() { + console.log('[SearchResultView] _addSeries promise resolve:', self.model); SeriesCollection.add(self.model); self.close(); diff --git a/src/UI/AddSeries/SearchResultViewTemplate.hbs b/src/UI/AddSeries/SearchResultViewTemplate.hbs index 2eafdf2b0..bccef713d 100644 --- a/src/UI/AddSeries/SearchResultViewTemplate.hbs +++ b/src/UI/AddSeries/SearchResultViewTemplate.hbs @@ -10,6 +10,7 @@

{{titleWithYear}} + {{artistName}} {{network}} @@ -41,8 +42,6 @@ - -

@@ -52,10 +51,9 @@ {{> ProfileSelectionPartial profiles}} -
- - {{> SeriesTypeSelectionPartial}} -
+
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..70763dac2 --- /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(seasonNumber) { + _.each(this.get('albums'), function(album) { + if (season.seasonNumber === seasonNumber) { + 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 From a08ebcc0c2bc54bf33b74c77ac23d8853a1722c4 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 30 Apr 2017 08:54:37 -0500 Subject: [PATCH 07/12] Now returns one artist back with albums. --- src/NzbDrone.Api/Music/AlbumResource.cs | 11 +-- src/NzbDrone.Api/Music/ArtistResource.cs | 17 +--- .../SkyHook/Resource/ArtistResource.cs | 1 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 78 ++++++++++++------- src/NzbDrone.Core/Music/Album.cs | 2 +- 5 files changed, 59 insertions(+), 50 deletions(-) diff --git a/src/NzbDrone.Api/Music/AlbumResource.cs b/src/NzbDrone.Api/Music/AlbumResource.cs index c0235e48a..ee9a66564 100644 --- a/src/NzbDrone.Api/Music/AlbumResource.cs +++ b/src/NzbDrone.Api/Music/AlbumResource.cs @@ -9,10 +9,10 @@ namespace NzbDrone.Api.Music public class AlbumResource { public int AlbumId { get; set; } + public string AlbumName { get; set; } public bool Monitored { get; set; } - //public string Overview { get; set; } public int Year { get; set; } - //public SeasonStatisticsResource Statistics { get; set; } + public List Genre { get; set; } } public static class AlbumResourceMapper @@ -25,8 +25,8 @@ namespace NzbDrone.Api.Music { AlbumId = model.AlbumId, Monitored = model.Monitored, - //Overview = model.Overview; //TODO: Inspect if Album needs an overview - Year = model.Year + Year = model.Year, + AlbumName = model.Title }; } @@ -38,7 +38,8 @@ namespace NzbDrone.Api.Music { AlbumId = resource.AlbumId, Monitored = resource.Monitored, - Year = resource.Year + Year = resource.Year, + Title = resource.AlbumName }; } diff --git a/src/NzbDrone.Api/Music/ArtistResource.cs b/src/NzbDrone.Api/Music/ArtistResource.cs index 0921ea1b5..a59df1798 100644 --- a/src/NzbDrone.Api/Music/ArtistResource.cs +++ b/src/NzbDrone.Api/Music/ArtistResource.cs @@ -16,12 +16,10 @@ namespace NzbDrone.Api.Music Monitored = true; } - //Todo: Sorters should be done completely on the client - //Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? - //Todo: We should get the entire Profile instead of ID and Name separately //View Only public string ArtistName { get; set; } + public int ItunesId { get; set; } //public List AlternateTitles { get; set; } //public string SortTitle { get; set; } @@ -55,17 +53,6 @@ namespace NzbDrone.Api.Music public bool ArtistFolder { get; set; } public bool Monitored { get; set; } - //public bool UseSceneNumbering { get; set; } - //public int Runtime { get; set; } - //public int TvdbId { get; set; } - //public int TvRageId { get; set; } - //public int TvMazeId { get; set; } - //public DateTime? FirstAired { get; set; } - //public DateTime? LastInfoSync { get; set; } - //public SeriesTypes SeriesType { get; set; } - public string CleanTitle { get; set; } - public int ItunesId { get; set; } - //public string TitleSlug { get; set; } public string RootFolderPath { get; set; } public string Certification { get; set; } public List Genres { get; set; } @@ -119,7 +106,6 @@ namespace NzbDrone.Api.Music //FirstAired = resource.FirstAired, //LastInfoSync = resource.LastInfoSync, //SeriesType = resource.SeriesType, - CleanTitle = model.CleanTitle, ItunesId = model.ItunesId, ArtistSlug = model.ArtistSlug, @@ -174,7 +160,6 @@ namespace NzbDrone.Api.Music //FirstAired = resource.FirstAired, //LastInfoSync = resource.LastInfoSync, //SeriesType = resource.SeriesType, - CleanTitle = resource.CleanTitle, ItunesId = resource.ItunesId, ArtistSlug = resource.ArtistSlug, diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs index 7e6040021..b7d8eff01 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource public string ArtistName { get; set; } public int ArtistId { get; set; } public string CollectionName { get; set; } + public int CollectionId { get; set; } } public class ArtistResource diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 0b68cb78f..a912109aa 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -99,7 +99,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook Console.WriteLine("httpRequest: ", httpRequest); - var httpResponse = _httpClient.Get(httpRequest); + var httpResponse = _httpClient.Get>(httpRequest); //Console.WriteLine("Response: ", httpResponse.GetType()); //_logger.Info("Response: ", httpResponse.Resource.ResultCount); @@ -111,7 +111,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook tempList.Add(tempSeries); return tempList; - //return httpResponse.Resource.Results.SelectList(MapArtist); + return httpResponse.Resource.SelectList(MapSeries); } catch (HttpException) { @@ -131,26 +131,26 @@ namespace NzbDrone.Core.MetadataSource.SkyHook 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(); - // } - //} + if (lowerTitle.StartsWith("itunes:") || lowerTitle.StartsWith("itunesid:")) + { + var slug = lowerTitle.Split(':')[1].Trim(); + + int itunesId; + + if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace) || !int.TryParse(slug, out itunesId) || itunesId <= 0) + { + return new List(); + } + + //try + //{ + // return new List { GetArtistInfo(itunesId).Item1 }; + //} + //catch (ArtistNotFoundException) + //{ + // return new List(); + //} + } var httpRequest = _requestBuilder.Create() .AddQueryParam("entity", "album") @@ -163,15 +163,37 @@ namespace NzbDrone.Core.MetadataSource.SkyHook 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 Artist(); - tempSeries.ArtistName = "AFI"; - tempList.Add(tempSeries); - return tempList; + //var tempList = new List(); + //var tempSeries = new Artist(); + //tempSeries.ArtistName = "AFI"; + //tempList.Add(tempSeries); + //return tempList; + + + Album tempAlbum; + // TODO: This needs to handle multiple artists. + Artist artist = new Artist(); + artist.ArtistName = httpResponse.Resource.Results[0].ArtistName; + artist.ItunesId = httpResponse.Resource.Results[0].ArtistId; + foreach (var album in httpResponse.Resource.Results) + { + tempAlbum = new Album(); + tempAlbum.AlbumId = album.CollectionId; + tempAlbum.Title = album.CollectionName; + + artist.Albums.Add(tempAlbum); + } + + var temp = new List(); + temp.Add(artist); + return temp; + + // I need to return a list of mapped artists. //return httpResponse.Resource.Results.SelectList(MapArtist); } @@ -189,7 +211,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook private static Artist MapArtist(ArtistResource artistQuery) { var artist = new Artist(); - //artist.ItunesId = artistQuery.artistId; + //artist.ItunesId = artistQuery.ItunesId; ; // artist.ArtistName = artistQuery.ArtistName; diff --git a/src/NzbDrone.Core/Music/Album.cs b/src/NzbDrone.Core/Music/Album.cs index 0466e479f..aa7923aef 100644 --- a/src/NzbDrone.Core/Music/Album.cs +++ b/src/NzbDrone.Core/Music/Album.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Music } public int AlbumId { get; set; } - public string Title { 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; } From de21685896c11383725d9721f989ae3afda0406f Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 30 Apr 2017 09:26:50 -0500 Subject: [PATCH 08/12] Multiple artists return to UI --- .../MetadataSource/SkyHook/SkyHookProxy.cs | 31 ++++++++++++++----- src/NzbDrone.Core/Music/Artist.cs | 14 +++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index a912109aa..c2292b271 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -175,23 +175,38 @@ namespace NzbDrone.Core.MetadataSource.SkyHook //return tempList; + Album tempAlbum; - // TODO: This needs to handle multiple artists. - Artist artist = new Artist(); - artist.ArtistName = httpResponse.Resource.Results[0].ArtistName; - artist.ItunesId = httpResponse.Resource.Results[0].ArtistId; + List artists = new List(); + ArtistComparer artistComparer = new ArtistComparer(); foreach (var album in httpResponse.Resource.Results) { tempAlbum = new Album(); + // TODO: Perform MapAlbum call here tempAlbum.AlbumId = album.CollectionId; tempAlbum.Title = album.CollectionName; - artist.Albums.Add(tempAlbum); + + int index = artists.FindIndex(a => a.ItunesId == album.ArtistId); + + + if (index >= 0) + { + artists[index].Albums.Add(tempAlbum); + } + else + { + Artist tempArtist = new Artist(); + // TODO: Perform the MapArtist call here + tempArtist.ItunesId = album.ArtistId; + tempArtist.ArtistName = album.ArtistName; + tempArtist.Albums.Add(tempAlbum); + artists.Add(tempArtist); + } + } - var temp = new List(); - temp.Add(artist); - return temp; + return artists; // I need to return a list of mapped artists. diff --git a/src/NzbDrone.Core/Music/Artist.cs b/src/NzbDrone.Core/Music/Artist.cs index 50c297a56..6cf856efe 100644 --- a/src/NzbDrone.Core/Music/Artist.cs +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -9,6 +9,20 @@ using System.Text; namespace NzbDrone.Core.Music { + + public class ArtistComparer : IEqualityComparer + { + public bool Equals(Artist x, Artist y) + { + return x.ItunesId == y.ItunesId; + } + + public int GetHashCode(Artist obj) + { + throw new NotImplementedException(); + } + } + public class Artist : ModelBase { public Artist() From 5b0f11b19ad05e8750055fb0cbdf9724aa6afd3b Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 30 Apr 2017 11:34:53 -0500 Subject: [PATCH 09/12] Albums can now be seen per artist from search. --- src/NzbDrone.Api/Music/AlbumResource.cs | 12 +++- .../SkyHook/Resource/ArtistResource.cs | 8 +++ .../MetadataSource/SkyHook/SkyHookProxy.cs | 44 +++--------- src/NzbDrone.Core/Music/Album.cs | 3 + src/NzbDrone.Core/Music/Artist.cs | 15 ----- src/UI/AddSeries/AddSeriesCollection.js | 3 +- src/UI/AddSeries/SearchResultView.js | 24 ++++--- src/UI/AddSeries/SearchResultViewTemplate.hbs | 67 +++++++++++++++---- src/UI/AddSeries/addSeries.less | 8 +++ src/UI/Series/Index/EmptyTemplate.hbs | 4 +- 10 files changed, 108 insertions(+), 80 deletions(-) diff --git a/src/NzbDrone.Api/Music/AlbumResource.cs b/src/NzbDrone.Api/Music/AlbumResource.cs index ee9a66564..a6d49d3bd 100644 --- a/src/NzbDrone.Api/Music/AlbumResource.cs +++ b/src/NzbDrone.Api/Music/AlbumResource.cs @@ -12,7 +12,9 @@ namespace NzbDrone.Api.Music public string AlbumName { get; set; } public bool Monitored { get; set; } public int Year { get; set; } - public List Genre { get; set; } + public List Genres { get; set; } + public string ArtworkUrl { get; set; } + } public static class AlbumResourceMapper @@ -26,7 +28,9 @@ namespace NzbDrone.Api.Music AlbumId = model.AlbumId, Monitored = model.Monitored, Year = model.Year, - AlbumName = model.Title + AlbumName = model.Title, + Genres = model.Genres, + ArtworkUrl = model.ArtworkUrl }; } @@ -39,7 +43,9 @@ namespace NzbDrone.Api.Music AlbumId = resource.AlbumId, Monitored = resource.Monitored, Year = resource.Year, - Title = resource.AlbumName + Title = resource.AlbumName, + Genres = resource.Genres, + ArtworkUrl = resource.ArtworkUrl }; } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs index b7d8eff01..88ad6eae6 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ArtistResource.cs @@ -16,6 +16,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource 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 diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index c2292b271..d11b60107 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -163,32 +163,12 @@ namespace NzbDrone.Core.MetadataSource.SkyHook 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 Artist(); - //tempSeries.ArtistName = "AFI"; - //tempList.Add(tempSeries); - //return tempList; - - - Album tempAlbum; List artists = new List(); - ArtistComparer artistComparer = new ArtistComparer(); foreach (var album in httpResponse.Resource.Results) { - tempAlbum = new Album(); - // TODO: Perform MapAlbum call here - tempAlbum.AlbumId = album.CollectionId; - tempAlbum.Title = album.CollectionName; - - int index = artists.FindIndex(a => a.ItunesId == album.ArtistId); - + tempAlbum = MapAlbum(album); if (index >= 0) { @@ -200,6 +180,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook // TODO: Perform the MapArtist call here tempArtist.ItunesId = album.ArtistId; tempArtist.ArtistName = album.ArtistName; + tempArtist.Genres.Add(album.PrimaryGenreName); tempArtist.Albums.Add(tempAlbum); artists.Add(tempArtist); } @@ -207,10 +188,6 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } return artists; - - // I need to return a list of mapped artists. - - //return httpResponse.Resource.Results.SelectList(MapArtist); } catch (HttpException) { @@ -223,15 +200,16 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } - private static Artist MapArtist(ArtistResource artistQuery) + private Album MapAlbum(AlbumResource albumQuery) { - var artist = new Artist(); - //artist.ItunesId = artistQuery.ItunesId; ; - - // artist.ArtistName = artistQuery.ArtistName; - - - return artist; + 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) diff --git a/src/NzbDrone.Core/Music/Album.cs b/src/NzbDrone.Core/Music/Album.cs index aa7923aef..c0c7fc19e 100644 --- a/src/NzbDrone.Core/Music/Album.cs +++ b/src/NzbDrone.Core/Music/Album.cs @@ -22,5 +22,8 @@ namespace NzbDrone.Core.Music 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 index 6cf856efe..6a8ccf959 100644 --- a/src/NzbDrone.Core/Music/Artist.cs +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -9,20 +9,6 @@ using System.Text; namespace NzbDrone.Core.Music { - - public class ArtistComparer : IEqualityComparer - { - public bool Equals(Artist x, Artist y) - { - return x.ItunesId == y.ItunesId; - } - - public int GetHashCode(Artist obj) - { - throw new NotImplementedException(); - } - } - public class Artist : ModelBase { public Artist() @@ -46,7 +32,6 @@ namespace NzbDrone.Core.Music //public SeriesStatusType Status { get; set; } public string Overview { get; set; } public bool Monitored { get; set; } - //public int ProfileId { get; set; } public bool AlbumFolder { get; set; } public DateTime? LastInfoSync { get; set; } //public int Runtime { get; set; } diff --git a/src/UI/AddSeries/AddSeriesCollection.js b/src/UI/AddSeries/AddSeriesCollection.js index d81094eca..a243649f4 100644 --- a/src/UI/AddSeries/AddSeriesCollection.js +++ b/src/UI/AddSeries/AddSeriesCollection.js @@ -15,9 +15,8 @@ module.exports = Backbone.Collection.extend({ if (self.unmappedFolderModel) { model.path = self.unmappedFolderModel.get('folder').path; } - console.log('model: ', model); }); - console.log('response: ', response); // Note: this gets called after api responds with artist model + console.log('response: ', response); return response; } diff --git a/src/UI/AddSeries/SearchResultView.js b/src/UI/AddSeries/SearchResultView.js index 348413a35..7e7f60f47 100644 --- a/src/UI/AddSeries/SearchResultView.js +++ b/src/UI/AddSeries/SearchResultView.js @@ -6,8 +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 SeriesCollection = require('../Artist/ArtistCollection'); +var ArtistCollection = require('../Artist/ArtistCollection'); var Config = require('../Config'); var Messenger = require('../Shared/Messenger'); var AsValidatedView = require('../Mixins/AsValidatedView'); @@ -94,7 +93,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(); @@ -170,20 +169,22 @@ var view = Marionette.ItemView.extend({ this._addSeries(true); }, - _addSeries : function(searchForMissingEpisodes) { - var addButton = this.ui.addButton; - var addSearchButton = this.ui.addSearchButton; + _addSeries : function(searchForMissing) { + // TODO: Refactor to handle multiple add buttons/albums + var addButton = this.ui.addButton[0]; + var addSearchButton = this.ui.addSearchButton[0]; + 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, @@ -197,7 +198,7 @@ var view = Marionette.ItemView.extend({ var self = this; var promise = this.model.save(); - if (searchForMissingEpisodes) { + if (searchForMissing) { this.ui.addSearchButton.spinForPromise(promise); } @@ -212,7 +213,7 @@ var view = Marionette.ItemView.extend({ promise.done(function() { console.log('[SearchResultView] _addSeries promise resolve:', self.model); - SeriesCollection.add(self.model); + ArtistCollection.add(self.model); self.close(); @@ -222,7 +223,7 @@ var view = Marionette.ItemView.extend({ goToSeries : { label : 'Go to Series', action : function() { - Backbone.history.navigate('/series/' + self.model.get('titleSlug'), { trigger : true }); + Backbone.history.navigate('/artist/' + self.model.get('titleSlug'), { trigger : true }); } } }, @@ -241,6 +242,7 @@ 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'); diff --git a/src/UI/AddSeries/SearchResultViewTemplate.hbs b/src/UI/AddSeries/SearchResultViewTemplate.hbs index bccef713d..12e4f9d44 100644 --- a/src/UI/AddSeries/SearchResultViewTemplate.hbs +++ b/src/UI/AddSeries/SearchResultViewTemplate.hbs @@ -1,31 +1,26 @@
-

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

-
+
{{#unless existing}} {{#unless path}} @@ -56,7 +51,7 @@
-->
- +
{{#unless existing}} - {{#if title}} + {{#if artistName}}
@@ -82,13 +77,56 @@ - +
+
+ {{else}} +
+ +
+ {{/if}} + {{else}} + + {{/unless}} +
+
+
+
+ {{#each albums}} +
+ +
+

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

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