New: Aphrodite Language Improvements

pull/2/head
Qstick 5 years ago
parent 965ed041ae
commit 10322a1867

@ -6,7 +6,7 @@ function MovieAlternateTitles({ alternateTitles }) {
return (
<ul>
{
alternateTitles.map((alternateTitle) => {
alternateTitles.filter((x, i, a) => a.indexOf(x) === i).map((alternateTitle) => {
return (
<li
key={alternateTitle}

@ -243,7 +243,7 @@ class MovieDetails extends Component {
collection,
overview,
youTubeTrailerId,
inCinemas,
isAvailable,
images,
tags,
isSaving,
@ -483,7 +483,7 @@ class MovieDetails extends Component {
<MovieStatusLabel
hasMovieFiles={hasMovieFiles}
monitored={monitored}
inCinemas={inCinemas}
isAvailable={isAvailable}
/>
</span>
</InfoLabel>
@ -722,6 +722,7 @@ MovieDetails.propTypes = {
studio: PropTypes.string,
collection: PropTypes.object,
youTubeTrailerId: PropTypes.string,
isAvailable: PropTypes.bool.isRequired,
inCinemas: PropTypes.string,
overview: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,

@ -1,10 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import moment from 'moment';
import styles from './MovieStatusLabel.css';
function getMovieStatus(hasFile, isMonitored, inCinemas) {
const currentTime = moment();
function getMovieStatus(hasFile, isMonitored, isAvailable) {
if (hasFile) {
return 'Downloaded';
@ -14,7 +12,7 @@ function getMovieStatus(hasFile, isMonitored, inCinemas) {
return 'Unmonitored';
}
if (inCinemas.isBefore(currentTime) && !hasFile) {
if (isAvailable && !hasFile) {
return 'Missing';
}
@ -25,10 +23,10 @@ function MovieStatusLabel(props) {
const {
hasMovieFiles,
monitored,
inCinemas
isAvailable
} = props;
const status = getMovieStatus(hasMovieFiles, monitored, moment(inCinemas));
const status = getMovieStatus(hasMovieFiles, monitored, isAvailable);
return (
<span
@ -42,7 +40,7 @@ function MovieStatusLabel(props) {
MovieStatusLabel.propTypes = {
hasMovieFiles: PropTypes.bool.isRequired,
monitored: PropTypes.bool.isRequired,
inCinemas: PropTypes.string
isAvailable: PropTypes.bool.isRequired
};
MovieStatusLabel.defaultProps = {

@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import titleCase from 'Utilities/String/titleCase';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import MovieLanguage from 'Movie/MovieLanguage';
@ -12,7 +13,8 @@ class MovieTitlesRow extends Component {
render() {
const {
title,
language
language,
sourceType
} = this.props;
// TODO - Fix languages to all take arrays
@ -31,6 +33,10 @@ class MovieTitlesRow extends Component {
/>
</TableRowCell>
<TableRowCell>
{titleCase(sourceType)}
</TableRowCell>
</TableRow>
);
}
@ -39,7 +45,8 @@ class MovieTitlesRow extends Component {
MovieTitlesRow.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
language: PropTypes.object.isRequired
language: PropTypes.object.isRequired,
sourceType: PropTypes.object.isRequired
};
export default MovieTitlesRow;

@ -16,6 +16,11 @@ const columns = [
name: 'language',
label: 'Language',
isVisible: true
},
{
name: 'sourceType',
label: 'Type',
isVisible: true
}
];

@ -18,11 +18,14 @@ function createMapStateToProps() {
items
} = languages;
const filterItems = ['Any', 'Unknown'];
const filteredLanguages = items.filter((lang) => !filterItems.includes(lang.name));
return {
isFetching,
isPopulated,
error,
items
items: filteredLanguages
};
}
);

@ -55,6 +55,7 @@ class UISettings extends Component {
hasSettings,
onInputChange,
onSavePress,
languages,
...otherProps
} = this.props;
@ -174,6 +175,22 @@ class UISettings extends Component {
/>
</FormGroup>
</FieldSet>
<FieldSet
legend="Language"
>
<FormGroup>
<FormLabel>Movie Info Language</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="movieInfoLanguage"
values={languages}
helpText="Language that Radarr will use to display Movie Title in UI"
onChange={onInputChange}
{...settings.movieInfoLanguage}
/>
</FormGroup>
</FieldSet>
</Form>
}
</PageContentBody>
@ -189,6 +206,7 @@ UISettings.propTypes = {
settings: PropTypes.object.isRequired,
hasSettings: PropTypes.bool.isRequired,
onSavePress: PropTypes.func.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
onInputChange: PropTypes.func.isRequired
};

@ -9,13 +9,38 @@ import UISettings from './UISettings';
const SECTION = 'ui';
function createLanguagesSelector() {
return createSelector(
(state) => state.settings.languages,
(languages) => {
const items = languages.items;
const filterItems = ['Any', 'Unknown'];
if (!items) {
return [];
}
const newItems = items.filter((lang) => !filterItems.includes(lang.name)).map((item) => {
return {
key: item.id,
value: item.name
};
});
return newItems;
}
);
}
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createSettingsSectionSelector(SECTION),
(advancedSettings, sectionSettings) => {
createLanguagesSelector(),
(advancedSettings, sectionSettings, languages) => {
return {
advancedSettings,
languages,
...sectionSettings
};
}

@ -133,9 +133,7 @@ namespace NzbDrone.Api.Calendar
occurrence.Description = movie.Overview;
occurrence.Categories = new List<string>() { movie.Studio };
var physicalText = movie.PhysicalReleaseNote.IsNotNullOrWhiteSpace()
? $"(Physical Release, {movie.PhysicalReleaseNote})"
: "(Physical Release)";
var physicalText = "(Physical Release)";
occurrence.Summary = $"{movie.Title} " + (cinemasRelease ? "(Theatrical Release)" : physicalText);
}
}

@ -27,7 +27,7 @@ namespace NzbDrone.Api.Config
{
Id = model.Id,
RenameEpisodes = model.RenameEpisodes,
RenameEpisodes = model.RenameMovies,
ReplaceIllegalCharacters = model.ReplaceIllegalCharacters,
ColonReplacementFormat = model.ColonReplacementFormat,
MultiEpisodeStyle = model.MultiEpisodeStyle,
@ -59,7 +59,7 @@ namespace NzbDrone.Api.Config
{
Id = resource.Id,
RenameEpisodes = resource.RenameEpisodes,
RenameMovies = resource.RenameEpisodes,
ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters,
ColonReplacementFormat = resource.ColonReplacementFormat,
StandardMovieFormat = resource.StandardMovieFormat,

@ -122,7 +122,6 @@ namespace NzbDrone.Api.Movies
SortTitle = model.SortTitle,
InCinemas = model.InCinemas,
PhysicalRelease = model.PhysicalRelease,
PhysicalReleaseNote = model.PhysicalReleaseNote,
HasFile = model.HasFile,
Downloaded = downloaded,
@ -139,7 +138,6 @@ namespace NzbDrone.Api.Movies
Year = model.Year,
SecondaryYear = model.SecondaryYear,
SecondaryYearSourceId = model.SecondaryYearSourceId,
Path = model.Path,
ProfileId = model.ProfileId,
@ -189,7 +187,6 @@ namespace NzbDrone.Api.Movies
SortTitle = resource.SortTitle,
InCinemas = resource.InCinemas,
PhysicalRelease = resource.PhysicalRelease,
PhysicalReleaseNote = resource.PhysicalReleaseNote,
//TotalEpisodeCount
//EpisodeCount
@ -203,7 +200,6 @@ namespace NzbDrone.Api.Movies
Year = resource.Year,
SecondaryYear = resource.SecondaryYear,
SecondaryYearSourceId = resource.SecondaryYearSourceId,
Path = resource.Path,
ProfileId = resource.ProfileId,

@ -0,0 +1,47 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
[TestFixture]
public class CleanupOrphanedMovieTranslationsFixture : DbTest<CleanupOrphanedMovieTranslations, MovieTranslation>
{
[Test]
public void should_delete_orphaned_movie_translation_items()
{
var translation = Builder<MovieTranslation>.CreateNew()
.With(h => h.MovieId = default)
.With(h => h.Language = Language.English)
.BuildNew();
Db.Insert(translation);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_unorphaned_movie_translation_items()
{
var movie = Builder<Movie>.CreateNew().BuildNew();
Db.Insert(movie);
var translation = Builder<MovieTranslation>.CreateNew()
.With(h => h.MovieId = default)
.With(h => h.Language = Language.English)
.With(b => b.MovieId = movie.Id)
.BuildNew();
Db.Insert(translation);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
}
}

@ -1,4 +1,4 @@
using FizzWare.NBuilder;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Download.Pending;
@ -28,12 +28,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
[Test]
public void should_not_delete_unorphaned_pending_items()
{
var series = Builder<Movie>.CreateNew().BuildNew();
var movie = Builder<Movie>.CreateNew().BuildNew();
Db.Insert(series);
Db.Insert(movie);
var pendingRelease = Builder<PendingRelease>.CreateNew()
.With(h => h.MovieId = series.Id)
.With(h => h.MovieId = movie.Id)
.With(h => h.ParsedMovieInfo = new ParsedMovieInfo())
.With(h => h.Release = new ReleaseInfo())
.BuildNew();

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
@ -24,7 +25,8 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
_movieSearchCriteria = new MovieSearchCriteria
{
Movie = new Movies.Movie { ImdbId = "tt0076759", Title = "Star Wars", Year = 1977 }
Movie = new Movies.Movie { ImdbId = "tt0076759", Title = "Star Wars", Year = 1977 },
SceneTitles = new List<string> { "Star Wars" }
};
}
@ -70,7 +72,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
var page = results.GetAllTiers().First().First();
page.Url.Query.Should().Contain("type=name");
page.Url.Query.Should().Contain("query=Star Wars 1977");
page.Url.Query.Should().Contain("query=Star+Wars+1977");
}
}
}

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Moq;
@ -25,7 +26,8 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_movieSearchCriteria = new MovieSearchCriteria
{
Movie = new Movies.Movie { ImdbId = "tt0076759", Title = "Star Wars", Year = 1977 }
Movie = new Movies.Movie { ImdbId = "tt0076759", Title = "Star Wars", Year = 1977, TmdbId = 11 },
SceneTitles = new List<string> { "Star Wars" }
};
_capabilities = new NewznabCapabilities();
@ -66,7 +68,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.GetAllTiers().Should().HaveCount(1);
results.GetAllTiers().Should().HaveCount(2);
var pages = results.GetAllTiers().First().Take(3).ToList();
@ -80,7 +82,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.GetAllTiers().Should().HaveCount(1);
results.GetAllTiers().Should().HaveCount(2);
var pages = results.GetAllTiers().First().Take(500).ToList();
@ -99,7 +101,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
var page = results.GetAllTiers().First().First();
page.Url.Query.Should().NotContain("imdbid=0076759");
page.Url.Query.Should().Contain("q=star");
page.Url.Query.Should().Contain("q=Star");
}
[Test]
@ -114,5 +116,93 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
page.Url.Query.Should().Contain("imdbid=0076759");
}
[Test]
public void should_search_by_tmdbid_if_supported()
{
_capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid" };
var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.GetTier(0).Should().HaveCount(1);
var page = results.GetAllTiers().First().First();
page.Url.Query.Should().Contain("tmdbid=11");
}
[Test]
public void should_prefer_search_by_tmdbid_if_rid_supported()
{
_capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid", "imdbid" };
var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.GetTier(0).Should().HaveCount(1);
var page = results.GetAllTiers().First().First();
page.Url.Query.Should().Contain("tmdbid=11");
page.Url.Query.Should().NotContain("imdbid=0076759");
}
[Test]
public void should_use_aggregrated_id_search_if_supported()
{
_capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid", "imdbid" };
_capabilities.SupportsAggregateIdSearch = true;
var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.GetTier(0).Should().HaveCount(1);
var page = results.GetTier(0).First().First();
page.Url.Query.Should().Contain("tmdbid=11");
page.Url.Query.Should().Contain("imdbid=0076759");
}
[Test]
public void should_not_use_aggregrated_id_search_if_no_ids_supported()
{
_capabilities.SupportedMovieSearchParameters = new[] { "q" };
_capabilities.SupportsAggregateIdSearch = true; // Turns true if indexer supplies supportedParams.
var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.Tiers.Should().Be(1);
results.GetTier(0).Should().HaveCount(1);
var page = results.GetTier(0).First().First();
page.Url.Query.Should().Contain("q=");
}
[Test]
public void should_not_use_aggregrated_id_search_if_no_ids_are_known()
{
_capabilities.SupportedMovieSearchParameters = new[] { "q", "imdbid" };
_capabilities.SupportsAggregateIdSearch = true; // Turns true if indexer supplies supportedParams.
_movieSearchCriteria.Movie.ImdbId = null;
var results = Subject.GetSearchRequests(_movieSearchCriteria);
var page = results.GetTier(0).First().First();
page.Url.Query.Should().Contain("q=");
}
[Test]
public void should_fallback_to_q()
{
_capabilities.SupportedMovieSearchParameters = new[] { "q", "tmdbid", "imdbid" };
_capabilities.SupportsAggregateIdSearch = true;
var results = Subject.GetSearchRequests(_movieSearchCriteria);
results.Tiers.Should().Be(2);
var pageTier2 = results.GetTier(1).First().First();
pageTier2.Url.Query.Should().NotContain("tmdbid=11");
pageTier2.Url.Query.Should().NotContain("imdbid=0076759");
pageTier2.Url.Query.Should().Contain("q=");
}
}
}

@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
[Test]
public void should_return_default_if_no_info_is_known()
{
Subject.Aggregate(_localMovie, false).Languages.Should().Contain(Language.English);
Subject.Aggregate(_localMovie, false).Languages.Should().Contain(Language.Unknown);
}
[Test]
@ -70,8 +70,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
[Test]
public void should_return_file_language_when_file_language_is_higher_than_others()
{
_localMovie.DownloadClientMovieInfo = GetParsedMovieInfo(new List<Language> { Language.English });
_localMovie.FolderMovieInfo = GetParsedMovieInfo(new List<Language> { Language.English });
_localMovie.DownloadClientMovieInfo = GetParsedMovieInfo(new List<Language> { Language.Unknown });
_localMovie.FolderMovieInfo = GetParsedMovieInfo(new List<Language> { Language.Unknown });
_localMovie.FileMovieInfo = GetParsedMovieInfo(new List<Language> { Language.French });
Subject.Aggregate(_localMovie, false).Languages.Should().Equal(_localMovie.FileMovieInfo.Languages);
@ -80,9 +80,9 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators
[Test]
public void should_return_multi_language()
{
_localMovie.DownloadClientMovieInfo = GetParsedMovieInfo(new List<Language> { Language.English });
_localMovie.DownloadClientMovieInfo = GetParsedMovieInfo(new List<Language> { Language.Unknown });
_localMovie.FolderMovieInfo = GetParsedMovieInfo(new List<Language> { Language.English, Language.German });
_localMovie.FileMovieInfo = GetParsedMovieInfo(new List<Language> { Language.English });
_localMovie.FileMovieInfo = GetParsedMovieInfo(new List<Language> { Language.Unknown });
Subject.Aggregate(_localMovie, false).Languages.Should().Equal(_localMovie.FolderMovieInfo.Languages);
}

@ -11,6 +11,7 @@ using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Credits;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;

@ -27,7 +27,10 @@ namespace NzbDrone.Core.Test.MovieTests.AlternativeTitleServiceTests
_title1 = titles[0];
_title2 = titles[1];
_title3 = titles[2];
_movie = Builder<Movie>.CreateNew().With(m => m.CleanTitle = "myothertitle").With(m => m.Id = 1).Build();
_movie = Builder<Movie>.CreateNew()
.With(m => m.CleanTitle = "myothertitle")
.With(m => m.Id = 1)
.Build();
}
private void GivenExistingTitles(params AlternativeTitle[] titles)

@ -4,6 +4,7 @@ using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;

@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_episodeFile = new MovieFile { Quality = new QualityModel(), ReleaseGroup = "SonarrTest" };
_namingConfig = NamingConfig.Default;
_namingConfig.RenameEpisodes = true;
_namingConfig.RenameMovies = true;
Mocker.GetMock<INamingConfigService>()
.Setup(c => c.GetConfig()).Returns(_namingConfig);

@ -6,9 +6,11 @@ using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
@ -21,18 +23,34 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
private Movie _movie;
private MovieFile _movieFile;
private List<MovieTranslation> _movieTranslations;
private NamingConfig _namingConfig;
[SetUp]
public void Setup()
{
_movieTranslations = new List<MovieTranslation>
{
new MovieTranslation
{
Language = Language.German,
Title = "German South Park"
},
new MovieTranslation
{
Language = Language.French,
Title = "French South Park"
}
};
_movie = Builder<Movie>
.CreateNew()
.With(s => s.Title = "South Park")
.Build();
_namingConfig = NamingConfig.Default;
_namingConfig.RenameEpisodes = true;
_namingConfig.RenameMovies = true;
Mocker.GetMock<INamingConfigService>()
.Setup(c => c.GetConfig()).Returns(_namingConfig);
@ -46,6 +64,10 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
Mocker.GetMock<ICustomFormatService>()
.Setup(v => v.All())
.Returns(new List<CustomFormat>());
Mocker.GetMock<IMovieTranslationService>()
.Setup(v => v.GetAllTranslationsForMovie(It.IsAny<int>()))
.Returns(_movieTranslations);
}
private void GivenProper()
@ -121,6 +143,51 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.Should().Be("south park");
}
[Test]
public void should_replace_translated_movie_title()
{
_namingConfig.StandardMovieFormat = "{Movie Title:FR}";
Subject.BuildFileName(_movie, _movieFile)
.Should().Be("French South Park");
}
[Test]
public void should_replace_translated_movie_title_with_base_title_if_invalid_code()
{
_namingConfig.StandardMovieFormat = "{Movie Title:JP}";
Subject.BuildFileName(_movie, _movieFile)
.Should().Be("South Park");
}
[Test]
public void should_replace_translated_movie_title_with_base_title_if_no_translation_exists()
{
_namingConfig.StandardMovieFormat = "{Movie Title:JA}";
Subject.BuildFileName(_movie, _movieFile)
.Should().Be("South Park");
}
[Test]
public void should_replace_translated_movie_title_with_fallback_if_no_translation_exists()
{
_namingConfig.StandardMovieFormat = "{Movie Title:JP|FR}";
Subject.BuildFileName(_movie, _movieFile)
.Should().Be("French South Park");
}
[Test]
public void should_replace_translated_movie_title_with_original_if_no_translation_or_fallback_exists()
{
_namingConfig.StandardMovieFormat = "{Movie Title:JP|CN}";
Subject.BuildFileName(_movie, _movieFile)
.Should().Be("South Park");
}
[Test]
public void should_cleanup_Movie_Title()
{
@ -172,7 +239,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
[Test]
public void use_file_name_when_sceneName_is_null()
{
_namingConfig.RenameEpisodes = false;
_namingConfig.RenameMovies = false;
_movieFile.RelativePath = "30 Rock - S01E01 - Test";
Subject.BuildFileName(_movie, _movieFile)
@ -182,7 +249,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
[Test]
public void use_path_when_sceneName_and_relative_path_are_null()
{
_namingConfig.RenameEpisodes = false;
_namingConfig.RenameMovies = false;
_movieFile.RelativePath = null;
_movieFile.Path = @"C:\Test\Unsorted\Movie - S01E01 - Test";
@ -193,7 +260,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
[Test]
public void use_file_name_when_sceneName_is_not_null()
{
_namingConfig.RenameEpisodes = false;
_namingConfig.RenameMovies = false;
_movieFile.SceneName = "30.Rock.S01E01.xvid-LOL";
_movieFile.RelativePath = "30 Rock - S01E01 - Test";
@ -317,7 +384,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
[Test]
public void should_use_existing_filename_when_scene_name_is_not_available()
{
_namingConfig.RenameEpisodes = true;
_namingConfig.RenameMovies = true;
_namingConfig.StandardMovieFormat = "{Original Title}";
_movieFile.SceneName = null;

@ -24,7 +24,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.Build();
_namingConfig = NamingConfig.Default;
_namingConfig.RenameEpisodes = true;
_namingConfig.RenameMovies = true;
Mocker.GetMock<INamingConfigService>()
.Setup(c => c.GetConfig()).Returns(_namingConfig);

@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_movieFile = new MovieFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "RadarrTest" };
_namingConfig = NamingConfig.Default;
_namingConfig.RenameEpisodes = true;
_namingConfig.RenameMovies = true;
Mocker.GetMock<INamingConfigService>()
.Setup(c => c.GetConfig()).Returns(_namingConfig);

@ -11,8 +11,6 @@ namespace NzbDrone.Core.Test.ParserTests
public class LanguageParserFixture : CoreTest
{
[TestCase("Pulp.Fiction.1994.English.1080p.XviD-LOL")]
[TestCase("The Danish Girl 2015")]
[TestCase("Fantastic.Beasts.The.Crimes.Of.Grindelwald.2018.2160p.WEBRip.x265.10bit.HDR.DD5.1-GASMASK")]
public void should_parse_language_english(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
@ -20,6 +18,15 @@ namespace NzbDrone.Core.Test.ParserTests
result.Languages.Should().BeEquivalentTo(Language.English);
}
[TestCase("The Danish Girl 2015")]
[TestCase("Fantastic.Beasts.The.Crimes.Of.Grindelwald.2018.2160p.WEBRip.x265.10bit.HDR.DD5.1-GASMASK")]
public void should_parse_language_unknown(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
result.Languages.Should().BeEquivalentTo(Language.Unknown);
}
[TestCase("Pulp.Fiction.1994.French.1080p.XviD-LOL")]
public void should_parse_language_french(string postTitle)
{
@ -28,6 +35,15 @@ namespace NzbDrone.Core.Test.ParserTests
result.Languages.Should().BeEquivalentTo(Language.French);
}
[TestCase("E.T. the Extra-Terrestrial.1982.Ger.Eng.AC3.DL.BDRip.x264-iNCEPTiON")]
public void should_parse_language_english_german(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
result.Languages.Should().Contain(Language.German);
result.Languages.Should().Contain(Language.English);
}
[TestCase("Pulp.Fiction.1994.Spanish.1080p.XviD-LOL")]
public void should_parse_language_spanish(string postTitle)
{

@ -171,7 +171,7 @@ namespace NzbDrone.Core.Test.ParserTests
{
var parsed = Parser.Parser.ParseMovieTitle(postTitle, true);
parsed.Languages.Count().Should().Be(1);
parsed.Languages.First().Should().Be(Language.English);
parsed.Languages.First().Should().Be(Language.Unknown);
}
[TestCase("The.Purge.3.Election.Year.2016.German.DTS.DL.720p.BluRay.x264-MULTiPLEX")]

@ -5,6 +5,7 @@ using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Parser;
@ -34,47 +35,55 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
.With(m => m.CleanTitle = "fackjugoethe2")
.With(m => m.Year = 2015)
.With(m => m.AlternativeTitles = new List<AlternativeTitle> { new AlternativeTitle("Fack Ju Göthe 2: Same same") })
.With(m => m.OriginalLanguage = Language.English)
.Build();
_parsedMovieInfo = new ParsedMovieInfo
{
MovieTitle = _movie.Title,
Languages = new List<Language> { Language.English },
Year = _movie.Year,
};
_wrongYearInfo = new ParsedMovieInfo
{
MovieTitle = _movie.Title,
Languages = new List<Language> { Language.English },
Year = 1900,
};
_wrongTitleInfo = new ParsedMovieInfo
{
MovieTitle = "Other Title",
Languages = new List<Language> { Language.English },
Year = 2015
};
_alternativeTitleInfo = new ParsedMovieInfo
{
MovieTitle = _movie.AlternativeTitles.First().Title,
Languages = new List<Language> { Language.English },
Year = _movie.Year,
};
_romanTitleInfo = new ParsedMovieInfo
{
MovieTitle = "Fack Ju Göthe II",
Languages = new List<Language> { Language.English },
Year = _movie.Year,
};
_umlautInfo = new ParsedMovieInfo
{
MovieTitle = "Fack Ju Goethe 2",
Languages = new List<Language> { Language.English },
Year = _movie.Year
};
_umlautAltInfo = new ParsedMovieInfo
{
MovieTitle = "Fack Ju Goethe 2: Same same",
Languages = new List<Language> { Language.English },
Year = _movie.Year
};

@ -1,13 +1,16 @@
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Movies;
using NzbDrone.Core.NetImport;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Test.CustomFormats;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Profiles
@ -107,5 +110,45 @@ namespace NzbDrone.Core.Test.Profiles
Mocker.GetMock<IProfileRepository>().Verify(c => c.Delete(1), Times.Once());
}
[Test]
public void get_acceptable_languages_should_return_profile_language()
{
var profile = Builder<Profile>.CreateNew().With(c => c.Language = Language.German).Build();
Mocker.GetMock<IProfileRepository>()
.Setup(s => s.Get(It.IsAny<int>()))
.Returns(profile);
var languages = Subject.GetAcceptableLanguages(profile.Id);
languages.Count.Should().Be(1);
languages.Should().Contain(Language.German);
}
[Test]
public void get_acceptable_languages_should_return_custom_format_positive_languages()
{
var profile = Builder<Profile>.CreateNew()
.With(c => c.Language = Language.German)
.Build();
var customFormat1 = new CustomFormat("My Format 1", new LanguageSpecification { Value = (int)Language.English }) { Id = 1 };
var customFormat2 = new CustomFormat("My Format 2", new LanguageSpecification { Value = (int)Language.French }) { Id = 2 };
CustomFormatsFixture.GivenCustomFormats(customFormat1, customFormat2);
profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(customFormat2.Name);
Mocker.GetMock<IProfileRepository>()
.Setup(s => s.Get(It.IsAny<int>()))
.Returns(profile);
var languages = Subject.GetAcceptableLanguages(profile.Id);
languages.Count.Should().Be(2);
languages.Should().Contain(Language.German);
languages.Should().Contain(Language.French);
}
}
}

@ -6,6 +6,7 @@ using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource.SkyHook.Resource;
@ -371,6 +372,13 @@ namespace NzbDrone.Core.Configuration
set { SetValue("EnableColorImpairedMode", value); }
}
public int MovieInfoLanguage
{
get { return GetValueInt("MovieInfoLanguage", (int)Language.English); }
set { SetValue("MovieInfoLanguage", value); }
}
public bool CleanupMetadataImages
{
get { return GetValueBoolean("CleanupMetadataImages", true); }

@ -75,6 +75,7 @@ namespace NzbDrone.Core.Configuration
string TimeFormat { get; set; }
bool ShowRelativeDates { get; set; }
bool EnableColorImpairedMode { get; set; }
int MovieInfoLanguage { get; set; }
//Internal
bool CleanupMetadataImages { get; set; }

@ -0,0 +1,115 @@
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text.Json;
using Dapper;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Core.Languages;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(177)]
public class language_improvements : NzbDroneMigrationBase
{
private readonly JsonSerializerOptions _serializerSettings;
public language_improvements()
{
_serializerSettings = new JsonSerializerOptions
{
AllowTrailingCommas = true,
IgnoreNullValues = false,
PropertyNameCaseInsensitive = true,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
}
protected override void MainDbUpgrade()
{
// Use original language to set default language fallback for releases
// Set all to English (1) on migration to ensure default behavior persists until refresh
Alter.Table("Movies").AddColumn("OriginalLanguage").AsInt32().WithDefaultValue((int)Language.English);
Alter.Table("Movies").AddColumn("OriginalTitle").AsString().Nullable();
Alter.Table("Movies").AddColumn("DigitalRelease").AsDateTime().Nullable();
// Column not used
Delete.Column("PhysicalReleaseNote").FromTable("Movies");
Delete.Column("SecondaryYearSourceId").FromTable("Movies");
Alter.Table("NamingConfig").AddColumn("RenameMovies").AsBoolean().WithDefaultValue(0);
Execute.Sql("UPDATE NamingConfig SET RenameMovies=RenameEpisodes");
Delete.Column("RenameEpisodes").FromTable("NamingConfig");
//Manual SQL, Fluent Migrator doesn't support multi-column unique contraint on table creation, SQLite doesn't support adding it after creation
Execute.Sql("CREATE TABLE MovieTranslations(" +
"Id INTEGER PRIMARY KEY, " +
"MovieId INTEGER NOT NULL, " +
"Title TEXT, " +
"CleanTitle TEXT, " +
"Overview TEXT, " +
"Language INTEGER NOT NULL, " +
"Unique(\"MovieId\", \"Language\"));");
// Prevent failure if two movies have same alt titles
Execute.Sql("DROP INDEX IF EXISTS \"IX_AlternativeTitles_CleanTitle\"");
Execute.WithConnection(FixLanguagesMoveFile);
Execute.WithConnection(FixLanguagesHistory);
}
private void FixLanguagesMoveFile(IDbConnection conn, IDbTransaction tran)
{
var rows = conn.Query<LanguageEntity177>($"SELECT Id, Languages FROM MovieFiles");
var corrected = new List<LanguageEntity177>();
foreach (var row in rows)
{
var languages = JsonSerializer.Deserialize<List<int>>(row.Languages, _serializerSettings);
var newLanguages = languages.Distinct().ToList();
corrected.Add(new LanguageEntity177
{
Id = row.Id,
Languages = JsonSerializer.Serialize(newLanguages, _serializerSettings)
});
}
var updateSql = "UPDATE MovieFiles SET Languages = @Languages WHERE Id = @Id";
conn.Execute(updateSql, corrected, transaction: tran);
}
private void FixLanguagesHistory(IDbConnection conn, IDbTransaction tran)
{
var rows = conn.Query<LanguageEntity177>($"SELECT Id, Languages FROM History");
var corrected = new List<LanguageEntity177>();
foreach (var row in rows)
{
var languages = JsonSerializer.Deserialize<List<int>>(row.Languages, _serializerSettings);
var newLanguages = languages.Distinct().ToList();
corrected.Add(new LanguageEntity177
{
Id = row.Id,
Languages = JsonSerializer.Serialize(newLanguages, _serializerSettings)
});
}
var updateSql = "UPDATE History SET Languages = @Languages WHERE Id = @Id";
conn.Execute(updateSql, corrected, transaction: tran);
}
private class LanguageEntity177 : ModelBase
{
public string Languages { get; set; }
}
}
}

@ -26,6 +26,7 @@ using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Movies.Credits;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.Core.NetImport;
using NzbDrone.Core.NetImport.ImportExclusions;
using NzbDrone.Core.Notifications;
@ -105,6 +106,8 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<AlternativeTitle>("AlternativeTitles").RegisterModel();
Mapper.Entity<MovieTranslation>("MovieTranslations").RegisterModel();
Mapper.Entity<Credit>("Credits").RegisterModel();
Mapper.Entity<ImportExclusion>("ImportExclusions").RegisterModel();

@ -0,0 +1,28 @@
using Dapper;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers
{
public class CleanupOrphanedMovieTranslations : IHousekeepingTask
{
private readonly IMainDatabase _database;
public CleanupOrphanedMovieTranslations(IMainDatabase database)
{
_database = database;
}
public void Clean()
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM MovieTranslations
WHERE Id IN (
SELECT MovieTranslations.Id FROM MovieTranslations
LEFT OUTER JOIN Movies
ON MovieTranslations.MovieId = Movies.Id
WHERE Movies.Id IS NULL)");
}
}
}
}

@ -9,7 +9,9 @@ using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles;
namespace NzbDrone.Core.IndexerSearch
{
@ -24,16 +26,22 @@ namespace NzbDrone.Core.IndexerSearch
private readonly IIndexerFactory _indexerFactory;
private readonly IMakeDownloadDecision _makeDownloadDecision;
private readonly IMovieService _movieService;
private readonly IMovieTranslationService _movieTranslationService;
private readonly IProfileService _profileService;
private readonly Logger _logger;
public NzbSearchService(IIndexerFactory indexerFactory,
IMakeDownloadDecision makeDownloadDecision,
IMovieService movieService,
IMovieTranslationService movieTranslationService,
IProfileService profileService,
Logger logger)
{
_indexerFactory = indexerFactory;
_makeDownloadDecision = makeDownloadDecision;
_movieService = movieService;
_movieTranslationService = movieTranslationService;
_profileService = profileService;
_logger = logger;
}
@ -60,6 +68,24 @@ namespace NzbDrone.Core.IndexerSearch
UserInvokedSearch = userInvokedSearch,
InteractiveSearch = interactiveSearch
};
var wantedLanguages = _profileService.GetAcceptableLanguages(movie.ProfileId);
var translations = _movieTranslationService.GetAllTranslationsForMovie(movie.Id);
var queryTranlations = new List<string>
{
movie.Title,
movie.OriginalTitle
};
//Add Translation of wanted languages to search query
foreach (var translation in translations.Where(a => wantedLanguages.Contains(a.Language)))
{
queryTranlations.Add(translation.Title);
}
spec.SceneTitles = queryTranlations.Distinct().ToList();
return spec;
}

@ -30,9 +30,12 @@ namespace NzbDrone.Core.Indexers.FileList
}
else
{
var titleYearSearchQuery = string.Format("{0} {1}", searchCriteria.Movie.Title, searchCriteria.Movie.Year);
foreach (var queryTitle in searchCriteria.QueryTitles)
{
var titleYearSearchQuery = string.Format("{0}+{1}", queryTitle, searchCriteria.Movie.Year);
pageableRequests.Add(GetRequest("search-torrents", string.Format("&type=name&query={0}", titleYearSearchQuery.Trim())));
}
}
return pageableRequests;
}

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
namespace NzbDrone.Core.Indexers.Newznab
{
@ -7,7 +7,6 @@ namespace NzbDrone.Core.Indexers.Newznab
public int DefaultPageSize { get; set; }
public int MaxPageSize { get; set; }
public string[] SupportedSearchParameters { get; set; }
public string[] SupportedTvSearchParameters { get; set; }
public string[] SupportedMovieSearchParameters { get; set; }
public bool SupportsAggregateIdSearch { get; set; }
public List<NewznabCategory> Categories { get; set; }
@ -18,7 +17,6 @@ namespace NzbDrone.Core.Indexers.Newznab
MaxPageSize = 100;
SupportedSearchParameters = new[] { "q" };
SupportedMovieSearchParameters = new[] { "q", "imdbid", "imdbtitle", "imdbyear" };
SupportedTvSearchParameters = new[] { "q", "rid", "season", "ep" }; // This should remain 'rid' for older newznab installs.
SupportsAggregateIdSearch = false;
Categories = new List<NewznabCategory>();
}

@ -118,17 +118,6 @@ namespace NzbDrone.Core.Indexers.Newznab
capabilities.SupportedSearchParameters = xmlBasicSearch.Attribute("supportedParams").Value.Split(',');
}
var xmlTvSearch = xmlSearching.Element("tv-search");
if (xmlTvSearch == null || xmlTvSearch.Attribute("available").Value != "yes")
{
capabilities.SupportedTvSearchParameters = null;
}
else if (xmlTvSearch.Attribute("supportedParams") != null)
{
capabilities.SupportedTvSearchParameters = xmlTvSearch.Attribute("supportedParams").Value.Split(',');
capabilities.SupportsAggregateIdSearch = true;
}
var xmlMovieSearch = xmlSearching.Element("movie-search");
if (xmlMovieSearch == null || xmlMovieSearch.Attribute("available").Value != "yes")
{

@ -22,7 +22,18 @@ namespace NzbDrone.Core.Indexers.Newznab
PageSize = 100;
}
private bool SupportsMovieSearch
private bool SupportsSearch
{
get
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
return capabilities.SupportedSearchParameters != null &&
capabilities.SupportedSearchParameters.Contains("q");
}
}
private bool SupportsImdbSearch
{
get
{
@ -33,6 +44,27 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
private bool SupportsTmdbSearch
{
get
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
return capabilities.SupportedMovieSearchParameters != null &&
capabilities.SupportedMovieSearchParameters.Contains("tmdbid");
}
}
private bool SupportsAggregatedIdSearch
{
get
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
return capabilities.SupportsAggregateIdSearch;
}
}
public virtual IndexerPageableRequestChain GetRecentRequests()
{
var pageableRequests = new IndexerPageableRequestChain();
@ -56,34 +88,68 @@ namespace NzbDrone.Core.Indexers.Newznab
{
var pageableRequests = new IndexerPageableRequestChain();
if (!Settings.SearchByTitle && SupportsMovieSearch && searchCriteria.Movie.ImdbId.IsNotNullOrWhiteSpace())
{
pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "movie", $"&imdbid={searchCriteria.Movie.ImdbId.Substring(2)}"));
AddMovieIdPageableRequests(pageableRequests, MaxPages, Settings.Categories, searchCriteria);
return pageableRequests;
}
else
private void AddMovieIdPageableRequests(IndexerPageableRequestChain chain, int maxPages, IEnumerable<int> categories, SearchCriteriaBase searchCriteria)
{
var altTitles = searchCriteria.Movie.AlternativeTitles.Take(5).Select(t => t.Title).ToList();
altTitles.Add(searchCriteria.Movie.Title);
var includeTmdbSearch = SupportsTmdbSearch && searchCriteria.Movie.TmdbId > 0;
var includeImdbSearch = SupportsImdbSearch && searchCriteria.Movie.ImdbId.IsNotNullOrWhiteSpace();
var realMaxPages = (int)MaxPages / altTitles.Count();
if (SupportsAggregatedIdSearch && (includeTmdbSearch || includeImdbSearch))
{
var ids = "";
//pageableRequests.Add(GetPagedRequests(MaxPages - (altTitles.Count() * realMaxPages), Settings.Categories, "search", $"&q={searchTitle}%20{searchCriteria.Movie.Year}"));
if (includeTmdbSearch)
{
ids += "&tmdbid=" + searchCriteria.Movie.TmdbId;
}
if (includeImdbSearch)
{
ids += "&imdbid=" + searchCriteria.Movie.ImdbId.Substring(2);
}
chain.Add(GetPagedRequests(maxPages, categories, "movie", ids));
}
else
{
if (includeTmdbSearch)
{
chain.Add(GetPagedRequests(maxPages,
categories,
"movie",
string.Format("&tmdbid={0}", searchCriteria.Movie.TmdbId)));
}
else if (includeImdbSearch)
{
chain.Add(GetPagedRequests(maxPages,
categories,
"movie",
string.Format("&imdbid={0}", searchCriteria.Movie.ImdbId.Substring(2))));
}
}
//Also use alt titles for searching.
foreach (string altTitle in altTitles)
if (SupportsSearch)
{
chain.AddTier();
foreach (var queryTitle in searchCriteria.QueryTitles)
{
var searchAltTitle = System.Web.HttpUtility.UrlPathEncode(Parser.Parser.ReplaceGermanUmlauts(Parser.Parser.NormalizeTitle(altTitle)));
var queryString = $"&q={searchAltTitle}";
var searchQuery = queryTitle;
if (!Settings.RemoveYear)
{
queryString += $"%20{searchCriteria.Movie.Year}";
searchQuery = string.Format("{0} {1}", searchQuery, searchCriteria.Movie.Year);
}
pageableRequests.Add(GetPagedRequests(realMaxPages, Settings.Categories, "search", queryString));
chain.Add(GetPagedRequests(MaxPages,
Settings.Categories,
"movie",
string.Format("&q={0}", NewsnabifyTitle(searchQuery))));
}
}
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(int maxPages, IEnumerable<int> categories, string searchType, string parameters)
@ -115,6 +181,11 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
private static string NewsnabifyTitle(string title)
{
return title.Replace("+", "%20");
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}

@ -77,21 +77,6 @@ namespace NzbDrone.Core.Indexers.Newznab
releaseInfo = base.ProcessItem(item, releaseInfo);
releaseInfo.ImdbId = GetImdbId(item);
//// This shouldn't be needed with changes to the DownloadDecisionMaker
//var imdbMovieTitle = GetImdbTitle(item);
//var imdbYear = GetImdbYear(item);
//// Fun, lets try to add year to the releaseTitle for our foriegn friends :)
//// if (!releaseInfo.Title.ContainsIgnoreCase(imdbMovieTitle + "." + imdbYear))
//var isMatch = Regex.IsMatch(releaseInfo.Title, $@"^{imdbMovieTitle}.*{imdbYear}", RegexOptions.IgnoreCase);
//if (!isMatch)
//{
// if (imdbYear != 1900 && imdbMovieTitle != string.Empty)
// {
// // releaseInfo.Title = releaseInfo.Title.Replace(imdbMovieTitle, imdbMovieTitle + "." + imdbYear);
// releaseInfo.Title = Regex.Replace(releaseInfo.Title, imdbMovieTitle, imdbMovieTitle + "." + imdbYear, RegexOptions.IgnoreCase);
// }
//}
return releaseInfo;
}

@ -88,13 +88,6 @@ namespace NzbDrone.Core.Indexers.Newznab
Type = FieldType.Checkbox)]
public bool RemoveYear { get; set; }
[FieldDefinition(7,
Label = "Search by Title",
HelpText = "By default, Radarr will try to search by IMDB ID if your indexer supports that. However, some indexers are not very good at tagging their releases correctly, so you can force Radarr to search that indexer by title instead.",
Advanced = true,
Type = FieldType.Checkbox)]
public bool SearchByTitle { get; set; }
// Field 8 is used by TorznabSettings MinimumSeeders
// If you need to add another field here, update TorznabSettings as well and this comment
public virtual NzbDroneValidationResult Validate()

@ -58,7 +58,16 @@ namespace NzbDrone.Core.Indexers.Nyaa
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
var pageableRequests = new IndexerPageableRequestChain();
foreach (var queryTitle in searchCriteria.QueryTitles)
{
pageableRequests.Add(GetPagedRequests(MaxPages,
string.Format("&term={0}",
PrepareQuery(string.Format("{0} {1}", queryTitle, searchCriteria.Movie.Year)))));
}
return pageableRequests;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }

@ -26,6 +26,19 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
foreach (var queryTitle in searchCriteria.QueryTitles)
{
pageableRequests.Add(GetPagedRequests(string.Format("{0}",
queryTitle)));
}
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(string query)
{
var url = new StringBuilder();
@ -40,16 +53,6 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs
yield return new IndexerRequest(url.ToString(), HttpAccept.Rss);
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}",
searchCriteria.Movie.Title)));
return pageableRequests;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}

