refactor(tv): integrate hierarchical monitoring and linter fixes

- Add SetTVShowMonitored/SetSeasonMonitored to monitoring service
- Add IsEffectivelyMonitored for Episode/Season
- Add GetEffectivelyMonitoredEpisodes
- Cascade unmonitoring through TV hierarchy
- Fix nullable types for TV entity IDs
- Simplify repositories with consistent patterns
- Add SeasonNumber to EpisodeFile for better queries

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
admin 2025-12-29 16:01:52 -06:00
parent 22eb0f76c6
commit b9e086cca6
23 changed files with 557 additions and 287 deletions

View file

@ -9,7 +9,7 @@ public class add_tv_tables : NzbDroneMigrationBase
protected override void MainDbUpgrade()
{
Create.TableForModel("TVShows")
.WithColumn("TvdbId").AsInt32().NotNullable()
.WithColumn("TvdbId").AsInt32().Nullable()
.WithColumn("TmdbId").AsInt32().Nullable()
.WithColumn("ImdbId").AsString().Nullable()
.WithColumn("AniDbId").AsInt32().Nullable()
@ -70,7 +70,9 @@ protected override void MainDbUpgrade()
.WithColumn("AirDateUtc").AsDateTime().Nullable()
.WithColumn("Runtime").AsInt32().Nullable()
.WithColumn("UnverifiedSceneNumbering").AsBoolean().NotNullable().WithDefaultValue(false)
.WithColumn("IsSpecial").AsBoolean().NotNullable().WithDefaultValue(false)
.WithColumn("EpisodeFileId").AsInt32().Nullable()
.WithColumn("MediaType").AsInt32().NotNullable().WithDefaultValue(2)
.WithColumn("Monitored").AsBoolean().NotNullable().WithDefaultValue(true)
.WithColumn("QualityProfileId").AsInt32().NotNullable()
.WithColumn("Path").AsString().Nullable()
@ -93,8 +95,9 @@ protected override void MainDbUpgrade()
Create.Index("IX_Episodes_Monitored").OnTable("Episodes").OnColumn("Monitored");
Create.TableForModel("EpisodeFiles")
.WithColumn("TVShowId").AsInt32().NotNullable()
.WithColumn("SeasonNumber").AsInt32().NotNullable()
.WithColumn("TVShowId").AsInt32().Nullable()
.WithColumn("SeasonId").AsInt32().Nullable()
.WithColumn("EpisodeId").AsInt32().Nullable()
.WithColumn("RelativePath").AsString().Nullable()
.WithColumn("Path").AsString().Nullable()
.WithColumn("Size").AsInt64().NotNullable()
@ -107,9 +110,8 @@ protected override void MainDbUpgrade()
.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();
Create.Index("IX_EpisodeFiles_SeasonId").OnTable("EpisodeFiles").OnColumn("SeasonId");
Create.Index("IX_EpisodeFiles_EpisodeId").OnTable("EpisodeFiles").OnColumn("EpisodeId");
}
}
}

View file

@ -0,0 +1,18 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.TV;
namespace NzbDrone.Core.Monitoring.Events
{
public class SeasonMonitoringChangedEvent : IEvent
{
public Season Season { get; private set; }
public bool PreviousMonitored { get; private set; }
public int AffectedEpisodesCount { get; set; }
public SeasonMonitoringChangedEvent(Season season, bool previousMonitored)
{
Season = season;
PreviousMonitored = previousMonitored;
}
}
}

View file

@ -0,0 +1,19 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.TV;
namespace NzbDrone.Core.Monitoring.Events
{
public class TVShowMonitoringChangedEvent : IEvent
{
public TVShow TVShow { get; private set; }
public bool PreviousMonitored { get; private set; }
public int AffectedSeasonsCount { get; set; }
public int AffectedEpisodesCount { get; set; }
public TVShowMonitoringChangedEvent(TVShow tvShow, bool previousMonitored)
{
TVShow = tvShow;
PreviousMonitored = previousMonitored;
}
}
}

View file

