feat(tv): add core TV entities (TVShow, Season, Episode)

Phase 5 Part 2: Creates foundational TV infrastructure with anime
support from day 1.

Entities:
- TVShow: Parent entity with TvdbId, TmdbId, ImdbId, AniDbId
- Season: Grouping layer with TVShowId foreign key
- Episode: Extends MediaItem with scene/absolute numbering
- EpisodeFile: Media file tracking

Features:
- SeriesType enum (Standard, Daily, Anime)
- TVShowStatus enum (Continuing, Ended, Upcoming, Canceled)
- Complete repository/service pattern
- Full event system for CRUD operations
- Migration 252 creates all tables with indexes

Also fixes SA1210 using directive order in BookSeriesController.
This commit is contained in:
admin 2025-12-29 15:04:08 -06:00
parent 398a4b4bdd
commit 230a01cced
24 changed files with 957 additions and 1 deletions

View file

@ -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();
}
}
}

View file

@ -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<MusicFile>("MusicFiles").RegisterModel();
Mapper.Entity<TVShow>("TVShows").RegisterModel();
Mapper.Entity<Season>("Seasons").RegisterModel();
Mapper.Entity<Episode>("Episodes").RegisterModel();
Mapper.Entity<EpisodeFile>("EpisodeFiles").RegisterModel()
.Ignore(f => f.Path);
Mapper.Entity<QualityDefinition>("QualityDefinitions").RegisterModel()
.Ignore(d => d.GroupName)
.Ignore(d => d.Weight);

View file

@ -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}";
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<EpisodeFile>
{
List<EpisodeFile> FindByTVShowId(int tvShowId);
List<EpisodeFile> FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber);
EpisodeFile FindByPath(string path);
}
public class EpisodeFileRepository : BasicRepository<EpisodeFile>, IEpisodeFileRepository
{
public EpisodeFileRepository(IMainDatabase database,
IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
public List<EpisodeFile> FindByTVShowId(int tvShowId)
{
return Query(f => f.TVShowId == tvShowId);
}
public List<EpisodeFile> 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();
}
}
}

View file

@ -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<Episode>
{
List<Episode> FindByTVShowId(int tvShowId);
List<Episode> FindBySeasonId(int seasonId);
List<Episode> FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber);
Episode FindByTVShowIdAndEpisode(int tvShowId, int seasonNumber, int episodeNumber);
Episode FindByTVShowIdAndAbsoluteNumber(int tvShowId, int absoluteNumber);
List<Episode> EpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored);
Episode FindByPath(string path);
Dictionary<int, string> AllEpisodePaths();
}
public class EpisodeRepository : BasicRepository<Episode>, IEpisodeRepository
{
public EpisodeRepository(IMainDatabase database,
IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
public List<Episode> FindByTVShowId(int tvShowId)
{
return Query(e => e.TVShowId == tvShowId);
}
public List<Episode> FindBySeasonId(int seasonId)
{
return Query(e => e.SeasonId == seasonId);
}
public List<Episode> 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<Episode> 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<int, string> AllEpisodePaths()
{
var episodes = All();
return episodes.ToDictionary(e => e.Id, e => e.Path);
}
}
}

View file

@ -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<Episode> GetEpisodes(IEnumerable<int> episodeIds);
List<Episode> GetEpisodesByTVShowId(int tvShowId);
List<Episode> GetEpisodesBySeasonId(int seasonId);
List<Episode> GetEpisodesByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber);
Episode FindByTVShowIdAndEpisode(int tvShowId, int seasonNumber, int episodeNumber);
Episode FindByTVShowIdAndAbsoluteNumber(int tvShowId, int absoluteNumber);
Episode AddEpisode(Episode newEpisode);
List<Episode> AddEpisodes(List<Episode> newEpisodes);
void DeleteEpisode(int episodeId, bool deleteFiles);
void DeleteEpisodes(List<int> episodeIds, bool deleteFiles);
Episode UpdateEpisode(Episode episode);
List<Episode> UpdateEpisodes(List<Episode> episodes);
List<Episode> GetEpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored);
Episode FindByPath(string path);
Dictionary<int, string> 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<Episode> GetEpisodes(IEnumerable<int> episodeIds) => _episodeRepository.Get(episodeIds).ToList();
public List<Episode> GetEpisodesByTVShowId(int tvShowId) => _episodeRepository.FindByTVShowId(tvShowId);
public List<Episode> GetEpisodesBySeasonId(int seasonId) => _episodeRepository.FindBySeasonId(seasonId);
public List<Episode> 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<int, string> AllEpisodePaths() => _episodeRepository.AllEpisodePaths();
public List<Episode> 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<Episode> AddEpisodes(List<Episode> 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<int> 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<Episode> UpdateEpisodes(List<Episode> episodes)
{
_episodeRepository.UpdateMany(episodes);
_eventAggregator.PublishEvent(new EpisodesBulkEditedEvent(episodes));
return episodes;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,15 @@
using System.Collections.Generic;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.TV.Events
{
public class EpisodesBulkEditedEvent : IEvent
{
public List<Episode> Episodes { get; private set; }
public EpisodesBulkEditedEvent(List<Episode> episodes)
{
Episodes = episodes;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,15 @@
using System.Collections.Generic;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.TV.Events
{
public class TVShowsBulkEditedEvent : IEvent
{
public List<TVShow> TVShows { get; private set; }
public TVShowsBulkEditedEvent(List<TVShow> tvShows)
{
TVShows = tvShows;
}
}
}

View file

@ -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}";
}
}
}

View file

@ -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<Season>
{
List<Season> FindByTVShowId(int tvShowId);
Season FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber);
List<Season> GetMonitored();
}
public class SeasonRepository : BasicRepository<Season>, ISeasonRepository
{
public SeasonRepository(IMainDatabase database,
IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
public List<Season> 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<Season> GetMonitored()
{
return Query(s => s.Monitored);
}
}
}