@ -35,7 +35,10 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
}
else if (searchCriteria.Movie.Year > 0)
{
pageableRequests.Add(GetRequest(string.Format("{0}&year={1}", searchCriteria.Movie.Title, searchCriteria.Movie.Year)));
foreach (var queryTitle in searchCriteria.QueryTitles)
{
pageableRequests.Add(GetRequest(string.Format("{0}&year={1}", queryTitle, searchCriteria.Movie.Year)));
}
}
return pageableRequests;

@ -25,37 +25,33 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Aggregation.Aggregators
languages.AddRange(localMovie.DownloadClientMovieInfo?.Languages ?? new List<Language>());
if (!languages.Any(l => l != Language.English))
if (!languages.Any(l => l != Language.Unknown))
{
languages = localMovie.FolderMovieInfo?.Languages ?? new List<Language>();
}
if (!languages.Any(l => l != Language.English))
if (!languages.Any(l => l != Language.Unknown))
{
languages = localMovie.FileMovieInfo?.Languages ?? new List<Language>();
}
if (!languages.Any())
{
languages.Add(Language.English);
languages.Add(Language.Unknown);
}
_logger.Debug("Using languages: {0}", languages.Select(l => l.Name).ToList().Join(","));
localMovie.Languages = languages.Distinct().ToList();
return localMovie;
}
languages = languages.Distinct().ToList();
private List<Language> GetLanguage(ParsedMovieInfo parsedMovieInfo)
if (languages.Count == 1 && languages.Contains(Language.Unknown))
{
if (parsedMovieInfo == null)
{
// English is the default language when otherwise unknown
return new List<Language> { Language.English };
languages = new List<Language> { localMovie.Movie.OriginalLanguage };
}
return parsedMovieInfo.Languages;
_logger.Debug("Using languages: {0}", languages.Select(l => l.Name).ToList().Join(","));
localMovie.Languages = languages;
return localMovie;
}
}
}

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Credits;
using NzbDrone.Core.Movies.Translations;
namespace NzbDrone.Core.MetadataSource
{

@ -9,6 +9,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
public string ImdbId { get; set; }
public string Overview { get; set; }
public string Title { get; set; }
public string OriginalTitle { get; set; }
public string TitleSlug { get; set; }
public List<RatingResource> Ratings { get; set; }
public int? Runtime { get; set; }

@ -3,6 +3,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
public class TranslationResource
{
public string Title { get; set; }
public string Overview { get; set; }
public string Language { get; set; }
}
}