@ -8,6 +8,7 @@
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Monitoring.Events;
using NzbDrone.Core.Music;
using NzbDrone.Core.TV;
namespace NzbDrone.Core.Monitoring
{
@ -22,6 +23,9 @@ public class HierarchicalMonitoringService : IHierarchicalMonitoringService
private readonly IArtistRepository _artistRepository;
private readonly IAlbumRepository _albumRepository;
private readonly ITrackRepository _trackRepository;
private readonly ITVShowRepository _tvShowRepository;
private readonly ISeasonRepository _seasonRepository;
private readonly IEpisodeRepository _episodeRepository;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
@ -33,6 +37,9 @@ public HierarchicalMonitoringService(
IArtistRepository artistRepository,
IAlbumRepository albumRepository,
ITrackRepository trackRepository,
ITVShowRepository tvShowRepository,
ISeasonRepository seasonRepository,
IEpisodeRepository episodeRepository,
IEventAggregator eventAggregator,
Logger logger)
{
@ -43,6 +50,9 @@ public HierarchicalMonitoringService(
_artistRepository = artistRepository;
_albumRepository = albumRepository;
_trackRepository = trackRepository;
_tvShowRepository = tvShowRepository;
_seasonRepository = seasonRepository;
_episodeRepository = episodeRepository;
_eventAggregator = eventAggregator;
_logger = logger;
}
@ -254,6 +264,149 @@ public List<Track> GetEffectivelyMonitoredTracks()
.ToList();
}
public bool IsEffectivelyMonitored(Episode episode)
{
return episode.Monitored && !IsTVAncestorUnmonitored(episode.SeasonId, episode.TVShowId);
}
public bool IsEffectivelyMonitored(Season season)
{
return season.Monitored && !IsTVAncestorUnmonitored(null, season.TVShowId);
}
public void SetTVShowMonitored(int tvShowId, bool monitored)
{
var tvShow = _tvShowRepository.Get(tvShowId);
if (tvShow == null)
{
_logger.Warn("TVShow with id {0} not found", tvShowId);
return;
}
var previousMonitored = tvShow.Monitored;
if (previousMonitored == monitored)
{
return;
}
tvShow.Monitored = monitored;
_tvShowRepository.Update(tvShow);
var changeEvent = new TVShowMonitoringChangedEvent(tvShow, previousMonitored);
if (previousMonitored && !monitored)
{
CascadeUnmonitorFromTVShow(tvShowId, changeEvent);
}
_eventAggregator.PublishEvent(changeEvent);
_logger.Info("TVShow {0} monitoring changed from {1} to {2}. Affected: {3} seasons, {4} episodes",
tvShow.Title,
previousMonitored,
monitored,
changeEvent.AffectedSeasonsCount,
changeEvent.AffectedEpisodesCount);
}
public void SetSeasonMonitored(int seasonId, bool monitored)
{
var season = _seasonRepository.Get(seasonId);
if (season == null)
{
_logger.Warn("Season with id {0} not found", seasonId);
return;
}
var previousMonitored = season.Monitored;
if (previousMonitored == monitored)
{
return;
}
season.Monitored = monitored;
_seasonRepository.Update(season);
var changeEvent = new SeasonMonitoringChangedEvent(season, previousMonitored);
if (previousMonitored && !monitored)
{
CascadeUnmonitorFromSeason(seasonId, changeEvent);
}
_eventAggregator.PublishEvent(changeEvent);
_logger.Info("Season {0} monitoring changed from {1} to {2}. Affected: {3} episodes",
season.SeasonNumber,
previousMonitored,
monitored,
changeEvent.AffectedEpisodesCount);
}
public List<Episode> GetEffectivelyMonitoredEpisodes()
{
var monitoredTVShows = _tvShowRepository.GetMonitored()
.Select(t => t.Id)
.ToHashSet();
var monitoredSeasons = _seasonRepository.All()
.Where(s => s.Monitored)
.Where(s => !s.TVShowId.HasValue || monitoredTVShows.Contains(s.TVShowId.Value))
.Select(s => s.Id)
.ToHashSet();
return _episodeRepository.All()
.Where(e => e.Monitored)
.Where(e => !e.SeasonId.HasValue || monitoredSeasons.Contains(e.SeasonId.Value))
.ToList();
}
private bool IsTVAncestorUnmonitored(int? seasonId, int? tvShowId)
{
if (seasonId.HasValue)
{
var season = _seasonRepository.Get(seasonId.Value);
if (season != null && !season.Monitored)
{
return true;
}
}
if (tvShowId.HasValue)
{
var tvShow = _tvShowRepository.Get(tvShowId.Value);
if (tvShow != null && !tvShow.Monitored)
{
return true;
}
}
return false;
}
private void CascadeUnmonitorFromTVShow(int tvShowId, TVShowMonitoringChangedEvent changeEvent)
{
var seasonsToUnmonitor = _seasonRepository.FindByTVShowId(tvShowId).Where(s => s.Monitored).ToList();
changeEvent.AffectedSeasonsCount = UnmonitorEntities(
seasonsToUnmonitor,
s => s.Monitored = false,
_seasonRepository.UpdateMany);
var seasonIds = seasonsToUnmonitor.Select(s => s.Id).ToList();
changeEvent.AffectedEpisodesCount = UnmonitorEntities(
seasonIds.SelectMany(id => _episodeRepository.FindBySeasonId(id)).Where(e => e.Monitored).ToList(),
e => e.Monitored = false,
_episodeRepository.UpdateMany);
}
private void CascadeUnmonitorFromSeason(int seasonId, SeasonMonitoringChangedEvent changeEvent)
{
changeEvent.AffectedEpisodesCount = UnmonitorEntities(
_episodeRepository.FindBySeasonId(seasonId).Where(e => e.Monitored).ToList(),
e => e.Monitored = false,
_episodeRepository.UpdateMany);
}
private bool IsMusicAncestorUnmonitored(int? artistId)
{
if (artistId.HasValue)

View file

@ -2,6 +2,7 @@
using NzbDrone.Core.Audiobooks;
using NzbDrone.Core.Books;
using NzbDrone.Core.Music;
using NzbDrone.Core.TV;
namespace NzbDrone.Core.Monitoring
{
@ -12,14 +13,19 @@ public interface IHierarchicalMonitoringService
bool IsEffectivelyMonitored(NzbDrone.Core.BookSeries.BookSeries bookSeries);
bool IsEffectivelyMonitored(Album album);
bool IsEffectivelyMonitored(Track track);
bool IsEffectivelyMonitored(Episode episode);
bool IsEffectivelyMonitored(Season season);
void SetAuthorMonitored(int authorId, bool monitored);
void SetBookSeriesMonitored(int bookSeriesId, bool monitored);
void SetArtistMonitored(int artistId, bool monitored);
void SetAlbumMonitored(int albumId, bool monitored);
void SetTVShowMonitored(int tvShowId, bool monitored);
void SetSeasonMonitored(int seasonId, bool monitored);
List<Book> GetEffectivelyMonitoredBooks();
List<Audiobook> GetEffectivelyMonitoredAudiobooks();
List<Track> GetEffectivelyMonitoredTracks();
List<Episode> GetEffectivelyMonitoredEpisodes();
}
}

View file

@ -11,8 +11,8 @@ public Episode()
MediaType = MediaType.TV;
}
public int TVShowId { get; set; }
public int SeasonId { get; set; }
public int? TVShowId { get; set; }
public int? SeasonId { get; set; }
public int SeasonNumber { get; set; }
public int EpisodeNumber { get; set; }
@ -24,12 +24,11 @@ public Episode()
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 IsSpecial { get; set; }
public bool UnverifiedSceneNumbering { get; set; }
public int? EpisodeFileId { get; set; }
@ -39,12 +38,7 @@ public Episode()
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}";
return $"S{SeasonNumber:00}E{EpisodeNumber:00} - {Title}";
}
}
}

