diff --git a/src/NzbDrone.Core/Datastore/Migration/252_add_tv_tables.cs b/src/NzbDrone.Core/Datastore/Migration/252_add_tv_tables.cs new file mode 100644 index 0000000000..f531d0b8e0 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/252_add_tv_tables.cs @@ -0,0 +1,114 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(252)] + public class add_tv_tables : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("TVShows") + .WithColumn("TvdbId").AsInt32().NotNullable() + .WithColumn("TmdbId").AsInt32().Nullable() + .WithColumn("ImdbId").AsString().Nullable() + .WithColumn("AniDbId").AsInt32().Nullable() + .WithColumn("Title").AsString().NotNullable() + .WithColumn("SortTitle").AsString().Nullable() + .WithColumn("CleanTitle").AsString().Nullable() + .WithColumn("Overview").AsString().Nullable() + .WithColumn("Network").AsString().Nullable() + .WithColumn("Status").AsInt32().NotNullable() + .WithColumn("Runtime").AsInt32().Nullable() + .WithColumn("AirTime").AsString().Nullable() + .WithColumn("Certification").AsString().Nullable() + .WithColumn("FirstAired").AsDateTime().Nullable() + .WithColumn("Year").AsInt32().NotNullable() + .WithColumn("Genres").AsString().Nullable() + .WithColumn("OriginalLanguage").AsString().Nullable() + .WithColumn("IsAnime").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("SeriesType").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("UseSceneNumbering").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("Path").AsString().Nullable() + .WithColumn("RootFolderPath").AsString().Nullable() + .WithColumn("QualityProfileId").AsInt32().NotNullable() + .WithColumn("SeasonFolder").AsBoolean().NotNullable().WithDefaultValue(true) + .WithColumn("Monitored").AsBoolean().NotNullable().WithDefaultValue(true) + .WithColumn("MonitorNewItems").AsBoolean().NotNullable().WithDefaultValue(true) + .WithColumn("Tags").AsString().Nullable() + .WithColumn("Added").AsDateTime().NotNullable() + .WithColumn("LastSearchTime").AsDateTime().Nullable(); + + Create.Index("IX_TVShows_TvdbId").OnTable("TVShows").OnColumn("TvdbId"); + Create.Index("IX_TVShows_Path").OnTable("TVShows").OnColumn("Path"); + Create.Index("IX_TVShows_Monitored").OnTable("TVShows").OnColumn("Monitored"); + + Create.TableForModel("Seasons") + .WithColumn("TVShowId").AsInt32().NotNullable() + .WithColumn("SeasonNumber").AsInt32().NotNullable() + .WithColumn("Title").AsString().Nullable() + .WithColumn("Overview").AsString().Nullable() + .WithColumn("Monitored").AsBoolean().NotNullable().WithDefaultValue(true); + + Create.Index("IX_Seasons_TVShowId").OnTable("Seasons").OnColumn("TVShowId"); + Create.Index("IX_Seasons_TVShowId_SeasonNumber").OnTable("Seasons") + .OnColumn("TVShowId").Ascending() + .OnColumn("SeasonNumber").Ascending(); + + Create.TableForModel("Episodes") + .WithColumn("TVShowId").AsInt32().NotNullable() + .WithColumn("SeasonId").AsInt32().NotNullable() + .WithColumn("SeasonNumber").AsInt32().NotNullable() + .WithColumn("EpisodeNumber").AsInt32().NotNullable() + .WithColumn("AbsoluteEpisodeNumber").AsInt32().Nullable() + .WithColumn("SceneSeasonNumber").AsInt32().Nullable() + .WithColumn("SceneEpisodeNumber").AsInt32().Nullable() + .WithColumn("SceneAbsoluteEpisodeNumber").AsInt32().Nullable() + .WithColumn("Title").AsString().Nullable() + .WithColumn("Overview").AsString().Nullable() + .WithColumn("AirDate").AsDateTime().Nullable() + .WithColumn("AirDateUtc").AsDateTime().Nullable() + .WithColumn("Runtime").AsInt32().Nullable() + .WithColumn("UnverifiedSceneNumbering").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("EpisodeFileId").AsInt32().Nullable() + .WithColumn("Monitored").AsBoolean().NotNullable().WithDefaultValue(true) + .WithColumn("QualityProfileId").AsInt32().NotNullable() + .WithColumn("Path").AsString().Nullable() + .WithColumn("RootFolderPath").AsString().Nullable() + .WithColumn("Added").AsDateTime().NotNullable() + .WithColumn("Tags").AsString().Nullable() + .WithColumn("LastSearchTime").AsDateTime().Nullable() + .WithColumn("AuthorId").AsInt32().Nullable() + .WithColumn("BookSeriesId").AsInt32().Nullable(); + + Create.Index("IX_Episodes_TVShowId").OnTable("Episodes").OnColumn("TVShowId"); + Create.Index("IX_Episodes_SeasonId").OnTable("Episodes").OnColumn("SeasonId"); + Create.Index("IX_Episodes_TVShowId_SeasonNumber_EpisodeNumber").OnTable("Episodes") + .OnColumn("TVShowId").Ascending() + .OnColumn("SeasonNumber").Ascending() + .OnColumn("EpisodeNumber").Ascending(); + Create.Index("IX_Episodes_TVShowId_AbsoluteEpisodeNumber").OnTable("Episodes") + .OnColumn("TVShowId").Ascending() + .OnColumn("AbsoluteEpisodeNumber").Ascending(); + Create.Index("IX_Episodes_Monitored").OnTable("Episodes").OnColumn("Monitored"); + + Create.TableForModel("EpisodeFiles") + .WithColumn("TVShowId").AsInt32().NotNullable() + .WithColumn("SeasonNumber").AsInt32().NotNullable() + .WithColumn("RelativePath").AsString().Nullable() + .WithColumn("Path").AsString().Nullable() + .WithColumn("Size").AsInt64().NotNullable() + .WithColumn("DateAdded").AsDateTime().NotNullable() + .WithColumn("SceneName").AsString().Nullable() + .WithColumn("ReleaseGroup").AsString().Nullable() + .WithColumn("Quality").AsString().Nullable() + .WithColumn("Language").AsString().Nullable() + .WithColumn("MediaInfo").AsString().Nullable(); + + Create.Index("IX_EpisodeFiles_TVShowId").OnTable("EpisodeFiles").OnColumn("TVShowId"); + Create.Index("IX_EpisodeFiles_TVShowId_SeasonNumber").OnTable("EpisodeFiles") + .OnColumn("TVShowId").Ascending() + .OnColumn("SeasonNumber").Ascending(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 65ce7f316b..841f245ba3 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -49,6 +49,7 @@ using NzbDrone.Core.RootFolders; using NzbDrone.Core.Tags; using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.TV; using NzbDrone.Core.Update.History; using static Dapper.SqlMapper; @@ -163,6 +164,15 @@ public static void Map() Mapper.Entity("MusicFiles").RegisterModel(); + Mapper.Entity("TVShows").RegisterModel(); + + Mapper.Entity("Seasons").RegisterModel(); + + Mapper.Entity("Episodes").RegisterModel(); + + Mapper.Entity("EpisodeFiles").RegisterModel() + .Ignore(f => f.Path); + Mapper.Entity("QualityDefinitions").RegisterModel() .Ignore(d => d.GroupName) .Ignore(d => d.Weight); diff --git a/src/NzbDrone.Core/TV/Episode.cs b/src/NzbDrone.Core/TV/Episode.cs new file mode 100644 index 0000000000..ee69138554 --- /dev/null +++ b/src/NzbDrone.Core/TV/Episode.cs @@ -0,0 +1,50 @@ +using System; +using NzbDrone.Core.MediaItems; +using NzbDrone.Core.MediaTypes; + +namespace NzbDrone.Core.TV +{ + public class Episode : MediaItem + { + public Episode() + { + MediaType = MediaType.TV; + } + + public int TVShowId { get; set; } + public int SeasonId { get; set; } + + public int SeasonNumber { get; set; } + public int EpisodeNumber { get; set; } + public int? AbsoluteEpisodeNumber { get; set; } + + public int? SceneSeasonNumber { get; set; } + public int? SceneEpisodeNumber { get; set; } + public int? SceneAbsoluteEpisodeNumber { get; set; } + + public string Title { get; set; } + public string Overview { get; set; } + + public DateTime? AirDate { get; set; } + public DateTime? AirDateUtc { get; set; } + public int? Runtime { get; set; } + + public bool IsSpecial => SeasonNumber == 0; + public bool UnverifiedSceneNumbering { get; set; } + + public int? EpisodeFileId { get; set; } + + public override string GetTitle() => Title; + public override int GetYear() => AirDate?.Year ?? 0; + + public override string ToString() + { + if (AbsoluteEpisodeNumber.HasValue && SeasonNumber > 0) + { + return $"{Title} - S{SeasonNumber:00}E{EpisodeNumber:00} ({AbsoluteEpisodeNumber})"; + } + + return $"{Title} - S{SeasonNumber:00}E{EpisodeNumber:00}"; + } + } +} diff --git a/src/NzbDrone.Core/TV/EpisodeFile.cs b/src/NzbDrone.Core/TV/EpisodeFile.cs new file mode 100644 index 0000000000..c3ed5e39ef --- /dev/null +++ b/src/NzbDrone.Core/TV/EpisodeFile.cs @@ -0,0 +1,31 @@ +using System; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.TV +{ + public class EpisodeFile : ModelBase + { + public int TVShowId { get; set; } + public int SeasonNumber { 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 Language Language { get; set; } + + public string MediaInfo { get; set; } + + public override string ToString() + { + return Path; + } + } +} diff --git a/src/NzbDrone.Core/TV/EpisodeFileRepository.cs b/src/NzbDrone.Core/TV/EpisodeFileRepository.cs new file mode 100644 index 0000000000..f307db64cf --- /dev/null +++ b/src/NzbDrone.Core/TV/EpisodeFileRepository.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.TV +{ + public interface IEpisodeFileRepository : IBasicRepository + { + List FindByTVShowId(int tvShowId); + List FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber); + EpisodeFile FindByPath(string path); + } + + public class EpisodeFileRepository : BasicRepository, IEpisodeFileRepository + { + public EpisodeFileRepository(IMainDatabase database, + IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public List FindByTVShowId(int tvShowId) + { + return Query(f => f.TVShowId == tvShowId); + } + + public List FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber) + { + return Query(f => f.TVShowId == tvShowId && f.SeasonNumber == seasonNumber); + } + + public EpisodeFile FindByPath(string path) + { + return Query(f => f.Path == path).FirstOrDefault(); + } + } +} diff --git a/src/NzbDrone.Core/TV/EpisodeRepository.cs b/src/NzbDrone.Core/TV/EpisodeRepository.cs new file mode 100644 index 0000000000..159268943b --- /dev/null +++ b/src/NzbDrone.Core/TV/EpisodeRepository.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.TV +{ + public interface IEpisodeRepository : IBasicRepository + { + List FindByTVShowId(int tvShowId); + List FindBySeasonId(int seasonId); + List FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber); + Episode FindByTVShowIdAndEpisode(int tvShowId, int seasonNumber, int episodeNumber); + Episode FindByTVShowIdAndAbsoluteNumber(int tvShowId, int absoluteNumber); + List EpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); + Episode FindByPath(string path); + Dictionary AllEpisodePaths(); + } + + public class EpisodeRepository : BasicRepository, IEpisodeRepository + { + public EpisodeRepository(IMainDatabase database, + IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public List FindByTVShowId(int tvShowId) + { + return Query(e => e.TVShowId == tvShowId); + } + + public List FindBySeasonId(int seasonId) + { + return Query(e => e.SeasonId == seasonId); + } + + public List FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber) + { + return Query(e => e.TVShowId == tvShowId && e.SeasonNumber == seasonNumber); + } + + public Episode FindByTVShowIdAndEpisode(int tvShowId, int seasonNumber, int episodeNumber) + { + return Query(e => e.TVShowId == tvShowId && + e.SeasonNumber == seasonNumber && + e.EpisodeNumber == episodeNumber).FirstOrDefault(); + } + + public Episode FindByTVShowIdAndAbsoluteNumber(int tvShowId, int absoluteNumber) + { + return Query(e => e.TVShowId == tvShowId && + e.AbsoluteEpisodeNumber == absoluteNumber).FirstOrDefault(); + } + + public List EpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored) + { + var query = Query(e => e.AirDateUtc >= start && e.AirDateUtc <= end); + + if (!includeUnmonitored) + { + query = query.Where(e => e.Monitored).ToList(); + } + + return query; + } + + public Episode FindByPath(string path) + { + return Query(e => e.Path == path).FirstOrDefault(); + } + + public Dictionary AllEpisodePaths() + { + var episodes = All(); + return episodes.ToDictionary(e => e.Id, e => e.Path); + } + } +} diff --git a/src/NzbDrone.Core/TV/EpisodeService.cs b/src/NzbDrone.Core/TV/EpisodeService.cs new file mode 100644 index 0000000000..65fff75b3c --- /dev/null +++ b/src/NzbDrone.Core/TV/EpisodeService.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.TV.Events; + +namespace NzbDrone.Core.TV +{ + public interface IEpisodeService + { + Episode GetEpisode(int episodeId); + List GetEpisodes(IEnumerable episodeIds); + List GetEpisodesByTVShowId(int tvShowId); + List GetEpisodesBySeasonId(int seasonId); + List GetEpisodesByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber); + Episode FindByTVShowIdAndEpisode(int tvShowId, int seasonNumber, int episodeNumber); + Episode FindByTVShowIdAndAbsoluteNumber(int tvShowId, int absoluteNumber); + Episode AddEpisode(Episode newEpisode); + List AddEpisodes(List newEpisodes); + void DeleteEpisode(int episodeId, bool deleteFiles); + void DeleteEpisodes(List episodeIds, bool deleteFiles); + Episode UpdateEpisode(Episode episode); + List UpdateEpisodes(List episodes); + List GetEpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); + Episode FindByPath(string path); + Dictionary AllEpisodePaths(); + } + + public class EpisodeService : IEpisodeService + { + private readonly IEpisodeRepository _episodeRepository; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + public EpisodeService(IEpisodeRepository episodeRepository, + IEventAggregator eventAggregator, + Logger logger) + { + _episodeRepository = episodeRepository; + _eventAggregator = eventAggregator; + _logger = logger; + } + + public Episode GetEpisode(int episodeId) => _episodeRepository.Get(episodeId); + public List GetEpisodes(IEnumerable episodeIds) => _episodeRepository.Get(episodeIds).ToList(); + public List GetEpisodesByTVShowId(int tvShowId) => _episodeRepository.FindByTVShowId(tvShowId); + public List GetEpisodesBySeasonId(int seasonId) => _episodeRepository.FindBySeasonId(seasonId); + public List GetEpisodesByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber) + => _episodeRepository.FindByTVShowIdAndSeasonNumber(tvShowId, seasonNumber); + public Episode FindByTVShowIdAndEpisode(int tvShowId, int seasonNumber, int episodeNumber) + => _episodeRepository.FindByTVShowIdAndEpisode(tvShowId, seasonNumber, episodeNumber); + public Episode FindByTVShowIdAndAbsoluteNumber(int tvShowId, int absoluteNumber) + => _episodeRepository.FindByTVShowIdAndAbsoluteNumber(tvShowId, absoluteNumber); + public Episode FindByPath(string path) => _episodeRepository.FindByPath(path); + public Dictionary AllEpisodePaths() => _episodeRepository.AllEpisodePaths(); + public List GetEpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored) + => _episodeRepository.EpisodesBetweenDates(start, end, includeUnmonitored); + + public Episode AddEpisode(Episode newEpisode) + { + var episode = _episodeRepository.Insert(newEpisode); + _eventAggregator.PublishEvent(new EpisodeAddedEvent(episode)); + return episode; + } + + public List AddEpisodes(List newEpisodes) + { + _episodeRepository.InsertMany(newEpisodes); + + foreach (var episode in newEpisodes) + { + _eventAggregator.PublishEvent(new EpisodeAddedEvent(episode)); + } + + return newEpisodes; + } + + public void DeleteEpisode(int episodeId, bool deleteFiles) + { + var episode = _episodeRepository.Get(episodeId); + _episodeRepository.Delete(episodeId); + _eventAggregator.PublishEvent(new EpisodeDeletedEvent(episode, deleteFiles)); + } + + public void DeleteEpisodes(List episodeIds, bool deleteFiles) + { + var episodes = _episodeRepository.Get(episodeIds).ToList(); + _episodeRepository.DeleteMany(episodeIds); + + foreach (var episode in episodes) + { + _eventAggregator.PublishEvent(new EpisodeDeletedEvent(episode, deleteFiles)); + } + } + + public Episode UpdateEpisode(Episode episode) + { + var storedEpisode = _episodeRepository.Get(episode.Id); + _episodeRepository.Update(episode); + _eventAggregator.PublishEvent(new EpisodeEditedEvent(episode, storedEpisode)); + return episode; + } + + public List UpdateEpisodes(List episodes) + { + _episodeRepository.UpdateMany(episodes); + _eventAggregator.PublishEvent(new EpisodesBulkEditedEvent(episodes)); + return episodes; + } + } +} diff --git a/src/NzbDrone.Core/TV/Events/EpisodeAddedEvent.cs b/src/NzbDrone.Core/TV/Events/EpisodeAddedEvent.cs new file mode 100644 index 0000000000..c3c0bae706 --- /dev/null +++ b/src/NzbDrone.Core/TV/Events/EpisodeAddedEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.TV.Events +{ + public class EpisodeAddedEvent : IEvent + { + public Episode Episode { get; private set; } + + public EpisodeAddedEvent(Episode episode) + { + Episode = episode; + } + } +} diff --git a/src/NzbDrone.Core/TV/Events/EpisodeDeletedEvent.cs b/src/NzbDrone.Core/TV/Events/EpisodeDeletedEvent.cs new file mode 100644 index 0000000000..cc4418793e --- /dev/null +++ b/src/NzbDrone.Core/TV/Events/EpisodeDeletedEvent.cs @@ -0,0 +1,16 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.TV.Events +{ + public class EpisodeDeletedEvent : IEvent + { + public Episode Episode { get; private set; } + public bool DeleteFiles { get; private set; } + + public EpisodeDeletedEvent(Episode episode, bool deleteFiles) + { + Episode = episode; + DeleteFiles = deleteFiles; + } + } +} diff --git a/src/NzbDrone.Core/TV/Events/EpisodeEditedEvent.cs b/src/NzbDrone.Core/TV/Events/EpisodeEditedEvent.cs new file mode 100644 index 0000000000..89b90174c6 --- /dev/null +++ b/src/NzbDrone.Core/TV/Events/EpisodeEditedEvent.cs @@ -0,0 +1,16 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.TV.Events +{ + public class EpisodeEditedEvent : IEvent + { + public Episode Episode { get; private set; } + public Episode OldEpisode { get; private set; } + + public EpisodeEditedEvent(Episode episode, Episode oldEpisode) + { + Episode = episode; + OldEpisode = oldEpisode; + } + } +} diff --git a/src/NzbDrone.Core/TV/Events/EpisodesBulkEditedEvent.cs b/src/NzbDrone.Core/TV/Events/EpisodesBulkEditedEvent.cs new file mode 100644 index 0000000000..0cfb96b73d --- /dev/null +++ b/src/NzbDrone.Core/TV/Events/EpisodesBulkEditedEvent.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.TV.Events +{ + public class EpisodesBulkEditedEvent : IEvent + { + public List Episodes { get; private set; } + + public EpisodesBulkEditedEvent(List episodes) + { + Episodes = episodes; + } + } +} diff --git a/src/NzbDrone.Core/TV/Events/TVShowAddedEvent.cs b/src/NzbDrone.Core/TV/Events/TVShowAddedEvent.cs new file mode 100644 index 0000000000..8689539fc5 --- /dev/null +++ b/src/NzbDrone.Core/TV/Events/TVShowAddedEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.TV.Events +{ + public class TVShowAddedEvent : IEvent + { + public TVShow TVShow { get; private set; } + + public TVShowAddedEvent(TVShow tvShow) + { + TVShow = tvShow; + } + } +} diff --git a/src/NzbDrone.Core/TV/Events/TVShowDeletedEvent.cs b/src/NzbDrone.Core/TV/Events/TVShowDeletedEvent.cs new file mode 100644 index 0000000000..d36be067ef --- /dev/null +++ b/src/NzbDrone.Core/TV/Events/TVShowDeletedEvent.cs @@ -0,0 +1,16 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.TV.Events +{ + public class TVShowDeletedEvent : IEvent + { + public TVShow TVShow { get; private set; } + public bool DeleteFiles { get; private set; } + + public TVShowDeletedEvent(TVShow tvShow, bool deleteFiles) + { + TVShow = tvShow; + DeleteFiles = deleteFiles; + } + } +} diff --git a/src/NzbDrone.Core/TV/Events/TVShowEditedEvent.cs b/src/NzbDrone.Core/TV/Events/TVShowEditedEvent.cs new file mode 100644 index 0000000000..55e57f7188 --- /dev/null +++ b/src/NzbDrone.Core/TV/Events/TVShowEditedEvent.cs @@ -0,0 +1,16 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.TV.Events +{ + public class TVShowEditedEvent : IEvent + { + public TVShow TVShow { get; private set; } + public TVShow OldTVShow { get; private set; } + + public TVShowEditedEvent(TVShow tvShow, TVShow oldTVShow) + { + TVShow = tvShow; + OldTVShow = oldTVShow; + } + } +} diff --git a/src/NzbDrone.Core/TV/Events/TVShowsBulkEditedEvent.cs b/src/NzbDrone.Core/TV/Events/TVShowsBulkEditedEvent.cs new file mode 100644 index 0000000000..50dab68ac6 --- /dev/null +++ b/src/NzbDrone.Core/TV/Events/TVShowsBulkEditedEvent.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.TV.Events +{ + public class TVShowsBulkEditedEvent : IEvent + { + public List TVShows { get; private set; } + + public TVShowsBulkEditedEvent(List tvShows) + { + TVShows = tvShows; + } + } +} diff --git a/src/NzbDrone.Core/TV/Season.cs b/src/NzbDrone.Core/TV/Season.cs new file mode 100644 index 0000000000..2c8ed63417 --- /dev/null +++ b/src/NzbDrone.Core/TV/Season.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.TV +{ + public class Season : ModelBase + { + public int TVShowId { get; set; } + public int SeasonNumber { get; set; } + + public string Title { get; set; } + public string Overview { get; set; } + + public bool Monitored { get; set; } + + public override string ToString() + { + return SeasonNumber == 0 ? "Specials" : $"Season {SeasonNumber}"; + } + } +} diff --git a/src/NzbDrone.Core/TV/SeasonRepository.cs b/src/NzbDrone.Core/TV/SeasonRepository.cs new file mode 100644 index 0000000000..dfca814574 --- /dev/null +++ b/src/NzbDrone.Core/TV/SeasonRepository.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.TV +{ + public interface ISeasonRepository : IBasicRepository + { + List FindByTVShowId(int tvShowId); + Season FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber); + List GetMonitored(); + } + + public class SeasonRepository : BasicRepository, ISeasonRepository + { + public SeasonRepository(IMainDatabase database, + IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public List FindByTVShowId(int tvShowId) + { + return Query(s => s.TVShowId == tvShowId); + } + + public Season FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber) + { + return Query(s => s.TVShowId == tvShowId && s.SeasonNumber == seasonNumber).FirstOrDefault(); + } + + public List GetMonitored() + { + return Query(s => s.Monitored); + } + } +} diff --git a/src/NzbDrone.Core/TV/SeasonService.cs b/src/NzbDrone.Core/TV/SeasonService.cs new file mode 100644 index 0000000000..cc7cefbe3f --- /dev/null +++ b/src/NzbDrone.Core/TV/SeasonService.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.TV +{ + public interface ISeasonService + { + Season GetSeason(int seasonId); + List GetSeasons(IEnumerable seasonIds); + List GetSeasonsByTVShowId(int tvShowId); + Season FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber); + Season AddSeason(Season newSeason); + List AddSeasons(List newSeasons); + void DeleteSeason(int seasonId); + void DeleteSeasons(List seasonIds); + Season UpdateSeason(Season season); + List UpdateSeasons(List seasons); + List GetMonitored(); + } + + public class SeasonService : ISeasonService + { + private readonly ISeasonRepository _seasonRepository; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + public SeasonService(ISeasonRepository seasonRepository, + IEventAggregator eventAggregator, + Logger logger) + { + _seasonRepository = seasonRepository; + _eventAggregator = eventAggregator; + _logger = logger; + } + + public Season GetSeason(int seasonId) => _seasonRepository.Get(seasonId); + public List GetSeasons(IEnumerable seasonIds) => _seasonRepository.Get(seasonIds).ToList(); + public List GetSeasonsByTVShowId(int tvShowId) => _seasonRepository.FindByTVShowId(tvShowId); + public Season FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber) + => _seasonRepository.FindByTVShowIdAndSeasonNumber(tvShowId, seasonNumber); + public List GetMonitored() => _seasonRepository.GetMonitored(); + + public Season AddSeason(Season newSeason) + { + return _seasonRepository.Insert(newSeason); + } + + public List AddSeasons(List newSeasons) + { + _seasonRepository.InsertMany(newSeasons); + return newSeasons; + } + + public void DeleteSeason(int seasonId) + { + _seasonRepository.Delete(seasonId); + } + + public void DeleteSeasons(List seasonIds) + { + _seasonRepository.DeleteMany(seasonIds); + } + + public Season UpdateSeason(Season season) + { + _seasonRepository.Update(season); + return season; + } + + public List UpdateSeasons(List seasons) + { + _seasonRepository.UpdateMany(seasons); + return seasons; + } + } +} diff --git a/src/NzbDrone.Core/TV/SeriesType.cs b/src/NzbDrone.Core/TV/SeriesType.cs new file mode 100644 index 0000000000..becb725829 --- /dev/null +++ b/src/NzbDrone.Core/TV/SeriesType.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.TV +{ + public enum SeriesType + { + Standard = 0, + Daily = 1, + Anime = 2 + } +} diff --git a/src/NzbDrone.Core/TV/TVShow.cs b/src/NzbDrone.Core/TV/TVShow.cs new file mode 100644 index 0000000000..d803d8c33a --- /dev/null +++ b/src/NzbDrone.Core/TV/TVShow.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.TV +{ + public class TVShow : ModelBase + { + public TVShow() + { + Tags = new HashSet(); + Genres = new List(); + } + + public int TvdbId { get; set; } + public int? TmdbId { get; set; } + public string ImdbId { get; set; } + public int? AniDbId { get; set; } + + public string Title { get; set; } + public string SortTitle { get; set; } + public string CleanTitle { get; set; } + public string Overview { get; set; } + public string Network { get; set; } + public TVShowStatus Status { get; set; } + + public int? Runtime { get; set; } + public string AirTime { get; set; } + public string Certification { get; set; } + public DateTime? FirstAired { get; set; } + public int Year { get; set; } + + public List Genres { get; set; } + public string OriginalLanguage { get; set; } + + public bool IsAnime { get; set; } + public SeriesType SeriesType { get; set; } + public bool UseSceneNumbering { get; set; } + + public string Path { get; set; } + public string RootFolderPath { get; set; } + public int QualityProfileId { get; set; } + public bool SeasonFolder { get; set; } + + public bool Monitored { get; set; } + public bool MonitorNewItems { get; set; } + public HashSet Tags { get; set; } + public DateTime Added { get; set; } + public DateTime? LastSearchTime { get; set; } + + public override string ToString() + { + return $"{Title} ({Year})"; + } + } +} diff --git a/src/NzbDrone.Core/TV/TVShowRepository.cs b/src/NzbDrone.Core/TV/TVShowRepository.cs new file mode 100644 index 0000000000..56d25ad8db --- /dev/null +++ b/src/NzbDrone.Core/TV/TVShowRepository.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.TV +{ + public interface ITVShowRepository : IBasicRepository + { + bool TVShowPathExists(string path); + TVShow FindByTvdbId(int tvdbId); + TVShow FindByImdbId(string imdbId); + TVShow FindByAniDbId(int aniDbId); + TVShow FindByTitle(string title); + TVShow FindByPath(string path); + List GetMonitored(); + Dictionary AllTVShowPaths(); + Dictionary> AllTVShowTags(); + } + + public class TVShowRepository : BasicRepository, ITVShowRepository + { + public TVShowRepository(IMainDatabase database, + IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public bool TVShowPathExists(string path) + { + return Query(s => s.Path == path).Any(); + } + + public TVShow FindByTvdbId(int tvdbId) + { + return Query(s => s.TvdbId == tvdbId).FirstOrDefault(); + } + + public TVShow FindByImdbId(string imdbId) + { + return Query(s => s.ImdbId == imdbId).FirstOrDefault(); + } + + public TVShow FindByAniDbId(int aniDbId) + { + return Query(s => s.AniDbId == aniDbId).FirstOrDefault(); + } + + public TVShow FindByTitle(string title) + { + return Query(s => s.Title == title).FirstOrDefault(); + } + + public TVShow FindByPath(string path) + { + return Query(s => s.Path == path).FirstOrDefault(); + } + + public List GetMonitored() + { + return Query(s => s.Monitored); + } + + public Dictionary AllTVShowPaths() + { + var tvShows = All(); + return tvShows.ToDictionary(s => s.Id, s => s.Path); + } + + public Dictionary> AllTVShowTags() + { + var tvShows = All(); + return tvShows.ToDictionary(s => s.Id, s => s.Tags.ToList()); + } + } +} diff --git a/src/NzbDrone.Core/TV/TVShowService.cs b/src/NzbDrone.Core/TV/TVShowService.cs new file mode 100644 index 0000000000..2c615d0cca --- /dev/null +++ b/src/NzbDrone.Core/TV/TVShowService.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.TV.Events; + +namespace NzbDrone.Core.TV +{ + public interface ITVShowService + { + TVShow GetTVShow(int tvShowId); + List GetTVShows(IEnumerable tvShowIds); + TVShow AddTVShow(TVShow newTVShow); + List AddTVShows(List newTVShows); + TVShow FindByTvdbId(int tvdbId); + TVShow FindByImdbId(string imdbId); + TVShow FindByAniDbId(int aniDbId); + TVShow FindByTitle(string title); + TVShow FindByPath(string path); + List GetMonitored(); + Dictionary AllTVShowPaths(); + void DeleteTVShow(int tvShowId, bool deleteFiles); + void DeleteTVShows(List tvShowIds, bool deleteFiles); + List GetAllTVShows(); + Dictionary> AllTVShowTags(); + TVShow UpdateTVShow(TVShow tvShow); + List UpdateTVShows(List tvShows); + bool TVShowPathExists(string folder); + } + + public class TVShowService : ITVShowService + { + private readonly ITVShowRepository _tvShowRepository; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + public TVShowService(ITVShowRepository tvShowRepository, + IEventAggregator eventAggregator, + Logger logger) + { + _tvShowRepository = tvShowRepository; + _eventAggregator = eventAggregator; + _logger = logger; + } + + public TVShow GetTVShow(int tvShowId) => _tvShowRepository.Get(tvShowId); + public List GetTVShows(IEnumerable tvShowIds) => _tvShowRepository.Get(tvShowIds).ToList(); + + public TVShow AddTVShow(TVShow newTVShow) + { + var tvShow = _tvShowRepository.Insert(newTVShow); + _eventAggregator.PublishEvent(new TVShowAddedEvent(tvShow)); + return tvShow; + } + + public List AddTVShows(List newTVShows) + { + _tvShowRepository.InsertMany(newTVShows); + + foreach (var tvShow in newTVShows) + { + _eventAggregator.PublishEvent(new TVShowAddedEvent(tvShow)); + } + + return newTVShows; + } + + public TVShow FindByTvdbId(int tvdbId) => _tvShowRepository.FindByTvdbId(tvdbId); + public TVShow FindByImdbId(string imdbId) => _tvShowRepository.FindByImdbId(imdbId); + public TVShow FindByAniDbId(int aniDbId) => _tvShowRepository.FindByAniDbId(aniDbId); + public TVShow FindByTitle(string title) => _tvShowRepository.FindByTitle(title); + public TVShow FindByPath(string path) => _tvShowRepository.FindByPath(path); + public List GetMonitored() => _tvShowRepository.GetMonitored(); + public Dictionary AllTVShowPaths() => _tvShowRepository.AllTVShowPaths(); + public Dictionary> AllTVShowTags() => _tvShowRepository.AllTVShowTags(); + public bool TVShowPathExists(string folder) => _tvShowRepository.TVShowPathExists(folder); + public List GetAllTVShows() => _tvShowRepository.All().ToList(); + + public void DeleteTVShow(int tvShowId, bool deleteFiles) + { + var tvShow = _tvShowRepository.Get(tvShowId); + _tvShowRepository.Delete(tvShowId); + _eventAggregator.PublishEvent(new TVShowDeletedEvent(tvShow, deleteFiles)); + } + + public void DeleteTVShows(List tvShowIds, bool deleteFiles) + { + var tvShows = _tvShowRepository.Get(tvShowIds).ToList(); + _tvShowRepository.DeleteMany(tvShowIds); + + foreach (var tvShow in tvShows) + { + _eventAggregator.PublishEvent(new TVShowDeletedEvent(tvShow, deleteFiles)); + } + } + + public TVShow UpdateTVShow(TVShow tvShow) + { + var storedTVShow = _tvShowRepository.Get(tvShow.Id); + _tvShowRepository.Update(tvShow); + _eventAggregator.PublishEvent(new TVShowEditedEvent(tvShow, storedTVShow)); + return tvShow; + } + + public List UpdateTVShows(List tvShows) + { + _tvShowRepository.UpdateMany(tvShows); + _eventAggregator.PublishEvent(new TVShowsBulkEditedEvent(tvShows)); + return tvShows; + } + } +} diff --git a/src/NzbDrone.Core/TV/TVShowStatus.cs b/src/NzbDrone.Core/TV/TVShowStatus.cs new file mode 100644 index 0000000000..1d4c625c6f --- /dev/null +++ b/src/NzbDrone.Core/TV/TVShowStatus.cs @@ -0,0 +1,10 @@ +namespace NzbDrone.Core.TV +{ + public enum TVShowStatus + { + Continuing = 0, + Ended = 1, + Upcoming = 2, + Canceled = 3 + } +} diff --git a/src/Radarr.Api.V3/BookSeries/BookSeriesController.cs b/src/Radarr.Api.V3/BookSeries/BookSeriesController.cs index 0501a2d590..fcef7dc170 100644 --- a/src/Radarr.Api.V3/BookSeries/BookSeriesController.cs +++ b/src/Radarr.Api.V3/BookSeries/BookSeriesController.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using FluentValidation; using Microsoft.AspNetCore.Mvc; -using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.BookSeries; +using NzbDrone.Core.Datastore.Events; using NzbDrone.SignalR; using Radarr.Http; using Radarr.Http.REST;