@ -15,6 +15,7 @@ using NzbDrone.Core.MetadataSource.SkyHook.Resource;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Movies.Credits;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.Core.NetImport.TMDb;
using NzbDrone.Core.Parser;
@ -29,11 +30,13 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
private readonly IHttpRequestBuilderFactory _radarrMetadata;
private readonly IConfigService _configService;
private readonly IMovieService _movieService;
private readonly IMovieTranslationService _movieTranslationService;
public SkyHookProxy(IHttpClient httpClient,
IRadarrCloudRequestBuilder requestBuilder,
IConfigService configService,
IMovieService movieService,
IMovieTranslationService movieTranslationService,
Logger logger)
{
_httpClient = httpClient;
@ -41,6 +44,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
_radarrMetadata = requestBuilder.RadarrMetadata;
_configService = configService;
_movieService = movieService;
_movieTranslationService = movieTranslationService;
_logger = logger;
}
@ -160,6 +164,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
movie.TmdbId = resource.TmdbId;
movie.ImdbId = resource.ImdbId;
movie.Title = resource.Title;
movie.OriginalTitle = resource.OriginalTitle;
movie.TitleSlug = resource.TitleSlug;
movie.CleanTitle = resource.Title.CleanSeriesTitle();
movie.SortTitle = Parser.Parser.NormalizeTitle(resource.Title);
@ -167,9 +172,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
movie.AlternativeTitles.AddRange(resource.AlternativeTitles.Select(MapAlternativeTitle));
movie.Translations.AddRange(resource.Translations.Select(MapTranslation));
movie.OriginalLanguage = IsoLanguages.Find(resource.OriginalLanguage.ToLower())?.Language ?? Language.English;
movie.Website = resource.Homepage;
movie.InCinemas = resource.InCinema;
movie.PhysicalRelease = resource.PhysicalRelease;
movie.DigitalRelease = resource.DigitalRelease;
movie.Year = resource.Year;
@ -195,44 +205,24 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
var now = DateTime.Now;
//handle the case when we have both theatrical and physical release dates
if (resource.InCinema.HasValue && resource.PhysicalRelease.HasValue)
{
if (now < resource.InCinema)
{
movie.Status = MovieStatusType.Announced;
}
else if (now >= resource.InCinema)
if (resource.InCinema.HasValue && now > resource.InCinema)
{
movie.Status = MovieStatusType.InCinemas;
}
if (now >= resource.PhysicalRelease)
if (!resource.PhysicalRelease.HasValue && !resource.DigitalRelease.HasValue && now > resource.InCinema.Value.AddDays(90))
{
movie.Status = MovieStatusType.Released;
}
}
//handle the case when we have theatrical release dates but we dont know the physical release date
else if (resource.InCinema.HasValue && (now >= resource.InCinema))
{
movie.Status = MovieStatusType.InCinemas;
}
//handle the case where we only have a physical release date
else if ((resource.PhysicalRelease.HasValue && (now >= resource.PhysicalRelease)) || (resource.DigitalRelease.HasValue && (now >= resource.DigitalRelease)))
if (resource.PhysicalRelease.HasValue && now >= resource.PhysicalRelease)
{
movie.Status = MovieStatusType.Released;
}
//otherwise the title has only been announced
else
{
movie.Status = MovieStatusType.Announced;
}
//since TMDB lacks alot of information lets assume that stuff is released if its been in cinemas for longer than 3 months.
if (!movie.PhysicalRelease.HasValue && (movie.Status == MovieStatusType.InCinemas) && (DateTime.Now.Subtract(movie.InCinemas.Value).TotalSeconds > 60 * 60 * 24 * 30 * 3))
if (resource.DigitalRelease.HasValue && now >= resource.DigitalRelease)
{
movie.Status = MovieStatusType.Released;
}
@ -425,6 +415,10 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
{
movie = MapMovie(result);
}
else
{
movie.Translations = _movieTranslationService.GetAllTranslationsForMovie(movie.Id);
}
return movie;
}
@ -474,6 +468,19 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
return newAlternativeTitle;
}
private static MovieTranslation MapTranslation(TranslationResource arg)
{
var newAlternativeTitle = new MovieTranslation
{
Title = arg.Title,
Overview = arg.Overview,
CleanTitle = arg.Title.CleanSeriesTitle(),
Language = IsoLanguages.Find(arg.Language.ToLower())?.Language
};
return newAlternativeTitle;
}
private static Ratings MapRatings(RatingResource rating)
{
if (rating == null)

@ -2,8 +2,10 @@ using System;
using System.Collections.Generic;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.Core.Profiles;
namespace NzbDrone.Core.Movies
@ -16,7 +18,9 @@ namespace NzbDrone.Core.Movies
Genres = new List<string>();
Tags = new HashSet<int>();
AlternativeTitles = new List<AlternativeTitle>();
Translations = new List<MovieTranslation>();
Recommendations = new List<int>();
OriginalLanguage = Language.English;
}
public int TmdbId { get; set; }
@ -46,7 +50,7 @@ namespace NzbDrone.Core.Movies
public DateTime Added { get; set; }
public DateTime? InCinemas { get; set; }
public DateTime? PhysicalRelease { get; set; }
public string PhysicalReleaseNote { get; set; }
public DateTime? DigitalRelease { get; set; }
public Profile Profile { get; set; }
public HashSet<int> Tags { get; set; }
public AddMovieOptions AddOptions { get; set; }
@ -56,10 +60,12 @@ namespace NzbDrone.Core.Movies
//Get Loaded via a Join Query
public List<AlternativeTitle> AlternativeTitles { get; set; }
public List<MovieTranslation> Translations { get; set; }
public int? SecondaryYear { get; set; }
public int SecondaryYearSourceId { get; set; }
public string YouTubeTrailerId { get; set; }
public string Studio { get; set; }
public string OriginalTitle { get; set; }
public Language OriginalLanguage { get; set; }
public List<int> Recommendations { get; set; }
public bool IsRecentMovie
@ -122,7 +128,23 @@ namespace NzbDrone.Core.Movies
case MovieStatusType.Released:
case MovieStatusType.PreDB:
default:
minimumAvailabilityDate = PhysicalRelease.HasValue ? PhysicalRelease.Value : (InCinemas.HasValue ? InCinemas.Value.AddDays(90) : DateTime.MaxValue);
if (PhysicalRelease.HasValue && DigitalRelease.HasValue)
{
minimumAvailabilityDate = new DateTime(Math.Min(PhysicalRelease.Value.Ticks, DigitalRelease.Value.Ticks));
}
else if (PhysicalRelease.HasValue)
{
minimumAvailabilityDate = PhysicalRelease.Value;
}
else if (DigitalRelease.HasValue)
{
minimumAvailabilityDate = DigitalRelease.Value;
}
else
{
minimumAvailabilityDate = InCinemas.HasValue ? InCinemas.Value.AddDays(90) : DateTime.MaxValue;
}
break;
}