View file

@ -7,7 +7,9 @@ namespace NzbDrone.Core.TV
{
public class EpisodeFile : ModelBase
{
public int TVShowId { get; set; }
public int? TVShowId { get; set; }
public int? SeasonId { get; set; }
public int? EpisodeId { get; set; }
public int SeasonNumber { get; set; }
public string RelativePath { get; set; }
@ -17,16 +19,15 @@ public class EpisodeFile : ModelBase
public string SceneName { get; set; }
public string ReleaseGroup { get; set; }
public QualityModel Quality { get; set; }
public Language Language { get; set; }
public StreamingSource StreamingSource { get; set; }
public Language Language { get; set; }
public string MediaInfo { get; set; }
public override string ToString()
{
return Path;
return RelativePath ?? Path;
}
}
}

View file

@ -8,14 +8,13 @@ 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);
List<EpisodeFile> FindBySeasonId(int seasonId);
EpisodeFile FindByEpisodeId(int episodeId);
}
public class EpisodeFileRepository : BasicRepository<EpisodeFile>, IEpisodeFileRepository
{
public EpisodeFileRepository(IMainDatabase database,
IEventAggregator eventAggregator)
public EpisodeFileRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
@ -25,14 +24,14 @@ public List<EpisodeFile> FindByTVShowId(int tvShowId)
return Query(f => f.TVShowId == tvShowId);
}
public List<EpisodeFile> FindByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber)
public List<EpisodeFile> FindBySeasonId(int seasonId)
{
return Query(f => f.TVShowId == tvShowId && f.SeasonNumber == seasonNumber);
return Query(f => f.SeasonId == seasonId);
}
public EpisodeFile FindByPath(string path)
public EpisodeFile FindByEpisodeId(int episodeId)
{
return Query(f => f.Path == path).FirstOrDefault();
return Query(f => f.EpisodeId == episodeId).FirstOrDefault();
}
}
}

