You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Sonarr/src/NzbDrone.Core/Tv/EpisodeRepository.cs

281 lines
12 KiB

using System;
using System.Collections.Generic;
using System.Linq;
using Dapper;
using NLog;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.Tv
{
public interface IEpisodeRepository : IBasicRepository<Episode>
{
Episode Find(int seriesId, int season, int episodeNumber);
Episode Find(int seriesId, int absoluteEpisodeNumber);
List<Episode> Find(int seriesId, string date);
List<Episode> GetEpisodes(int seriesId);
List<Episode> GetEpisodes(int seriesId, int seasonNumber);
List<Episode> GetEpisodesBySeriesIds(List<int> seriesIds);
List<Episode> GetEpisodesBySceneSeason(int seriesId, int sceneSeasonNumber);
List<Episode> GetEpisodeByFileId(int fileId);
List<Episode> EpisodesWithFiles(int seriesId);
PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec, bool includeSpecials);
PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, bool includeSpecials);
List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber);
List<Episode> FindEpisodesBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber);
List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored);
void SetMonitoredFlat(Episode episode, bool monitored);
void SetMonitoredBySeason(int seriesId, int seasonNumber, bool monitored);
void SetMonitored(IEnumerable<int> ids, bool monitored);
void SetFileId(Episode episode, int fileId);
void ClearFileId(Episode episode, bool unmonitor);
}
public class EpisodeRepository : BasicRepository<Episode>, IEpisodeRepository
{
private readonly Logger _logger;
public EpisodeRepository(IMainDatabase database, IEventAggregator eventAggregator, Logger logger)
: base(database, eventAggregator)
{
_logger = logger;
}
protected override IEnumerable<Episode> PagedQuery(SqlBuilder builder) =>
_database.QueryJoined<Episode, Series>(builder, (episode, series) =>
{
episode.Series = series;
return episode;
});
public Episode Find(int seriesId, int season, int episodeNumber)
{
return Query(s => s.SeriesId == seriesId && s.SeasonNumber == season && s.EpisodeNumber == episodeNumber)
.SingleOrDefault();
}
public Episode Find(int seriesId, int absoluteEpisodeNumber)
{
return Query(s => s.SeriesId == seriesId && s.AbsoluteEpisodeNumber == absoluteEpisodeNumber)
.SingleOrDefault();
}
public List<Episode> Find(int seriesId, string date)
{
return Query(s => s.SeriesId == seriesId && s.AirDate == date).ToList();
}
public List<Episode> GetEpisodes(int seriesId)
{
return Query(s => s.SeriesId == seriesId).ToList();
}
public List<Episode> GetEpisodes(int seriesId, int seasonNumber)
{
return Query(s => s.SeriesId == seriesId && s.SeasonNumber == seasonNumber).ToList();
}
public List<Episode> GetEpisodesBySeriesIds(List<int> seriesIds)
{
return Query(s => seriesIds.Contains(s.SeriesId)).ToList();
}
public List<Episode> GetEpisodesBySceneSeason(int seriesId, int seasonNumber)
{
return Query(s => s.SeriesId == seriesId && s.SceneSeasonNumber == seasonNumber).ToList();
}
public List<Episode> GetEpisodeByFileId(int fileId)
{
return Query(e => e.EpisodeFileId == fileId).ToList();
}
public List<Episode> EpisodesWithFiles(int seriesId)
{
var builder = Builder()
.Join<Episode, EpisodeFile>((e, ef) => e.EpisodeFileId == ef.Id)
.Where<Episode>(e => e.SeriesId == seriesId);
return _database.QueryJoined<Episode, EpisodeFile>(
builder,
(episode, episodeFile) =>
{
episode.EpisodeFile = episodeFile;
return episode;
}).ToList();
}
public PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec, bool includeSpecials)
{
var currentTime = DateTime.UtcNow;
var startingSeasonNumber = 1;
if (includeSpecials)
{
startingSeasonNumber = 0;
}
pagingSpec.Records = GetPagedRecords(EpisodesWithoutFilesBuilder(currentTime, startingSeasonNumber), pagingSpec, PagedQuery);
pagingSpec.TotalRecords = GetPagedRecordCount(EpisodesWithoutFilesBuilder(currentTime, startingSeasonNumber).SelectCountDistinct<Episode>(x => x.Id), pagingSpec);
return pagingSpec;
}
public PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, bool includeSpecials)
{
var startingSeasonNumber = 1;
if (includeSpecials)
{
startingSeasonNumber = 0;
}
pagingSpec.Records = GetPagedRecords(EpisodesWhereCutoffUnmetBuilder(qualitiesBelowCutoff, startingSeasonNumber), pagingSpec, PagedQuery);
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(Episode))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\"";
pagingSpec.TotalRecords = GetPagedRecordCount(EpisodesWhereCutoffUnmetBuilder(qualitiesBelowCutoff, startingSeasonNumber).Select(typeof(Episode)), pagingSpec, countTemplate);
return pagingSpec;
}
public List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber)
{
return Query(s => s.SeriesId == seriesId && s.SceneSeasonNumber == seasonNumber && s.SceneEpisodeNumber == episodeNumber).ToList();
}
public List<Episode> FindEpisodesBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber)
{
return Query(s => s.SeriesId == seriesId && s.SceneAbsoluteEpisodeNumber == sceneAbsoluteEpisodeNumber).ToList();
}
public List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored)
{
var builder = Builder().Where<Episode>(rg => rg.AirDateUtc >= startDate && rg.AirDateUtc <= endDate);
if (!includeUnmonitored)
{
builder = builder.Where<Episode>(e => e.Monitored == true)
.Join<Episode, Series>((l, r) => l.SeriesId == r.Id)
.Where<Series>(e => e.Monitored == true);
}
return Query(builder);
}
public void SetMonitoredFlat(Episode episode, bool monitored)
{
episode.Monitored = monitored;
SetFields(episode, p => p.Monitored);
ModelUpdated(episode, true);
}
public void SetMonitoredBySeason(int seriesId, int seasonNumber, bool monitored)
{
using (var conn = _database.OpenConnection())
{
conn.Execute("UPDATE \"Episodes\" SET \"Monitored\" = @monitored WHERE \"SeriesId\" = @seriesId AND \"SeasonNumber\" = @seasonNumber AND \"Monitored\" != @monitored",
new { seriesId = seriesId, seasonNumber = seasonNumber, monitored = monitored });
}
}
public void SetMonitored(IEnumerable<int> ids, bool monitored)
{
var episodes = ids.Select(x => new Episode { Id = x, Monitored = monitored }).ToList();
SetFields(episodes, p => p.Monitored);
}
public void SetFileId(Episode episode, int fileId)
{
episode.EpisodeFileId = fileId;
SetFields(episode, ep => ep.EpisodeFileId);
ModelUpdated(episode, true);
}
public void ClearFileId(Episode episode, bool unmonitor)
{
episode.EpisodeFileId = 0;
episode.Monitored &= !unmonitor;
SetFields(episode, ep => ep.EpisodeFileId, ep => ep.Monitored);
ModelUpdated(episode, true);
}
private SqlBuilder EpisodesWithoutFilesBuilder(DateTime currentTime, int startingSeasonNumber) => Builder()
.Join<Episode, Series>((l, r) => l.SeriesId == r.Id)
.Where<Episode>(f => f.EpisodeFileId == 0)
.Where<Episode>(f => f.SeasonNumber >= startingSeasonNumber)
.Where(BuildAirDateUtcCutoffWhereClause(currentTime));
private string BuildAirDateUtcCutoffWhereClause(DateTime currentTime)
{
if (_database.DatabaseType == DatabaseType.PostgreSQL)
{
return string.Format("\"Episodes\".\"AirDateUtc\" + make_interval(mins => \"Series\".\"Runtime\") <= '{0}'",
currentTime.ToString("yyyy-MM-dd HH:mm:ss"));
}
return string.Format("datetime(strftime('%s', \"Episodes\".\"AirDateUtc\") + \"Series\".\"Runtime\" * 60, 'unixepoch') <= '{0}'",
currentTime.ToString("yyyy-MM-dd HH:mm:ss"));
}
private SqlBuilder EpisodesWhereCutoffUnmetBuilder(List<QualitiesBelowCutoff> qualitiesBelowCutoff, int startingSeasonNumber) => Builder()
.Join<Episode, Series>((e, s) => e.SeriesId == s.Id)
.LeftJoin<Episode, EpisodeFile>((e, ef) => e.EpisodeFileId == ef.Id)
.Where<Episode>(e => e.EpisodeFileId != 0)
.Where<Episode>(e => e.SeasonNumber >= startingSeasonNumber)
.Where(
string.Format("({0})",
BuildQualityCutoffWhereClause(qualitiesBelowCutoff)))
.GroupBy<Episode>(e => e.Id)
.GroupBy<Series>(s => s.Id);
private string BuildQualityCutoffWhereClause(List<QualitiesBelowCutoff> qualitiesBelowCutoff)
{
var clauses = new List<string>();
foreach (var profile in qualitiesBelowCutoff)
{
foreach (var belowCutoff in profile.QualityIds)
{
clauses.Add(string.Format("(\"Series\".\"QualityProfileId\" = {0} AND \"EpisodeFiles\".\"Quality\" LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff));
}
}
return string.Format("({0})", string.Join(" OR ", clauses));
}
private Episode FindOneByAirDate(int seriesId, string date)
{
var episodes = Query(s => s.SeriesId == seriesId && s.AirDate == date).ToList();
if (!episodes.Any())
{
return null;
}
if (episodes.Count == 1)
{
return episodes.First();
}
_logger.Debug("Multiple episodes with the same air date were found, will exclude specials");
var regularEpisodes = episodes.Where(e => e.SeasonNumber > 0).ToList();
if (regularEpisodes.Count == 1)
{
_logger.Debug("Left with one episode after excluding specials");
return regularEpisodes.First();
}
throw new InvalidOperationException("Multiple episodes with the same air date found");
}
}
}