@ -15,6 +15,7 @@ using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Movies.Commands;
using NzbDrone.Core.Movies.Credits;
using NzbDrone.Core.Movies.Events;
using NzbDrone.Core.Movies.Translations;
namespace NzbDrone.Core.Movies
{
@ -22,6 +23,7 @@ namespace NzbDrone.Core.Movies
{
private readonly IProvideMovieInfo _movieInfo;
private readonly IMovieService _movieService;
private readonly IMovieTranslationService _movieTranslationService;
private readonly IAlternativeTitleService _titleService;
private readonly ICreditService _creditService;
private readonly IEventAggregator _eventAggregator;
@ -33,6 +35,7 @@ namespace NzbDrone.Core.Movies
public RefreshMovieService(IProvideMovieInfo movieInfo,
IMovieService movieService,
IMovieTranslationService movieTranslationService,
IAlternativeTitleService titleService,
ICreditService creditService,
IEventAggregator eventAggregator,
@ -43,6 +46,7 @@ namespace NzbDrone.Core.Movies
{
_movieInfo = movieInfo;
_movieService = movieService;
_movieTranslationService = movieTranslationService;
_titleService = titleService;
_creditService = creditService;
_eventAggregator = eventAggregator;
@ -104,8 +108,11 @@ namespace NzbDrone.Core.Movies
movie.Year = movieInfo.Year;
movie.SecondaryYear = movieInfo.SecondaryYear;
movie.PhysicalRelease = movieInfo.PhysicalRelease;
movie.DigitalRelease = movieInfo.DigitalRelease;
movie.YouTubeTrailerId = movieInfo.YouTubeTrailerId;
movie.Studio = movieInfo.Studio;
movie.OriginalTitle = movieInfo.OriginalTitle;
movie.OriginalLanguage = movieInfo.OriginalLanguage;
movie.HasPreDBEntry = movieInfo.HasPreDBEntry;
movie.Recommendations = movieInfo.Recommendations;
@ -120,6 +127,7 @@ namespace NzbDrone.Core.Movies
}
movie.AlternativeTitles = _titleService.UpdateTitles(movieInfo.AlternativeTitles, movie);
_movieTranslationService.UpdateTranslations(movieInfo.Translations, movie);
_movieService.UpdateMovie(new List<Movie> { movie }, true);
_creditService.UpdateCredits(credits, movie);

@ -0,0 +1,14 @@
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Languages;
namespace NzbDrone.Core.Movies.Translations
{
public class MovieTranslation : ModelBase
{
public int MovieId { get; set; }
public string Title { get; set; }
public string CleanTitle { get; set; }
public string Overview { get; set; }
public Language Language { get; set; }
}
}

@ -0,0 +1,37 @@
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Movies.Translations
{
public interface IMovieTranslationRepository : IBasicRepository<MovieTranslation>
{
List<MovieTranslation> FindByMovieId(int movieId);
List<MovieTranslation> FindByLanguage(Language language);
void DeleteForMovies(List<int> movieIds);
}
public class MovieTranslationRepository : BasicRepository<MovieTranslation>, IMovieTranslationRepository
{
public MovieTranslationRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
public List<MovieTranslation> FindByMovieId(int movieId)
{
return Query(x => x.MovieId == movieId);
}
public List<MovieTranslation> FindByLanguage(Language language)
{
return Query(x => x.Language == language);
}
public void DeleteForMovies(List<int> movieIds)
{
Delete(x => movieIds.Contains(x.MovieId));
}
}
}

@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Movies.Events;
namespace NzbDrone.Core.Movies.Translations
{
public interface IMovieTranslationService
{
List<MovieTranslation> GetAllTranslationsForMovie(int movieId);
List<MovieTranslation> GetAllTranslationsForLanguage(Language language);
List<MovieTranslation> UpdateTranslations(List<MovieTranslation> titles, Movie movie);
}
public class MovieTranslationService : IMovieTranslationService, IHandleAsync<MoviesDeletedEvent>
{
private readonly IMovieTranslationRepository _translationRepo;
private readonly Logger _logger;
public MovieTranslationService(IMovieTranslationRepository translationRepo,
Logger logger)
{
_translationRepo = translationRepo;
_logger = logger;
}
public List<MovieTranslation> GetAllTranslationsForMovie(int movieId)
{
return _translationRepo.FindByMovieId(movieId).ToList();
}
public List<MovieTranslation> GetAllTranslationsForLanguage(Language language)
{
return _translationRepo.FindByLanguage(language).ToList();
}
public void RemoveTitle(MovieTranslation title)
{
_translationRepo.Delete(title);
}
public List<MovieTranslation> UpdateTranslations(List<MovieTranslation> translations, Movie movie)
{
int movieId = movie.Id;
// First update the movie ids so we can correlate them later
translations.ForEach(t => t.MovieId = movieId);
// Then throw out any we don't have languages for
translations = translations.Where(t => t.Language != null).ToList();
// Then make sure they are all distinct languages
translations = translations.DistinctBy(t => t.Language).ToList();
// Now find translations to delete, update and insert
var existingTranslations = _translationRepo.FindByMovieId(movieId);
translations.ForEach(c => c.Id = existingTranslations.FirstOrDefault(t => t.Language == c.Language)?.Id ?? 0);
var insert = translations.Where(t => t.Id == 0).ToList();
var update = translations.Where(t => t.Id > 0).ToList();
var delete = existingTranslations.Where(t => !translations.Any(c => c.Language == t.Language)).ToList();
_translationRepo.DeleteMany(delete.ToList());
_translationRepo.UpdateMany(update.ToList());
_translationRepo.InsertMany(insert.ToList());
return translations;
}
public void HandleAsync(MoviesDeletedEvent message)
{
_translationRepo.DeleteForMovies(message.Movies.Select(m => m.Id).ToList());
}
}
}

@ -11,6 +11,9 @@ using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.Organizer
@ -30,10 +33,11 @@ namespace NzbDrone.Core.Organizer
private readonly INamingConfigService _namingConfigService;
private readonly IQualityDefinitionService _qualityDefinitionService;
private readonly IUpdateMediaInfo _mediaInfoUpdater;
private readonly IMovieTranslationService _movieTranslationService;
private readonly ICustomFormatService _formatService;
private readonly Logger _logger;
private static readonly Regex TitleRegex = new Regex(@"\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[a-z0-9]+))?(?<suffix>[- ._)\]]*)\}",
private static readonly Regex TitleRegex = new Regex(@"\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[a-z0-9|]+))?(?<suffix>[- ._)\]]*)\}",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex TagsRegex = new Regex(@"(?<tags>\{tags(?:\:0+)?})",
@ -50,7 +54,7 @@ namespace NzbDrone.Core.Organizer
public static readonly Regex SeriesTitleRegex = new Regex(@"(?<token>\{(?:Series)(?<separator>[- ._])(Clean)?Title\})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly Regex MovieTitleRegex = new Regex(@"(?<token>\{((?:(Movie|Original))(?<separator>[- ._])(Clean)?(Title|Filename)(The)?)\})",
public static readonly Regex MovieTitleRegex = new Regex(@"(?<token>\{((?:(Movie|Original))(?<separator>[- ._])(Clean)?(Title|Filename)(The)?)(?::(?<customFormat>[a-z0-9]+))?\})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled);
@ -69,12 +73,14 @@ namespace NzbDrone.Core.Organizer
public FileNameBuilder(INamingConfigService namingConfigService,
IQualityDefinitionService qualityDefinitionService,
IUpdateMediaInfo mediaInfoUpdater,
IMovieTranslationService movieTranslationService,
ICustomFormatService formatService,
Logger logger)
{
_namingConfigService = namingConfigService;
_qualityDefinitionService = qualityDefinitionService;
_mediaInfoUpdater = mediaInfoUpdater;
_movieTranslationService = movieTranslationService;
_formatService = formatService;
_logger = logger;
}
@ -86,7 +92,7 @@ namespace NzbDrone.Core.Organizer
namingConfig = _namingConfigService.GetConfig();
}
if (!namingConfig.RenameEpisodes)
if (!namingConfig.RenameMovies)
{
return GetOriginalTitle(movieFile);
}
@ -226,13 +232,40 @@ namespace NzbDrone.Core.Organizer
private void AddMovieTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Movie movie)
{
tokenHandlers["{Movie Title}"] = m => movie.Title;
tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title);
tokenHandlers["{Movie Title}"] = m => GetLanguageTitle(movie, m.CustomFormat);
tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(GetLanguageTitle(movie, m.CustomFormat));
tokenHandlers["{Movie Title The}"] = m => TitleThe(movie.Title);
tokenHandlers["{Movie Certification}"] = mbox => movie.Certification;
tokenHandlers["{Movie TitleFirstCharacter}"] = m => TitleThe(movie.Title).Substring(0, 1).FirstCharToUpper();
}
private string GetLanguageTitle(Movie movie, string isoCodes)
{
if (isoCodes.IsNotNullOrWhiteSpace())
{
foreach (var isoCode in isoCodes.Split('|'))
{
var language = IsoLanguages.Find(isoCode.ToLower())?.Language;
if (language == null)
{
continue;
}
var titles = movie.Translations.Where(t => t.Language == language).ToList();
if (!movie.Translations.Any())
{
titles = _movieTranslationService.GetAllTranslationsForMovie(movie.Id).Where(t => t.Language == language).ToList();
}
return titles.FirstOrDefault()?.Title ?? movie.Title;
}
}
return movie.Title;
}
private void AddTagsTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, MovieFile movieFile)
{
if (movieFile.Edition.IsNotNullOrWhiteSpace())

@ -6,15 +6,15 @@ namespace NzbDrone.Core.Organizer
{
public static NamingConfig Default => new NamingConfig
{
RenameEpisodes = false,
RenameMovies = false,
ReplaceIllegalCharacters = true,
ColonReplacementFormat = 0,
MultiEpisodeStyle = 0,
MovieFolderFormat = "{Movie Title} ({Release Year})",
StandardMovieFormat = "{Movie Title} ({Release Year}) {Quality Full}",
MovieFolderFormat = "{Movie Title:EN} ({Release Year})",
StandardMovieFormat = "{Movie Title:EN} ({Release Year}) {Quality Full}",
};
public bool RenameEpisodes { get; set; }
public bool RenameMovies { get; set; }
public bool ReplaceIllegalCharacters { get; set; }
public ColonReplacementFormat ColonReplacementFormat { get; set; }
public int MultiEpisodeStyle { get; set; }

@ -14,7 +14,7 @@ namespace NzbDrone.Core.Parser
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(LanguageParser));
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_|^)(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VOSTFR|VO|VFF|VFQ|VFI|VF2|TRUEFRENCH)(?:\W|_))|(?<russian>\brus\b)|(?<dutch>nl\W?subs?)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)",
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_|^)(?<italian>\b(?:ita|italian)\b)|(?<german>\b(?:german|videomann|ger)\b)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VOSTFR|VO|VFF|VFQ|VFI|VF2|TRUEFRENCH)(?:\W|_))|(?<russian>\brus\b)|(?<english>\beng\b)|(?<dutch>nl\W?subs?)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex CaseSensitiveLanguageRegex = new Regex(@"(?<lithuanian>\bLT\b)|(?<czech>\bCZ\b)",
@ -140,8 +140,10 @@ namespace NzbDrone.Core.Parser
languages.Add(Language.Czech);
}
var match = LanguageRegex.Match(title);
var matches = LanguageRegex.Matches(title);
foreach (Match match in matches)
{
if (match.Groups["italian"].Captures.Cast<Capture>().Any())
{
languages.Add(Language.Italian);
@ -172,6 +174,11 @@ namespace NzbDrone.Core.Parser
languages.Add(Language.Russian);
}
if (match.Groups["english"].Success)
{
languages.Add(Language.English);
}
if (match.Groups["dutch"].Success)
{
languages.Add(Language.Dutch);
@ -186,6 +193,7 @@ namespace NzbDrone.Core.Parser
{
languages.Add(Language.Hebrew);
}
}
if (title.ToLower().Contains("multi"))
{
@ -198,7 +206,7 @@ namespace NzbDrone.Core.Parser
if (!languages.Any())
{
languages.Add(Language.English);
languages.Add(Language.Unknown);
}
return languages.DistinctBy(l => (int)l).ToList();

@ -6,6 +6,7 @@ using NLog;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Parser.Augmenters;
@ -141,6 +142,12 @@ namespace NzbDrone.Core.Parser
result.Movie = null;
}
//Use movie language as fallback if we could't parse a language (more accurate than just using English)
if (parsedMovieInfo.Languages.Count <= 1 && parsedMovieInfo.Languages.First() == Language.Unknown && result.Movie != null)
{
parsedMovieInfo.Languages = new List<Language> { result.Movie.OriginalLanguage };
}
result.RemoteMovie.ParsedMovieInfo = parsedMovieInfo;
return result;

@ -21,6 +21,7 @@ namespace NzbDrone.Core.Profiles
Profile Get(int id);
bool Exists(int id);
Profile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed);
List<Language> GetAcceptableLanguages(int profileId);
}
public class ProfileService : IProfileService,
@ -265,6 +266,24 @@ namespace NzbDrone.Core.Profiles
return qualityProfile;
}
public List<Language> GetAcceptableLanguages(int profileId)
{
var profile = Get(profileId);
var wantedTitleLanguages = profile.FormatItems.Where(i => i.Score > 0).Select(item => item.Format)
.SelectMany(format => format.Specifications)
.Where(specification => specification is LanguageSpecification && !specification.Negate)
.Cast<LanguageSpecification>()
.Where(specification => specification.Value > 0)
.Select(specification => (Language)specification.Value)
.Distinct()
.ToList();
wantedTitleLanguages.Add(profile.Language);
return wantedTitleLanguages;
}
private Profile AddDefaultProfile(string name, Quality cutoff, params Quality[] allowed)
{
var profile = GetDefaultProfile(name, cutoff, allowed);

@ -123,9 +123,7 @@ namespace Radarr.Api.V3.Calendar
occurrence.Description = movie.Overview;
occurrence.Categories = new List<string>() { movie.Studio };
var physicalText = movie.PhysicalReleaseNote.IsNotNullOrWhiteSpace()
? $"(Physical Release, {movie.PhysicalReleaseNote})"
: "(Physical Release)";
var physicalText = "(Physical Release)";
occurrence.Summary = $"{movie.Title} " + (cinemasRelease ? "(Theatrical Release)" : physicalText);
}
}