View file

@ -10,21 +10,15 @@ 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 FindByTVShowIdAndEpisodeNumber(int tvShowId, int seasonNumber, int episodeNumber);
Episode FindByTVShowIdAndAbsoluteNumber(int tvShowId, int absoluteNumber);
Episode FindByAirDate(int tvShowId, string airDate);
List<Episode> FindBySeasonAndEpisode(int tvShowId, int seasonNumber, int[] episodeNumbers);
List<Episode> FindByAbsoluteEpisodeNumber(int tvShowId, int[] absoluteEpisodeNumbers);
List<Episode> EpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored);
Episode FindByPath(string path);
Dictionary<int, string> AllEpisodePaths();
List<Episode> FindByAirDate(int tvShowId, DateTime airDate);
List<Episode> GetMonitored();
}
public class EpisodeRepository : BasicRepository<Episode>, IEpisodeRepository
{
public EpisodeRepository(IMainDatabase database,
IEventAggregator eventAggregator)
public EpisodeRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
@ -39,71 +33,29 @@ 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)
public Episode FindByTVShowIdAndEpisodeNumber(int tvShowId, int seasonNumber, int episodeNumber)
{
return Query(e => e.TVShowId == tvShowId &&
e.SeasonNumber == seasonNumber &&
e.EpisodeNumber == episodeNumber).FirstOrDefault();
e.SeasonNumber == seasonNumber &&
e.EpisodeNumber == episodeNumber).FirstOrDefault();
}
public Episode FindByTVShowIdAndAbsoluteNumber(int tvShowId, int absoluteNumber)
{
return Query(e => e.TVShowId == tvShowId &&
e.AbsoluteEpisodeNumber == absoluteNumber).FirstOrDefault();
e.AbsoluteEpisodeNumber == absoluteNumber).FirstOrDefault();
}
public Episode FindByAirDate(int tvShowId, string airDate)
{
if (!DateTime.TryParse(airDate, out var date))
{
return null;
}
return Query(e => e.TVShowId == tvShowId &&
e.AirDate.HasValue &&
e.AirDate.Value.Date == date.Date).FirstOrDefault();
}
public List<Episode> FindBySeasonAndEpisode(int tvShowId, int seasonNumber, int[] episodeNumbers)
public List<Episode> FindByAirDate(int tvShowId, DateTime airDate)
{
return Query(e => e.TVShowId == tvShowId &&
e.SeasonNumber == seasonNumber &&
episodeNumbers.Contains(e.EpisodeNumber));
e.AirDate.HasValue &&
e.AirDate.Value.Date == airDate.Date);
}
public List<Episode> FindByAbsoluteEpisodeNumber(int tvShowId, int[] absoluteEpisodeNumbers)
public List<Episode> GetMonitored()
{
return Query(e => e.TVShowId == tvShowId &&
e.AbsoluteEpisodeNumber.HasValue &&
absoluteEpisodeNumbers.Contains(e.AbsoluteEpisodeNumber.Value));
}
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);
return Query(e => e.Monitored);
}
}
}

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.TV.Events;
@ -12,65 +11,81 @@ 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 FindByAirDate(int tvShowId, string airDate);
List<Episode> GetEpisodesBySeason(int tvShowId, int seasonNumber);
List<Episode> FindBySeasonAndEpisode(int tvShowId, int seasonNumber, int[] episodeNumbers);
List<Episode> FindByAbsoluteEpisodeNumber(int tvShowId, int[] absoluteEpisodeNumbers);
List<Episode> GetEpisodesBySeasonId(int seasonId);
Episode GetEpisode(int tvShowId, int seasonNumber, int episodeNumber);
Episode GetEpisodeByAbsoluteNumber(int tvShowId, int absoluteNumber);
List<Episode> GetEpisodesByAirDate(int tvShowId, DateTime airDate);
Episode AddEpisode(Episode newEpisode);
List<Episode> AddEpisodes(List<Episode> newEpisodes);
void DeleteEpisode(int episodeId, bool deleteFiles);
void DeleteEpisodes(List<int> episodeIds, bool deleteFiles);
void DeleteEpisode(int episodeId);
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();
List<Episode> GetEpisodesBySeason(int tvShowId, int seasonNumber);
Episode FindByAirDate(int tvShowId, string airDate);
List<Episode> FindByAbsoluteEpisodeNumber(int tvShowId, IEnumerable<int> absoluteNumbers);
List<Episode> FindBySeasonAndEpisode(int tvShowId, int seasonNumber, IEnumerable<int> episodeNumbers);
}
public class EpisodeService : IEpisodeService
{
private readonly IEpisodeRepository _episodeRepository;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public EpisodeService(IEpisodeRepository episodeRepository,
IEventAggregator eventAggregator,
Logger logger)
public EpisodeService(
IEpisodeRepository episodeRepository,
IEventAggregator eventAggregator)
{
_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 Episode GetEpisode(int episodeId)
{
return _episodeRepository.Get(episodeId);
}
public List<Episode> GetEpisodes(IEnumerable<int> episodeIds)
{
return _episodeRepository.Get(episodeIds).ToList();
}
public List<Episode> GetEpisodesByTVShowId(int tvShowId)
{
return _episodeRepository.FindByTVShowId(tvShowId);
}
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 FindByAirDate(int tvShowId, string airDate)
=> _episodeRepository.FindByAirDate(tvShowId, airDate);
public List<Episode> GetEpisodesBySeason(int tvShowId, int seasonNumber)
=> GetEpisodesByTVShowIdAndSeasonNumber(tvShowId, seasonNumber);
public List<Episode> FindBySeasonAndEpisode(int tvShowId, int seasonNumber, int[] episodeNumbers)
=> _episodeRepository.FindBySeasonAndEpisode(tvShowId, seasonNumber, episodeNumbers);
public List<Episode> FindByAbsoluteEpisodeNumber(int tvShowId, int[] absoluteEpisodeNumbers)
=> _episodeRepository.FindByAbsoluteEpisodeNumber(tvShowId, absoluteEpisodeNumbers);
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);
{
return _episodeRepository.FindByTVShowId(tvShowId)
.Where(e => e.SeasonNumber == seasonNumber)
.ToList();
}
public List<Episode> GetEpisodesBySeasonId(int seasonId)
{
return _episodeRepository.FindBySeasonId(seasonId);
}
public Episode GetEpisode(int tvShowId, int seasonNumber, int episodeNumber)
{
return _episodeRepository.FindByTVShowIdAndEpisodeNumber(tvShowId, seasonNumber, episodeNumber);
}
public Episode GetEpisodeByAbsoluteNumber(int tvShowId, int absoluteNumber)
{
return _episodeRepository.FindByTVShowIdAndAbsoluteNumber(tvShowId, absoluteNumber);
}
public List<Episode> GetEpisodesByAirDate(int tvShowId, DateTime airDate)
{
return _episodeRepository.FindByAirDate(tvShowId, airDate);
}
public Episode AddEpisode(Episode newEpisode)
{
newEpisode.Added = DateTime.UtcNow;
var episode = _episodeRepository.Insert(newEpisode);
_eventAggregator.PublishEvent(new EpisodeAddedEvent(episode));
return episode;
@ -78,6 +93,12 @@ public Episode AddEpisode(Episode newEpisode)
public List<Episode> AddEpisodes(List<Episode> newEpisodes)
{
var now = DateTime.UtcNow;
foreach (var episode in newEpisodes)
{
episode.Added = now;
}
_episodeRepository.InsertMany(newEpisodes);
foreach (var episode in newEpisodes)
@ -88,30 +109,19 @@ public List<Episode> AddEpisodes(List<Episode> newEpisodes)
return newEpisodes;
}
public void DeleteEpisode(int episodeId, bool deleteFiles)
public void DeleteEpisode(int episodeId)
{
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));
}
_eventAggregator.PublishEvent(new EpisodeDeletedEvent(episode));
}
public Episode UpdateEpisode(Episode episode)
{
var storedEpisode = _episodeRepository.Get(episode.Id);
_episodeRepository.Update(episode);
_eventAggregator.PublishEvent(new EpisodeEditedEvent(episode, storedEpisode));
return episode;
var existingEpisode = _episodeRepository.Get(episode.Id);
var updatedEpisode = _episodeRepository.Update(episode);
_eventAggregator.PublishEvent(new EpisodeEditedEvent(updatedEpisode, existingEpisode));
return updatedEpisode;
}
public List<Episode> UpdateEpisodes(List<Episode> episodes)
@ -120,5 +130,50 @@ public List<Episode> UpdateEpisodes(List<Episode> episodes)
_eventAggregator.PublishEvent(new EpisodesBulkEditedEvent(episodes));
return episodes;
}
public List<Episode> GetEpisodesBySeason(int tvShowId, int seasonNumber)
{
return GetEpisodesByTVShowIdAndSeasonNumber(tvShowId, seasonNumber);
}
public Episode FindByAirDate(int tvShowId, string airDate)
{
if (!DateTime.TryParse(airDate, out var parsedDate))
{
return null;
}
return _episodeRepository.FindByAirDate(tvShowId, parsedDate).FirstOrDefault();
}
public List<Episode> FindByAbsoluteEpisodeNumber(int tvShowId, IEnumerable<int> absoluteNumbers)
{
var episodes = new List<Episode>();
foreach (var absNum in absoluteNumbers)
{
var episode = _episodeRepository.FindByTVShowIdAndAbsoluteNumber(tvShowId, absNum);
if (episode != null)
{
episodes.Add(episode);
}
}
return episodes;
}
public List<Episode> FindBySeasonAndEpisode(int tvShowId, int seasonNumber, IEnumerable<int> episodeNumbers)
{
var episodes = new List<Episode>();
foreach (var epNum in episodeNumbers)
{
var episode = _episodeRepository.FindByTVShowIdAndEpisodeNumber(tvShowId, seasonNumber, epNum);
if (episode != null)
{
episodes.Add(episode);
}
}
return episodes;
}
}
}

