diff --git a/src/NzbDrone.Core.Test/Extras/ExtraServiceFixture.cs b/src/NzbDrone.Core.Test/Extras/ExtraServiceFixture.cs new file mode 100644 index 000000000..16331ebbc --- /dev/null +++ b/src/NzbDrone.Core.Test/Extras/ExtraServiceFixture.cs @@ -0,0 +1,206 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Extras; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Extras +{ + [TestFixture] + public class ExtraServiceFixture : CoreTest + { + private Series _series; + private EpisodeFile _episodeFile; + private LocalEpisode _localEpisode; + + private string _seriesFolder; + private string _episodeFolder; + + private Mock _subtitleService; + private Mock _otherExtraService; + + [SetUp] + public void Setup() + { + _seriesFolder = @"C:\Test\TV\Series Title".AsOsAgnostic(); + _episodeFolder = @"C:\Test\Unsorted TV\Series.Title.S01".AsOsAgnostic(); + + _series = Builder.CreateNew() + .With(s => s.Path = _seriesFolder) + .Build(); + + var episodes = Builder.CreateListOfSize(1) + .All() + .With(e => e.SeasonNumber = 1) + .Build() + .ToList(); + + + _episodeFile = Builder.CreateNew() + .With(f => f.Path = Path.Combine(_series.Path, "Season 1", "Series Title - S01E01.mkv").AsOsAgnostic()) + .With(f => f.RelativePath = @"Season 1\Series Title - S01E01.mkv".AsOsAgnostic()) + .Build(); + + _localEpisode = Builder.CreateNew() + .With(l => l.Series = _series) + .With(l => l.Episodes = episodes) + .With(l => l.Path = Path.Combine(_episodeFolder, "Series.Title.S01E01.mkv").AsOsAgnostic()) + .Build(); + + _subtitleService = new Mock(); + _subtitleService.SetupGet(s => s.Order).Returns(0); + _subtitleService.Setup(s => s.CanImportFile(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(false); + _subtitleService.Setup(s => s.CanImportFile(It.IsAny(), It.IsAny(), It.IsAny(), ".srt", It.IsAny())) + .Returns(true); + + _otherExtraService = new Mock(); + _otherExtraService.SetupGet(s => s.Order).Returns(1); + _otherExtraService.Setup(s => s.CanImportFile(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(true); + + Mocker.SetConstant>(new[] { + _subtitleService.Object, + _otherExtraService.Object + }); + + Mocker.GetMock().Setup(s => s.FolderExists(It.IsAny())) + .Returns(false); + + Mocker.GetMock().Setup(s => s.GetParentFolder(It.IsAny())) + .Returns((string path) => Directory.GetParent(path).FullName); + + WithExistingFolder(_series.Path); + WithExistingFile(_episodeFile.Path); + WithExistingFile(_localEpisode.Path); + + Mocker.GetMock().Setup(v => v.ImportExtraFiles).Returns(true); + Mocker.GetMock().Setup(v => v.ExtraFileExtensions).Returns("nfo,srt"); + } + + private void WithExistingFolder(string path, bool exists = true) + { + var dir = Path.GetDirectoryName(path); + + if (exists && dir.IsNotNullOrWhiteSpace()) + { + WithExistingFolder(dir); + } + + Mocker.GetMock().Setup(v => v.FolderExists(path)).Returns(exists); + } + + private void WithExistingFile(string path, bool exists = true, int size = 1000) + { + var dir = Path.GetDirectoryName(path); + + if (exists && dir.IsNotNullOrWhiteSpace()) + { + WithExistingFolder(dir); + } + + Mocker.GetMock().Setup(v => v.FileExists(path)).Returns(exists); + Mocker.GetMock().Setup(v => v.GetFileSize(path)).Returns(size); + } + + private void WithExistingFiles(List files) + { + foreach (string file in files) + { + WithExistingFile(file); + } + + Mocker.GetMock().Setup(s => s.GetFiles(_episodeFolder, SearchOption.AllDirectories)) + .Returns(files.ToArray()); + } + + [Test] + public void should_not_pass_file_if_import_disabled() + { + Mocker.GetMock().Setup(v => v.ImportExtraFiles).Returns(false); + + var nfofile = Path.Combine(_episodeFolder, "Series.Title.S01E01.nfo").AsOsAgnostic(); + + var files = new List { + _localEpisode.Path, + nfofile + }; + + WithExistingFiles(files); + + Subject.ImportEpisode(_localEpisode, _episodeFile, true); + + _subtitleService.Verify(v => v.CanImportFile(_localEpisode, _episodeFile, It.IsAny(), It.IsAny(), true), Times.Never()); + _otherExtraService.Verify(v => v.CanImportFile(_localEpisode, _episodeFile, It.IsAny(), It.IsAny(), true), Times.Never()); + } + + [Test] + [TestCase("Series Title - S01E01.sub")] + [TestCase("Series Title - S01E01.ass")] + public void should_not_pass_unwanted_file(string filePath) + { + Mocker.GetMock().Setup(v => v.ImportExtraFiles).Returns(false); + + var nfofile = Path.Combine(_episodeFolder, filePath).AsOsAgnostic(); + + var files = new List { + _localEpisode.Path, + nfofile + }; + + WithExistingFiles(files); + + Subject.ImportEpisode(_localEpisode, _episodeFile, true); + + _subtitleService.Verify(v => v.CanImportFile(_localEpisode, _episodeFile, It.IsAny(), It.IsAny(), true), Times.Never()); + _otherExtraService.Verify(v => v.CanImportFile(_localEpisode, _episodeFile, It.IsAny(), It.IsAny(), true), Times.Never()); + } + + [Test] + public void should_pass_subtitle_file_to_subtitle_service() + { + var subtitleFile = Path.Combine(_episodeFolder, "Series.Title.S01E01.en.srt").AsOsAgnostic(); + + var files = new List { + _localEpisode.Path, + subtitleFile + }; + + WithExistingFiles(files); + + Subject.ImportEpisode(_localEpisode, _episodeFile, true); + + _subtitleService.Verify(v => v.ImportFiles(_localEpisode, _episodeFile, new List { subtitleFile }, true), Times.Once()); + _otherExtraService.Verify(v => v.ImportFiles(_localEpisode, _episodeFile, new List { subtitleFile }, true), Times.Never()); + } + + [Test] + public void should_pass_nfo_file_to_other_service() + { + var nfofile = Path.Combine(_episodeFolder, "Series.Title.S01E01.nfo").AsOsAgnostic(); + + var files = new List { + _localEpisode.Path, + nfofile + }; + + WithExistingFiles(files); + + Subject.ImportEpisode(_localEpisode, _episodeFile, true); + + _subtitleService.Verify(v => v.ImportFiles(_localEpisode, _episodeFile, new List { nfofile }, true), Times.Never()); + _otherExtraService.Verify(v => v.ImportFiles(_localEpisode, _episodeFile, new List { nfofile }, true), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/Extras/Others/OtherExtraServiceFixture.cs b/src/NzbDrone.Core.Test/Extras/Others/OtherExtraServiceFixture.cs new file mode 100644 index 000000000..fc0fc8fb2 --- /dev/null +++ b/src/NzbDrone.Core.Test/Extras/Others/OtherExtraServiceFixture.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FluentAssertions; +using FizzWare.NBuilder; +using NUnit.Framework; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Others; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Extras.Others +{ + [TestFixture] + public class OtherExtraServiceFixture : CoreTest + { + private Series _series; + private EpisodeFile _episodeFile; + private LocalEpisode _localEpisode; + + private string _seriesFolder; + private string _episodeFolder; + + [SetUp] + public void Setup() + { + _seriesFolder = @"C:\Test\TV\Series Title".AsOsAgnostic(); + _episodeFolder = @"C:\Test\Unsorted TV\Series.Title.S01".AsOsAgnostic(); + + _series = Builder.CreateNew() + .With(s => s.Path = _seriesFolder) + .Build(); + + var episodes = Builder.CreateListOfSize(1) + .All() + .With(e => e.SeasonNumber = 1) + .Build() + .ToList(); + + + _episodeFile = Builder.CreateNew() + .With(f => f.Path = Path.Combine(_series.Path, "Season 1", "Series Title - S01E01.mkv").AsOsAgnostic()) + .With(f => f.RelativePath = @"Season 1\Series Title - S01E01.mkv") + .Build(); + + _localEpisode = Builder.CreateNew() + .With(l => l.Series = _series) + .With(l => l.Episodes = episodes) + .With(l => l.Path = Path.Combine(_episodeFolder, "Series.Title.S01E01.mkv").AsOsAgnostic()) + .With(l => l.FileEpisodeInfo = new ParsedEpisodeInfo + { + SeasonNumber = 1, + EpisodeNumbers = new[] { 1 } + }) + .Build(); + } + + [Test] + [TestCase("Series Title - S01E01.nfo", "Series Title - S01E01.nfo")] + [TestCase("Series.Title.S01E01.nfo", "Series Title - S01E01.nfo")] + [TestCase("Series-Title-S01E01.nfo", "Series Title - S01E01.nfo")] + [TestCase("Series Title S01E01.nfo", "Series Title - S01E01.nfo")] + [TestCase("Series_Title_S01E01.nfo", "Series Title - S01E01.nfo")] + [TestCase("S01E01.thumb.jpg", "Series Title - S01E01.jpg")] + [TestCase(@"Series.Title.S01E01\thumb.jpg", "Series Title - S01E01.jpg")] + public void should_import_matching_file(string filePath, string expectedOutputPath) + { + var files = new List { Path.Combine(_episodeFolder, filePath).AsOsAgnostic() }; + + var results = Subject.ImportFiles(_localEpisode, _episodeFile, files, true).ToList(); + + results.Count().Should().Be(1); + + results[0].RelativePath.AsOsAgnostic().PathEquals(Path.Combine("Season 1", expectedOutputPath).AsOsAgnostic()).Should().Be(true); + } + + [Test] + public void should_not_import_multiple_nfo_files() + { + var files = new List + { + Path.Combine(_episodeFolder, "Series.Title.S01E01.nfo").AsOsAgnostic(), + Path.Combine(_episodeFolder, "Series_Title_S01E01.nfo").AsOsAgnostic(), + }; + + var results = Subject.ImportFiles(_localEpisode, _episodeFile, files, true).ToList(); + + results.Count().Should().Be(1); + } + } +} diff --git a/src/NzbDrone.Core.Test/Extras/Subtitles/SubtitleServiceFixture.cs b/src/NzbDrone.Core.Test/Extras/Subtitles/SubtitleServiceFixture.cs new file mode 100644 index 000000000..ae11f1479 --- /dev/null +++ b/src/NzbDrone.Core.Test/Extras/Subtitles/SubtitleServiceFixture.cs @@ -0,0 +1,181 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FluentAssertions; +using FizzWare.NBuilder; +using NUnit.Framework; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Subtitles; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; +using Moq; +using NzbDrone.Common.Disk; + +namespace NzbDrone.Core.Test.Extras.Subtitles +{ + [TestFixture] + public class SubtitleServiceFixture : CoreTest + { + private Series _series; + private EpisodeFile _episodeFile; + private LocalEpisode _localEpisode; + + private string _seriesFolder; + private string _episodeFolder; + + [SetUp] + public void Setup() + { + _seriesFolder = @"C:\Test\TV\Series Title".AsOsAgnostic(); + _episodeFolder = @"C:\Test\Unsorted TV\Series.Title.S01".AsOsAgnostic(); + + _series = Builder.CreateNew() + .With(s => s.Path = _seriesFolder) + .Build(); + + var episodes = Builder.CreateListOfSize(1) + .All() + .With(e => e.SeasonNumber = 1) + .Build() + .ToList(); + + + _episodeFile = Builder.CreateNew() + .With(f => f.Path = Path.Combine(_series.Path, "Season 1", "Series Title - S01E01.mkv").AsOsAgnostic()) + .With(f => f.RelativePath = @"Season 1\Series Title - S01E01.mkv".AsOsAgnostic()) + .Build(); + + _localEpisode = Builder.CreateNew() + .With(l => l.Series = _series) + .With(l => l.Episodes = episodes) + .With(l => l.Path = Path.Combine(_episodeFolder, "Series.Title.S01E01.mkv").AsOsAgnostic()) + .With(l => l.FileEpisodeInfo = new ParsedEpisodeInfo + { + SeasonNumber = 1, + EpisodeNumbers = new[] { 1 } + }) + .Build(); + + Mocker.GetMock().Setup(s => s.GetParentFolder(It.IsAny())) + .Returns((string path) => Directory.GetParent(path).FullName); + + Mocker.GetMock().Setup(s => s.IsSample(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(DetectSampleResult.NotSample); + } + + [Test] + [TestCase("Series.Title.S01E01.en.nfo")] + public void should_not_import_non_subtitle_file(string filePath) + { + var files = new List { Path.Combine(_episodeFolder, filePath).AsOsAgnostic() }; + + var results = Subject.ImportFiles(_localEpisode, _episodeFile, files, true).ToList(); + + results.Count().Should().Be(0); + } + + [Test] + [TestCase("Series Title - S01E01.srt", "Series Title - S01E01.srt")] + [TestCase("Series.Title.S01E01.en.srt", "Series Title - S01E01.en.srt")] + [TestCase("Series.Title.S01E01.english.srt", "Series Title - S01E01.en.srt")] + [TestCase("Series-Title-S01E01-fr-cc.srt", "Series Title - S01E01.fr.srt")] + [TestCase("Series Title S01E01_en_sdh_forced.srt", "Series Title - S01E01.en.srt")] + [TestCase("Series_Title_S01E01 en.srt", "Series Title - S01E01.en.srt")] + [TestCase(@"Subs\S01E01.en.srt", "Series Title - S01E01.en.srt")] + [TestCase(@"Subs\Series.Title.S01E01\2_en.srt", "Series Title - S01E01.en.srt")] + public void should_import_matching_subtitle_file(string filePath, string expectedOutputPath) + { + var files = new List { Path.Combine(_episodeFolder, filePath).AsOsAgnostic() }; + + var results = Subject.ImportFiles(_localEpisode, _episodeFile, files, true).ToList(); + + results.Count().Should().Be(1); + + results[0].RelativePath.AsOsAgnostic().PathEquals(Path.Combine("Season 1", expectedOutputPath).AsOsAgnostic()).Should().Be(true); + } + + [Test] + public void should_import_multiple_subtitle_files_per_language() + { + var files = new List + { + Path.Combine(_episodeFolder, "Series.Title.S01E01.en.srt").AsOsAgnostic(), + Path.Combine(_episodeFolder, "Series.Title.S01E01.english.srt").AsOsAgnostic(), + Path.Combine(_episodeFolder, "Subs", "Series_Title_S01E01_en_forced.srt").AsOsAgnostic(), + Path.Combine(_episodeFolder, "Subs", "Series.Title.S01E01", "2_fr.srt").AsOsAgnostic() + }; + + var expectedOutputs = new string[] + { + "Series Title - S01E01.1.en.srt", + "Series Title - S01E01.2.en.srt", + "Series Title - S01E01.3.en.srt", + "Series Title - S01E01.fr.srt", + }; + + var results = Subject.ImportFiles(_localEpisode, _episodeFile, files, true).ToList(); + + results.Count().Should().Be(expectedOutputs.Length); + + for (int i = 0; i < expectedOutputs.Length; i++) + { + results[i].RelativePath.AsOsAgnostic().PathEquals(Path.Combine("Season 1", expectedOutputs[i]).AsOsAgnostic()).Should().Be(true); + } + } + + [Test] + [TestCase("sub.srt", "Series Title - S01E01.srt")] + [TestCase(@"Subs\2_en.srt", "Series Title - S01E01.en.srt")] + public void should_import_unmatching_subtitle_file_if_only_episode(string filePath, string expectedOutputPath) + { + var subtitleFile = Path.Combine(_episodeFolder, filePath).AsOsAgnostic(); + + var sampleFile = Path.Combine(_series.Path, "Season 1", "Series Title - S01E01.sample.mkv").AsOsAgnostic(); + + var videoFiles = new string[] + { + _localEpisode.Path, + sampleFile + }; + + Mocker.GetMock().Setup(s => s.GetFiles(It.IsAny(), SearchOption.AllDirectories)) + .Returns(videoFiles); + + Mocker.GetMock().Setup(s => s.IsSample(It.IsAny(), sampleFile, It.IsAny())) + .Returns(DetectSampleResult.Sample); + + var results = Subject.ImportFiles(_localEpisode, _episodeFile, new List { subtitleFile }, true).ToList(); + + results.Count().Should().Be(1); + + results[0].RelativePath.AsOsAgnostic().PathEquals(Path.Combine("Season 1", expectedOutputPath).AsOsAgnostic()).Should().Be(true); + + ExceptionVerification.ExpectedWarns(1); + } + + [Test] + [TestCase("sub.srt")] + [TestCase(@"Subs\2_en.srt")] + public void should_not_import_unmatching_subtitle_file_if_multiple_episodes(string filePath) + { + var subtitleFile = Path.Combine(_episodeFolder, filePath).AsOsAgnostic(); + + var videoFiles = new string[] + { + _localEpisode.Path, + Path.Combine(_series.Path, "Season 1", "Series Title - S01E01.sample.mkv").AsOsAgnostic() + }; + + Mocker.GetMock().Setup(s => s.GetFiles(It.IsAny(), SearchOption.AllDirectories)) + .Returns(videoFiles); + + var results = Subject.ImportFiles(_localEpisode, _episodeFile, new List { subtitleFile }, true).ToList(); + + results.Count().Should().Be(0); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index 9fc34d6d1..f1ffd843b 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -35,6 +35,22 @@ namespace NzbDrone.Core.Test.ParserTests result.Should().Be(Language.Unknown); } + [TestCase("Series Title - S01E01 - Pilot.en.sub")] + [TestCase("Series Title - S01E01 - Pilot.EN.sub")] + [TestCase("Series Title - S01E01 - Pilot.eng.sub")] + [TestCase("Series Title - S01E01 - Pilot.ENG.sub")] + [TestCase("Series Title - S01E01 - Pilot.English.sub")] + [TestCase("Series Title - S01E01 - Pilot.english.sub")] + [TestCase("Series Title - S01E01 - Pilot.en.cc.sub")] + [TestCase("Series Title - S01E01 - Pilot.en.sdh.sub")] + [TestCase("Series Title - S01E01 - Pilot.en.forced.sub")] + [TestCase("Series Title - S01E01 - Pilot.en.sdh.forced.sub")] + public void should_parse_subtitle_language_english(string fileName) + { + var result = LanguageParser.ParseSubtitleLanguage(fileName); + result.Should().Be(Language.English); + } + [TestCase("Title.the.Series.2009.S01E14.French.HDTV.XviD-LOL")] [TestCase("Title.the.Series.The.1x13.Tueurs.De.Flics.FR.DVDRip.XviD")] public void should_parse_language_french(string postTitle) diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index a75b0edae..69b5aac3d 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -32,13 +32,12 @@ namespace NzbDrone.Core.Extras private readonly IDiskProvider _diskProvider; private readonly IConfigService _configService; private readonly List _extraFileManagers; - private readonly Logger _logger; public ExtraService(IMediaFileService mediaFileService, IEpisodeService episodeService, IDiskProvider diskProvider, IConfigService configService, - List extraFileManagers, + IEnumerable extraFileManagers, Logger logger) { _mediaFileService = mediaFileService; @@ -46,13 +45,12 @@ namespace NzbDrone.Core.Extras _diskProvider = diskProvider; _configService = configService; _extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList(); - _logger = logger; } public void ImportEpisode(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly) { ImportExtraFiles(localEpisode, episodeFile, isReadOnly); - + CreateAfterEpisodeImport(localEpisode.Series, episodeFile); } @@ -63,62 +61,38 @@ namespace NzbDrone.Core.Extras return; } - var sourcePath = localEpisode.Path; - var sourceFolder = _diskProvider.GetParentFolder(sourcePath); - var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath); - var files = _diskProvider.GetFiles(sourceFolder, SearchOption.TopDirectoryOnly); - var wantedExtensions = _configService.ExtraFileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(e => e.Trim(' ', '.')) + .Select(e => e.Trim(' ', '.') + .Insert(0, ".")) .ToList(); - var matchingFilenames = files.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(sourceFileName, StringComparison.InvariantCultureIgnoreCase)).ToList(); - var filteredFilenames = new List(); - var hasNfo = false; + var sourceFolder = _diskProvider.GetParentFolder(localEpisode.Path); + var files = _diskProvider.GetFiles(sourceFolder, SearchOption.AllDirectories); + var managedFiles = _extraFileManagers.Select((i) => new List()).ToArray(); - foreach (var matchingFilename in matchingFilenames) + foreach (var file in files) { - // Filter out duplicate NFO files - - if (matchingFilename.EndsWith(".nfo", StringComparison.InvariantCultureIgnoreCase)) - { - if (hasNfo) - { - continue; - } - - hasNfo = true; - } - - filteredFilenames.Add(matchingFilename); - } - - foreach (var matchingFilename in filteredFilenames) - { - var matchingExtension = wantedExtensions.FirstOrDefault(e => matchingFilename.EndsWith(e)); + var extension = Path.GetExtension(file); + var matchingExtension = wantedExtensions.FirstOrDefault(e => e.Equals(extension)); if (matchingExtension == null) { continue; } - try + for (int i = 0; i < _extraFileManagers.Count; i++) { - foreach (var extraFileManager in _extraFileManagers) + if (_extraFileManagers[i].CanImportFile(localEpisode, episodeFile, file, extension, isReadOnly)) { - var extension = Path.GetExtension(matchingFilename); - var extraFile = extraFileManager.Import(localEpisode.Series, episodeFile, matchingFilename, extension, isReadOnly); - - if (extraFile != null) - { - break; - } + managedFiles[i].Add(file); + break; } } - catch (Exception ex) - { - _logger.Warn(ex, "Failed to import extra file: {0}", matchingFilename); - } + } + + for (int i = 0; i < _extraFileManagers.Count; i++) + { + _extraFileManagers[i].ImportFiles(localEpisode, episodeFile, managedFiles[i], isReadOnly); } } diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs index 949218d7c..e03508495 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Extras.Files @@ -19,7 +20,8 @@ namespace NzbDrone.Core.Extras.Files IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); IEnumerable CreateAfterEpisodeFolder(Series series, string seriesFolder, string seasonFolder); IEnumerable MoveFilesAfterRename(Series series, List episodeFiles); - ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly); + bool CanImportFile(LocalEpisode localEpisode, EpisodeFile episodeFile, string path, string extension, bool readOnly); + IEnumerable ImportFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, List files, bool isReadOnly); } public abstract class ExtraFileManager : IManageExtraFiles @@ -48,7 +50,8 @@ namespace NzbDrone.Core.Extras.Files public abstract IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); public abstract IEnumerable CreateAfterEpisodeFolder(Series series, string seriesFolder, string seasonFolder); public abstract IEnumerable MoveFilesAfterRename(Series series, List episodeFiles); - public abstract ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly); + public abstract bool CanImportFile(LocalEpisode localEpisode, EpisodeFile episodeFile, string path, string extension, bool readOnly); + public abstract IEnumerable ImportFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, List files, bool isReadOnly); protected TExtraFile ImportFile(Series series, EpisodeFile episodeFile, string path, bool readOnly, string extension, string fileNameSuffix = null) { diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs index 78b143acb..c3305cf2d 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Others; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Extras.Metadata @@ -202,9 +203,14 @@ namespace NzbDrone.Core.Extras.Metadata return movedFiles; } - public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) + public override bool CanImportFile(LocalEpisode localEpisode, EpisodeFile episodeFile, string path, string extension, bool readOnly) { - return null; + return false; + } + + public override IEnumerable ImportFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, List files, bool isReadOnly) + { + return Enumerable.Empty(); } private List GetMetadataFilesForConsumer(IMetadata consumer, List seriesMetadata) diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs index 9e2cad223..c7b5b1ab0 100644 --- a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs @@ -8,14 +8,17 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Extras.Others { public class OtherExtraService : ExtraFileManager { + private readonly IDiskProvider _diskProvider; private readonly IOtherExtraFileService _otherExtraFileService; private readonly IMediaFileAttributeService _mediaFileAttributeService; + private readonly Logger _logger; public OtherExtraService(IConfigService configService, IDiskProvider diskProvider, @@ -25,8 +28,10 @@ namespace NzbDrone.Core.Extras.Others Logger logger) : base(configService, diskProvider, diskTransferService, logger) { + _diskProvider = diskProvider; _otherExtraFileService = otherExtraFileService; _mediaFileAttributeService = mediaFileAttributeService; + _logger = logger; } public override int Order => 2; @@ -71,14 +76,79 @@ namespace NzbDrone.Core.Extras.Others return movedFiles; } - public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) + public override bool CanImportFile(LocalEpisode localEpisode, EpisodeFile episodeFile, string path, string extension, bool readOnly) { - var extraFile = ImportFile(series, episodeFile, path, readOnly, extension, null); + return true; + } - _mediaFileAttributeService.SetFilePermissions(path); - _otherExtraFileService.Upsert(extraFile); + public override IEnumerable ImportFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, List files, bool isReadOnly) + { + var importedFiles = new List(); + var filteredFiles = files.Where(f => CanImportFile(localEpisode, episodeFile, f, Path.GetExtension(f), isReadOnly)).ToList(); + var sourcePath = localEpisode.Path; + var sourceFolder = _diskProvider.GetParentFolder(sourcePath); + var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath); + var matchingFiles = new List(); + var hasNfo = false; - return extraFile; + foreach (var file in filteredFiles) + { + try + { + // Filter out duplicate NFO files + if (file.EndsWith(".nfo", StringComparison.InvariantCultureIgnoreCase)) + { + if (hasNfo) + { + continue; + } + + hasNfo = true; + } + + // Filename match + if (Path.GetFileNameWithoutExtension(file).StartsWith(sourceFileName, StringComparison.InvariantCultureIgnoreCase)) + { + matchingFiles.Add(file); + continue; + } + + // Season and episode match + var fileEpisodeInfo = Parser.Parser.ParsePath(file) ?? new ParsedEpisodeInfo(); + + if (fileEpisodeInfo.EpisodeNumbers.Length == 0) + { + continue; + } + + if (fileEpisodeInfo.SeasonNumber == localEpisode.FileEpisodeInfo.SeasonNumber && + fileEpisodeInfo.EpisodeNumbers.SequenceEqual(localEpisode.FileEpisodeInfo.EpisodeNumbers)) + { + matchingFiles.Add(file); + } + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to import extra file: {0}", file); + } + } + + foreach (string file in matchingFiles) + { + try + { + var extraFile = ImportFile(localEpisode.Series, episodeFile, file, isReadOnly, Path.GetExtension(file), null); + _mediaFileAttributeService.SetFilePermissions(file); + _otherExtraFileService.Upsert(extraFile); + importedFiles.Add(extraFile); + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to import extra file: {0}", file); + } + } + + return importedFiles; } } } diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs index 4a9c16a64..83cd74b94 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -9,13 +10,17 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Extras.Subtitles { public class SubtitleService : ExtraFileManager { + private readonly IDiskProvider _diskProvider; + private readonly IDetectSample _detectSample; private readonly ISubtitleFileService _subtitleFileService; private readonly IMediaFileAttributeService _mediaFileAttributeService; private readonly Logger _logger; @@ -23,11 +28,14 @@ namespace NzbDrone.Core.Extras.Subtitles public SubtitleService(IConfigService configService, IDiskProvider diskProvider, IDiskTransferService diskTransferService, + IDetectSample detectSample, ISubtitleFileService subtitleFileService, IMediaFileAttributeService mediaFileAttributeService, Logger logger) : base(configService, diskProvider, diskTransferService, logger) { + _diskProvider = diskProvider; + _detectSample = detectSample; _subtitleFileService = subtitleFileService; _mediaFileAttributeService = mediaFileAttributeService; _logger = logger; @@ -71,11 +79,6 @@ namespace NzbDrone.Core.Extras.Subtitles var groupCount = group.Count(); var copy = 1; - if (groupCount > 1) - { - _logger.Warn("Multiple subtitle files found with the same language and extension for {0}", Path.Combine(series.Path, episodeFile.RelativePath)); - } - foreach (var subtitleFile in group) { var suffix = GetSuffix(subtitleFile.Language, copy, groupCount > 1); @@ -91,23 +94,129 @@ namespace NzbDrone.Core.Extras.Subtitles return movedFiles; } - public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) + public override bool CanImportFile(LocalEpisode localEpisode, EpisodeFile episodeFile, string path, string extension, bool readOnly) { - if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path))) + return SubtitleFileExtensions.Extensions.Contains(extension.ToLowerInvariant()); + } + + public override IEnumerable ImportFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, List files, bool isReadOnly) + { + var importedFiles = new List(); + + var filteredFiles = files.Where(f => CanImportFile(localEpisode, episodeFile, f, Path.GetExtension(f), isReadOnly)).ToList(); + + var sourcePath = localEpisode.Path; + var sourceFolder = _diskProvider.GetParentFolder(sourcePath); + var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath); + + var matchingFiles = new List(); + + foreach (var file in filteredFiles) { - var language = LanguageParser.ParseSubtitleLanguage(path); - var suffix = GetSuffix(language, 1, false); - var subtitleFile = ImportFile(series, episodeFile, path, readOnly, extension, suffix); - subtitleFile.Language = language; + try + { + // Filename match + if (Path.GetFileNameWithoutExtension(file).StartsWith(sourceFileName, StringComparison.InvariantCultureIgnoreCase)) + { + matchingFiles.Add(file); + continue; + } - _mediaFileAttributeService.SetFilePermissions(path); - _subtitleFileService.Upsert(subtitleFile); + // Season and episode match + var fileEpisodeInfo = Parser.Parser.ParsePath(file) ?? new ParsedEpisodeInfo(); + if (fileEpisodeInfo.EpisodeNumbers.Length == 0) + { + continue; + } - return subtitleFile; + if (fileEpisodeInfo.SeasonNumber == localEpisode.FileEpisodeInfo.SeasonNumber && + fileEpisodeInfo.EpisodeNumbers.SequenceEqual(localEpisode.FileEpisodeInfo.EpisodeNumbers)) + { + matchingFiles.Add(file); + } + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to import subtitle file: {0}", file); + } } - return null; + // Use any sub if only episode in folder + if (matchingFiles.Count == 0 && filteredFiles.Count > 0) + { + var videoFiles = _diskProvider.GetFiles(sourceFolder, SearchOption.AllDirectories) + .Where(file => MediaFileExtensions.Extensions.Contains(Path.GetExtension(file))) + .ToList(); + + if (videoFiles.Count() > 2) + { + return importedFiles; + } + + // Filter out samples + videoFiles = videoFiles.Where(file => + { + var sample = _detectSample.IsSample(localEpisode.Series, file, false); + + if (sample == DetectSampleResult.Sample) + { + return false; + } + + return true; + }).ToList(); + + if (videoFiles.Count == 1) + { + matchingFiles.AddRange(filteredFiles); + + _logger.Warn("Imported any available subtitle file for episode: {0}", localEpisode); + } + } + + var subtitleFiles = new List>(); + + foreach (string file in matchingFiles) + { + var language = LanguageParser.ParseSubtitleLanguage(file); + var extension = Path.GetExtension(file); + subtitleFiles.Add(new Tuple(file, language, extension)); + } + + var groupedSubtitleFiles = subtitleFiles.GroupBy(s => s.Item2 + s.Item3).ToList(); + + foreach (var group in groupedSubtitleFiles) + { + var groupCount = group.Count(); + var copy = 1; + + foreach (var file in group) + { + try + { + var path = file.Item1; + var language = file.Item2; + var extension = file.Item3; + var suffix = GetSuffix(language, copy, groupCount > 1); + var subtitleFile = ImportFile(localEpisode.Series, episodeFile, path, isReadOnly, extension, suffix); + subtitleFile.Language = language; + + _mediaFileAttributeService.SetFilePermissions(path); + _subtitleFileService.Upsert(subtitleFile); + + importedFiles.Add(subtitleFile); + + copy++; + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to import subtitle file: {0}", file.Item1); + } + } + } + + return importedFiles; } private string GetSuffix(Language language, int copy, bool multipleCopies = false) diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index 65f54c745..b1e7d0127 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -24,7 +24,7 @@ namespace NzbDrone.Core.Parser RegexOptions.Compiled); - private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?[a-z]{2,3})$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?[a-z]{2,3})([-_. ](?full|forced|foreign|default|cc|psdh|sdh))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); public static Language ParseLanguage(string title, bool defaultToEnglish = true) { @@ -124,12 +124,12 @@ namespace NzbDrone.Core.Parser if (languageMatch.Success) { var isoCode = languageMatch.Groups["iso_code"].Value; - var isoLanguage = IsoLanguages.Find(isoCode); + var isoLanguage = IsoLanguages.Find(isoCode.ToLower()); return isoLanguage?.Language ?? Language.Unknown; } - foreach (Language language in Enum.GetValues(typeof(Language))) + foreach (Language language in Language.All) { if (simpleFilename.EndsWith(language.ToString(), StringComparison.OrdinalIgnoreCase)) {