@ -2,8 +2,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Nancy;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.SignalR;
using Radarr.Api.V3.Movies;
using Radarr.Http;
@ -13,15 +16,21 @@ namespace Radarr.Api.V3.Calendar
public class CalendarModule : RadarrRestModuleWithSignalR<MovieResource, Movie>
{
private readonly IMovieService _moviesService;
private readonly IMovieTranslationService _movieTranslationService;
private readonly IUpgradableSpecification _qualityUpgradableSpecification;
private readonly IConfigService _configService;
public CalendarModule(IBroadcastSignalRMessage signalR,
IMovieService moviesService,
IUpgradableSpecification qualityUpgradableSpecification)
IMovieTranslationService movieTranslationService,
IUpgradableSpecification qualityUpgradableSpecification,
IConfigService configService)
: base(signalR, "calendar")
{
_moviesService = moviesService;
_movieTranslationService = movieTranslationService;
_qualityUpgradableSpecification = qualityUpgradableSpecification;
_configService = configService;
GetResourceAll = GetCalendar;
}
@ -63,7 +72,8 @@ namespace Radarr.Api.V3.Calendar
return null;
}
var resource = movie.ToResource(_qualityUpgradableSpecification);
var translation = _movieTranslationService.GetAllTranslationsForMovie(movie.Id).FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage);
var resource = movie.ToResource(_qualityUpgradableSpecification, translation);
return resource;
}