View file

@ -5,12 +5,10 @@ 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)
public EpisodeDeletedEvent(Episode episode)
{
Episode = episode;
DeleteFiles = deleteFiles;
}
}
}

View file

@ -0,0 +1,14 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.TV.Events
{
public class SeasonAddedEvent : IEvent
{
public Season Season { get; private set; }
public SeasonAddedEvent(Season season)
{
Season = season;
}
}
}

View file

@ -0,0 +1,14 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.TV.Events
{
public class SeasonDeletedEvent : IEvent
{
public Season Season { get; private set; }
public SeasonDeletedEvent(Season season)
{
Season = season;
}
}
}

View file

@ -0,0 +1,16 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.TV.Events
{
public class SeasonEditedEvent : IEvent
{
public Season Season { get; private set; }
public Season OldSeason { get; private set; }
public SeasonEditedEvent(Season season, Season oldSeason)
{
Season = season;
OldSeason = oldSeason;
}
}
}

View file

@ -4,17 +4,15 @@ namespace NzbDrone.Core.TV
{
public class Season : ModelBase
{
public int TVShowId { get; set; }
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}";
return $"Season {SeasonNumber}";
}
}
}

View file

@ -14,8 +14,7 @@ public interface ISeasonRepository : IBasicRepository<Season>
public class SeasonRepository : BasicRepository<Season>, ISeasonRepository
{
public SeasonRepository(IMainDatabase database,
IEventAggregator eventAggregator)
public SeasonRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}

