diff --git a/src/Marr.Data/QGen/SelectQuery.cs b/src/Marr.Data/QGen/SelectQuery.cs index 886e0d651..97aa15f10 100644 --- a/src/Marr.Data/QGen/SelectQuery.cs +++ b/src/Marr.Data/QGen/SelectQuery.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Linq; +using System.Text; using Marr.Data.Mapping; using Marr.Data.QGen.Dialects; @@ -129,7 +130,16 @@ namespace Marr.Data.QGen public void BuildOrderClause(StringBuilder sql) { sql.Append(OrderBy.ToString()); - } + } + + public void BuildGroupBy(StringBuilder sql) + { + var baseTable = this.Tables.First(); + var primaryKeyColumn = baseTable.Columns.Single(c => c.ColumnInfo.IsPrimaryKey); + + string token = this.Dialect.CreateToken(string.Concat(baseTable.Alias, ".", primaryKeyColumn.ColumnInfo.Name)); + sql.AppendFormat(" GROUP BY {0}", token); + } private string TranslateJoin(JoinType join) { diff --git a/src/Marr.Data/QGen/SqliteRowCountQueryDecorator.cs b/src/Marr.Data/QGen/SqliteRowCountQueryDecorator.cs index 0766c3114..5d0e8c762 100644 --- a/src/Marr.Data/QGen/SqliteRowCountQueryDecorator.cs +++ b/src/Marr.Data/QGen/SqliteRowCountQueryDecorator.cs @@ -14,8 +14,21 @@ namespace Marr.Data.QGen public string Generate() { StringBuilder sql = new StringBuilder(); - + BuildSelectCountClause(sql); + + if (_innerQuery.IsJoin) + { + sql.Append(" FROM ("); + _innerQuery.BuildSelectClause(sql); + _innerQuery.BuildFromClause(sql); + _innerQuery.BuildJoinClauses(sql); + _innerQuery.BuildGroupBy(sql); + sql.Append(") "); + + return sql.ToString(); + } + _innerQuery.BuildFromClause(sql); _innerQuery.BuildJoinClauses(sql); _innerQuery.BuildWhereClause(sql); diff --git a/src/NzbDrone.Api/Indexers/ReleaseModule.cs b/src/NzbDrone.Api/Indexers/ReleaseModule.cs index 7f92215fb..e625d8046 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseModule.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseModule.cs @@ -46,7 +46,7 @@ namespace NzbDrone.Api.Indexers GetResourceAll = GetReleases; Post["/"] = x => DownloadRelease(this.Bind()); - PostValidator.RuleFor(s => s.DownloadAllowed).Equal(true); + //PostValidator.RuleFor(s => s.DownloadAllowed).Equal(true); PostValidator.RuleFor(s => s.Guid).NotEmpty(); _remoteEpisodeCache = cacheManager.GetCache(GetType(), "remoteEpisodes"); @@ -70,7 +70,7 @@ namespace NzbDrone.Api.Indexers try { - _downloadService.DownloadReport(remoteMovie); + _downloadService.DownloadReport(remoteMovie, false); } catch (ReleaseDownloadException ex) { diff --git a/src/NzbDrone.Api/Indexers/ReleaseResource.cs b/src/NzbDrone.Api/Indexers/ReleaseResource.cs index ff76fd2a7..35c6f1adb 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseResource.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseResource.cs @@ -8,6 +8,7 @@ using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.DecisionEngine; using System.Linq; +using NzbDrone.Core.Datastore.Migration; namespace NzbDrone.Api.Indexers { @@ -29,8 +30,8 @@ namespace NzbDrone.Api.Indexers public bool FullSeason { get; set; } public int SeasonNumber { get; set; } public Language Language { get; set; } - public string AirDate { get; set; } - public string SeriesTitle { get; set; } + public int Year { get; set; } + public string MovieTitle { get; set; } public int[] EpisodeNumbers { get; set; } public int[] AbsoluteEpisodeNumbers { get; set; } public bool Approved { get; set; } @@ -43,8 +44,9 @@ namespace NzbDrone.Api.Indexers public string CommentUrl { get; set; } public string DownloadUrl { get; set; } public string InfoUrl { get; set; } - public bool DownloadAllowed { get; set; } + public MappingResultType MappingResult { get; set; } public int ReleaseWeight { get; set; } + public int SuspectedMovieId { get; set; } public IEnumerable IndexerFlags { get; set; } @@ -88,11 +90,12 @@ namespace NzbDrone.Api.Indexers var parsedEpisodeInfo = model.RemoteEpisode.ParsedEpisodeInfo; var remoteEpisode = model.RemoteEpisode; var torrentInfo = (model.RemoteEpisode.Release as TorrentInfo) ?? new TorrentInfo(); - var downloadAllowed = model.RemoteEpisode.DownloadAllowed; + var mappingResult = MappingResultType.Success; if (model.IsForMovie) { - downloadAllowed = model.RemoteMovie.DownloadAllowed; + mappingResult = model.RemoteMovie.MappingResult; var parsedMovieInfo = model.RemoteMovie.ParsedMovieInfo; + var movieId = model.RemoteMovie.Movie?.Id ?? 0; return new ReleaseResource { @@ -111,8 +114,8 @@ namespace NzbDrone.Api.Indexers //FullSeason = parsedMovieInfo.FullSeason, //SeasonNumber = parsedMovieInfo.SeasonNumber, Language = parsedMovieInfo.Language, - AirDate = "", - SeriesTitle = parsedMovieInfo.MovieTitle, + Year = parsedMovieInfo.Year, + MovieTitle = parsedMovieInfo.MovieTitle, EpisodeNumbers = new int[0], AbsoluteEpisodeNumbers = new int[0], Approved = model.Approved, @@ -125,8 +128,10 @@ namespace NzbDrone.Api.Indexers CommentUrl = releaseInfo.CommentUrl, DownloadUrl = releaseInfo.DownloadUrl, InfoUrl = releaseInfo.InfoUrl, - DownloadAllowed = downloadAllowed, + MappingResult = mappingResult, //ReleaseWeight + + SuspectedMovieId = movieId, MagnetUrl = torrentInfo.MagnetUrl, InfoHash = torrentInfo.InfoHash, @@ -161,8 +166,8 @@ namespace NzbDrone.Api.Indexers FullSeason = parsedEpisodeInfo.FullSeason, SeasonNumber = parsedEpisodeInfo.SeasonNumber, Language = parsedEpisodeInfo.Language, - AirDate = parsedEpisodeInfo.AirDate, - SeriesTitle = parsedEpisodeInfo.SeriesTitle, + //AirDate = parsedEpisodeInfo.AirDate, + //SeriesTitle = parsedEpisodeInfo.SeriesTitle, EpisodeNumbers = parsedEpisodeInfo.EpisodeNumbers, AbsoluteEpisodeNumbers = parsedEpisodeInfo.AbsoluteEpisodeNumbers, Approved = model.Approved, @@ -175,7 +180,7 @@ namespace NzbDrone.Api.Indexers CommentUrl = releaseInfo.CommentUrl, DownloadUrl = releaseInfo.DownloadUrl, InfoUrl = releaseInfo.InfoUrl, - DownloadAllowed = downloadAllowed, + //DownloadAllowed = downloadAllowed, //ReleaseWeight MagnetUrl = torrentInfo.MagnetUrl, diff --git a/src/NzbDrone.Api/Movies/AlternativeTitleModule.cs b/src/NzbDrone.Api/Movies/AlternativeTitleModule.cs new file mode 100644 index 000000000..45937114d --- /dev/null +++ b/src/NzbDrone.Api/Movies/AlternativeTitleModule.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Marr.Data; +using Nancy; +using NzbDrone.Api; +using NzbDrone.Api.Movie; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.MetadataSource.RadarrAPI; +using NzbDrone.Core.Movies.AlternativeTitles; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Api.Movie +{ + public class AlternativeTitleModule : NzbDroneRestModule + { + private readonly IAlternativeTitleService _altTitleService; + private readonly IMovieService _movieService; + private readonly IRadarrAPIClient _radarrApi; + private readonly IEventAggregator _eventAggregator; + + public AlternativeTitleModule(IAlternativeTitleService altTitleService, IMovieService movieService, IRadarrAPIClient radarrApi, IEventAggregator eventAggregator) + : base("/alttitle") + { + _altTitleService = altTitleService; + _movieService = movieService; + _radarrApi = radarrApi; + CreateResource = AddTitle; + GetResourceById = GetTitle; + _eventAggregator = eventAggregator; + } + + private int AddTitle(AlternativeTitleResource altTitle) + { + var title = altTitle.ToModel(); + var movie = _movieService.GetMovie(altTitle.MovieId); + var newTitle = _radarrApi.AddNewAlternativeTitle(title, movie.TmdbId); + + var addedTitle = _altTitleService.AddAltTitle(newTitle, movie); + _eventAggregator.PublishEvent(new MovieUpdatedEvent(movie)); + return addedTitle.Id; + } + + private AlternativeTitleResource GetTitle(int id) + { + return _altTitleService.GetById(id).ToResource(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Movies/AlternativeYearModule.cs b/src/NzbDrone.Api/Movies/AlternativeYearModule.cs new file mode 100644 index 000000000..9da0898ec --- /dev/null +++ b/src/NzbDrone.Api/Movies/AlternativeYearModule.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Marr.Data; +using Nancy; +using NzbDrone.Api; +using NzbDrone.Api.Movie; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.MetadataSource.RadarrAPI; +using NzbDrone.Core.Movies.AlternativeTitles; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Api.Movie +{ + public class AlternativeYearModule : NzbDroneRestModule + { + private readonly IMovieService _movieService; + private readonly IRadarrAPIClient _radarrApi; + private readonly ICached _yearCache; + private readonly IEventAggregator _eventAggregator; + + public AlternativeYearModule(IMovieService movieService, IRadarrAPIClient radarrApi, ICacheManager cacheManager, IEventAggregator eventAggregator) + : base("/altyear") + { + _movieService = movieService; + _radarrApi = radarrApi; + CreateResource = AddYear; + GetResourceById = GetYear; + _yearCache = cacheManager.GetCache(GetType(), "altYears"); + _eventAggregator = eventAggregator; + } + + private int AddYear(AlternativeYearResource altYear) + { + var id = new Random().Next(); + _yearCache.Set(id.ToString(), altYear.Year, TimeSpan.FromMinutes(1)); + var movie = _movieService.GetMovie(altYear.MovieId); + var newYear = _radarrApi.AddNewAlternativeYear(altYear.Year, movie.TmdbId); + movie.SecondaryYear = newYear.Year; + movie.SecondaryYearSourceId = newYear.SourceId; + _movieService.UpdateMovie(movie); + _eventAggregator.PublishEvent(new MovieUpdatedEvent(movie)); + return id; + } + + private AlternativeYearResource GetYear(int id) + { + return new AlternativeYearResource + { + Year = _yearCache.Find(id.ToString()) + }; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Movies/AlternativeYearResource.cs b/src/NzbDrone.Api/Movies/AlternativeYearResource.cs new file mode 100644 index 000000000..d145fd8ae --- /dev/null +++ b/src/NzbDrone.Api/Movies/AlternativeYearResource.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Api.REST; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Qualities; +using NzbDrone.Api.Series; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Movies.AlternativeTitles; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Api.Movie +{ + public class AlternativeYearResource : RestResource + { + public AlternativeYearResource() + { + + } + + //Todo: Sorters should be done completely on the client + //Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? + //Todo: We should get the entire Profile instead of ID and Name separately + + public int MovieId { get; set; } + public int Year { get; set; } + + //TODO: Add series statistics as a property of the series (instead of individual properties) + } + + /*public static class AlternativeYearResourceMapper + { + /*public static AlternativeYearResource ToResource(this AlternativeTitle model) + { + if (model == null) return null; + + AlternativeTitleResource resource = null; + + return new AlternativeTitleResource + { + Id = model.Id, + SourceType = model.SourceType, + MovieId = model.MovieId, + Title = model.Title, + SourceId = model.SourceId, + Votes = model.Votes, + VoteCount = model.VoteCount, + Language = model.Language + }; + } + + public static AlternativeTitle ToModel(this AlternativeTitleResource resource) + { + if (resource == null) return null; + + return new AlternativeTitle + { + Id = resource.Id, + SourceType = resource.SourceType, + MovieId = resource.MovieId, + Title = resource.Title, + SourceId = resource.SourceId, + Votes = resource.Votes, + VoteCount = resource.VoteCount, + Language = resource.Language + }; + } + + public static List ToResource(this IEnumerable movies) + { + return movies.Select(ToResource).ToList(); + } + }*/ +} diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index c2bf53e79..7f9dc925c 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -119,6 +119,9 @@ + + + @@ -240,7 +243,7 @@ - + diff --git a/src/NzbDrone.Api/Queue/QueueActionModule.cs b/src/NzbDrone.Api/Queue/QueueActionModule.cs index 33ff98c87..5971d9b97 100644 --- a/src/NzbDrone.Api/Queue/QueueActionModule.cs +++ b/src/NzbDrone.Api/Queue/QueueActionModule.cs @@ -105,7 +105,7 @@ namespace NzbDrone.Api.Queue throw new NotFoundException(); } - _downloadService.DownloadReport(pendingRelease.RemoteMovie); + _downloadService.DownloadReport(pendingRelease.RemoteMovie, false); return resource.AsResponse(); } diff --git a/src/NzbDrone.Api/Series/AlternateTitleResource.cs b/src/NzbDrone.Api/Series/AlternateTitleResource.cs deleted file mode 100644 index b1d6cc22c..000000000 --- a/src/NzbDrone.Api/Series/AlternateTitleResource.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Api.Series -{ - public class AlternateTitleResource - { - public string Title { get; set; } - public int? SeasonNumber { get; set; } - public int? SceneSeasonNumber { get; set; } - } -} diff --git a/src/NzbDrone.Api/Series/AlternativeTitleResource.cs b/src/NzbDrone.Api/Series/AlternativeTitleResource.cs new file mode 100644 index 000000000..b0b538dc9 --- /dev/null +++ b/src/NzbDrone.Api/Series/AlternativeTitleResource.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Api.REST; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Qualities; +using NzbDrone.Api.Series; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Movies.AlternativeTitles; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Api.Movie +{ + public class AlternativeTitleResource : RestResource + { + public AlternativeTitleResource() + { + + } + + //Todo: Sorters should be done completely on the client + //Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? + //Todo: We should get the entire Profile instead of ID and Name separately + + public SourceType SourceType { get; set; } + public int MovieId { get; set; } + public string Title { get; set; } + public string CleanTitle { get; set; } + public int SourceId { get; set; } + public int Votes { get; set; } + public int VoteCount { get; set; } + public Language Language { get; set; } + + //TODO: Add series statistics as a property of the series (instead of individual properties) + } + + public static class AlternativeTitleResourceMapper + { + public static AlternativeTitleResource ToResource(this AlternativeTitle model) + { + if (model == null) return null; + + AlternativeTitleResource resource = null; + + return new AlternativeTitleResource + { + Id = model.Id, + SourceType = model.SourceType, + MovieId = model.MovieId, + Title = model.Title, + SourceId = model.SourceId, + Votes = model.Votes, + VoteCount = model.VoteCount, + Language = model.Language + }; + } + + public static AlternativeTitle ToModel(this AlternativeTitleResource resource) + { + if (resource == null) return null; + + return new AlternativeTitle + { + Id = resource.Id, + SourceType = resource.SourceType, + MovieId = resource.MovieId, + Title = resource.Title, + SourceId = resource.SourceId, + Votes = resource.Votes, + VoteCount = resource.VoteCount, + Language = resource.Language + }; + } + + public static List ToResource(this IEnumerable movies) + { + return movies.Select(ToResource).ToList(); + } + } +} diff --git a/src/NzbDrone.Api/Series/MovieResource.cs b/src/NzbDrone.Api/Series/MovieResource.cs index fcd71aa8f..4ef7c31bf 100644 --- a/src/NzbDrone.Api/Series/MovieResource.cs +++ b/src/NzbDrone.Api/Series/MovieResource.cs @@ -21,7 +21,9 @@ namespace NzbDrone.Api.Movie //View Only public string Title { get; set; } - public List AlternateTitles { get; set; } + public List AlternativeTitles { get; set; } + public int? SecondaryYear { get; set; } + public int SecondaryYearSourceId { get; set; } public string SortTitle { get; set; } public long? SizeOnDisk { get; set; } public MovieStatusType Status { get; set; } @@ -62,7 +64,7 @@ namespace NzbDrone.Api.Movie public DateTime Added { get; set; } public AddMovieOptions AddOptions { get; set; } public Ratings Ratings { get; set; } - public List AlternativeTitles { get; set; } + //public List AlternativeTitles { get; set; } public MovieFileResource MovieFile { get; set; } //TODO: Add series statistics as a property of the series (instead of individual properties) @@ -107,6 +109,8 @@ namespace NzbDrone.Api.Movie downloaded = true; movieFile = model.MovieFile.Value.ToResource(); } + + //model.AlternativeTitles.LazyLoad(); return new MovieResource { @@ -131,6 +135,8 @@ namespace NzbDrone.Api.Movie Images = model.Images, Year = model.Year, + SecondaryYear = model.SecondaryYear, + SecondaryYearSourceId = model.SecondaryYearSourceId, Path = model.Path, ProfileId = model.ProfileId, @@ -156,7 +162,7 @@ namespace NzbDrone.Api.Movie Tags = model.Tags, Added = model.Added, AddOptions = model.AddOptions, - AlternativeTitles = model.AlternativeTitles, + AlternativeTitles = model.AlternativeTitles.ToResource(), Ratings = model.Ratings, MovieFile = movieFile, YouTubeTrailerId = model.YouTubeTrailerId, @@ -189,6 +195,8 @@ namespace NzbDrone.Api.Movie Images = resource.Images, Year = resource.Year, + SecondaryYear = resource.SecondaryYear, + SecondaryYearSourceId = resource.SecondaryYearSourceId, Path = resource.Path, ProfileId = resource.ProfileId, @@ -209,7 +217,7 @@ namespace NzbDrone.Api.Movie Tags = resource.Tags, Added = resource.Added, AddOptions = resource.AddOptions, - AlternativeTitles = resource.AlternativeTitles, + //AlternativeTitles = resource.AlternativeTitles, Ratings = resource.Ratings, YouTubeTrailerId = resource.YouTubeTrailerId, Studio = resource.Studio diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs index 75e02b249..5d0529be6 100644 --- a/src/NzbDrone.Api/Series/SeriesModule.cs +++ b/src/NzbDrone.Api/Series/SeriesModule.cs @@ -199,7 +199,7 @@ namespace NzbDrone.Api.Series if (mappings == null) return; - resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList(); + //resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList(); } public void Handle(EpisodeImportedEvent message) diff --git a/src/NzbDrone.Api/Series/SeriesResource.cs b/src/NzbDrone.Api/Series/SeriesResource.cs index 176377a86..198c6602c 100644 --- a/src/NzbDrone.Api/Series/SeriesResource.cs +++ b/src/NzbDrone.Api/Series/SeriesResource.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Api.Series //View Only public string Title { get; set; } - public List AlternateTitles { get; set; } + //public List AlternateTitles { get; set; } public string SortTitle { get; set; } public int SeasonCount diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs index 9673a3978..52736c9fb 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs @@ -263,7 +263,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests result.Should().HaveCount(1); - result.First().RemoteMovie.DownloadAllowed.Should().BeFalse(); + //result.First().RemoteMovie.DownloadAllowed.Should().BeFalse(); } [Test] @@ -278,7 +278,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests result.Should().HaveCount(1); - result.First().RemoteMovie.DownloadAllowed.Should().BeFalse(); + //result.First().RemoteMovie.DownloadAllowed.Should().BeFalse(); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs index 62802b35d..f77a2c30d 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs @@ -76,7 +76,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests decisions.Add(new DownloadDecision(remoteMovie)); Subject.ProcessDecisions(decisions); - Mocker.GetMock().Verify(v => v.DownloadReport(It.IsAny()), Times.Once()); + Mocker.GetMock().Verify(v => v.DownloadReport(It.IsAny(), false), Times.Once()); } [Test] @@ -89,7 +89,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests decisions.Add(new DownloadDecision(remoteMovie)); Subject.ProcessDecisions(decisions); - Mocker.GetMock().Verify(v => v.DownloadReport(It.IsAny()), Times.Once()); + Mocker.GetMock().Verify(v => v.DownloadReport(It.IsAny(), false), Times.Once()); } [Test] @@ -157,7 +157,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests var decisions = new List(); decisions.Add(new DownloadDecision(remoteMovie)); - Mocker.GetMock().Setup(s => s.DownloadReport(It.IsAny())).Throws(new Exception()); + Mocker.GetMock().Setup(s => s.DownloadReport(It.IsAny(), false)).Throws(new Exception()); Subject.ProcessDecisions(decisions).Grabbed.Should().BeEmpty(); ExceptionVerification.ExpectedWarns(1); } @@ -183,7 +183,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests decisions.Add(new DownloadDecision(remoteMovie)); Subject.ProcessDecisions(decisions); - Mocker.GetMock().Verify(v => v.DownloadReport(It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(v => v.DownloadReport(It.IsAny(), false), Times.Never()); } [Test] diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs index 2b36a1bc7..4d3190aaf 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs @@ -6,7 +6,9 @@ using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Core.DataAugmentation.Scene; +using NzbDrone.Core.Datastore; using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Movies.AlternativeTitles; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; @@ -43,7 +45,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests .With(m => m.Title = "Fack Ju Göthe 2") .With(m => m.CleanTitle = "fackjugoethe2") .With(m => m.Year = 2015) - .With(m => m.AlternativeTitles = new List { "Fack Ju Göthe 2: Same same" }) + .With(m => m.AlternativeTitles = new LazyList( new List {new AlternativeTitle("Fack Ju Göthe 2: Same same")})) .Build(); _episodes = Builder.CreateListOfSize(1) @@ -80,7 +82,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests _alternativeTitleInfo = new ParsedMovieInfo { - MovieTitle = _movie.AlternativeTitles.First(), + MovieTitle = _movie.AlternativeTitles.First().Title, Year = _movie.Year, }; diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index 95d3c0ae6..2d0d603e9 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -307,6 +307,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Movie.Title.2016.1080p.KORSUB.WEBRip.x264.AAC2.0-RADARR", "korsub")] [TestCase("Movie.Title.2016.1080p.KORSUBS.WEBRip.x264.AAC2.0-RADARR", "korsubs")] + [TestCase("Wonder Woman 2017 HC 720p HDRiP DD5 1 x264-LEGi0N", "Generic Hardcoded Subs")] public void should_parse_hardcoded_subs(string postTitle, string sub) { QualityParser.ParseQuality(postTitle).HardcodedSubs.Should().Be(sub); diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index db36edc1e..88a78536e 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Data; using System.Linq; using System.Linq.Expressions; +using System.Web.Hosting; using Marr.Data; using Marr.Data.QGen; +using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Extensions; @@ -48,7 +50,7 @@ namespace NzbDrone.Core.Datastore _eventAggregator = eventAggregator; } - protected QueryBuilder Query => DataMapper.Query(); + protected QueryBuilder Query => AddJoinQueries(DataMapper.Query()); protected void Delete(Expression> filter) { @@ -246,18 +248,23 @@ namespace NzbDrone.Core.Datastore public virtual PagingSpec GetPaged(PagingSpec pagingSpec) { - pagingSpec.Records = GetPagedQuery(Query, pagingSpec).ToList(); + pagingSpec.Records = GetPagedQuery(Query, pagingSpec).Skip(pagingSpec.PagingOffset()) + .Take(pagingSpec.PageSize).ToList(); pagingSpec.TotalRecords = GetPagedQuery(Query, pagingSpec).GetRowCount(); + var queryStr = GetPagedQuery(Query, pagingSpec).BuildQuery(); + var beforeQuery = Query.BuildQuery(); + + pagingSpec.SortKey = beforeQuery; + pagingSpec.SortKey = queryStr; + return pagingSpec; } protected virtual SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) { return query.Where(pagingSpec.FilterExpression) - .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) - .Skip(pagingSpec.PagingOffset()) - .Take(pagingSpec.PageSize); + .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()); } protected void ModelCreated(TModel model) @@ -283,6 +290,11 @@ namespace NzbDrone.Core.Datastore } } + protected virtual QueryBuilder AddJoinQueries(QueryBuilder baseQuery) + { + return baseQuery; + } + protected virtual bool PublishModelEvents => false; } } diff --git a/src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs index 7c5669c99..9374e0b97 100644 --- a/src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs @@ -28,12 +28,12 @@ namespace NzbDrone.Core.Datastore.Extensions return mapBuilder.Relationships.AutoMapComplexTypeProperties(); } - public static RelationshipBuilder HasMany(this RelationshipBuilder relationshipBuilder, Expression>> portalExpression, Func childIdSelector) + public static RelationshipBuilder HasMany(this RelationshipBuilder relationshipBuilder, Expression>> portalExpression, Func parentIdSelector) where TParent : ModelBase where TChild : ModelBase { return relationshipBuilder.For(portalExpression.GetMemberName()) - .LazyLoad((db, parent) => db.Query().Where(c => c.Id == childIdSelector(parent)).ToList()); + .LazyLoad((db, parent) => db.Query().Where(c => parentIdSelector(c) == parent.Id).ToList()); } private static string GetMemberName(this Expression> member) diff --git a/src/NzbDrone.Core/Datastore/Migration/140_add_alternative_titles_table.cs b/src/NzbDrone.Core/Datastore/Migration/140_add_alternative_titles_table.cs new file mode 100644 index 000000000..41fc0bd5d --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/140_add_alternative_titles_table.cs @@ -0,0 +1,72 @@ +using System.Data; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.Text; +using System.Collections.Generic; +using System.Collections; +using System.Linq; +using System.Text.RegularExpressions; +using System.Globalization; +using Marr.Data.QGen; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(140)] + public class add_alternative_titles_table : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + if (!this.Schema.Schema("dbo").Table("alternative_titles").Exists()) + { + Create.TableForModel("AlternativeTitles") + .WithColumn("MovieId").AsInt64().NotNullable() + .WithColumn("Title").AsString().NotNullable() + .WithColumn("CleanTitle").AsString().NotNullable() + .WithColumn("SourceType").AsInt64().WithDefault(0) + .WithColumn("SourceId").AsInt64().WithDefault(0) + .WithColumn("Votes").AsInt64().WithDefault(0) + .WithColumn("VoteCount").AsInt64().WithDefault(0) + .WithColumn("Language").AsInt64().WithDefault(0); + + Delete.Column("AlternativeTitles").FromTable("Movies"); + } + + Alter.Table("Movies").AddColumn("SecondaryYear").AsInt32().Nullable(); + Alter.Table("Movies").AddColumn("SecondaryYearSourceId").AsInt64().Nullable().WithDefault(0); + + Execute.WithConnection(AddExisting); + } + + private void AddExisting(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand getSeriesCmd = conn.CreateCommand()) + { + getSeriesCmd.Transaction = tran; + getSeriesCmd.CommandText = @"SELECT Key, Value FROM Config WHERE Key = 'importexclusions'"; + TextInfo textInfo = new CultureInfo("en-US", false).TextInfo; + using (IDataReader seriesReader = getSeriesCmd.ExecuteReader()) + { + while (seriesReader.Read()) + { + var Key = seriesReader.GetString(0); + var Value = seriesReader.GetString(1); + + var importExclusions = Value.Split(',').Select(x => { + return string.Format("(\"{0}\", \"{1}\")", Regex.Replace(x, @"^.*\-(.*)$", "$1"), + textInfo.ToTitleCase(string.Join(" ", x.Split('-').DropLast(1)))); + }).ToList(); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "INSERT INTO ImportExclusions (tmdbid, MovieTitle) VALUES " + string.Join(", ", importExclusions); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 5ee687fe3..46d381e0e 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -36,6 +36,45 @@ using NzbDrone.Core.Extras.Subtitles; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.NetImport; using NzbDrone.Core.NetImport.ImportExclusions; +using System; +using System.Collections.Generic; +using Marr.Data; +using Marr.Data.Mapping; +using NzbDrone.Common.Reflection; +using NzbDrone.Core.Blacklisting; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.DataAugmentation.Scene; +using NzbDrone.Core.Datastore.Converters; +using NzbDrone.Core.Datastore.Extensions; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Instrumentation; +using NzbDrone.Core.Jobs; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Profiles.Delay; +using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Core.Notifications; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Restrictions; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.SeriesStats; +using NzbDrone.Core.Tags; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Extras.Metadata; +using NzbDrone.Core.Extras.Metadata.Files; +using NzbDrone.Core.Extras.Others; +using NzbDrone.Core.Extras.Subtitles; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Movies.AlternativeTitles; +using NzbDrone.Core.NetImport; +using NzbDrone.Core.NetImport.ImportExclusions; namespace NzbDrone.Core.Datastore { @@ -101,12 +140,19 @@ namespace NzbDrone.Core.Datastore query: (db, parent) => db.Query().Where(c => c.MovieFileId == parent.Id).ToList()) .HasOne(file => file.Movie, file => file.MovieId); - Mapper.Entity().RegisterModel("Movies") + Mapper.Entity().RegisterModel("Movies") .Ignore(s => s.RootFolderPath) .Relationship() .HasOne(s => s.Profile, s => s.ProfileId) .HasOne(m => m.MovieFile, m => m.MovieFileId); + Mapper.Entity().RegisterModel("AlternativeTitles") + .For(t => t.Id) + .SetAltName("AltTitle_Id") + .Relationship() + .HasOne(t => t.Movie, t => t.MovieId); + + Mapper.Entity().RegisterModel("ImportExclusions"); diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index ad345c2c9..b5c8726cc 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -113,11 +113,11 @@ namespace NzbDrone.Core.DecisionEngine var remoteMovie = result.RemoteMovie; remoteMovie.Release = report; + remoteMovie.MappingResult = result.MappingResultType; if (result.MappingResultType != MappingResultType.Success && result.MappingResultType != MappingResultType.SuccessLenientMapping) { var rejection = result.ToRejection(); - remoteMovie.Movie = null; // HACK: For now! decision = new DownloadDecision(remoteMovie, rejection); } @@ -125,7 +125,7 @@ namespace NzbDrone.Core.DecisionEngine { if (parsedMovieInfo.Quality.HardcodedSubs.IsNotNullOrWhiteSpace()) { - remoteMovie.DownloadAllowed = true; + //remoteMovie.DownloadAllowed = true; if (_configService.AllowHardcodedSubs) { decision = GetDecisionForReport(remoteMovie, searchCriteria); @@ -146,7 +146,7 @@ namespace NzbDrone.Core.DecisionEngine } else { - remoteMovie.DownloadAllowed = true; + //remoteMovie.DownloadAllowed = true; decision = GetDecisionForReport(remoteMovie, searchCriteria); } diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs index b6468ab1e..568641b3a 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser; namespace NzbDrone.Core.DecisionEngine { @@ -36,13 +37,13 @@ namespace NzbDrone.Core.DecisionEngine public List PrioritizeDecisionsForMovies(List decisions) { - return decisions.Where(c => c.RemoteMovie.Movie != null) + return decisions.Where(c => c.RemoteMovie.MappingResult == MappingResultType.Success || c.RemoteMovie.MappingResult == MappingResultType.SuccessLenientMapping) .GroupBy(c => c.RemoteMovie.Movie.Id, (movieId, downloadDecisions) => { return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_delayProfileService, _configService)); }) .SelectMany(c => c) - .Union(decisions.Where(c => c.RemoteMovie.Movie == null)) + .Union(decisions.Where(c => c.RemoteMovie.MappingResult != MappingResultType.Success || c.RemoteMovie.MappingResult != MappingResultType.SuccessLenientMapping)) .ToList(); } } diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index c8068c044..a0b6df3e9 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.Download public interface IDownloadService { void DownloadReport(RemoteEpisode remoteEpisode); - void DownloadReport(RemoteMovie remoteMovie); + void DownloadReport(RemoteMovie remoteMovie, bool forceDownload); } @@ -92,7 +92,7 @@ namespace NzbDrone.Core.Download _eventAggregator.PublishEvent(episodeGrabbedEvent); } - public void DownloadReport(RemoteMovie remoteMovie) + public void DownloadReport(RemoteMovie remoteMovie, bool foceDownload = false) { //Ensure.That(remoteEpisode.Series, () => remoteEpisode.Series).IsNotNull(); //Ensure.That(remoteEpisode.Episodes, () => remoteEpisode.Episodes).HasItems(); TODO update this shit diff --git a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs index 5b0b410a3..64a7a58f5 100644 --- a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs +++ b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs @@ -88,7 +88,7 @@ namespace NzbDrone.Core.Download try { - _downloadService.DownloadReport(remoteMovie); + _downloadService.DownloadReport(remoteMovie, false); grabbed.Add(report); } catch (Exception e) diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs index 8844b512a..4ceec5b9e 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Core.Indexers.Newznab else { var searchTitle = System.Web.HttpUtility.UrlPathEncode(Parser.Parser.ReplaceGermanUmlauts(Parser.Parser.NormalizeTitle(searchCriteria.Movie.Title))); - var altTitles = searchCriteria.Movie.AlternativeTitles.DistinctBy(t => Parser.Parser.CleanSeriesTitle(t)).Take(5).ToList(); + var altTitles = searchCriteria.Movie.AlternativeTitles.Take(5).Select(t => t.Title).ToList(); var realMaxPages = (int)MaxPages / (altTitles.Count() + 1); diff --git a/src/NzbDrone.Core/MetadataSource/RadarrAPI/RadarrAPIClient.cs b/src/NzbDrone.Core/MetadataSource/RadarrAPI/RadarrAPIClient.cs index 3f6ebdd62..70b88f4f1 100644 --- a/src/NzbDrone.Core/MetadataSource/RadarrAPI/RadarrAPIClient.cs +++ b/src/NzbDrone.Core/MetadataSource/RadarrAPI/RadarrAPIClient.cs @@ -3,7 +3,10 @@ using NzbDrone.Core.Configuration; using System; using Newtonsoft.Json; using System.Collections.Generic; +using System.Linq; using NzbDrone.Core.MetadataSource.SkyHook.Resource; +using NzbDrone.Core.Movies.AlternativeTitles; +using NzbDrone.Core.Parser; namespace NzbDrone.Core.MetadataSource.RadarrAPI { @@ -11,6 +14,10 @@ namespace NzbDrone.Core.MetadataSource.RadarrAPI { IHttpRequestBuilderFactory RadarrAPI { get; } List DiscoverMovies(string action, Func enhanceRequest); + List AlternativeTitlesForMovie(int TmdbId); + Tuple, AlternativeYear> AlternativeTitlesAndYearForMovie(int tmdbId); + AlternativeTitle AddNewAlternativeTitle(AlternativeTitle title, int TmdbId); + AlternativeYear AddNewAlternativeYear(int year, int tmdbId); string APIURL { get; } } @@ -65,7 +72,7 @@ namespace NzbDrone.Core.MetadataSource.RadarrAPI { var error = JsonConvert.DeserializeObject(response.Content); - if (error != null && error.Errors.Count != 0) + if (error != null && error.Errors != null && error.Errors.Count != 0) { throw new RadarrAPIException(error); } @@ -96,6 +103,83 @@ namespace NzbDrone.Core.MetadataSource.RadarrAPI return Execute>(request); } + + public List AlternativeTitlesForMovie(int TmdbId) + { + var request = RadarrAPI.Create().SetSegment("route", "mappings").SetSegment("action", "find").AddQueryParam("tmdbid", TmdbId).Build(); + + var mappings = Execute(request); + + var titles = new List(); + + foreach (var altTitle in mappings.Mappings.Titles) + { + titles.Add(new NzbDrone.Core.Movies.AlternativeTitles.AlternativeTitle(altTitle.Info.AkaTitle, SourceType.Mappings, altTitle.Id)); + } + + return titles; + } + + public Tuple, AlternativeYear> AlternativeTitlesAndYearForMovie(int tmdbId) + { + var request = RadarrAPI.Create().SetSegment("route", "mappings").SetSegment("action", "find").AddQueryParam("tmdbid", tmdbId).Build(); + + var mappings = Execute(request); + + var titles = new List(); + + foreach (var altTitle in mappings.Mappings.Titles) + { + titles.Add(new NzbDrone.Core.Movies.AlternativeTitles.AlternativeTitle(altTitle.Info.AkaTitle, SourceType.Mappings, altTitle.Id)); + } + + var year = mappings.Mappings.Years.Where(y => y.Votes >= 3).OrderBy(y => y.Votes).FirstOrDefault(); + + AlternativeYear newYear = null; + + if (year != null) + { + newYear = new AlternativeYear + { + Year = year.Info.AkaYear, + SourceId = year.Id + }; + } + + return new Tuple, AlternativeYear>(titles, newYear); + } + + public AlternativeTitle AddNewAlternativeTitle(AlternativeTitle title, int TmdbId) + { + var request = RadarrAPI.Create().SetSegment("route", "mappings").SetSegment("action", "add") + .AddQueryParam("tmdbid", TmdbId).AddQueryParam("type", "title") + .AddQueryParam("language", IsoLanguages.Get(title.Language).TwoLetterCode) + .AddQueryParam("aka_title", title.Title).Build(); + + var newMapping = Execute(request); + + var newTitle = new AlternativeTitle(newMapping.Info.AkaTitle, SourceType.Mappings, newMapping.Id, title.Language); + newTitle.VoteCount = newMapping.VoteCount; + newTitle.Votes = newMapping.Votes; + + return newTitle; + } + + public AlternativeYear AddNewAlternativeYear(int year, int tmdbId) + { + var request = RadarrAPI.Create().SetSegment("route", "mappings").SetSegment("action", "add") + .AddQueryParam("tmdbid", tmdbId).AddQueryParam("type", "year") + .AddQueryParam("aka_year", year).Build(); + + var newYear = Execute(request); + + return new AlternativeYear + { + Year = newYear.Info.AkaYear, + SourceId = newYear.Id + }; + } + public IHttpRequestBuilderFactory RadarrAPI { get; private set; } } } diff --git a/src/NzbDrone.Core/MetadataSource/RadarrAPI/RadarrResources.cs b/src/NzbDrone.Core/MetadataSource/RadarrAPI/RadarrResources.cs index 88c068cb6..0e071b055 100644 --- a/src/NzbDrone.Core/MetadataSource/RadarrAPI/RadarrResources.cs +++ b/src/NzbDrone.Core/MetadataSource/RadarrAPI/RadarrResources.cs @@ -27,21 +27,183 @@ namespace NzbDrone.Core.MetadataSource.RadarrAPI public class RadarrAPIException : Exception { - RadarrError APIErrors; + public RadarrError APIErrors; public RadarrAPIException(RadarrError apiError) : base(HumanReadable(apiError)) { - + APIErrors = apiError; } - private static string HumanReadable(RadarrError APIErrors) + private static string HumanReadable(RadarrError apiErrors) { - var firstError = APIErrors.Errors.First(); - var details = string.Join("\n", APIErrors.Errors.Select(error => + var firstError = apiErrors.Errors.First(); + var details = string.Join("\n", apiErrors.Errors.Select(error => { return $"{error.Title} ({error.Status}, RayId: {error.RayId}), Details: {error.Detail}"; })); return $"Error while calling api: {firstError.Title}\nFull error(s): {details}"; } } + + public class TitleInfo + { + + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("aka_title")] + public string AkaTitle { get; set; } + + [JsonProperty("aka_clean_title")] + public string AkaCleanTitle { get; set; } + } + + public class YearInfo + { + + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("aka_year")] + public int AkaYear { get; set; } + } + + public class Title + { + + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("tmdbid")] + public int Tmdbid { get; set; } + + [JsonProperty("votes")] + public int Votes { get; set; } + + [JsonProperty("vote_count")] + public int VoteCount { get; set; } + + [JsonProperty("locked")] + public bool Locked { get; set; } + + [JsonProperty("info_type")] + public string InfoType { get; set; } + + [JsonProperty("info_id")] + public int InfoId { get; set; } + + [JsonProperty("info")] + public TitleInfo Info { get; set; } + } + + public class Year + { + + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("tmdbid")] + public int Tmdbid { get; set; } + + [JsonProperty("votes")] + public int Votes { get; set; } + + [JsonProperty("vote_count")] + public int VoteCount { get; set; } + + [JsonProperty("locked")] + public bool Locked { get; set; } + + [JsonProperty("info_type")] + public string InfoType { get; set; } + + [JsonProperty("info_id")] + public int InfoId { get; set; } + + [JsonProperty("info")] + public YearInfo Info { get; set; } + } + + public class Mappings + { + + [JsonProperty("titles")] + public IList Titles { get; set; } + + [JsonProperty("years")] + public IList<Year> Years { get; set; } + } + + public class Mapping + { + + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("imdb_id")] + public string ImdbId { get; set; } + + [JsonProperty("mappings")] + public Mappings Mappings { get; set; } + } + + public class AddTitleMapping + { + + [JsonProperty("tmdbid")] + public string Tmdbid { get; set; } + + [JsonProperty("info_type")] + public string InfoType { get; set; } + + [JsonProperty("info_id")] + public int InfoId { get; set; } + + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("info")] + public TitleInfo Info { get; set; } + + [JsonProperty("votes")] + public int Votes { get; set; } + + [JsonProperty("vote_count")] + public int VoteCount { get; set; } + + [JsonProperty("locked")] + public bool Locked { get; set; } + } + + public class AddYearMapping + { + + [JsonProperty("tmdbid")] + public string Tmdbid { get; set; } + + [JsonProperty("info_type")] + public string InfoType { get; set; } + + [JsonProperty("info_id")] + public int InfoId { get; set; } + + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("info")] + public YearInfo Info { get; set; } + + [JsonProperty("votes")] + public int Votes { get; set; } + + [JsonProperty("vote_count")] + public int VoteCount { get; set; } + + [JsonProperty("locked")] + public bool Locked { get; set; } + } + } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 7ad1f82be..c7652f406 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -2,7 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using NLog; + using System.ServiceModel; + using NLog; using NzbDrone.Common.Cloud; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; @@ -19,6 +20,7 @@ using NzbDrone.Common.Serializer; using NzbDrone.Core.NetImport.ImportExclusions; using NzbDrone.Core.Configuration; using NzbDrone.Core.MetadataSource.RadarrAPI; + using NzbDrone.Core.Movies.AlternativeTitles; namespace NzbDrone.Core.MetadataSource.SkyHook { @@ -33,12 +35,13 @@ namespace NzbDrone.Core.MetadataSource.SkyHook private readonly IMovieService _movieService; private readonly IPreDBService _predbService; private readonly IImportExclusionsService _exclusionService; + private readonly IAlternativeTitleService _altTitleService; private readonly IRadarrAPIClient _radarrAPI; private readonly IHttpRequestBuilderFactory _apiBuilder; public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, ITmdbConfigService configService, IMovieService movieService, - IPreDBService predbService, IImportExclusionsService exclusionService, IRadarrAPIClient radarrAPI, Logger logger) + IPreDBService predbService, IImportExclusionsService exclusionService, IAlternativeTitleService altTitleService, IRadarrAPIClient radarrAPI, Logger logger) { _httpClient = httpClient; _requestBuilder = requestBuilder.SkyHookTvdb; @@ -47,6 +50,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook _movieService = movieService; _predbService = predbService; _exclusionService = exclusionService; + _altTitleService = altTitleService; _radarrAPI = radarrAPI; _logger = logger; @@ -133,21 +137,28 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } var movie = new Movie(); + var altTitles = new List<AlternativeTitle>(); - if (langCode != "us") + if (langCode != "en") { - movie.AlternativeTitles.Add(resource.original_title); + var iso = IsoLanguages.Find(resource.original_language); + if (iso != null) + { + altTitles.Add(new AlternativeTitle(resource.original_title, SourceType.TMDB, TmdbId, iso.Language)); + } + + //movie.AlternativeTitles.Add(resource.original_title); } foreach (var alternativeTitle in resource.alternative_titles.titles) { if (alternativeTitle.iso_3166_1.ToLower() == langCode) { - movie.AlternativeTitles.Add(alternativeTitle.title); + altTitles.Add(new AlternativeTitle(alternativeTitle.title, SourceType.TMDB, TmdbId, IsoLanguages.Find(alternativeTitle.iso_3166_1.ToLower()).Language)); } else if (alternativeTitle.iso_3166_1.ToLower() == "us") { - movie.AlternativeTitles.Add(alternativeTitle.title); + altTitles.Add(new AlternativeTitle(alternativeTitle.title, SourceType.TMDB, TmdbId, Language.English)); } } @@ -321,6 +332,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } + movie.AlternativeTitles.AddRange(altTitles); + return movie; } diff --git a/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitle.cs b/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitle.cs new file mode 100644 index 000000000..75088d5d9 --- /dev/null +++ b/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitle.cs @@ -0,0 +1,77 @@ +using System; +using Marr.Data; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Movies.AlternativeTitles +{ + public class AlternativeTitle : ModelBase + { + public SourceType SourceType { get; set; } + public int MovieId { get; set; } + public string Title { get; set; } + public string CleanTitle { get; set; } + public int SourceId { get; set; } + public int Votes { get; set; } + public int VoteCount { get; set; } + public Language Language { get; set; } + public LazyLoaded<Movie> Movie { get; set; } + + public AlternativeTitle() + { + + } + + public AlternativeTitle(string title, SourceType sourceType = SourceType.TMDB, int sourceId = 0, Language language = Language.English) + { + Title = title; + CleanTitle = title.CleanSeriesTitle(); + SourceType = sourceType; + SourceId = sourceId; + Language = language; + } + + public bool IsTrusted(int minVotes = 3) + { + switch (SourceType) + { + case SourceType.TMDB: + return Votes >= minVotes; + default: + return true; + } + } + + public override bool Equals(object obj) + { + var item = obj as AlternativeTitle; + + if (item == null) + { + return false; + } + + return item.CleanTitle == this.CleanTitle; + } + + public override String ToString() + { + return Title; + } + } + + public enum SourceType + { + TMDB = 0, + Mappings = 1, + User = 2, + Indexer = 3 + } + + public class AlternativeYear + { + public int Year { get; set; } + public int SourceId { get; set; } + } +} diff --git a/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleRepository.cs b/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleRepository.cs new file mode 100644 index 000000000..5a711b872 --- /dev/null +++ b/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleRepository.cs @@ -0,0 +1,21 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Movies.AlternativeTitles +{ + public interface IAlternativeTitleRepository : IBasicRepository<AlternativeTitle> + { + + } + + public class AlternativeTitleRepository : BasicRepository<AlternativeTitle>, IAlternativeTitleRepository + { + protected IMainDatabase _database; + + public AlternativeTitleRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + _database = database; + } + } +} diff --git a/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleService.cs b/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleService.cs new file mode 100644 index 000000000..aee69c0d2 --- /dev/null +++ b/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleService.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.DataAugmentation.Scene; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Core.Movies.AlternativeTitles +{ + public interface IAlternativeTitleService + { + List<AlternativeTitle> GetAllTitlesForMovie(Movie movie); + AlternativeTitle AddAltTitle(AlternativeTitle title, Movie movie); + List<AlternativeTitle> AddAltTitles(List<AlternativeTitle> titles, Movie movie); + AlternativeTitle GetById(int id); + } + + public class AlternativeTitleService : IAlternativeTitleService + { + private readonly IAlternativeTitleRepository _titleRepo; + private readonly IConfigService _configService; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + + public AlternativeTitleService(IAlternativeTitleRepository titleRepo, + IEventAggregator eventAggregator, + IConfigService configService, + Logger logger) + { + _titleRepo = titleRepo; + _eventAggregator = eventAggregator; + _configService = configService; + _logger = logger; + } + + public List<AlternativeTitle> GetAllTitlesForMovie(Movie movie) + { + return _titleRepo.All().ToList(); + } + + public AlternativeTitle AddAltTitle(AlternativeTitle title, Movie movie) + { + title.MovieId = movie.Id; + return _titleRepo.Insert(title); + } + + public List<AlternativeTitle> AddAltTitles(List<AlternativeTitle> titles, Movie movie) + { + titles.ForEach(t => t.MovieId = movie.Id); + _titleRepo.InsertMany(titles); + return titles; + } + + public AlternativeTitle GetById(int id) + { + return _titleRepo.Get(id); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index a77da315c..6a3d474cc 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -125,6 +125,7 @@ <Compile Include="Authentication\UserRepository.cs" /> <Compile Include="Authentication\UserService.cs" /> <Compile Include="Datastore\Migration\123_create_netimport_table.cs" /> + <Compile Include="Datastore\Migration\140_add_alternative_titles_table.cs" /> <Compile Include="MediaFiles\Events\MovieFileUpdatedEvent.cs" /> <Compile Include="Datastore\Migration\134_add_remux_qualities_for_the_wankers.cs" /> <Compile Include="Datastore\Migration\129_add_parsed_movie_info_to_pending_release.cs" /> @@ -134,6 +135,9 @@ <Compile Include="Datastore\Migration\133_add_minimumavailability.cs" /> <Compile Include="IndexerSearch\CutoffUnmetMoviesSearchCommand.cs" /> <Compile Include="Indexers\HDBits\HDBitsInfo.cs" /> + <Compile Include="Movies\AlternativeTitles\AlternativeTitle.cs" /> + <Compile Include="Movies\AlternativeTitles\AlternativeTitleRepository.cs" /> + <Compile Include="Movies\AlternativeTitles\AlternativeTitleService.cs" /> <Compile Include="NetImport\NetImportListLevels.cs" /> <Compile Include="NetImport\TMDb\TMDbLanguageCodes.cs" /> <Compile Include="NetImport\TMDb\TMDbSettings.cs" /> diff --git a/src/NzbDrone.Core/Parser/Model/RemoteMovie.cs b/src/NzbDrone.Core/Parser/Model/RemoteMovie.cs index 1e6f5f5cc..8a2145b06 100644 --- a/src/NzbDrone.Core/Parser/Model/RemoteMovie.cs +++ b/src/NzbDrone.Core/Parser/Model/RemoteMovie.cs @@ -11,7 +11,7 @@ namespace NzbDrone.Core.Parser.Model public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } //TODO: Change to ParsedMovieInfo, for now though ParsedEpisodeInfo will do. public ParsedMovieInfo ParsedMovieInfo { get; set; } public Movie Movie { get; set; } - public bool DownloadAllowed { get; set; } + public MappingResultType MappingResult { get; set; } public override string ToString() { diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 72d7e4d57..83ebf29a3 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Movies.AlternativeTitles; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.RomanNumerals; using NzbDrone.Core.Tv; @@ -381,7 +382,7 @@ namespace NzbDrone.Core.Parser { var movie = _movieService.FindByImdbId(imdbId); //Should fix practically all problems, where indexer is shite at adding correct imdbids to movies. - if (movie != null && parsedMovieInfo.Year > 1800 && parsedMovieInfo.Year != movie.Year) + if (movie != null && parsedMovieInfo.Year > 1800 && (parsedMovieInfo.Year != movie.Year && movie.SecondaryYear != parsedMovieInfo.Year)) { result = new MappingResult { Movie = movie, MappingResultType = MappingResultType.WrongYear}; return false; @@ -458,9 +459,9 @@ namespace NzbDrone.Core.Parser possibleTitles.Add(searchCriteria.Movie.CleanTitle); - foreach (string altTitle in searchCriteria.Movie.AlternativeTitles) + foreach (AlternativeTitle altTitle in searchCriteria.Movie.AlternativeTitles) { - possibleTitles.Add(altTitle.CleanSeriesTitle()); + possibleTitles.Add(altTitle.CleanTitle); } string cleanTitle = parsedMovieInfo.MovieTitle.CleanSeriesTitle(); @@ -494,7 +495,7 @@ namespace NzbDrone.Core.Parser if (possibleMovie != null) { - if (parsedMovieInfo.Year < 1800 || possibleMovie.Year == parsedMovieInfo.Year) + if (parsedMovieInfo.Year < 1800 || possibleMovie.Year == parsedMovieInfo.Year || possibleMovie.SecondaryYear == parsedMovieInfo.Year) { result = new MappingResult { Movie = possibleMovie, MappingResultType = MappingResultType.Success }; return true; @@ -509,7 +510,7 @@ namespace NzbDrone.Core.Parser cleanTitle.Contains(searchCriteria.Movie.CleanTitle)) { possibleMovie = searchCriteria.Movie; - if (parsedMovieInfo.Year > 1800 && parsedMovieInfo.Year == possibleMovie.Year) + if (parsedMovieInfo.Year > 1800 && parsedMovieInfo.Year == possibleMovie.Year || possibleMovie.SecondaryYear == parsedMovieInfo.Year) { result = new MappingResult {Movie = possibleMovie, MappingResultType = MappingResultType.SuccessLenientMapping}; return true; diff --git a/src/NzbDrone.Core/Tv/Movie.cs b/src/NzbDrone.Core/Tv/Movie.cs index 8bfd0d8c0..ae876d40c 100644 --- a/src/NzbDrone.Core/Tv/Movie.cs +++ b/src/NzbDrone.Core/Tv/Movie.cs @@ -6,6 +6,8 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Profiles; using NzbDrone.Core.MediaFiles; using System.IO; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.AlternativeTitles; namespace NzbDrone.Core.Tv { @@ -17,7 +19,7 @@ namespace NzbDrone.Core.Tv Genres = new List<string>(); Actors = new List<Actor>(); Tags = new HashSet<int>(); - AlternativeTitles = new List<string>(); + AlternativeTitles = new List<AlternativeTitle>(); } public int TmdbId { get; set; } public string ImdbId { get; set; } @@ -52,7 +54,10 @@ namespace NzbDrone.Core.Tv public LazyLoaded<MovieFile> MovieFile { get; set; } public bool HasPreDBEntry { get; set; } public int MovieFileId { get; set; } - public List<string> AlternativeTitles { get; set; } + //Get Loaded via a Join Query + public List<AlternativeTitle> AlternativeTitles { get; set; } + public int? SecondaryYear { get; set; } + public int SecondaryYearSourceId { get; set; } public string YouTubeTrailerId{ get; set; } public string Studio { get; set; } diff --git a/src/NzbDrone.Core/Tv/MovieRepository.cs b/src/NzbDrone.Core/Tv/MovieRepository.cs index 84447ee49..56af7b8f2 100644 --- a/src/NzbDrone.Core/Tv/MovieRepository.cs +++ b/src/NzbDrone.Core/Tv/MovieRepository.cs @@ -6,6 +6,7 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Datastore.Extensions; using Marr.Data.QGen; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Movies.AlternativeTitles; using NzbDrone.Core.Parser.RomanNumerals; using NzbDrone.Core.Qualities; using CoreParser = NzbDrone.Core.Parser.Parser; @@ -103,7 +104,7 @@ namespace NzbDrone.Core.Tv public override PagingSpec<Movie> GetPaged(PagingSpec<Movie> pagingSpec) { - if (pagingSpec.SortKey == "downloadedQuality") + /*if (pagingSpec.SortKey == "downloadedQuality") { var mapper = _database.GetDataMapper(); var offset = pagingSpec.PagingOffset(); @@ -113,7 +114,7 @@ namespace NzbDrone.Core.Tv { direction = "DESC"; } - var q = mapper.Query<Movie>($"SELECT * from \"Movies\" , \"MovieFiles\", \"QualityDefinitions\" WHERE Movies.MovieFileId=MovieFiles.Id AND instr(MovieFiles.Quality, ('quality\": ' || QualityDefinitions.Quality || \",\")) > 0 ORDER BY QualityDefinitions.Title {direction} LIMIT {offset},{limit};"); + var q = Query.Select($"SELECT * from \"Movies\" , \"MovieFiles\", \"QualityDefinitions\" WHERE Movies.MovieFileId=MovieFiles.Id AND instr(MovieFiles.Quality, ('quality\": ' || QualityDefinitions.Quality || \",\")) > 0 ORDER BY QualityDefinitions.Title {direction} LIMIT {offset},{limit};"); var q2 = mapper.Query<Movie>("SELECT * from \"Movies\" , \"MovieFiles\", \"QualityDefinitions\" WHERE Movies.MovieFileId=MovieFiles.Id AND instr(MovieFiles.Quality, ('quality\": ' || QualityDefinitions.Quality || \",\")) > 0 ORDER BY QualityDefinitions.Title ASC;"); //var ok = q.BuildQuery(); @@ -122,9 +123,11 @@ namespace NzbDrone.Core.Tv pagingSpec.TotalRecords = q2.Count(); } - else + else*/ { pagingSpec = base.GetPaged(pagingSpec); + //pagingSpec.Records = GetPagedQuery(Query, pagingSpec).ToList(); + //pagingSpec.TotalRecords = GetPagedQuery(Query, pagingSpec).GetRowCount(); } if (pagingSpec.Records.Count == 0 && pagingSpec.Page != 1) @@ -136,6 +139,22 @@ namespace NzbDrone.Core.Tv return pagingSpec; } + + /*protected override SortBuilder<Movie> GetPagedQuery(QueryBuilder<Movie> query, PagingSpec<Movie> pagingSpec) + { + return DataMapper.Query<Movie>().Join<Movie, AlternativeTitle>(JoinType.Left, m => m.AlternativeTitles, + (m, t) => m.Id == t.MovieId).Where(pagingSpec.FilterExpression) + .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) + .Skip(pagingSpec.PagingOffset()) + .Take(pagingSpec.PageSize); + }*/ + + /*protected override SortBuilder<Movie> GetPagedQuery(QueryBuilder<Movie> query, PagingSpec<Movie> pagingSpec) + { + var newQuery = base.GetPagedQuery(query.Join<Movie, AlternativeTitle>(JoinType.Left, m => m.JoinAlternativeTitles, (movie, title) => title.MovieId == movie.Id), pagingSpec); + System.Console.WriteLine(newQuery.ToString()); + return newQuery; + }*/ public SortBuilder<Movie> GetMoviesWithoutFilesQuery(PagingSpec<Movie> pagingSpec) { @@ -247,22 +266,39 @@ namespace NzbDrone.Core.Tv if (result == null) { - IEnumerable<Movie> movies = All(); + /*IEnumerable<Movie> movies = All(); Func<string, string> titleCleaner = title => CoreParser.CleanSeriesTitle(title.ToLower()); - Func<IEnumerable<string>, string, bool> altTitleComparer = + Func<IEnumerable<AlternativeTitle>, string, bool> altTitleComparer = (alternativeTitles, atitle) => - alternativeTitles.Any(altTitle => titleCleaner(altTitle) == atitle); + alternativeTitles.Any(altTitle => altTitle.CleanTitle == atitle);*/ - result = movies.Where(m => altTitleComparer(m.AlternativeTitles, cleanTitle) || + /*result = movies.Where(m => altTitleComparer(m.AlternativeTitles, cleanTitle) || altTitleComparer(m.AlternativeTitles, cleanTitleWithRomanNumbers) || - altTitleComparer(m.AlternativeTitles, cleanTitleWithArabicNumbers)).FirstWithYear(year); + altTitleComparer(m.AlternativeTitles, cleanTitleWithArabicNumbers)).FirstWithYear(year);*/ + + //result = Query.Join<Movie, AlternativeTitle>(JoinType.Inner, m => m._newAltTitles, + //(m, t) => m.Id == t.MovieId && (t.CleanTitle == cleanTitle)).FirstWithYear(year); + result = Query.Where<AlternativeTitle>(t => + t.CleanTitle == cleanTitle || t.CleanTitle == cleanTitleWithArabicNumbers + || t.CleanTitle == cleanTitleWithRomanNumbers).FirstWithYear(year); } } return result; /*return year.HasValue ? results?.FirstOrDefault(movie => movie.Year == year.Value) - : results?.FirstOrDefault();*/ + + + : results?.FirstOrDefault();*/ + } + + protected override QueryBuilder<Movie> AddJoinQueries(QueryBuilder<Movie> baseQuery) + { + baseQuery = base.AddJoinQueries(baseQuery); + baseQuery = baseQuery.Join<Movie, AlternativeTitle>(JoinType.Left, m => m.AlternativeTitles, + (m, t) => m.Id == t.MovieId); + + return baseQuery; } public Movie FindByTmdbId(int tmdbid) diff --git a/src/NzbDrone.Core/Tv/QueryExtensions.cs b/src/NzbDrone.Core/Tv/QueryExtensions.cs index 36927f864..07bbe4acb 100644 --- a/src/NzbDrone.Core/Tv/QueryExtensions.cs +++ b/src/NzbDrone.Core/Tv/QueryExtensions.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core { public static Movie FirstWithYear(this SortBuilder<Movie> query, int? year) { - return year.HasValue ? query.FirstOrDefault(movie => movie.Year == year) : query.FirstOrDefault(); + return year.HasValue ? query.FirstOrDefault(movie => movie.Year == year || movie.SecondaryYear == year) : query.FirstOrDefault(); } } @@ -24,7 +24,7 @@ namespace NzbDrone.Core { public static Movie FirstWithYear(this IEnumerable<Movie> query, int? year) { - return year.HasValue ? query.FirstOrDefault(movie => movie.Year == year) : query.FirstOrDefault(); + return year.HasValue ? query.FirstOrDefault(movie => movie.Year == year || movie.SecondaryYear == year) : query.FirstOrDefault(); } } } diff --git a/src/NzbDrone.Core/Tv/RefreshMovieService.cs b/src/NzbDrone.Core/Tv/RefreshMovieService.cs index 4f8cd8935..a866c52b6 100644 --- a/src/NzbDrone.Core/Tv/RefreshMovieService.cs +++ b/src/NzbDrone.Core/Tv/RefreshMovieService.cs @@ -14,6 +14,8 @@ using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Tv.Commands; using NzbDrone.Core.Tv.Events; using NzbDrone.Core.MediaFiles.Commands; +using NzbDrone.Core.MetadataSource.RadarrAPI; +using NzbDrone.Core.Movies.AlternativeTitles; namespace NzbDrone.Core.Tv { @@ -21,27 +23,34 @@ namespace NzbDrone.Core.Tv { private readonly IProvideMovieInfo _movieInfo; private readonly IMovieService _movieService; + private readonly IAlternativeTitleService _titleService; private readonly IRefreshEpisodeService _refreshEpisodeService; private readonly IEventAggregator _eventAggregator; private readonly IManageCommandQueue _commandQueueManager; private readonly IDiskScanService _diskScanService; private readonly ICheckIfMovieShouldBeRefreshed _checkIfMovieShouldBeRefreshed; + private readonly IRadarrAPIClient _apiClient; + private readonly Logger _logger; public RefreshMovieService(IProvideMovieInfo movieInfo, IMovieService movieService, + IAlternativeTitleService titleService, IRefreshEpisodeService refreshEpisodeService, IEventAggregator eventAggregator, IDiskScanService diskScanService, + IRadarrAPIClient apiClient, ICheckIfMovieShouldBeRefreshed checkIfMovieShouldBeRefreshed, IManageCommandQueue commandQueue, Logger logger) { _movieInfo = movieInfo; _movieService = movieService; + _titleService = titleService; _refreshEpisodeService = refreshEpisodeService; _eventAggregator = eventAggregator; - _commandQueueManager = commandQueue; + _apiClient = apiClient; + _commandQueueManager = commandQueue; _diskScanService = diskScanService; _checkIfMovieShouldBeRefreshed = checkIfMovieShouldBeRefreshed; _logger = logger; @@ -85,7 +94,7 @@ namespace NzbDrone.Core.Tv movie.Certification = movieInfo.Certification; movie.InCinemas = movieInfo.InCinemas; movie.Website = movieInfo.Website; - movie.AlternativeTitles = movieInfo.AlternativeTitles; + //movie.AlternativeTitles = movieInfo.AlternativeTitles; movie.Year = movieInfo.Year; movie.PhysicalRelease = movieInfo.PhysicalRelease; movie.YouTubeTrailerId = movieInfo.YouTubeTrailerId; @@ -102,8 +111,47 @@ namespace NzbDrone.Core.Tv _logger.Warn(e, "Couldn't update movie path for " + movie.Path); } + movieInfo.AlternativeTitles = movieInfo.AlternativeTitles.Where(t => t.CleanTitle != movie.CleanTitle) + .DistinctBy(t => t.CleanTitle) + .ExceptBy(t => t.CleanTitle, movie.AlternativeTitles, t => t.CleanTitle, EqualityComparer<string>.Default).ToList(); + + try + { + var mappings = _apiClient.AlternativeTitlesAndYearForMovie(movieInfo.TmdbId); + var mappingsTitles = mappings.Item1; + + movie.AlternativeTitles.AddRange(_titleService.AddAltTitles(movieInfo.AlternativeTitles, movie)); + + mappingsTitles = mappingsTitles.ExceptBy(t => t.CleanTitle, movie.AlternativeTitles, + t => t.CleanTitle, EqualityComparer<string>.Default).ToList(); + + movie.AlternativeTitles.AddRange(_titleService.AddAltTitles(mappingsTitles, movie)); + + if (mappings.Item2 != null) + { + movie.SecondaryYear = mappings.Item2.Year; + movie.SecondaryYearSourceId = mappings.Item2.SourceId; + } + } + catch (RadarrAPIException ex) + { + //Not that wild, could just be a 404. + } + + _movieService.UpdateMovie(movie); + try + { + var newTitles = movieInfo.AlternativeTitles.Except(movie.AlternativeTitles); + //_titleService.AddAltTitles(newTitles.ToList(), movie); + } + catch (Exception e) + { + _logger.Debug(e, "Failed adding alternative titles."); + throw; + } + _logger.Debug("Finished movie refresh for {0}", movie.Title); _eventAggregator.PublishEvent(new MovieUpdatedEvent(movie)); } diff --git a/src/NzbDrone.Core/Tv/ShouldRefreshMovie.cs b/src/NzbDrone.Core/Tv/ShouldRefreshMovie.cs index 7845b6a04..6c7cc91cb 100644 --- a/src/NzbDrone.Core/Tv/ShouldRefreshMovie.cs +++ b/src/NzbDrone.Core/Tv/ShouldRefreshMovie.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Core.Tv public bool ShouldRefresh(Movie movie) { + //return false; if (movie.LastInfoSync < DateTime.UtcNow.AddDays(-30)) { _logger.Trace("Movie {0} last updated more than 30 days ago, should refresh.", movie.Title); diff --git a/src/NzbDrone.Integration.Test/ApiTests/ReleaseFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/ReleaseFixture.cs index 924a61a2a..1b5df1c33 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/ReleaseFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/ReleaseFixture.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Integration.Test.ApiTests releaseResource.Age.Should().BeGreaterOrEqualTo(-1); releaseResource.Title.Should().NotBeNullOrWhiteSpace(); releaseResource.DownloadUrl.Should().NotBeNullOrWhiteSpace(); - releaseResource.SeriesTitle.Should().NotBeNullOrWhiteSpace(); + releaseResource.MovieTitle.Should().NotBeNullOrWhiteSpace(); //TODO: uncomment these after moving to restsharp for rss //releaseResource.NzbInfoUrl.Should().NotBeNullOrWhiteSpace(); //releaseResource.Size.Should().BeGreaterThan(0); diff --git a/src/UI/Content/icons.less b/src/UI/Content/icons.less index a135e0182..fe5b57fa9 100644 --- a/src/UI/Content/icons.less +++ b/src/UI/Content/icons.less @@ -268,6 +268,11 @@ .fa-icon-color(@brand-warning); } +.icon-radarr-download-warning { + .fa-icon-content(@fa-var-download); + .fa-icon-color(@brand-warning); +} + .icon-sonarr-shutdown { .fa-icon-content(@fa-var-power-off); .fa-icon-color(@brand-danger); diff --git a/src/UI/Handlebars/Helpers/Series.js b/src/UI/Handlebars/Helpers/Series.js index 98795f366..c11fa0437 100644 --- a/src/UI/Handlebars/Helpers/Series.js +++ b/src/UI/Handlebars/Helpers/Series.js @@ -64,6 +64,11 @@ Handlebars.registerHelper('alternativeTitlesString', function() { if (titles.length === 0) { return ""; } + + titles = _.map(titles, function(item){ + return item.title; + }); + if (titles.length === 1) { return titles[0]; } diff --git a/src/UI/Movies/Details/MoviesDetailsLayout.js b/src/UI/Movies/Details/MoviesDetailsLayout.js index be03da2e6..26c84115c 100644 --- a/src/UI/Movies/Details/MoviesDetailsLayout.js +++ b/src/UI/Movies/Details/MoviesDetailsLayout.js @@ -12,6 +12,7 @@ var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEdito var HistoryLayout = require('../History/MovieHistoryLayout'); var SearchLayout = require('../Search/MovieSearchLayout'); var FilesLayout = require("../Files/FilesLayout"); +var TitlesLayout = require("../Titles/TitlesLayout"); require('backstrech'); require('../../Mixins/backbone.signalr.mixin'); @@ -24,7 +25,8 @@ module.exports = Marionette.Layout.extend({ info : '#info', search : '#movie-search', history : '#movie-history', - files : "#movie-files" + files : "#movie-files", + titles: "#movie-titles", }, @@ -39,7 +41,8 @@ module.exports = Marionette.Layout.extend({ manualSearch : '.x-manual-search', history : '.x-movie-history', search : '.x-movie-search', - files : ".x-movie-files" + files : ".x-movie-files", + titles: ".x-movie-titles", }, events : { @@ -53,6 +56,7 @@ module.exports = Marionette.Layout.extend({ 'click .x-movie-history' : '_showHistory', 'click .x-movie-search' : '_showSearch', "click .x-movie-files" : "_showFiles", + "click .x-movie-titles" : "_showTitles", }, initialize : function() { @@ -83,6 +87,7 @@ module.exports = Marionette.Layout.extend({ this.searchLayout.startManualSearch = true; this.filesLayout = new FilesLayout({ model : this.model }); + this.titlesLayout = new TitlesLayout({ model : this.model }); this._showBackdrop(); this._showSeasons(); @@ -170,6 +175,15 @@ module.exports = Marionette.Layout.extend({ this.files.show(this.filesLayout); }, + _showTitles : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.titles.tab("show"); + this.titles.show(this.titlesLayout); + }, + _toggleMonitored : function() { var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); diff --git a/src/UI/Movies/Details/MoviesDetailsTemplate.hbs b/src/UI/Movies/Details/MoviesDetailsTemplate.hbs index 8e5bb874e..b2d457ac3 100644 --- a/src/UI/Movies/Details/MoviesDetailsTemplate.hbs +++ b/src/UI/Movies/Details/MoviesDetailsTemplate.hbs @@ -6,7 +6,8 @@ <div> <h1 class="header-text"> <i class="x-monitored" title="Toggle monitored state for movie"/> - {{title}} + {{title}} <span class="year">({{year}}{{#if secondaryYear}} / <a href="https://mappings.radarr.video/mapping/{{secondaryYearSourceId}}" target="_blank"><span title="Secondary year pulled from Radarr Mappings. + Click to head on over there and tell us whether this is correct or not.">{{secondaryYear}}</span></a>{{/if}})</span> <div class="movie-actions pull-right"> <div class="x-episode-file-editor"> <i class="icon-sonarr-episode-file" title="Modify movie files"/> @@ -43,11 +44,13 @@ <li><a href="#movie-history" class="x-movie-history">History</a></li> <li><a href="#movie-search" class="x-movie-search">Search</a></li> <li><a href="#movie-files" class="x-movie-files">Files</a></li> + <li><a href="#movie-titles" class="x-movie-titles">Titles</a></li> </ul> <div class="tab-content"> <div class="tab-pane" id="movie-history"/> <div class="tab-pane" id="movie-search"/> <div class="tab-pane" id="movie-files"/> + <div class="tab-pane" id="movie-titles"/> </div> </div> </div> diff --git a/src/UI/Movies/Titles/LanguageCell.js b/src/UI/Movies/Titles/LanguageCell.js new file mode 100644 index 000000000..0014a9e45 --- /dev/null +++ b/src/UI/Movies/Titles/LanguageCell.js @@ -0,0 +1,22 @@ +var NzbDroneCell = require('../../Cells/NzbDroneCell'); + +module.exports = NzbDroneCell.extend({ + className : 'language-cell', + + render : function() { + this.$el.empty(); + + var language = this.model.get("language"); + + this.$el.html(this.toTitleCase(language)); + + return this; + }, + + toTitleCase : function(str) + { + return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();}); + } + + +}); diff --git a/src/UI/Movies/Titles/NoTitlesView.js b/src/UI/Movies/Titles/NoTitlesView.js new file mode 100644 index 000000000..417dc84ff --- /dev/null +++ b/src/UI/Movies/Titles/NoTitlesView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Movies/Titles/NoTitlesViewTemplate' +}); diff --git a/src/UI/Movies/Titles/NoTitlesViewTemplate.hbs b/src/UI/Movies/Titles/NoTitlesViewTemplate.hbs new file mode 100644 index 000000000..870b63d23 --- /dev/null +++ b/src/UI/Movies/Titles/NoTitlesViewTemplate.hbs @@ -0,0 +1,3 @@ +<p class="text-warning"> + No alternative titles for this movie. +</p> diff --git a/src/UI/Movies/Titles/SourceCell.js b/src/UI/Movies/Titles/SourceCell.js new file mode 100644 index 000000000..f23a64267 --- /dev/null +++ b/src/UI/Movies/Titles/SourceCell.js @@ -0,0 +1,42 @@ +var NzbDroneCell = require('../../Cells/NzbDroneCell'); + +module.exports = NzbDroneCell.extend({ + className : 'title-source-cell', + + render : function() { + this.$el.empty(); + + var link = undefined; + var sourceTitle = this.model.get("sourceType"); + var sourceId = this.model.get("sourceId"); + + switch (sourceTitle) { + case "tmdb": + sourceTitle = "TMDB"; + link = "https://themoviedb.org/movie/" + sourceId; + break; + case "mappings": + sourceTitle = "Radarr Mappings"; + link = "https://mappings.radarr.video/mapping/" + sourceId; + break; + case "user": + sourceTitle = "Force Download"; + break; + case "indexer": + sourceTitle = "Indexer"; + break; + } + + var a = "{0}"; + + if (link) { + a = "<a href='"+link+"' target='_blank'>{0}</a>" + } + + this.$el.html(a.format(sourceTitle)); + + return this; + } + + +}); diff --git a/src/UI/Movies/Titles/TitleCell.js b/src/UI/Movies/Titles/TitleCell.js new file mode 100644 index 000000000..012164798 --- /dev/null +++ b/src/UI/Movies/Titles/TitleCell.js @@ -0,0 +1,6 @@ +var TemplatedCell = require('../../Cells/TemplatedCell'); + +module.exports = TemplatedCell.extend({ + className : 'series-title-cell', + template : 'Movies/Titles/TitleTemplate' +}); \ No newline at end of file diff --git a/src/UI/Movies/Titles/TitleModel.js b/src/UI/Movies/Titles/TitleModel.js new file mode 100644 index 000000000..d51ee555f --- /dev/null +++ b/src/UI/Movies/Titles/TitleModel.js @@ -0,0 +1,3 @@ +var Backbone = require('backbone'); + +module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Movies/Titles/TitleTemplate.hbs b/src/UI/Movies/Titles/TitleTemplate.hbs new file mode 100644 index 000000000..70c8f8d73 --- /dev/null +++ b/src/UI/Movies/Titles/TitleTemplate.hbs @@ -0,0 +1 @@ +{{this}} \ No newline at end of file diff --git a/src/UI/Movies/Titles/TitlesCollection.js b/src/UI/Movies/Titles/TitlesCollection.js new file mode 100644 index 000000000..4b7914955 --- /dev/null +++ b/src/UI/Movies/Titles/TitlesCollection.js @@ -0,0 +1,30 @@ +var PagableCollection = require('backbone.pageable'); +var TitleModel = require('./TitleModel'); +var AsSortedCollection = require('../../Mixins/AsSortedCollection'); + +var Collection = PagableCollection.extend({ + url : window.NzbDrone.ApiRoot + "/aka", + model : TitleModel, + + state : { + pageSize : 2000, + sortKey : 'title', + order : -1 + }, + + mode : 'client', + + sortMappings : { + "source" : { + sortKey : "sourceType" + }, + "language" : { + sortKey : "language" + } + }, + +}); + +Collection = AsSortedCollection.call(Collection); + +module.exports = Collection; diff --git a/src/UI/Movies/Titles/TitlesLayout.js b/src/UI/Movies/Titles/TitlesLayout.js new file mode 100644 index 000000000..4c9e8f2b5 --- /dev/null +++ b/src/UI/Movies/Titles/TitlesLayout.js @@ -0,0 +1,117 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +//var ButtonsView = require('./ButtonsView'); +//var ManualSearchLayout = require('./ManualLayout'); +var TitlesCollection = require('./TitlesCollection'); +var CommandController = require('../../Commands/CommandController'); +var LoadingView = require('../../Shared/LoadingView'); +var NoResultsView = require('./NoTitlesView'); +var TitleModel = require("./TitleModel"); +var TitleCell = require("./TitleCell"); +var SourceCell = require("./SourceCell"); +var LanguageCell = require("./LanguageCell"); + +module.exports = Marionette.Layout.extend({ + template : 'Movies/Titles/TitlesLayoutTemplate', + + regions : { + main : '#movie-titles-region', + grid : "#movie-titles-grid" + }, + + events : { + 'click .x-search-auto' : '_searchAuto', + 'click .x-search-manual' : '_searchManual', + 'click .x-search-back' : '_showButtons' + }, + + columns : [ + { + name : 'title', + label : 'Title', + cell : Backgrid.StringCell + }, + { + name : "this", + label : "Source", + cell : SourceCell, + sortKey : "sourceType", + }, + { + name : "this", + label : "Language", + cell : LanguageCell + } + ], + + + initialize : function(movie) { + this.titlesCollection = new TitlesCollection(); + var titles = movie.model.get("alternativeTitles"); + this.movie = movie; + this.titlesCollection.add(titles); + //this.listenTo(this.releaseCollection, 'sync', this._showSearchResults); + this.listenTo(this.model, 'change', function(model, options) { + if (options && options.changeSource === 'signalr') { + this._refresh(model); + } + }); + + //vent.on(vent.Commands.MovieFileEdited, this._showGrid, this); + }, + + _refresh : function(model) { + this.titlesCollection = new TitlesCollection(); + var file = model.get("alternativeTitles"); + this.titlesCollection.add(file); + + + this.onShow(); + }, + + _refreshClose : function(options) { + this.titlesCollection = new TitlesCollection(); + var file = this.movie.model.get("alternativeTitles"); + this.titlesCollection.add(file); + this._showGrid(); + }, + + onShow : function() { + this.grid.show(new Backgrid.Grid({ + row : Backgrid.Row, + columns : this.columns, + collection : this.titlesCollection, + className : 'table table-hover' + })); + }, + + _showGrid : function() { + this.regionManager.get('grid').show(new Backgrid.Grid({ + row : Backgrid.Row, + columns : this.columns, + collection : this.titlesCollection, + className : 'table table-hover' + })); + }, + + _showMainView : function() { + this.main.show(this.mainView); + }, + + _showButtons : function() { + this._showMainView(); + }, + + _showSearchResults : function() { + if (this.releaseCollection.length === 0) { + this.mainView = new NoResultsView(); + } + + else { + //this.mainView = new ManualSearchLayout({ collection : this.releaseCollection }); + } + + this._showMainView(); + } +}); diff --git a/src/UI/Movies/Titles/TitlesLayoutTemplate.hbs b/src/UI/Movies/Titles/TitlesLayoutTemplate.hbs new file mode 100644 index 000000000..c4899268f --- /dev/null +++ b/src/UI/Movies/Titles/TitlesLayoutTemplate.hbs @@ -0,0 +1,3 @@ +<div id="movie-titles-region"> + <div id="movie-titles-grid" class="table-responsive"></div> +</div> diff --git a/src/UI/Movies/movies.less b/src/UI/Movies/movies.less index dc68b2f57..dc123a29d 100644 --- a/src/UI/Movies/movies.less +++ b/src/UI/Movies/movies.less @@ -534,3 +534,9 @@ list-style-type : none; } } + +.header-text { + .year { + color : gray; + } +} \ No newline at end of file diff --git a/src/UI/Release/AlternativeTitleModel.js b/src/UI/Release/AlternativeTitleModel.js new file mode 100644 index 000000000..14b7db864 --- /dev/null +++ b/src/UI/Release/AlternativeTitleModel.js @@ -0,0 +1,6 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); + +module.exports = Backbone.Model.extend({ + urlRoot : window.NzbDrone.ApiRoot + '/alttitle', +}); diff --git a/src/UI/Release/AlternativeYearModel.js b/src/UI/Release/AlternativeYearModel.js new file mode 100644 index 000000000..1477167f5 --- /dev/null +++ b/src/UI/Release/AlternativeYearModel.js @@ -0,0 +1,6 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); + +module.exports = Backbone.Model.extend({ + urlRoot : window.NzbDrone.ApiRoot + '/altyear', +}); diff --git a/src/UI/Release/DownloadReportCell.js b/src/UI/Release/DownloadReportCell.js index c422446fc..5a973d60d 100644 --- a/src/UI/Release/DownloadReportCell.js +++ b/src/UI/Release/DownloadReportCell.js @@ -1,4 +1,6 @@ var Backgrid = require('backgrid'); +var AppLayout = require('../AppLayout'); +var ForceDownloadView = require('./ForceDownloadView'); module.exports = Backgrid.Cell.extend({ className : 'download-report-cell', @@ -8,7 +10,12 @@ module.exports = Backgrid.Cell.extend({ }, _onClick : function() { - if (!this.model.get('downloadAllowed')) { + if (!this.model.downloadOk()) { + var view = new ForceDownloadView({ + release : this.model + }); + AppLayout.modalRegion.show(view); + return; } @@ -38,10 +45,11 @@ module.exports = Backgrid.Cell.extend({ if (this.model.get('queued')) { this.$el.html('<i class="icon-sonarr-downloading" title="Added to downloaded queue" />'); - } else if (this.model.get('downloadAllowed')) { + } else if (this.model.downloadOk()) { this.$el.html('<i class="icon-sonarr-download" title="Add to download queue" />'); - } else { - this.className = 'no-download-report-cell'; + } else if (this.model.forceDownloadOk()){ + this.$el.html('<i class="icon-radarr-download-warning" title="Force add to download queue."/>'); + this.className = 'force-download-report-cell'; } return this; diff --git a/src/UI/Release/ForceDownloadView.js b/src/UI/Release/ForceDownloadView.js new file mode 100644 index 000000000..7cede7b24 --- /dev/null +++ b/src/UI/Release/ForceDownloadView.js @@ -0,0 +1,81 @@ +var _ = require('underscore'); +var $ = require('jquery'); +var vent = require('vent'); +var AppLayout = require('../AppLayout'); +var Marionette = require('marionette'); +var Config = require('../Config'); +var LanguageCollection = require('../Settings/Profile/Language/LanguageCollection'); +var AltTitleModel = require("./AlternativeTitleModel"); +var AltYearModel = require("./AlternativeYearModel"); +var Messenger = require('../Shared/Messenger'); +require('../Form/FormBuilder'); +require('bootstrap'); + +module.exports = Marionette.ItemView.extend({ + template : 'Release/ForceDownloadViewTemplate', + + events : { + 'click .x-download' : '_forceDownload', + }, + + ui : { + titleMapping : "#title-mapping", + yearMapping : "#year-mapping", + language : "#language-selection", + indicator : ".x-indicator", + }, + + initialize : function(options) { + this.release = options.release; + this.templateHelpers = {}; + + this._configureTemplateHelpers(); + }, + + onShow : function() { + if (this.release.get("mappingResult") == "wrongYear") { + this.ui.titleMapping.hide(); + } else { + this.ui.yearMapping.hide(); + } + }, + + _configureTemplateHelpers : function() { + this.templateHelpers.release = this.release.toJSON(); + this.templateHelpers.languages = LanguageCollection.toJSON() + }, + + _forceDownload : function() { + this.ui.indicator.show(); + var self = this; + + if (this.release.get("mappingResult") == "wrongYear") { + var altYear = new AltYearModel({ + movieId : this.release.get("suspectedMovieId"), + year : this.release.get("year") + }); + this.savePromise = altYear.save(); + } else { + var altTitle = new AltTitleModel({ + movieId : this.release.get("suspectedMovieId"), + title : this.release.get("movieTitle"), + language : this.ui.language.val(), + }); + + this.savePromise = altTitle.save(); + } + + this.savePromise.always(function(){ + self.ui.indicator.hide(); + }); + + this.savePromise.success(function(){ + self.release.save(null, { + success : function() { + self.release.set('queued', true); + vent.trigger(vent.Commands.CloseModalCommand); + } + }); + }); + }, +}); \ No newline at end of file diff --git a/src/UI/Release/ForceDownloadViewTemplate.hbs b/src/UI/Release/ForceDownloadViewTemplate.hbs new file mode 100644 index 000000000..8f107ade1 --- /dev/null +++ b/src/UI/Release/ForceDownloadViewTemplate.hbs @@ -0,0 +1,44 @@ +<div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" aria-hidden="true" data-dismiss="modal">×</button> + <h3>Force Download</h3> + </div> + <div class="modal-body indexer-modal"> + <div id="title-mapping"> + <p>The title "{{release.movieTitle}}" could not be found amongst the alternative titles of the movie. This could lead to problems when Radarr wants to import your movie. + If you click force download below, the title will be added to the alternative titles using the language selected below.</p> + <div class="form-horizontal"> + <div class="form-group"> + <label class="col-sm-3 control-label">Language</label> + + <div class="col-sm-5"> + <select id="language-selection" class="form-control" name="language"> + {{#each languages}} + {{#unless_eq nameLower compare="unknown"}} + <option value="{{nameLower}}" {{#if_eq nameLower compare="english"}} selected {{/if_eq}}>{{name}}</option> + {{/unless_eq}} + {{/each}} + </select> + </div> + + <div class="col-sm-1 help-inline"> + <i class="icon-sonarr-form-info" title="Language of the alternative title."/> + </div> + </div> + </div> + + </div> + <div id="year-mapping"> + <p>The year {{release.year}} does not match the expected release year. This could lead to problems when Radarr wants to import your movie. + If you click force download below, the year will be added as a secondary year for this movie.</p> + </div> + </div> + <div class="modal-footer"> + <span class="indicator x-indicator"><i class="icon-sonarr-spinner fa-spin"></i></span> + <button class="btn" data-dismiss="modal">Cancel</button> + + <div class="btn-group"> + <button class="btn btn-primary x-download">Force Download</button> + </div> + </div> +</div> \ No newline at end of file diff --git a/src/UI/Release/ReleaseModel.js b/src/UI/Release/ReleaseModel.js index 3986a5948..eda716ec2 100644 --- a/src/UI/Release/ReleaseModel.js +++ b/src/UI/Release/ReleaseModel.js @@ -1,3 +1,11 @@ var Backbone = require('backbone'); -module.exports = Backbone.Model.extend({}); \ No newline at end of file +module.exports = Backbone.Model.extend({ + downloadOk : function() { + return this.get("mappingResult") == "success" || this.get("mappingResult") == "successLenientMapping"; + }, + + forceDownloadOk : function() { + return this.get("mappingResult") == "wrongYear" || this.get("mappingResult") == "wrongTitle"; + } +}); \ No newline at end of file