@ -16,7 +16,7 @@ namespace Radarr.Api.V3.Config
{
Id = model.Id,
RenameMovies = model.RenameEpisodes,
RenameMovies = model.RenameMovies,
ReplaceIllegalCharacters = model.ReplaceIllegalCharacters,
ColonReplacementFormat = model.ColonReplacementFormat,
StandardMovieFormat = model.StandardMovieFormat,
@ -43,7 +43,7 @@ namespace Radarr.Api.V3.Config
{
Id = resource.Id,
RenameEpisodes = resource.RenameMovies,
RenameMovies = resource.RenameMovies,
ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters,
ColonReplacementFormat = resource.ColonReplacementFormat,
StandardMovieFormat = resource.StandardMovieFormat,

@ -16,6 +16,7 @@ namespace Radarr.Api.V3.Config
public bool ShowRelativeDates { get; set; }
public bool EnableColorImpairedMode { get; set; }
public int MovieInfoLanguage { get; set; }
}
public static class UiConfigResourceMapper
@ -33,6 +34,7 @@ namespace Radarr.Api.V3.Config
ShowRelativeDates = model.ShowRelativeDates,
EnableColorImpairedMode = model.EnableColorImpairedMode,
MovieInfoLanguage = model.MovieInfoLanguage
};
}
}