View file

@ -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<Season> GetSeasons(IEnumerable<int> seasonIds);
List<Season> GetSeasonsByTVShowId(int tvShowId);
Season FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber);
Season AddSeason(Season newSeason);
List<Season> AddSeasons(List<Season> newSeasons);
void DeleteSeason(int seasonId);
void DeleteSeasons(List<int> seasonIds);
Season UpdateSeason(Season season);
List<Season> UpdateSeasons(List<Season> seasons);
List<Season> 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<Season> GetSeasons(IEnumerable<int> seasonIds) => _seasonRepository.Get(seasonIds).ToList();
public List<Season> GetSeasonsByTVShowId(int tvShowId) => _seasonRepository.FindByTVShowId(tvShowId);
public Season FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber)
=> _seasonRepository.FindByTVShowIdAndSeasonNumber(tvShowId, seasonNumber);
public List<Season> GetMonitored() => _seasonRepository.GetMonitored();
public Season AddSeason(Season newSeason)
{
return _seasonRepository.Insert(newSeason);
}
public List<Season> AddSeasons(List<Season> newSeasons)
{
_seasonRepository.InsertMany(newSeasons);
return newSeasons;
}
public void DeleteSeason(int seasonId)
{
_seasonRepository.Delete(seasonId);
}
public void DeleteSeasons(List<int> seasonIds)
{
_seasonRepository.DeleteMany(seasonIds);
}
public Season UpdateSeason(Season season)
{
_seasonRepository.Update(season);
return season;
}
public List<Season> UpdateSeasons(List<Season> seasons)
{
_seasonRepository.UpdateMany(seasons);
return seasons;
}
}
}

View file

@ -0,0 +1,9 @@
namespace NzbDrone.Core.TV
{
public enum SeriesType
{
Standard = 0,
Daily = 1,
Anime = 2
}
}

View file

@ -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<int>();
Genres = new List<string>();
}
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<string> 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<int> Tags { get; set; }
public DateTime Added { get; set; }
public DateTime? LastSearchTime { get; set; }
public override string ToString()
{
return $"{Title} ({Year})";
}
}
}

View file

@ -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<TVShow>
{
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<TVShow> GetMonitored();
Dictionary<int, string> AllTVShowPaths();
Dictionary<int, List<int>> AllTVShowTags();
}
public class TVShowRepository : BasicRepository<TVShow>, 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<TVShow> GetMonitored()
{
return Query(s => s.Monitored);
}
public Dictionary<int, string> AllTVShowPaths()
{
var tvShows = All();
return tvShows.ToDictionary(s => s.Id, s => s.Path);
}
public Dictionary<int, List<int>> AllTVShowTags()
{
var tvShows = All();
return tvShows.ToDictionary(s => s.Id, s => s.Tags.ToList());
}
}
}

View file

@ -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<TVShow> GetTVShows(IEnumerable<int> tvShowIds);
TVShow AddTVShow(TVShow newTVShow);
List<TVShow> AddTVShows(List<TVShow> newTVShows);
TVShow FindByTvdbId(int tvdbId);
TVShow FindByImdbId(string imdbId);
TVShow FindByAniDbId(int aniDbId);
TVShow FindByTitle(string title);
TVShow FindByPath(string path);
List<TVShow> GetMonitored();
Dictionary<int, string> AllTVShowPaths();
void DeleteTVShow(int tvShowId, bool deleteFiles);
void DeleteTVShows(List<int> tvShowIds, bool deleteFiles);
List<TVShow> GetAllTVShows();
Dictionary<int, List<int>> AllTVShowTags();
TVShow UpdateTVShow(TVShow tvShow);
List<TVShow> UpdateTVShows(List<TVShow> 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<TVShow> GetTVShows(IEnumerable<int> tvShowIds) => _tvShowRepository.Get(tvShowIds).ToList();
public TVShow AddTVShow(TVShow newTVShow)
{
var tvShow = _tvShowRepository.Insert(newTVShow);
_eventAggregator.PublishEvent(new TVShowAddedEvent(tvShow));
return tvShow;
}
public List<TVShow> AddTVShows(List<TVShow> 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<TVShow> GetMonitored() => _tvShowRepository.GetMonitored();
public Dictionary<int, string> AllTVShowPaths() => _tvShowRepository.AllTVShowPaths();
public Dictionary<int, List<int>> AllTVShowTags() => _tvShowRepository.AllTVShowTags();
public bool TVShowPathExists(string folder) => _tvShowRepository.TVShowPathExists(folder);
public List<TVShow> 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<int> 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<TVShow> UpdateTVShows(List<TVShow> tvShows)
{
_tvShowRepository.UpdateMany(tvShows);
_eventAggregator.PublishEvent(new TVShowsBulkEditedEvent(tvShows));
return tvShows;
}
}
}

View file

@ -0,0 +1,10 @@
namespace NzbDrone.Core.TV
{
public enum TVShowStatus
{
Continuing = 0,
Ended = 1,
Upcoming = 2,
Canceled = 3
}
}

View file

@ -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;