View file

@ -1,7 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Monitoring;
using NzbDrone.Core.TV.Events;
namespace NzbDrone.Core.TV
{
@ -10,63 +11,75 @@ 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 GetSeasonByTVShowIdAndSeasonNumber(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 IHierarchicalMonitoringService _hierarchicalMonitoringService;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public SeasonService(ISeasonRepository seasonRepository,
IEventAggregator eventAggregator,
Logger logger)
public SeasonService(
ISeasonRepository seasonRepository,
IHierarchicalMonitoringService hierarchicalMonitoringService,
IEventAggregator eventAggregator)
{
_seasonRepository = seasonRepository;
_hierarchicalMonitoringService = hierarchicalMonitoringService;
_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 GetSeason(int seasonId)
{
return _seasonRepository.Get(seasonId);
}
public List<Season> GetSeasons(IEnumerable<int> seasonIds)
{
return _seasonRepository.Get(seasonIds).ToList();
}
public List<Season> GetSeasonsByTVShowId(int tvShowId)
{
return _seasonRepository.FindByTVShowId(tvShowId);
}
public Season GetSeasonByTVShowIdAndSeasonNumber(int tvShowId, int seasonNumber)
{
return _seasonRepository.FindByTVShowIdAndSeasonNumber(tvShowId, seasonNumber);
}
public Season AddSeason(Season newSeason)
{
return _seasonRepository.Insert(newSeason);
}
public List<Season> AddSeasons(List<Season> newSeasons)
{
_seasonRepository.InsertMany(newSeasons);
return newSeasons;
var season = _seasonRepository.Insert(newSeason);
_eventAggregator.PublishEvent(new SeasonAddedEvent(season));
return season;
}
public void DeleteSeason(int seasonId)
{
var season = _seasonRepository.Get(seasonId);
_seasonRepository.Delete(seasonId);
}
public void DeleteSeasons(List<int> seasonIds)
{
_seasonRepository.DeleteMany(seasonIds);
_eventAggregator.PublishEvent(new SeasonDeletedEvent(season));
}
public Season UpdateSeason(Season season)
{
_seasonRepository.Update(season);
return season;
var existingSeason = _seasonRepository.Get(season.Id);
if (existingSeason.Monitored != season.Monitored)
{
_hierarchicalMonitoringService.SetSeasonMonitored(season.Id, season.Monitored);
}
var updatedSeason = _seasonRepository.Update(season);
_eventAggregator.PublishEvent(new SeasonEditedEvent(updatedSeason, existingSeason));
return updatedSeason;
}
public List<Season> UpdateSeasons(List<Season> seasons)

View file

@ -3,34 +3,47 @@ namespace NzbDrone.Core.TV
public enum StreamingSource
{
Unknown = 0,
Amazon, // AMZN
Netflix, // NF
Disney, // DSNP
AppleTV, // ATVP
Hulu, // HULU
HBO, // HBO, HMAX
Peacock, // PCOK
Paramount, // PMTP
CrunchyRoll, // CR
Funimation, // FUNI
Hidive, // HIDV
VRV, // VRV
Rakuten, // RKTN
ITunes, // iTunes
Vudu, // VUDU
Stan, // STAN
BBC, // iP
ITV, // ITV
All4, // 4OD
Now, // NOW
Canal, // CANAL
Wakanim, // WAKA
DCUniverse, // DCU
Quibi, // QIBI
Spectrum, // SPEC
Showtime, // SHO
Starz, // STRP
TVLand, // TVLAND
BritBox // BRTBX
Amazon,
Netflix,
Disney,
Hulu,
AppleTV,
Peacock,
HBO,
HBOMax,
Paramount,
Crunchyroll,
CrunchyRoll,
Funimation,
Hidive,
VRV,
YouTube,
Tubi,
Pluto,
Roku,
ITV,
ITVX,
BBC,
Channel4,
All4,
Stan,
Binge,
Crave,
SkyShowtime,
Discovery,
Showtime,
Starz,
AMC,
BritBox,
Acorn,
Rakuten,
ITunes,
Vudu,
Now,
Canal,
Wakanim,
DCUniverse,
Quibi,
Spectrum
}
}

View file

@ -12,7 +12,7 @@ public TVShow()
Genres = new List<string>();
}
public int TvdbId { get; set; }
public int? TvdbId { get; set; }
public int? TmdbId { get; set; }
public string ImdbId { get; set; }
public int? AniDbId { get; set; }
@ -23,13 +23,11 @@ public TVShow()
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; }
@ -41,11 +39,10 @@ public TVShow()
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 HashSet<int> Tags { get; set; }
public DateTime? LastSearchTime { get; set; }
public override string ToString()