@ -39,8 +39,7 @@ namespace Radarr.Api.V3.ManualImport
Id = HashConverter.GetHashInt31(model.Path),
Path = model.Path,
RelativePath = model.RelativePath,
// FolderName = model.FolderName,
FolderName = model.FolderName,
Name = model.Name,
Size = model.Size,
Movie = model.Movie.ToResource(),

@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using Nancy;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Movies;
@ -15,13 +17,18 @@ namespace Radarr.Api.V3.Movies
private readonly ISearchForNewMovie _searchProxy;
private readonly IProvideMovieInfo _movieInfo;
private readonly IBuildFileNames _fileNameBuilder;
private readonly IConfigService _configService;
public MovieLookupModule(ISearchForNewMovie searchProxy, IProvideMovieInfo movieInfo, IBuildFileNames fileNameBuilder)
public MovieLookupModule(ISearchForNewMovie searchProxy,
IProvideMovieInfo movieInfo,
IBuildFileNames fileNameBuilder,
IConfigService configService)
: base("/movie/lookup")
{
_movieInfo = movieInfo;
_searchProxy = searchProxy;
_fileNameBuilder = fileNameBuilder;
_configService = configService;
Get("/", x => Search());
Get("/tmdb", x => SearchByTmdbId());
Get("/imdb", x => SearchByImdbId());
@ -33,7 +40,8 @@ namespace Radarr.Api.V3.Movies
if (int.TryParse(Request.Query.tmdbId, out tmdbId))
{
var result = _movieInfo.GetMovieInfo(tmdbId).Item1;
return result.ToResource();
var translation = result.Translations.FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage);
return result.ToResource(translation);
}
throw new BadRequestException("Tmdb Id was not valid");
@ -43,20 +51,24 @@ namespace Radarr.Api.V3.Movies
{
string imdbId = Request.Query.imdbId;
var result = _movieInfo.GetMovieByImdbId(imdbId);
return result.ToResource();
var translation = result.Translations.FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage);
return result.ToResource(translation);
}
private object Search()
{
var imdbResults = _searchProxy.SearchForNewMovie((string)Request.Query.term);
return MapToResource(imdbResults);
var searchResults = _searchProxy.SearchForNewMovie((string)Request.Query.term);
return MapToResource(searchResults);
}
private IEnumerable<MovieResource> MapToResource(IEnumerable<Movie> movies)
{
foreach (var currentMovie in movies)
{
var resource = currentMovie.ToResource();
var translation = currentMovie.Translations.FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage);
var resource = currentMovie.ToResource(translation);
var poster = currentMovie.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster);
if (poster != null)
{

@ -1,9 +1,12 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using Nancy;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
@ -12,6 +15,7 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Commands;
using NzbDrone.Core.Movies.Events;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
using NzbDrone.SignalR;
@ -30,17 +34,21 @@ namespace Radarr.Api.V3.Movies
IHandle<MediaCoversUpdatedEvent>
{
private readonly IMovieService _moviesService;
private readonly IMovieTranslationService _movieTranslationService;
private readonly IAddMovieService _addMovieService;
private readonly IMapCoversToLocal _coverMapper;
private readonly IManageCommandQueue _commandQueueManager;
private readonly IUpgradableSpecification _qualityUpgradableSpecification;
private readonly IConfigService _configService;
public MovieModule(IBroadcastSignalRMessage signalRBroadcaster,
IMovieService moviesService,
IMovieTranslationService movieTranslationService,
IAddMovieService addMovieService,
IMapCoversToLocal coverMapper,
IManageCommandQueue commandQueueManager,
IUpgradableSpecification qualityUpgradableSpecification,
IConfigService configService,
RootFolderValidator rootFolderValidator,
MappedNetworkDriveValidator mappedNetworkDriveValidator,
MoviePathValidator moviesPathValidator,
@ -52,8 +60,10 @@ namespace Radarr.Api.V3.Movies
: base(signalRBroadcaster)
{
_moviesService = moviesService;
_movieTranslationService = movieTranslationService;
_addMovieService = addMovieService;
_qualityUpgradableSpecification = qualityUpgradableSpecification;
_configService = configService;
_coverMapper = coverMapper;
_commandQueueManager = commandQueueManager;
@ -95,33 +105,44 @@ namespace Radarr.Api.V3.Movies
if (tmdbId > 0)
{
moviesResources.AddIfNotNull(_moviesService.FindByTmdbId(tmdbId).ToResource(_qualityUpgradableSpecification));
var movie = _moviesService.FindByTmdbId(tmdbId);
var translation = _movieTranslationService.GetAllTranslationsForMovie(movie.Id).Where(t => t.Language == (Language)_configService.MovieInfoLanguage).FirstOrDefault();
moviesResources.AddIfNotNull(movie.ToResource(_qualityUpgradableSpecification, translation));
}
else
{
moviesResources.AddRange(_moviesService.GetAllMovies().ToResource(_qualityUpgradableSpecification));
var translations = _movieTranslationService.GetAllTranslationsForLanguage((Language)_configService.MovieInfoLanguage);
var movies = _moviesService.GetAllMovies();
foreach (var movie in movies)
{
var translation = translations.FirstOrDefault(t => t.MovieId == movie.Id);
moviesResources.Add(movie.ToResource(_qualityUpgradableSpecification, translation));
}
}
MapCoversToLocal(moviesResources.ToArray());
PopulateAlternateTitles(moviesResources);
return moviesResources;
}
private MovieResource GetMovie(int id)
{
var movies = _moviesService.GetMovie(id);
return MapToResource(movies);
var movie = _moviesService.GetMovie(id);
return MapToResource(movie);
}
protected MovieResource MapToResource(Movie movies)
protected MovieResource MapToResource(Movie movie)
{
if (movies == null)
if (movie == null)
{
return null;
}
var resource = movies.ToResource();
var translation = _movieTranslationService.GetAllTranslationsForMovie(movie.Id).FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage);
var resource = movie.ToResource(_qualityUpgradableSpecification, translation);
MapCoversToLocal(resource);
return resource;
@ -155,9 +176,10 @@ namespace Radarr.Api.V3.Movies
var model = moviesResource.ToModel(movie);
_moviesService.UpdateMovie(model);
var updatedMovie = _moviesService.UpdateMovie(model);
var translation = _movieTranslationService.GetAllTranslationsForMovie(updatedMovie.Id).FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage);
BroadcastResourceChange(ModelAction.Updated, moviesResource);
BroadcastResourceChange(ModelAction.Updated, updatedMovie.ToResource(_qualityUpgradableSpecification, translation));
}
private void DeleteMovie(int id)
@ -176,28 +198,10 @@ namespace Radarr.Api.V3.Movies
}
}
private void PopulateAlternateTitles(List<MovieResource> resources)
{
foreach (var resource in resources)
{
PopulateAlternateTitles(resource);
}
}
private void PopulateAlternateTitles(MovieResource resource)
{
//var mappings = null;//_sceneMappingService.FindByTvdbId(resource.TvdbId);
//if (mappings == null) return;
//Not necessary anymore
//resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList();
}
public void Handle(MovieImportedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.ImportedMovie.MovieId);
var translation = _movieTranslationService.GetAllTranslationsForMovie(message.ImportedMovie.MovieId).FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage);
BroadcastResourceChange(ModelAction.Updated, message.ImportedMovie.Movie.ToResource(_qualityUpgradableSpecification, translation));
}
public void Handle(MovieFileDeletedEvent message)
@ -212,12 +216,14 @@ namespace Radarr.Api.V3.Movies
public void Handle(MovieUpdatedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.Movie.Id);
var translation = _movieTranslationService.GetAllTranslationsForMovie(message.Movie.Id).FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage);
BroadcastResourceChange(ModelAction.Updated, message.Movie.ToResource(_qualityUpgradableSpecification, translation));
}
public void Handle(MovieEditedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.Movie.Id);
var translation = _movieTranslationService.GetAllTranslationsForMovie(message.Movie.Id).FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage);
BroadcastResourceChange(ModelAction.Updated, message.Movie.ToResource(_qualityUpgradableSpecification, translation));
}
public void Handle(MoviesDeletedEvent message)
@ -230,7 +236,8 @@ namespace Radarr.Api.V3.Movies
public void Handle(MovieRenamedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.Movie.Id);
var translation = _movieTranslationService.GetAllTranslationsForMovie(message.Movie.Id).FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage);
BroadcastResourceChange(ModelAction.Updated, message.Movie.ToResource(_qualityUpgradableSpecification, translation));
}
public void Handle(MediaCoversUpdatedEvent message)

