Radarr/src/NzbDrone.Core/Movies/MovieRepository.cs

381 lines
17 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.IO;
2019-12-22 22:08:53 +00:00
using System.Linq;
2019-12-18 21:56:41 +00:00
using Dapper;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.MediaFiles;
2019-12-22 22:08:53 +00:00
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Movies.Translations;
2019-12-18 21:56:41 +00:00
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.Movies
{
public interface IMovieRepository : IBasicRepository<Movie>
{
bool MoviePathExists(string path);
2019-12-18 21:56:41 +00:00
List<Movie> FindByTitles(List<string> titles);
Movie FindByImdbId(string imdbid);
Movie FindByTmdbId(int tmdbid);
2019-12-18 21:56:41 +00:00
List<Movie> FindByTmdbId(List<int> tmdbids);
2017-01-16 21:40:59 +00:00
List<Movie> MoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored);
PagingSpec<Movie> MoviesWithoutFiles(PagingSpec<Movie> pagingSpec);
List<Movie> GetMoviesByFileId(int fileId);
2022-03-08 02:03:00 +00:00
List<Movie> GetMoviesByCollectionTmdbId(int collectionId);
void SetFileId(int fileId, int movieId);
PagingSpec<Movie> MoviesWhereCutoffUnmet(PagingSpec<Movie> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff);
2019-07-16 02:27:35 +00:00
Movie FindByPath(string path);
Dictionary<int, string> AllMoviePaths();
List<int> AllMovieTmdbIds();
Dictionary<int, List<int>> AllMovieTags();
List<int> GetRecommendations();
bool ExistsByMetadataId(int metadataId);
}
public class MovieRepository : BasicRepository<Movie>, IMovieRepository
{
2019-12-18 21:56:41 +00:00
private readonly IProfileRepository _profileRepository;
2021-02-03 21:26:10 +00:00
private readonly IAlternativeTitleRepository _alternativeTitleRepository;
2019-12-18 21:56:41 +00:00
public MovieRepository(IMainDatabase database,
IProfileRepository profileRepository,
2021-02-03 21:26:10 +00:00
IAlternativeTitleRepository alternativeTitleRepository,
2019-12-18 21:56:41 +00:00
IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
2019-12-18 21:56:41 +00:00
_profileRepository = profileRepository;
2021-02-03 21:26:10 +00:00
_alternativeTitleRepository = alternativeTitleRepository;
2019-12-18 21:56:41 +00:00
}
protected override SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
2019-12-18 21:56:41 +00:00
.Join<Movie, Profile>((m, p) => m.ProfileId == p.Id)
2022-03-20 15:55:47 +00:00
.Join<Movie, MovieMetadata>((m, p) => m.MovieMetadataId == p.Id)
.LeftJoin<Movie, MovieFile>((m, f) => m.Id == f.MovieId)
.LeftJoin<MovieMetadata, AlternativeTitle>((mm, t) => mm.Id == t.MovieMetadataId);
2019-12-18 21:56:41 +00:00
2022-03-20 15:55:47 +00:00
private Movie Map(Dictionary<int, Movie> dict, Movie movie, Profile profile, MovieFile movieFile, AlternativeTitle altTitle = null, MovieTranslation translation = null)
2019-12-18 21:56:41 +00:00
{
Movie movieEntry;
if (!dict.TryGetValue(movie.Id, out movieEntry))
{
movieEntry = movie;
movieEntry.Profile = profile;
movieEntry.MovieFile = movieFile;
dict.Add(movieEntry.Id, movieEntry);
}
if (altTitle != null)
{
2022-03-20 15:55:47 +00:00
movieEntry.MovieMetadata.Value.AlternativeTitles.Add(altTitle);
2019-12-18 21:56:41 +00:00
}
if (translation != null)
{
2022-03-20 15:55:47 +00:00
movieEntry.MovieMetadata.Value.Translations.Add(translation);
}
2019-12-18 21:56:41 +00:00
return movieEntry;
}
protected override List<Movie> Query(SqlBuilder builder)
2019-12-18 21:56:41 +00:00
{
var movieDictionary = new Dictionary<int, Movie>();
2022-03-20 15:55:47 +00:00
_ = _database.QueryJoined<Movie, Profile, MovieFile, AlternativeTitle>(
builder,
2022-03-20 15:55:47 +00:00
(movie, profile, file, altTitle) => Map(movieDictionary, movie, profile, file, altTitle));
2019-12-18 21:56:41 +00:00
return movieDictionary.Values.ToList();
2019-12-18 21:56:41 +00:00
}
public override IEnumerable<Movie> All()
{
2021-02-03 21:26:10 +00:00
// the skips the join on profile and alternative title and populates manually
// to avoid repeatedly deserializing the same profile / movie
var builder = new SqlBuilder(_database.DatabaseType)
2022-03-20 15:55:47 +00:00
.LeftJoin<Movie, MovieFile>((m, f) => m.MovieFileId == f.Id)
.LeftJoin<Movie, MovieMetadata>((m, f) => m.MovieMetadataId == f.Id);
2019-12-18 21:56:41 +00:00
var profiles = _profileRepository.All().ToDictionary(x => x.Id);
2021-02-03 21:26:10 +00:00
var titles = _alternativeTitleRepository.All()
2022-03-20 15:55:47 +00:00
.GroupBy(x => x.MovieMetadataId)
2021-02-03 21:26:10 +00:00
.ToDictionary(x => x.Key, y => y.ToList());
2019-12-18 21:56:41 +00:00
2022-03-20 15:55:47 +00:00
return _database.QueryJoined<Movie, MovieFile, MovieMetadata>(
builder,
2022-03-20 15:55:47 +00:00
(movie, file, metadata) =>
2021-02-03 21:26:10 +00:00
{
movie.MovieFile = file;
2022-03-20 15:55:47 +00:00
movie.MovieMetadata = metadata;
2021-02-03 21:26:10 +00:00
movie.Profile = profiles[movie.ProfileId];
2022-03-20 15:55:47 +00:00
if (titles.TryGetValue(movie.MovieMetadataId, out var altTitles))
2021-02-03 21:26:10 +00:00
{
2022-03-20 15:55:47 +00:00
movie.MovieMetadata.Value.AlternativeTitles = altTitles;
2021-02-03 21:26:10 +00:00
}
return movie;
});
}
public bool MoviePathExists(string path)
{
2019-12-18 21:56:41 +00:00
return Query(x => x.Path == path).Any();
}
2019-12-18 21:56:41 +00:00
public List<Movie> FindByTitles(List<string> titles)
{
var distinct = titles.Distinct().ToList();
2020-10-07 20:45:04 +00:00
var results = new List<Movie>();
results.AddRange(FindByMovieTitles(distinct));
results.AddRange(FindByAltTitles(distinct));
results.AddRange(FindByTransTitles(distinct));
return results.DistinctBy(x => x.Id).ToList();
}
// This is a bit of a hack, but if you try to combine / rationalise these then
// SQLite makes a mess of the query plan and ends up doing a table scan
private List<Movie> FindByMovieTitles(List<string> titles)
{
var movieDictionary = new Dictionary<int, Movie>();
2022-03-20 15:55:47 +00:00
var builder = new SqlBuilder(_database.DatabaseType)
.Join<Movie, Profile>((m, p) => m.ProfileId == p.Id)
2022-03-20 15:55:47 +00:00
.Join<Movie, MovieMetadata>((m, p) => m.MovieMetadataId == p.Id)
2020-10-07 20:45:04 +00:00
.LeftJoin<Movie, MovieFile>((m, f) => m.Id == f.MovieId)
2022-03-20 15:55:47 +00:00
.Where<MovieMetadata>(x => titles.Contains(x.CleanTitle) || titles.Contains(x.CleanOriginalTitle));
2022-03-20 15:55:47 +00:00
_ = _database.QueryJoined<Movie, Profile, MovieFile>(
builder,
2022-03-20 15:55:47 +00:00
(movie, profile, file) => Map(movieDictionary, movie, profile, file));
return movieDictionary.Values.ToList();
}
2020-10-07 20:45:04 +00:00
private List<Movie> FindByAltTitles(List<string> titles)
{
var movieDictionary = new Dictionary<int, Movie>();
2022-03-20 15:55:47 +00:00
var builder = new SqlBuilder(_database.DatabaseType)
.Join<AlternativeTitle, MovieMetadata>((t, mm) => t.MovieMetadataId == mm.Id)
.Join<MovieMetadata, Movie>((mm, m) => mm.Id == m.MovieMetadataId)
.Join<Movie, Profile>((m, p) => m.ProfileId == p.Id)
.LeftJoin<Movie, MovieFile>((m, f) => m.Id == f.MovieId)
.Where<AlternativeTitle>(x => titles.Contains(x.CleanTitle));
2020-10-07 20:45:04 +00:00
2022-03-20 15:55:47 +00:00
_ = _database.QueryJoined<AlternativeTitle, Profile, Movie, MovieFile>(
2020-10-07 20:45:04 +00:00
builder,
2022-03-20 15:55:47 +00:00
(altTitle, profile, movie, file) =>
2020-10-07 20:45:04 +00:00
{
2022-03-20 15:55:47 +00:00
_ = Map(movieDictionary, movie, profile, file, altTitle);
2020-10-07 20:45:04 +00:00
return null;
});
return movieDictionary.Values.ToList();
}
private List<Movie> FindByTransTitles(List<string> titles)
{
var movieDictionary = new Dictionary<int, Movie>();
2022-03-20 15:55:47 +00:00
var builder = new SqlBuilder(_database.DatabaseType)
.Join<MovieTranslation, MovieMetadata>((t, mm) => t.MovieMetadataId == mm.Id)
.Join<MovieMetadata, Movie>((mm, m) => mm.Id == m.MovieMetadataId)
2020-10-07 20:45:04 +00:00
.Join<Movie, Profile>((m, p) => m.ProfileId == p.Id)
2022-03-20 15:55:47 +00:00
.LeftJoin<Movie, MovieFile>((m, f) => m.Id == f.MovieId)
2020-10-07 20:45:04 +00:00
.Where<MovieTranslation>(x => titles.Contains(x.CleanTitle));
2022-03-20 15:55:47 +00:00
_ = _database.QueryJoined<MovieTranslation, Profile, Movie, MovieFile>(
2020-10-07 20:45:04 +00:00
builder,
2022-03-20 15:55:47 +00:00
(trans, profile, movie, file) =>
2020-10-07 20:45:04 +00:00
{
2022-03-20 15:55:47 +00:00
_ = Map(movieDictionary, movie, profile, file, null, trans);
2020-10-07 20:45:04 +00:00
return null;
});
return movieDictionary.Values.ToList();
}
public Movie FindByImdbId(string imdbid)
{
var imdbIdWithPrefix = Parser.Parser.NormalizeImdbId(imdbid);
2022-03-20 15:55:47 +00:00
return imdbIdWithPrefix == null ? null : Query(x => x.MovieMetadata.Value.ImdbId == imdbIdWithPrefix).FirstOrDefault();
2019-12-18 21:56:41 +00:00
}
public Movie FindByTmdbId(int tmdbid)
{
2022-03-20 15:55:47 +00:00
return Query(x => x.MovieMetadata.Value.TmdbId == tmdbid).FirstOrDefault();
2019-12-18 21:56:41 +00:00
}
public List<Movie> FindByTmdbId(List<int> tmdbids)
{
return Query(x => tmdbids.Contains(x.TmdbId));
}
public List<Movie> GetMoviesByFileId(int fileId)
{
2019-12-18 21:56:41 +00:00
return Query(x => x.MovieFileId == fileId);
}
2022-03-08 02:03:00 +00:00
public List<Movie> GetMoviesByCollectionTmdbId(int collectionId)
{
return Query(x => x.MovieMetadata.Value.CollectionTmdbId == collectionId);
}
public void SetFileId(int fileId, int movieId)
{
SetFields(new Movie { Id = movieId, MovieFileId = fileId }, movie => movie.MovieFileId);
}
2017-01-16 21:40:59 +00:00
public List<Movie> MoviesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored)
{
2019-12-18 21:56:41 +00:00
var builder = Builder()
.Where<Movie>(m =>
2022-03-20 15:55:47 +00:00
(m.MovieMetadata.Value.InCinemas >= start && m.MovieMetadata.Value.InCinemas <= end) ||
(m.MovieMetadata.Value.PhysicalRelease >= start && m.MovieMetadata.Value.PhysicalRelease <= end) ||
(m.MovieMetadata.Value.DigitalRelease >= start && m.MovieMetadata.Value.DigitalRelease <= end));
2019-12-22 21:24:11 +00:00
2019-12-22 22:08:53 +00:00
if (!includeUnmonitored)
{
2019-12-18 21:56:41 +00:00
builder.Where<Movie>(x => x.Monitored == true);
2019-12-22 22:08:53 +00:00
}
2017-01-16 21:40:59 +00:00
2019-12-18 21:56:41 +00:00
return Query(builder);
2017-01-16 21:40:59 +00:00
}
public SqlBuilder MoviesWithoutFilesBuilder() => Builder()
.Where<Movie>(x => x.MovieFileId == 0);
2019-12-18 21:56:41 +00:00
public PagingSpec<Movie> MoviesWithoutFiles(PagingSpec<Movie> pagingSpec)
{
pagingSpec.Records = GetPagedRecords(MoviesWithoutFilesBuilder(), pagingSpec, PagedQuery);
2019-12-18 21:56:41 +00:00
pagingSpec.TotalRecords = GetPagedRecordCount(MoviesWithoutFilesBuilder().SelectCount(), pagingSpec);
2017-01-19 18:08:15 +00:00
return pagingSpec;
}
public SqlBuilder MoviesWhereCutoffUnmetBuilder(List<QualitiesBelowCutoff> qualitiesBelowCutoff) => Builder()
2019-12-18 21:56:41 +00:00
.Where<Movie>(x => x.MovieFileId != 0)
.Where(BuildQualityCutoffWhereClause(qualitiesBelowCutoff));
public PagingSpec<Movie> MoviesWhereCutoffUnmet(PagingSpec<Movie> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff)
{
pagingSpec.Records = GetPagedRecords(MoviesWhereCutoffUnmetBuilder(qualitiesBelowCutoff), pagingSpec, PagedQuery);
2019-12-18 21:56:41 +00:00
pagingSpec.TotalRecords = GetPagedRecordCount(MoviesWhereCutoffUnmetBuilder(qualitiesBelowCutoff).SelectCount(), pagingSpec);
return pagingSpec;
}
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($"(\"{_table}\".\"ProfileId\" = {profile.ProfileId} AND \"MovieFiles\".\"Quality\" LIKE '%_quality_: {belowCutoff},%')"));
}
}
return string.Format("({0})", string.Join(" OR ", clauses));
}
2019-12-18 21:56:41 +00:00
public Movie FindByPath(string path)
2019-12-22 21:24:10 +00:00
{
2019-12-18 21:56:41 +00:00
return Query(x => x.Path == path).FirstOrDefault();
2019-12-22 21:24:10 +00:00
}
public Dictionary<int, string> AllMoviePaths()
{
2019-12-18 21:56:41 +00:00
using (var conn = _database.OpenConnection())
{
var strSql = "SELECT \"Id\" AS \"Key\", \"Path\" AS \"Value\" FROM \"Movies\"";
return conn.Query<KeyValuePair<int, string>>(strSql).ToDictionary(x => x.Key, x => x.Value);
}
}
public List<int> AllMovieTmdbIds()
{
using (var conn = _database.OpenConnection())
{
2022-03-20 15:55:47 +00:00
return conn.Query<int>("SELECT \"TmdbId\" FROM \"MovieMetadata\" JOIN \"Movies\" ON (\"Movies\".\"MovieMetadataId\" = \"MovieMetadata\".\"Id\")").ToList();
}
}
public Dictionary<int, List<int>> AllMovieTags()
{
using (var conn = _database.OpenConnection())
{
2022-09-25 00:01:47 +00:00
var strSql = "SELECT \"Id\" AS \"Key\", \"Tags\" AS \"Value\" FROM \"Movies\" WHERE \"Tags\" IS NOT NULL";
return conn.Query<KeyValuePair<int, List<int>>>(strSql).ToDictionary(x => x.Key, x => x.Value);
}
}
public List<int> GetRecommendations()
{
var recommendations = new List<int>();
if (_database.Version < new Version("3.9.0"))
{
return recommendations;
}
using (var conn = _database.OpenConnection())
{
if (_database.DatabaseType == DatabaseType.PostgreSQL)
{
recommendations = conn.Query<int>(@"SELECT DISTINCT ""Rec"" FROM (
SELECT DISTINCT ""Rec"" FROM
2021-01-18 20:12:10 +00:00
(
2022-03-20 15:55:47 +00:00
SELECT DISTINCT CAST(""value"" AS INT) AS ""Rec"" FROM ""MovieMetadata"", json_array_elements_text((""MovieMetadata"".""Recommendations"")::json)
WHERE CAST(""value"" AS INT) NOT IN (SELECT ""TmdbId"" FROM ""MovieMetadata"" union SELECT ""TmdbId"" from ""ImportExclusions"" as sub1) LIMIT 10
) as sub2
UNION
SELECT ""Rec"" FROM
(
2022-03-20 15:55:47 +00:00
SELECT CAST(""value"" AS INT) AS ""Rec"" FROM ""MovieMetadata"", json_array_elements_text((""MovieMetadata"".""Recommendations"")::json)
WHERE CAST(""value"" AS INT) NOT IN (SELECT ""TmdbId"" FROM ""MovieMetadata"" union SELECT ""TmdbId"" from ""ImportExclusions"" as sub2)
GROUP BY ""Rec"" ORDER BY count(*) DESC LIMIT 120
) as sub4
) as sub5
LIMIT 100;").ToList();
}
else
{
recommendations = conn.Query<int>(@"SELECT DISTINCT ""Rec"" FROM (
SELECT DISTINCT ""Rec"" FROM
(
2022-03-20 15:55:47 +00:00
SELECT DISTINCT CAST(""j"".""value"" AS INT) AS ""Rec"" FROM ""MovieMetadata"" CROSS JOIN json_each(""MovieMetadata"".""Recommendations"") AS ""j""
WHERE ""Rec"" NOT IN (SELECT ""TmdbId"" FROM ""MovieMetadata"" union SELECT ""TmdbId"" from ""ImportExclusions"") LIMIT 10
2021-01-18 20:12:10 +00:00
)
UNION
SELECT ""Rec"" FROM
2021-01-18 20:12:10 +00:00
(
2022-03-20 15:55:47 +00:00
SELECT CAST(""j"".""value"" AS INT) AS ""Rec"" FROM ""MovieMetadata"" CROSS JOIN json_each(""MovieMetadata"".""Recommendations"") AS ""j""
WHERE ""Rec"" NOT IN (SELECT ""TmdbId"" FROM ""MovieMetadata"" union SELECT ""TmdbId"" from ""ImportExclusions"")
GROUP BY ""Rec"" ORDER BY count(*) DESC LIMIT 120
2021-01-18 20:12:10 +00:00
)
)
LIMIT 100;").ToList();
}
}
return recommendations;
}
public bool ExistsByMetadataId(int metadataId)
{
var movies = Query(x => x.MovieMetadataId == metadataId);
return movies.Any();
}
}
Min availability (#816) * availability specification to prevent downloading titles before their release * pull inCinamas status out of js handlebars and set it in SkyHook * minor code improvement * add incinemas to footer * typo * another typo * release date handling * still print cinema date out for announced titles * revert a minor change from before since its unnecessary * early implementation of minimumAvailability --> when does radarr consider a movie "available" should be specified by user default to "Physical release?" this isn't functional yet, but it has a skeleton + comments. I dont know how to have the minimumavailability attribute default to something or to have it actually populate the Movieinfo object could use some help with that * adding another comment for another location that might need to be updated to handle minimumAvailability * the implementation is now function; however, i still need to specify default values for minimumAvailability * missed these changes in the previous commit * fix rounded corners on new field in editmovie dialog * add minimum availability specification to the addMovie page * minor adjustment from last commit * handle the case where minimumavailability has never yet been set nullstring.. if its never been set, default to Released (Physical/Web) represented by integer value 3 * minAvailability specification on NetImport lists * add support for min availability to the movie editor * use enum MovieStatusType values directly makes for cleaner code * need to fix up the migration forgot in last commit * cleaning up code, proper case * erroneous code added in this feature needed to be removed * update "Wanted" page to take into account minimumAvailability * implement preDB minimumAvailability as default.. behaves same as Physical/Web a few comments with TODO for when preDB is implemented * minor adjustment * remove some unused code (leave commented for now) * improve code for minimumavailability and add option for availabilitydelay (but doesnt do anything yet) * improve isAvailable method * clean up and fix helper info on indexer configuration page * add buttons in Wanted/Missing view
2017-02-23 05:03:48 +00:00
}