diff --git a/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs b/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs index dbef200d9..db353fa69 100644 --- a/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Aggregation/Aggregators/AggregateLanguagesFixture.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; +using Moq; using NUnit.Framework; using NzbDrone.Core.Download.Aggregation.Aggregators; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.TorrentRss; using NzbDrone.Core.Languages; using NzbDrone.Core.Movies; using NzbDrone.Core.Parser.Model; @@ -61,6 +64,143 @@ public void should_return_parsed_language() Subject.Aggregate(_remoteMovie).Languages.Should().Equal(_remoteMovie.ParsedMovieInfo.Languages); } + [Test] + public void should_return_multi_languages_when_indexer_id_has_multi_languages_configuration() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + var indexerDefinition = new IndexerDefinition + { + Id = 1, + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List { Language.Original.Id, Language.French.Id } } + }; + Mocker.GetMock() + .Setup(v => v.Get(1)) + .Returns(indexerDefinition); + + _remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List { }, releaseTitle); + _remoteMovie.Release.IndexerId = 1; + _remoteMovie.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List { _movie.MovieMetadata.Value.OriginalLanguage, Language.French }); + Mocker.GetMock().Verify(c => c.Get(1), Times.Once()); + Mocker.GetMock().VerifyNoOtherCalls(); + } + + [Test] + public void should_return_multi_languages_from_indexer_with_id_when_indexer_id_and_name_are_set() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + var indexerDefinition1 = new IndexerDefinition + { + Id = 1, + Name = "MyIndexer1", + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List { Language.Original.Id, Language.French.Id } } + }; + var indexerDefinition2 = new IndexerDefinition + { + Id = 2, + Name = "MyIndexer2", + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List { Language.Original.Id, Language.German.Id } } + }; + + Mocker.GetMock() + .Setup(v => v.Get(1)) + .Returns(indexerDefinition1); + + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(new List() { indexerDefinition1, indexerDefinition2 }); + + _remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List { }, releaseTitle); + _remoteMovie.Release.IndexerId = 1; + _remoteMovie.Release.Indexer = "MyIndexer2"; + _remoteMovie.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List { _movie.MovieMetadata.Value.OriginalLanguage, Language.French }); + Mocker.GetMock().Verify(c => c.Get(1), Times.Once()); + Mocker.GetMock().VerifyNoOtherCalls(); + } + + [Test] + public void should_return_multi_languages_when_indexer_name_has_multi_languages_configuration() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + var indexerDefinition = new IndexerDefinition + { + Id = 1, + Name = "MyIndexer (Prowlarr)", + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List { Language.Original.Id, Language.French.Id } } + }; + + Mocker.GetMock() + .Setup(v => v.FindByName("MyIndexer (Prowlarr)")) + .Returns(indexerDefinition); + + _remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List { }, releaseTitle); + _remoteMovie.Release.Indexer = "MyIndexer (Prowlarr)"; + _remoteMovie.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List { _movie.MovieMetadata.Value.OriginalLanguage, Language.French }); + Mocker.GetMock().Verify(c => c.FindByName("MyIndexer (Prowlarr)"), Times.Once()); + Mocker.GetMock().VerifyNoOtherCalls(); + } + + [Test] + public void should_return_multi_languages_when_release_as_unknown_as_default_language_and_indexer_has_multi_languages_configuration() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + var indexerDefinition = new IndexerDefinition + { + Id = 1, + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List { Language.Original.Id, Language.French.Id } } + }; + Mocker.GetMock() + .Setup(v => v.Get(1)) + .Returns(indexerDefinition); + + _remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List { Language.Unknown }, releaseTitle); + _remoteMovie.Release.IndexerId = 1; + _remoteMovie.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List { _movie.MovieMetadata.Value.OriginalLanguage, Language.French }); + Mocker.GetMock().Verify(c => c.Get(1), Times.Once()); + Mocker.GetMock().VerifyNoOtherCalls(); + } + + [Test] + public void should_return_original_when_indexer_has_no_multi_languages_configuration() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + var indexerDefinition = new IndexerDefinition + { + Id = 1, + Settings = new TorrentRssIndexerSettings { } + }; + Mocker.GetMock() + .Setup(v => v.Get(1)) + .Returns(indexerDefinition); + + _remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List { }, releaseTitle); + _remoteMovie.Release.IndexerId = 1; + _remoteMovie.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List { _movie.MovieMetadata.Value.OriginalLanguage }); + Mocker.GetMock().Verify(c => c.Get(1), Times.Once()); + Mocker.GetMock().VerifyNoOtherCalls(); + } + + [Test] + public void should_return_original_when_no_indexer_value() + { + var releaseTitle = "Series.Title.S01E01.MULTi.1080p.WEB.H265-RlsGroup"; + + _remoteMovie.ParsedMovieInfo = GetParsedMovieInfo(new List { }, releaseTitle); + _remoteMovie.Release.Title = releaseTitle; + + Subject.Aggregate(_remoteMovie).Languages.Should().BeEquivalentTo(new List { _movie.MovieMetadata.Value.OriginalLanguage }); + Mocker.GetMock().VerifyNoOtherCalls(); + } + [Test] public void should_exclude_language_that_is_part_of_episode_title_when_release_tokens_contains_episode_title() { diff --git a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs index bb3606005..54544923f 100644 --- a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs @@ -7,6 +7,8 @@ using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.History; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.TorrentRss; +using NzbDrone.Core.Languages; using NzbDrone.Core.Movies; using NzbDrone.Core.Movies.Events; using NzbDrone.Core.Parser; @@ -84,6 +86,77 @@ public void should_track_downloads_using_the_source_title_if_it_cannot_be_found_ trackedDownload.RemoteMovie.Movie.Id.Should().Be(3); } + [Test] + public void should_set_indexer() + { + var episodeHistory = new MovieHistory() + { + DownloadId = "35238", + SourceTitle = "TV Series S01", + MovieId = 3, + EventType = MovieHistoryEventType.Grabbed, + }; + episodeHistory.Data.Add("indexer", "MyIndexer (Prowlarr)"); + Mocker.GetMock() + .Setup(s => s.FindByDownloadId(It.Is(sr => sr == "35238"))) + .Returns(new List() + { + episodeHistory + }); + + var indexerDefinition = new IndexerDefinition + { + Id = 1, + Name = "MyIndexer (Prowlarr)", + Settings = new TorrentRssIndexerSettings { MultiLanguages = new List { Language.Original.Id, Language.French.Id } } + }; + Mocker.GetMock() + .Setup(v => v.Get(indexerDefinition.Id)) + .Returns(indexerDefinition); + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(new List() { indexerDefinition }); + + var remoteEpisode = new RemoteMovie + { + Movie = new Movie() { Id = 3 }, + ParsedMovieInfo = new ParsedMovieInfo() + { + MovieTitles = new List { "A Movie" }, + Year = 1998 + } + }; + + Mocker.GetMock() + .Setup(s => s.Map(It.IsAny(), It.IsAny(), It.IsAny(), null)) + .Returns(remoteEpisode); + + var client = new DownloadClientDefinition() + { + Id = 1, + Protocol = DownloadProtocol.Torrent + }; + + var item = new DownloadClientItem() + { + Title = "A Movie 1998", + DownloadId = "35238", + DownloadClientInfo = new DownloadClientItemClientInfo + { + Protocol = client.Protocol, + Id = client.Id, + Name = client.Name + } + }; + + var trackedDownload = Subject.TrackDownload(client, item); + + trackedDownload.Should().NotBeNull(); + trackedDownload.RemoteMovie.Should().NotBeNull(); + trackedDownload.RemoteMovie.Release.Should().NotBeNull(); + trackedDownload.RemoteMovie.Release.Indexer.Should().Be("MyIndexer (Prowlarr)"); + } + [Test] public void should_unmap_tracked_download_if_movie_deleted() { diff --git a/src/NzbDrone.Core.Test/Indexers/IndexerRepositoryFixture.cs b/src/NzbDrone.Core.Test/Indexers/IndexerRepositoryFixture.cs new file mode 100644 index 000000000..ebc8fb4c8 --- /dev/null +++ b/src/NzbDrone.Core.Test/Indexers/IndexerRepositoryFixture.cs @@ -0,0 +1,44 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Indexers +{ + [TestFixture] + public class IndexerRepositoryFixture : DbTest + { + private void GivenIndexers() + { + var indexers = Builder.CreateListOfSize(2) + .All() + .With(c => c.Id = 0) + .TheFirst(1) + .With(x => x.Name = "MyIndexer (Prowlarr)") + .TheNext(1) + .With(x => x.Name = "My Second Indexer (Prowlarr)") + .BuildList(); + + Subject.InsertMany(indexers); + } + + [Test] + public void should_finds_with_name() + { + GivenIndexers(); + var found = Subject.FindByName("MyIndexer (Prowlarr)"); + found.Should().NotBeNull(); + found.Name.Should().Be("MyIndexer (Prowlarr)"); + found.Id.Should().Be(1); + } + + [Test] + public void should_not_find_with_incorrect_case_name() + { + GivenIndexers(); + var found = Subject.FindByName("myindexer (prowlarr)"); + found.Should().BeNull(); + } + } +} diff --git a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs index ef105afc4..33bd341d6 100644 --- a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs +++ b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregateLanguages.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Languages; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -10,10 +12,13 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators { public class AggregateLanguages : IAggregateRemoteMovie { + private readonly IIndexerFactory _indexerFactory; private readonly Logger _logger; - public AggregateLanguages(Logger logger) + public AggregateLanguages(IIndexerFactory indexerFactory, + Logger logger) { + _indexerFactory = indexerFactory; _logger = logger; } @@ -66,6 +71,26 @@ public RemoteMovie Aggregate(RemoteMovie remoteMovie) languages = languages.Except(languagesToRemove).ToList(); } + if ((languages.Count == 0 || (languages.Count == 1 && languages.First() == Language.Unknown)) && releaseInfo?.Title?.IsNotNullOrWhiteSpace() == true) + { + IndexerDefinition indexer = null; + + if (releaseInfo is { IndexerId: > 0 }) + { + indexer = _indexerFactory.Get(releaseInfo.IndexerId); + } + else if (releaseInfo.Indexer?.IsNotNullOrWhiteSpace() == true) + { + indexer = _indexerFactory.FindByName(releaseInfo.Indexer); + } + + if (indexer?.Settings is IIndexerSettings settings && settings.MultiLanguages.Any() && Parser.Parser.HasMultipleLanguages(releaseInfo.Title)) + { + // Use indexer setting for Multi-languages + languages = settings.MultiLanguages.Select(i => (Language)i).ToList(); + } + } + // Use movie language as fallback if we couldn't parse a language if (languages.Count == 0 || (languages.Count == 1 && languages.First() == Language.Unknown)) { diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index d7bc173db..341536f5b 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -124,8 +124,6 @@ public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, Do if (parsedMovieInfo != null) { trackedDownload.RemoteMovie = _parsingService.Map(parsedMovieInfo, "", 0, null); - - _aggregationService.Augment(trackedDownload.RemoteMovie); } var downloadHistory = _downloadHistoryService.GetLatestDownloadHistoryItem(downloadItem.DownloadId); @@ -156,17 +154,24 @@ public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, Do } } - if (trackedDownload.RemoteMovie != null && - Enum.TryParse(grabbedEvent?.Data?.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags)) + if (trackedDownload.RemoteMovie != null) { trackedDownload.RemoteMovie.Release ??= new ReleaseInfo(); - trackedDownload.RemoteMovie.Release.IndexerFlags = flags; + trackedDownload.RemoteMovie.Release.Indexer = trackedDownload.Indexer; + trackedDownload.RemoteMovie.Release.Title = trackedDownload.RemoteMovie.ParsedMovieInfo?.ReleaseTitle; + + if (Enum.TryParse(grabbedEvent?.Data?.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags)) + { + trackedDownload.RemoteMovie.Release.IndexerFlags = flags; + } } } - // Calculate custom formats if (trackedDownload.RemoteMovie != null) { + _aggregationService.Augment(trackedDownload.RemoteMovie); + + // Calculate custom formats trackedDownload.RemoteMovie.CustomFormats = _formatCalculator.ParseCustomFormat(trackedDownload.RemoteMovie, downloadItem.TotalSize); } diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index ea6b8f5cc..335486bc2 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; using FluentValidation.Results; using NLog; @@ -19,8 +18,6 @@ namespace NzbDrone.Core.Indexers public abstract class IndexerBase : IIndexer where TSettings : IIndexerSettings, new() { - private static readonly Regex MultiRegex = new (@"[_. ](?multi)[_. ]", RegexOptions.Compiled | RegexOptions.IgnoreCase); - protected readonly IIndexerStatusService _indexerStatusService; protected readonly IConfigService _configService; protected readonly IParsingService _parsingService; @@ -83,7 +80,7 @@ protected virtual IList CleanupReleases(IEnumerable re result.ForEach(c => { // Use multi languages from setting if ReleaseInfo languages is empty - if (c.Languages.Empty() && MultiRegex.IsMatch(c.Title) && settings.MultiLanguages.Any()) + if (c.Languages.Empty() && settings.MultiLanguages.Any() && Parser.Parser.HasMultipleLanguages(c.Title)) { c.Languages = settings.MultiLanguages.Select(i => (Language)i).ToList(); } diff --git a/src/NzbDrone.Core/Indexers/IndexerFactory.cs b/src/NzbDrone.Core/Indexers/IndexerFactory.cs index d26ed6b56..620d36ed2 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFactory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFactory.cs @@ -13,10 +13,12 @@ public interface IIndexerFactory : IProviderFactory List RssEnabled(bool filterBlockedIndexers = true); List AutomaticSearchEnabled(bool filterBlockedIndexers = true); List InteractiveSearchEnabled(bool filterBlockedIndexers = true); + IndexerDefinition FindByName(string name); } public class IndexerFactory : ProviderFactory, IIndexerFactory { + private readonly IIndexerRepository _indexerRepository; private readonly IIndexerStatusService _indexerStatusService; private readonly Logger _logger; @@ -28,6 +30,7 @@ public IndexerFactory(IIndexerStatusService indexerStatusService, Logger logger) : base(providerRepository, providers, container, eventAggregator, logger) { + _indexerRepository = providerRepository; _indexerStatusService = indexerStatusService; _logger = logger; } @@ -82,6 +85,11 @@ public List InteractiveSearchEnabled(bool filterBlockedIndexers = true return enabledIndexers.ToList(); } + public IndexerDefinition FindByName(string name) + { + return _indexerRepository.FindByName(name); + } + private IEnumerable FilterBlockedIndexers(IEnumerable indexers) { var blockedIndexers = _indexerStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId, v => v); diff --git a/src/NzbDrone.Core/Indexers/IndexerRepository.cs b/src/NzbDrone.Core/Indexers/IndexerRepository.cs index c4858f415..dac9e0fb2 100644 --- a/src/NzbDrone.Core/Indexers/IndexerRepository.cs +++ b/src/NzbDrone.Core/Indexers/IndexerRepository.cs @@ -1,4 +1,5 @@ -using NzbDrone.Core.Datastore; +using System.Linq; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider; @@ -6,6 +7,7 @@ namespace NzbDrone.Core.Indexers { public interface IIndexerRepository : IProviderRepository { + IndexerDefinition FindByName(string name); } public class IndexerRepository : ProviderRepository, IIndexerRepository @@ -14,5 +16,10 @@ public IndexerRepository(IMainDatabase database, IEventAggregator eventAggregato : base(database, eventAggregator) { } + + public IndexerDefinition FindByName(string name) + { + return Query(i => i.Name == name).SingleOrDefault(); + } } } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 068f3abfc..ec7d1ad54 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -171,6 +171,9 @@ public static class Parser private static readonly Regex RequestInfoRegex = new Regex(@"^(?:\[.+?\])+", RegexOptions.Compiled); private static readonly string[] Numbers = new[] { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" }; + + private static readonly Regex MultiRegex = new (@"[_. ](?multi)[_. ]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static Dictionary _umlautMappings = new Dictionary { { "ö", "oe" }, @@ -584,6 +587,11 @@ public static string RemoveFileExtension(string title) return title; } + public static bool HasMultipleLanguages(string title) + { + return MultiRegex.IsMatch(title); + } + private static ParsedMovieInfo ParseMovieMatchCollection(MatchCollection matchCollection) { if (!matchCollection[0].Groups["title"].Success || matchCollection[0].Groups["title"].Value == "(")