@ -2,8 +2,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.Core.Parser;
using Radarr.Api.V3.MovieFiles;
using Radarr.Http.REST;
@ -22,6 +25,7 @@ namespace Radarr.Api.V3.Movies
//View Only
public string Title { get; set; }
public string OriginalTitle { get; set; }
public List<AlternativeTitleResource> AlternateTitles { get; set; }
public int? SecondaryYear { get; set; }
public int SecondaryYearSourceId { get; set; }
@ -86,10 +90,10 @@ namespace Radarr.Api.V3.Movies
Id = model.Id,
TmdbId = model.TmdbId,
Title = model.Title,
OriginalTitle = model.Title,
SortTitle = model.SortTitle,
InCinemas = model.InCinemas,
PhysicalRelease = model.PhysicalRelease,
PhysicalReleaseNote = model.PhysicalReleaseNote,
HasFile = model.HasFile,
SizeOnDisk = size,
@ -100,7 +104,6 @@ namespace Radarr.Api.V3.Movies
Year = model.Year,
SecondaryYear = model.SecondaryYear,
SecondaryYearSourceId = model.SecondaryYearSourceId,
Path = model.Path,
QualityProfileId = model.ProfileId,
@ -131,7 +134,69 @@ namespace Radarr.Api.V3.Movies
};
}
public static MovieResource ToResource(this Movie model, IUpgradableSpecification upgradableSpecification)
public static MovieResource ToResource(this Movie model, MovieTranslation movieTranslation)
{
if (model == null)
{
return null;
}
long size = model.MovieFile?.Size ?? 0;
MovieFileResource movieFile = model.MovieFile?.ToResource(model);
var translatedTitle = movieTranslation?.Title ?? model.Title;
var translatedOverview = movieTranslation?.Overview ?? model.Overview;
return new MovieResource
{
Id = model.Id,
TmdbId = model.TmdbId,
Title = translatedTitle,
OriginalTitle = model.Title,
SortTitle = translatedTitle.NormalizeTitle(),
InCinemas = model.InCinemas,
PhysicalRelease = model.PhysicalRelease,
HasFile = model.HasFile,
SizeOnDisk = size,
Status = model.Status,
Overview = translatedOverview,
Images = model.Images,
Year = model.Year,
SecondaryYear = model.SecondaryYear,
Path = model.Path,
QualityProfileId = model.ProfileId,
Monitored = model.Monitored,
MinimumAvailability = model.MinimumAvailability,
IsAvailable = model.IsAvailable(),
FolderName = model.FolderName(),
Runtime = model.Runtime,
CleanTitle = model.CleanTitle,
ImdbId = model.ImdbId,
TitleSlug = model.TitleSlug,
RootFolderPath = model.RootFolderPath,
Certification = model.Certification,
Website = model.Website,
Genres = model.Genres,
Tags = model.Tags,
Added = model.Added,
AddOptions = model.AddOptions,
AlternateTitles = model.AlternativeTitles.ToResource(),
Ratings = model.Ratings,
MovieFile = movieFile,
YouTubeTrailerId = model.YouTubeTrailerId,
Studio = model.Studio,
Collection = model.Collection
};
}
public static MovieResource ToResource(this Movie model, IUpgradableSpecification upgradableSpecification, MovieTranslation movieTranslation)
{
if (model == null)
{
@ -141,26 +206,27 @@ namespace Radarr.Api.V3.Movies
long size = model.MovieFile?.Size ?? 0;
MovieFileResource movieFile = model.MovieFile?.ToResource(model, upgradableSpecification);
var translatedTitle = movieTranslation?.Title ?? model.Title;
var translatedOverview = movieTranslation?.Overview ?? model.Overview;
return new MovieResource
{
Id = model.Id,
TmdbId = model.TmdbId,
Title = model.Title,
SortTitle = model.SortTitle,
Title = translatedTitle,
SortTitle = translatedTitle.NormalizeTitle(),
InCinemas = model.InCinemas,
PhysicalRelease = model.PhysicalRelease,
PhysicalReleaseNote = model.PhysicalReleaseNote,
HasFile = model.HasFile,
SizeOnDisk = size,
Status = model.Status,
Overview = model.Overview,
Overview = translatedOverview,
Images = model.Images,
Year = model.Year,
SecondaryYear = model.SecondaryYear,
SecondaryYearSourceId = model.SecondaryYearSourceId,
Path = model.Path,
QualityProfileId = model.ProfileId,
@ -207,7 +273,6 @@ namespace Radarr.Api.V3.Movies
SortTitle = resource.SortTitle,
InCinemas = resource.InCinemas,
PhysicalRelease = resource.PhysicalRelease,
PhysicalReleaseNote = resource.PhysicalReleaseNote,
Overview = resource.Overview,
@ -215,7 +280,6 @@ namespace Radarr.Api.V3.Movies
Year = resource.Year,
SecondaryYear = resource.SecondaryYear,
SecondaryYearSourceId = resource.SecondaryYearSourceId,
Path = resource.Path,
ProfileId = resource.QualityProfileId,
@ -254,11 +318,6 @@ namespace Radarr.Api.V3.Movies
return movies.Select(ToResource).ToList();
}
public static List<MovieResource> ToResource(this IEnumerable<Movie> movies, IUpgradableSpecification upgradableSpecification)
{
return movies.ToList().ConvertAll(f => f.ToResource(upgradableSpecification));
}
public static List<Movie> ToModel(this IEnumerable<MovieResource> resources)
{
return resources.Select(ToModel).ToList();

Loading…
Cancel
Save