View file

@ -7,70 +7,43 @@ namespace NzbDrone.Core.TV
{
public interface ITVShowRepository : IBasicRepository<TVShow>
{
bool TVShowPathExists(string path);
TVShow FindByTitle(string title);
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();
bool TVShowPathExists(string path);
}
public class TVShowRepository : BasicRepository<TVShow>, ITVShowRepository
{
public TVShowRepository(IMainDatabase database,
IEventAggregator eventAggregator)
public TVShowRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
public bool TVShowPathExists(string path)
public TVShow FindByTitle(string title)
{
return Query(s => s.Path == path).Any();
return Query(t => t.Title == title).FirstOrDefault();
}
public TVShow FindByTvdbId(int tvdbId)
{
return Query(s => s.TvdbId == tvdbId).FirstOrDefault();
return Query(t => t.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();
return Query(t => t.ImdbId == imdbId).FirstOrDefault();
}
public List<TVShow> GetMonitored()
{
return Query(s => s.Monitored);
return Query(t => t.Monitored);
}
public Dictionary<int, string> AllTVShowPaths()
public bool TVShowPathExists(string path)
{
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());
return Query(t => t.Path == path).Any();
}
}
}

View file

@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Monitoring;
using NzbDrone.Core.TV.Events;
namespace NzbDrone.Core.TV
@ -12,42 +13,47 @@ public interface ITVShowService
List<TVShow> GetTVShows(IEnumerable<int> tvShowIds);
TVShow AddTVShow(TVShow newTVShow);
List<TVShow> AddTVShows(List<TVShow> newTVShows);
TVShow FindByTitle(string title);
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();
List<TVShow> GetMonitoredTVShows();
TVShow UpdateTVShow(TVShow tvShow);
List<TVShow> UpdateTVShows(List<TVShow> tvShows);
bool TVShowPathExists(string folder);
bool TVShowPathExists(string path);
}
public class TVShowService : ITVShowService
{
private readonly ITVShowRepository _tvShowRepository;
private readonly IHierarchicalMonitoringService _hierarchicalMonitoringService;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public TVShowService(ITVShowRepository tvShowRepository,
IEventAggregator eventAggregator,
Logger logger)
public TVShowService(
ITVShowRepository tvShowRepository,
IHierarchicalMonitoringService hierarchicalMonitoringService,
IEventAggregator eventAggregator)
{
_tvShowRepository = tvShowRepository;
_hierarchicalMonitoringService = hierarchicalMonitoringService;
_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 GetTVShow(int tvShowId)
{
return _tvShowRepository.Get(tvShowId);
}
public List<TVShow> GetTVShows(IEnumerable<int> tvShowIds)
{
return _tvShowRepository.Get(tvShowIds).ToList();
}
public TVShow AddTVShow(TVShow newTVShow)
{
newTVShow.Added = DateTime.UtcNow;
var tvShow = _tvShowRepository.Insert(newTVShow);
_eventAggregator.PublishEvent(new TVShowAddedEvent(tvShow));
return tvShow;
@ -55,6 +61,12 @@ public TVShow AddTVShow(TVShow newTVShow)
public List<TVShow> AddTVShows(List<TVShow> newTVShows)
{
var now = DateTime.UtcNow;
foreach (var tvShow in newTVShows)
{
tvShow.Added = now;
}
_tvShowRepository.InsertMany(newTVShows);
foreach (var tvShow in newTVShows)
@ -65,16 +77,20 @@ public List<TVShow> AddTVShows(List<TVShow> newTVShows)
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 TVShow FindByTitle(string title)
{
return _tvShowRepository.FindByTitle(title);
}
public TVShow FindByTvdbId(int tvdbId)
{
return _tvShowRepository.FindByTvdbId(tvdbId);
}
public TVShow FindByImdbId(string imdbId)
{
return _tvShowRepository.FindByImdbId(imdbId);
}
public void DeleteTVShow(int tvShowId, bool deleteFiles)
{
@ -94,19 +110,39 @@ public void DeleteTVShows(List<int> tvShowIds, bool deleteFiles)
}
}
public List<TVShow> GetAllTVShows()
{
return _tvShowRepository.All().ToList();
}
public List<TVShow> GetMonitoredTVShows()
{
return _tvShowRepository.GetMonitored();
}
public TVShow UpdateTVShow(TVShow tvShow)
{
var storedTVShow = _tvShowRepository.Get(tvShow.Id);
_tvShowRepository.Update(tvShow);
_eventAggregator.PublishEvent(new TVShowEditedEvent(tvShow, storedTVShow));
return tvShow;
var existingTVShow = _tvShowRepository.Get(tvShow.Id);
if (existingTVShow.Monitored != tvShow.Monitored)
{
_hierarchicalMonitoringService.SetTVShowMonitored(tvShow.Id, tvShow.Monitored);
}
var updatedTVShow = _tvShowRepository.Update(tvShow);
_eventAggregator.PublishEvent(new TVShowEditedEvent(updatedTVShow, existingTVShow));
return updatedTVShow;
}
public List<TVShow> UpdateTVShows(List<TVShow> tvShows)
{
_tvShowRepository.UpdateMany(tvShows);
_eventAggregator.PublishEvent(new TVShowsBulkEditedEvent(tvShows));
return tvShows;
}
public bool TVShowPathExists(string path)
{
return _tvShowRepository.TVShowPathExists(path);
}
}
}

View file

@ -35,7 +35,7 @@ public static SeasonResource ToResource(this Season model)
return new SeasonResource
{
Id = model.Id,
TVShowId = model.TVShowId,
TVShowId = model.TVShowId ?? 0,
SeasonNumber = model.SeasonNumber,
Title = model.Title,
Overview = model.Overview,

View file

@ -14,7 +14,7 @@ public TVShowResource()
Monitored = true;
}
public int TvdbId { get; set; }
public int? TvdbId { get; set; }
public int? TmdbId { get; set; }
public string ImdbId { get; set; }
public int? AniDbId { get; set; }