From dd5bc41eda88cc1ca4ebff291410cbe7b9d5b23c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Dupont?= Date: Sun, 27 Feb 2022 21:37:23 +0100 Subject: [PATCH] New: Import subtitles from sub folders --- .../Extras/ExtraServiceFixture.cs | 245 ++++++++++++++++++ .../Extras/Others/OtherExtraServiceFixture.cs | 84 ++++++ .../Subtitles/SubtitleServiceFixture.cs | 179 +++++++++++++ .../ParserTests/LanguageParserFixture.cs | 16 ++ src/NzbDrone.Core/Extras/ExtraService.cs | 61 ++--- .../Extras/Files/ExtraFileManager.cs | 7 +- .../Extras/Metadata/MetadataService.cs | 10 +- .../Extras/Others/OtherExtraService.cs | 81 +++++- .../Extras/Subtitles/SubtitleService.cs | 140 ++++++++-- src/NzbDrone.Core/Parser/LanguageParser.cs | 12 +- 10 files changed, 767 insertions(+), 68 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Extras/ExtraServiceFixture.cs create mode 100644 src/NzbDrone.Core.Test/Extras/Others/OtherExtraServiceFixture.cs create mode 100644 src/NzbDrone.Core.Test/Extras/Subtitles/SubtitleServiceFixture.cs diff --git a/src/NzbDrone.Core.Test/Extras/ExtraServiceFixture.cs b/src/NzbDrone.Core.Test/Extras/ExtraServiceFixture.cs new file mode 100644 index 000000000..9a8830a98 --- /dev/null +++ b/src/NzbDrone.Core.Test/Extras/ExtraServiceFixture.cs @@ -0,0 +1,245 @@ +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.Movies; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Extras +{ + [TestFixture] + public class ExtraServiceFixture : CoreTest + { + private Movie _movie; + private MovieFile _movieFile; + private LocalMovie _localMovie; + + private string _movieFolder; + private string _releaseFolder; + + private Mock _subtitleService; + private Mock _otherExtraService; + + [SetUp] + public void Setup() + { + _movieFolder = @"C:\Test\Movies\Movie Title".AsOsAgnostic(); + _releaseFolder = @"C:\Test\Unsorted TV\Movie.Title.2022".AsOsAgnostic(); + + _movie = Builder.CreateNew() + .With(s => s.Path = _movieFolder) + .Build(); + + _movieFile = Builder.CreateNew() + .With(f => f.Path = Path.Combine(_movie.Path, "Movie Title - 2022.mkv").AsOsAgnostic()) + .With(f => f.RelativePath = @"Movie Title - 2022.mkv".AsOsAgnostic()) + .Build(); + + _localMovie = Builder.CreateNew() + .With(l => l.Movie = _movie) + .With(l => l.Path = Path.Combine(_releaseFolder, "Movie.Title.2022.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(_movie.Path); + WithExistingFile(_movieFile.Path); + WithExistingFile(_localMovie.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(_releaseFolder, It.IsAny())) + .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(_releaseFolder, "Movie.Title.2022.nfo").AsOsAgnostic(); + + var files = new List + { + _localMovie.Path, + nfofile + }; + + WithExistingFiles(files); + + Subject.ImportMovie(_localMovie, _movieFile, true); + + _subtitleService.Verify(v => v.CanImportFile(_localMovie, _movieFile, It.IsAny(), It.IsAny(), true), Times.Never()); + _otherExtraService.Verify(v => v.CanImportFile(_localMovie, _movieFile, It.IsAny(), It.IsAny(), true), Times.Never()); + } + + [Test] + [TestCase("Movie Title - 2022.sub")] + [TestCase("Movie Title - 2022.ass")] + public void should_not_pass_unwanted_file(string filePath) + { + Mocker.GetMock().Setup(v => v.ImportExtraFiles).Returns(false); + + var nfofile = Path.Combine(_releaseFolder, filePath).AsOsAgnostic(); + + var files = new List + { + _localMovie.Path, + nfofile + }; + + WithExistingFiles(files); + + Subject.ImportMovie(_localMovie, _movieFile, true); + + _subtitleService.Verify(v => v.CanImportFile(_localMovie, _movieFile, It.IsAny(), It.IsAny(), true), Times.Never()); + _otherExtraService.Verify(v => v.CanImportFile(_localMovie, _movieFile, It.IsAny(), It.IsAny(), true), Times.Never()); + } + + [Test] + public void should_pass_subtitle_file_to_subtitle_service() + { + var subtitleFile = Path.Combine(_releaseFolder, "Movie.Title.2022.en.srt").AsOsAgnostic(); + + var files = new List + { + _localMovie.Path, + subtitleFile + }; + + WithExistingFiles(files); + + Subject.ImportMovie(_localMovie, _movieFile, true); + + _subtitleService.Verify(v => v.ImportFiles(_localMovie, _movieFile, new List { subtitleFile }, true), Times.Once()); + _otherExtraService.Verify(v => v.ImportFiles(_localMovie, _movieFile, new List { subtitleFile }, true), Times.Never()); + } + + [Test] + public void should_pass_nfo_file_to_other_service() + { + var nfofile = Path.Combine(_releaseFolder, "Movie.Title.2022.nfo").AsOsAgnostic(); + + var files = new List + { + _localMovie.Path, + nfofile + }; + + WithExistingFiles(files); + + Subject.ImportMovie(_localMovie, _movieFile, true); + + _subtitleService.Verify(v => v.ImportFiles(_localMovie, _movieFile, new List { nfofile }, true), Times.Never()); + _otherExtraService.Verify(v => v.ImportFiles(_localMovie, _movieFile, new List { nfofile }, true), Times.Once()); + } + + [Test] + public void should_search_subtitles_when_importing_from_job_folder() + { + _localMovie.FolderMovieInfo = new ParsedMovieInfo(); + + var subtitleFile = Path.Combine(_releaseFolder, "Movie.Title.2022.en.srt").AsOsAgnostic(); + + var files = new List + { + _localMovie.Path, + subtitleFile + }; + + WithExistingFiles(files); + + Subject.ImportMovie(_localMovie, _movieFile, true); + + Mocker.GetMock().Verify(v => v.GetFiles(_releaseFolder, SearchOption.AllDirectories), Times.Once); + Mocker.GetMock().Verify(v => v.GetFiles(_releaseFolder, SearchOption.TopDirectoryOnly), Times.Never); + } + + [Test] + public void should_not_search_subtitles_when_not_importing_from_job_folder() + { + _localMovie.FolderMovieInfo = null; + + var subtitleFile = Path.Combine(_releaseFolder, "Movie.Title.2022.en.srt").AsOsAgnostic(); + + var files = new List + { + _localMovie.Path, + subtitleFile + }; + + WithExistingFiles(files); + + Subject.ImportMovie(_localMovie, _movieFile, true); + + Mocker.GetMock().Verify(v => v.GetFiles(_releaseFolder, SearchOption.AllDirectories), Times.Never); + Mocker.GetMock().Verify(v => v.GetFiles(_releaseFolder, SearchOption.TopDirectoryOnly), 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..7c9e71f08 --- /dev/null +++ b/src/NzbDrone.Core.Test/Extras/Others/OtherExtraServiceFixture.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Others; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Extras.Others +{ + [TestFixture] + public class OtherExtraServiceFixture : CoreTest + { + private Movie _movie; + private MovieFile _movieFile; + private LocalMovie _localMovie; + + private string _movieFolder; + private string _releaseFolder; + + [SetUp] + public void Setup() + { + _movieFolder = @"C:\Test\Movies\Movie Title".AsOsAgnostic(); + _releaseFolder = @"C:\Test\Unsorted Movies\Movie.Title.2022".AsOsAgnostic(); + + _movie = Builder.CreateNew() + .With(s => s.Path = _movieFolder) + .Build(); + + _movieFile = Builder.CreateNew() + .With(f => f.Path = Path.Combine(_movie.Path, "Movie Title - 2022.mkv").AsOsAgnostic()) + .With(f => f.RelativePath = @"Movie Title - 2022.mkv") + .Build(); + + _localMovie = Builder.CreateNew() + .With(l => l.Movie = _movie) + .With(l => l.Path = Path.Combine(_releaseFolder, "Movie.Title.2022.mkv").AsOsAgnostic()) + .With(l => l.FileMovieInfo = new ParsedMovieInfo + { + MovieTitles = new List { "Movie Title" }, + Year = 2022 + }) + .Build(); + } + + [Test] + [TestCase("Movie Title - 2022.nfo", "Movie Title - 2022.nfo")] + [TestCase("Movie.Title.2022.nfo", "Movie Title - 2022.nfo")] + [TestCase("Movie Title 2022.nfo", "Movie Title - 2022.nfo")] + [TestCase("Movie_Title_2022.nfo", "Movie Title - 2022.nfo")] + [TestCase(@"Movie.Title.2022\thumb.jpg", "Movie Title - 2022.jpg")] + public void should_import_matching_file(string filePath, string expectedOutputPath) + { + var files = new List { Path.Combine(_releaseFolder, filePath).AsOsAgnostic() }; + + var results = Subject.ImportFiles(_localMovie, _movieFile, files, true).ToList(); + + results.Count().Should().Be(1); + + results[0].RelativePath.AsOsAgnostic().PathEquals(expectedOutputPath.AsOsAgnostic()).Should().Be(true); + } + + [Test] + public void should_not_import_multiple_nfo_files() + { + var files = new List + { + Path.Combine(_releaseFolder, "Movie.Title.2022.nfo").AsOsAgnostic(), + Path.Combine(_releaseFolder, "Movie_Title_2022.nfo").AsOsAgnostic(), + }; + + var results = Subject.ImportFiles(_localMovie, _movieFile, 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..e16c3e025 --- /dev/null +++ b/src/NzbDrone.Core.Test/Extras/Subtitles/SubtitleServiceFixture.cs @@ -0,0 +1,179 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Subtitles; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.MovieImport; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Extras.Subtitles +{ + [TestFixture] + public class SubtitleServiceFixture : CoreTest + { + private Movie _movie; + private MovieFile _movieFile; + private LocalMovie _localMovie; + + private string _MovieFolder; + private string _releaseFolder; + + [SetUp] + public void Setup() + { + _MovieFolder = @"C:\Test\Movies\Movie Title".AsOsAgnostic(); + _releaseFolder = @"C:\Test\Unsorted Movies\Movie.Title.2022".AsOsAgnostic(); + + _movie = Builder.CreateNew() + .With(s => s.Path = _MovieFolder) + .Build(); + + _movieFile = Builder.CreateNew() + .With(f => f.Path = Path.Combine(_movie.Path, "Movie Title - 2022.mkv").AsOsAgnostic()) + .With(f => f.RelativePath = @"Movie Title - 2022.mkv".AsOsAgnostic()) + .Build(); + + _localMovie = Builder.CreateNew() + .With(l => l.Movie = _movie) + .With(l => l.Path = Path.Combine(_releaseFolder, "Movie.Title.2022.mkv").AsOsAgnostic()) + .With(l => l.FileMovieInfo = new ParsedMovieInfo + { + MovieTitles = new List { "Movie Title" }, + Year = 2022 + }) + .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())) + .Returns(DetectSampleResult.NotSample); + } + + [Test] + [TestCase("Movie.Title.2022.en.nfo")] + public void should_not_import_non_subtitle_file(string filePath) + { + var files = new List { Path.Combine(_releaseFolder, filePath).AsOsAgnostic() }; + + var results = Subject.ImportFiles(_localMovie, _movieFile, files, true).ToList(); + + results.Count().Should().Be(0); + } + + [Test] + [TestCase("Movie Title - 2022.srt", "Movie Title - 2022.srt")] + [TestCase("Movie.Title.2022.en.srt", "Movie Title - 2022.en.srt")] + [TestCase("Movie.Title.2022.english.srt", "Movie Title - 2022.en.srt")] + [TestCase("Movie Title 2022_en_sdh_forced.srt", "Movie Title - 2022.en.srt")] + [TestCase("Movie_Title_2022 en.srt", "Movie Title - 2022.en.srt")] + [TestCase(@"Subs\Movie.Title.2022\2_en.srt", "Movie Title - 2022.en.srt")] + [TestCase("sub.srt", "Movie Title - 2022.srt")] + public void should_import_matching_subtitle_file(string filePath, string expectedOutputPath) + { + var files = new List { Path.Combine(_releaseFolder, filePath).AsOsAgnostic() }; + + var results = Subject.ImportFiles(_localMovie, _movieFile, files, true).ToList(); + + results.Count().Should().Be(1); + + results[0].RelativePath.AsOsAgnostic().PathEquals(expectedOutputPath.AsOsAgnostic()).Should().Be(true); + } + + [Test] + public void should_import_multiple_subtitle_files_per_language() + { + var files = new List + { + Path.Combine(_releaseFolder, "Movie.Title.2022.en.srt").AsOsAgnostic(), + Path.Combine(_releaseFolder, "Movie.Title.2022.english.srt").AsOsAgnostic(), + Path.Combine(_releaseFolder, "Subs", "Movie_Title_2022_en_forced.srt").AsOsAgnostic(), + Path.Combine(_releaseFolder, "Subs", "Movie.Title.2022", "2_fr.srt").AsOsAgnostic() + }; + + var expectedOutputs = new string[] + { + "Movie Title - 2022.1.en.srt", + "Movie Title - 2022.2.en.srt", + "Movie Title - 2022.3.en.srt", + "Movie Title - 2022.fr.srt", + }; + + var results = Subject.ImportFiles(_localMovie, _movieFile, files, true).ToList(); + + results.Count().Should().Be(expectedOutputs.Length); + + for (int i = 0; i < expectedOutputs.Length; i++) + { + results[i].RelativePath.AsOsAgnostic().PathEquals(expectedOutputs[i].AsOsAgnostic()).Should().Be(true); + } + } + + [Test] + public void should_import_multiple_subtitle_files_per_language_with_tags() + { + var files = new List + { + Path.Combine(_releaseFolder, "Movie.Title.2022.en.forced.cc.srt").AsOsAgnostic(), + Path.Combine(_releaseFolder, "Movie.Title.2022.other.en.forced.cc.srt").AsOsAgnostic(), + Path.Combine(_releaseFolder, "Movie.Title.2022.en.forced.sdh.srt").AsOsAgnostic(), + Path.Combine(_releaseFolder, "Movie.Title.2022.en.forced.default.srt").AsOsAgnostic(), + }; + + var expectedOutputs = new[] + { + "Movie Title - 2022.1.en.forced.cc.srt", + "Movie Title - 2022.2.en.forced.cc.srt", + "Movie Title - 2022.en.forced.sdh.srt", + "Movie Title - 2022.en.forced.default.srt" + }; + + var results = Subject.ImportFiles(_localMovie, _movieFile, files, true).ToList(); + + results.Count().Should().Be(expectedOutputs.Length); + + for (int i = 0; i < expectedOutputs.Length; i++) + { + results[i].RelativePath.AsOsAgnostic().PathEquals(expectedOutputs[i].AsOsAgnostic()).Should().Be(true); + } + } + + [Test] + [TestCase(@"Subs\2_en.srt", "Movie Title - 2022.en.srt")] + public void should_import_unmatching_subtitle_file_if_only_episode(string filePath, string expectedOutputPath) + { + var subtitleFile = Path.Combine(_releaseFolder, filePath).AsOsAgnostic(); + + var sampleFile = Path.Combine(_movie.Path, "Movie Title - 2022.sample.mkv").AsOsAgnostic(); + + var videoFiles = new string[] + { + _localMovie.Path, + sampleFile + }; + + Mocker.GetMock().Setup(s => s.GetFiles(It.IsAny(), SearchOption.AllDirectories)) + .Returns(videoFiles); + + Mocker.GetMock().Setup(s => s.IsSample(It.IsAny(), sampleFile)) + .Returns(DetectSampleResult.Sample); + + var results = Subject.ImportFiles(_localMovie, _movieFile, new List { subtitleFile }, true).ToList(); + + results.Count().Should().Be(1); + + results[0].RelativePath.AsOsAgnostic().PathEquals(expectedOutputPath.AsOsAgnostic()).Should().Be(true); + + ExceptionVerification.ExpectedWarns(1); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index ec4eee5bc..a29df3b07 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -27,6 +27,22 @@ public void should_parse_language_unknown(string postTitle) result.Languages.Should().BeEquivalentTo(Language.Unknown); } + [TestCase("Movie Title - 2022.en.sub")] + [TestCase("Movie Title - 2022.EN.sub")] + [TestCase("Movie Title - 2022.eng.sub")] + [TestCase("Movie Title - 2022.ENG.sub")] + [TestCase("Movie Title - 2022.English.sub")] + [TestCase("Movie Title - 2022.english.sub")] + [TestCase("Movie Title - 2022.en.cc.sub")] + [TestCase("Movie Title - 2022.en.sdh.sub")] + [TestCase("Movie Title - 2022.en.forced.sub")] + [TestCase("Movie Title - 2022.en.sdh.forced.sub")] + public void should_parse_subtitle_language_english(string fileName) + { + var result = LanguageParser.ParseSubtitleLanguage(fileName); + result.Should().Be(Language.English); + } + [TestCase("Movie.Title.1994.French.1080p.XviD-LOL")] [TestCase("Movie Title : Other Title 2011 AVC.1080p.Blu-ray HD.VOSTFR.VFF")] [TestCase("Movie Title - Other Title 2011 Bluray 4k HDR HEVC AC3 VFF")] diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index c7637d4d3..255e75c4c 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -31,7 +31,6 @@ public class ExtraService : IExtraService, private readonly IDiskProvider _diskProvider; private readonly IConfigService _configService; private readonly List _extraFileManagers; - private readonly Logger _logger; public ExtraService(IMediaFileService mediaFileService, IMovieService movieService, @@ -45,7 +44,6 @@ public ExtraService(IMediaFileService mediaFileService, _diskProvider = diskProvider; _configService = configService; _extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList(); - _logger = logger; } public void ImportMovie(LocalMovie localMovie, MovieFile movieFile, bool isReadOnly) @@ -62,61 +60,42 @@ public void ImportExtraFiles(LocalMovie localMovie, MovieFile movieFile, bool is return; } - var sourcePath = localMovie.Path; - var sourceFolder = _diskProvider.GetParentFolder(sourcePath); - var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath); - var files = _diskProvider.GetFiles(sourceFolder, SearchOption.AllDirectories).Where(f => f != localMovie.Path); + var folderSearchOption = localMovie.FolderMovieInfo == null + ? SearchOption.TopDirectoryOnly + : SearchOption.AllDirectories; 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(localMovie.Path); + var files = _diskProvider.GetFiles(sourceFolder, folderSearchOption); + 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(localMovie, movieFile, file, extension, isReadOnly)) { - var extension = Path.GetExtension(matchingFilename); - var extraFile = extraFileManager.Import(localMovie.Movie, movieFile, 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(localMovie, movieFile, managedFiles[i], isReadOnly); } } diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs index 00a6f9c8e..77c4623b7 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs @@ -8,6 +8,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Movies; +using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Extras.Files { @@ -19,7 +20,8 @@ public interface IManageExtraFiles IEnumerable CreateAfterMovieImport(Movie movie, MovieFile movieFile); IEnumerable CreateAfterMovieFolder(Movie movie, string movieFolder); IEnumerable MoveFilesAfterRename(Movie movie, List movieFiles); - ExtraFile Import(Movie movie, MovieFile movieFile, string path, string extension, bool readOnly); + bool CanImportFile(LocalMovie localMovie, MovieFile movieFile, string path, string extension, bool readOnly); + IEnumerable ImportFiles(LocalMovie localMovie, MovieFile movieFile, List files, bool isReadOnly); } public abstract class ExtraFileManager : IManageExtraFiles @@ -47,7 +49,8 @@ public ExtraFileManager(IConfigService configService, public abstract IEnumerable CreateAfterMovieImport(Movie movie, MovieFile movieFile); public abstract IEnumerable CreateAfterMovieFolder(Movie movie, string movieFolder); public abstract IEnumerable MoveFilesAfterRename(Movie movie, List movieFiles); - public abstract ExtraFile Import(Movie movie, MovieFile movieFile, string path, string extension, bool readOnly); + public abstract bool CanImportFile(LocalMovie localMovie, MovieFile movieFile, string path, string extension, bool readOnly); + public abstract IEnumerable ImportFiles(LocalMovie localMovie, MovieFile movieFile, List files, bool isReadOnly); protected TExtraFile ImportFile(Movie movie, MovieFile movieFile, 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 5c37973c9..05e8fffec 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs @@ -13,6 +13,7 @@ using NzbDrone.Core.Extras.Others; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Movies; +using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Extras.Metadata { @@ -191,9 +192,14 @@ public override IEnumerable MoveFilesAfterRename(Movie movie, List ImportFiles(LocalMovie localMovie, MovieFile movieFile, List files, bool isReadOnly) + { + return Enumerable.Empty(); } private List GetMetadataFilesForConsumer(IMetadata consumer, List movieMetadata) diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs index fc85c066d..80fa48fdd 100644 --- a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using NLog; using NzbDrone.Common.Disk; @@ -7,13 +9,16 @@ using NzbDrone.Core.Extras.Files; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Movies; +using NzbDrone.Core.Parser.Model; 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, @@ -23,8 +28,10 @@ public OtherExtraService(IConfigService configService, Logger logger) : base(configService, diskProvider, diskTransferService, logger) { + _diskProvider = diskProvider; _otherExtraFileService = otherExtraFileService; _mediaFileAttributeService = mediaFileAttributeService; + _logger = logger; } public override int Order => 2; @@ -69,17 +76,79 @@ public override IEnumerable MoveFilesAfterRename(Movie movie, List ImportFiles(LocalMovie localMovie, MovieFile movieFile, List files, bool isReadOnly) + { + var importedFiles = new List(); + var filteredFiles = files.Where(f => CanImportFile(localMovie, movieFile, f, Path.GetExtension(f), isReadOnly)).ToList(); + var sourcePath = localMovie.Path; + var sourceFolder = _diskProvider.GetParentFolder(sourcePath); + var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath); + var matchingFiles = new List(); + var hasNfo = false; + + foreach (var file in filteredFiles) { - _mediaFileAttributeService.SetFilePermissions(path); - _otherExtraFileService.Upsert(extraFile); + 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; + } + + // Movie match + var fileMovieInfo = Parser.Parser.ParseMoviePath(file) ?? new ParsedMovieInfo(); + + if (fileMovieInfo.MovieTitle == null) + { + continue; + } + + if (fileMovieInfo.MovieTitle == localMovie.FileMovieInfo.MovieTitle && + fileMovieInfo.Year.Equals(localMovie.FileMovieInfo.Year)) + { + matchingFiles.Add(file); + } + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to import extra file: {0}", file); + } } - return extraFile; + foreach (string file in matchingFiles) + { + try + { + var extraFile = ImportFile(localMovie.Movie, movieFile, 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 0e3737a97..b45902711 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -9,13 +10,17 @@ using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.MovieImport; using NzbDrone.Core.Movies; using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; 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 @@ public class SubtitleService : ExtraFileManager 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 @@ public override IEnumerable MoveFilesAfterRename(Movie movie, List 1) - { - _logger.Warn("Multiple subtitle files found with the same language and extension for {0}", Path.Combine(movie.Path, movieFile.RelativePath)); - } - foreach (var subtitleFile in group) { var suffix = GetSuffix(subtitleFile.Language, copy, groupCount > 1); @@ -91,22 +94,129 @@ public override IEnumerable MoveFilesAfterRename(Movie movie, List ImportFiles(LocalMovie localMovie, MovieFile movieFile, List files, bool isReadOnly) + { + var importedFiles = new List(); + + var filteredFiles = files.Where(f => CanImportFile(localMovie, movieFile, f, Path.GetExtension(f), isReadOnly)).ToList(); + + var sourcePath = localMovie.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(movie, movieFile, 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); + // Movie match + var fileMovieInfo = Parser.Parser.ParseMoviePath(file) ?? new ParsedMovieInfo(); - return subtitleFile; + if (fileMovieInfo.MovieTitle == null) + { + continue; + } + + if (fileMovieInfo.MovieTitle == localMovie.FileMovieInfo.MovieTitle && + fileMovieInfo.Year.Equals(localMovie.FileMovieInfo.Year)) + { + 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(localMovie.Movie.MovieMetadata, file); + + if (sample == DetectSampleResult.Sample) + { + return false; + } + + return true; + }).ToList(); + + if (videoFiles.Count == 1) + { + matchingFiles.AddRange(filteredFiles); + + _logger.Warn("Imported any available subtitle file for movie: {0}", localMovie); + } + } + + 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(localMovie.Movie, movieFile, 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 00aab3f98..0296a3a26 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -36,7 +36,7 @@ public static class LanguageParser (?\bSK\b)", RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace); - private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?[a-z]{2,3})(?:[-_. ]forced)?$", 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 List ParseLanguages(string title) { @@ -316,11 +316,19 @@ public static Language ParseSubtitleLanguage(string fileName) 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 Language.All) + { + if (simpleFilename.EndsWith(language.ToString(), StringComparison.OrdinalIgnoreCase)) + { + return language; + } + } + Logger.Debug("Unable to parse langauge from subtitle file: {0}", fileName); } catch (Exception)