New: Import subtitles from sub folders

Closes #2513
This commit is contained in:
Stéphane Dupont 2022-02-27 21:37:23 +01:00 committed by GitHub
parent c10677dfe7
commit 4bfcd0de1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 733 additions and 74 deletions

View File

@ -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<ExtraService>
{
private Series _series;
private EpisodeFile _episodeFile;
private LocalEpisode _localEpisode;
private string _seriesFolder;
private string _episodeFolder;
private Mock<IManageExtraFiles> _subtitleService;
private Mock<IManageExtraFiles> _otherExtraService;
[SetUp]
public void Setup()
{
_seriesFolder = @"C:\Test\TV\Series Title".AsOsAgnostic();
_episodeFolder = @"C:\Test\Unsorted TV\Series.Title.S01".AsOsAgnostic();
_series = Builder<Series>.CreateNew()
.With(s => s.Path = _seriesFolder)
.Build();
var episodes = Builder<Episode>.CreateListOfSize(1)
.All()
.With(e => e.SeasonNumber = 1)
.Build()
.ToList();
_episodeFile = Builder<EpisodeFile>.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<LocalEpisode>.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<IManageExtraFiles>();
_subtitleService.SetupGet(s => s.Order).Returns(0);
_subtitleService.Setup(s => s.CanImportFile(It.IsAny<LocalEpisode>(), It.IsAny<EpisodeFile>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()))
.Returns(false);
_subtitleService.Setup(s => s.CanImportFile(It.IsAny<LocalEpisode>(), It.IsAny<EpisodeFile>(), It.IsAny<string>(), ".srt", It.IsAny<bool>()))
.Returns(true);
_otherExtraService = new Mock<IManageExtraFiles>();
_otherExtraService.SetupGet(s => s.Order).Returns(1);
_otherExtraService.Setup(s => s.CanImportFile(It.IsAny<LocalEpisode>(), It.IsAny<EpisodeFile>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()))
.Returns(true);
Mocker.SetConstant<IEnumerable<IManageExtraFiles>>(new[] {
_subtitleService.Object,
_otherExtraService.Object
});
Mocker.GetMock<IDiskProvider>().Setup(s => s.FolderExists(It.IsAny<string>()))
.Returns(false);
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetParentFolder(It.IsAny<string>()))
.Returns((string path) => Directory.GetParent(path).FullName);
WithExistingFolder(_series.Path);
WithExistingFile(_episodeFile.Path);
WithExistingFile(_localEpisode.Path);
Mocker.GetMock<IConfigService>().Setup(v => v.ImportExtraFiles).Returns(true);
Mocker.GetMock<IConfigService>().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<IDiskProvider>().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<IDiskProvider>().Setup(v => v.FileExists(path)).Returns(exists);
Mocker.GetMock<IDiskProvider>().Setup(v => v.GetFileSize(path)).Returns(size);
}
private void WithExistingFiles(List<string> files)
{
foreach (string file in files)
{
WithExistingFile(file);
}
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetFiles(_episodeFolder, SearchOption.AllDirectories))
.Returns(files.ToArray());
}
[Test]
public void should_not_pass_file_if_import_disabled()
{
Mocker.GetMock<IConfigService>().Setup(v => v.ImportExtraFiles).Returns(false);
var nfofile = Path.Combine(_episodeFolder, "Series.Title.S01E01.nfo").AsOsAgnostic();
var files = new List<string> {
_localEpisode.Path,
nfofile
};
WithExistingFiles(files);
Subject.ImportEpisode(_localEpisode, _episodeFile, true);
_subtitleService.Verify(v => v.CanImportFile(_localEpisode, _episodeFile, It.IsAny<string>(), It.IsAny<string>(), true), Times.Never());
_otherExtraService.Verify(v => v.CanImportFile(_localEpisode, _episodeFile, It.IsAny<string>(), It.IsAny<string>(), 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<IConfigService>().Setup(v => v.ImportExtraFiles).Returns(false);
var nfofile = Path.Combine(_episodeFolder, filePath).AsOsAgnostic();
var files = new List<string> {
_localEpisode.Path,
nfofile
};
WithExistingFiles(files);
Subject.ImportEpisode(_localEpisode, _episodeFile, true);
_subtitleService.Verify(v => v.CanImportFile(_localEpisode, _episodeFile, It.IsAny<string>(), It.IsAny<string>(), true), Times.Never());
_otherExtraService.Verify(v => v.CanImportFile(_localEpisode, _episodeFile, It.IsAny<string>(), It.IsAny<string>(), 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<string> {
_localEpisode.Path,
subtitleFile
};
WithExistingFiles(files);
Subject.ImportEpisode(_localEpisode, _episodeFile, true);
_subtitleService.Verify(v => v.ImportFiles(_localEpisode, _episodeFile, new List<string> { subtitleFile }, true), Times.Once());
_otherExtraService.Verify(v => v.ImportFiles(_localEpisode, _episodeFile, new List<string> { 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<string> {
_localEpisode.Path,
nfofile
};
WithExistingFiles(files);
Subject.ImportEpisode(_localEpisode, _episodeFile, true);
_subtitleService.Verify(v => v.ImportFiles(_localEpisode, _episodeFile, new List<string> { nfofile }, true), Times.Never());
_otherExtraService.Verify(v => v.ImportFiles(_localEpisode, _episodeFile, new List<string> { nfofile }, true), Times.Once());
}
}
}

View File

@ -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<OtherExtraService>
{
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<Series>.CreateNew()
.With(s => s.Path = _seriesFolder)
.Build();
var episodes = Builder<Episode>.CreateListOfSize(1)
.All()
.With(e => e.SeasonNumber = 1)
.Build()
.ToList();
_episodeFile = Builder<EpisodeFile>.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<LocalEpisode>.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<string> { 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<string>
{
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);
}
}
}

View File

@ -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<SubtitleService>
{
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<Series>.CreateNew()
.With(s => s.Path = _seriesFolder)
.Build();
var episodes = Builder<Episode>.CreateListOfSize(1)
.All()
.With(e => e.SeasonNumber = 1)
.Build()
.ToList();
_episodeFile = Builder<EpisodeFile>.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<LocalEpisode>.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<IDiskProvider>().Setup(s => s.GetParentFolder(It.IsAny<string>()))
.Returns((string path) => Directory.GetParent(path).FullName);
Mocker.GetMock<IDetectSample>().Setup(s => s.IsSample(It.IsAny<Series>(), It.IsAny<string>(), It.IsAny<bool>()))
.Returns(DetectSampleResult.NotSample);
}
[Test]
[TestCase("Series.Title.S01E01.en.nfo")]
public void should_not_import_non_subtitle_file(string filePath)
{
var files = new List<string> { 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<string> { 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<string>
{
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<IDiskProvider>().Setup(s => s.GetFiles(It.IsAny<string>(), SearchOption.AllDirectories))
.Returns(videoFiles);
Mocker.GetMock<IDetectSample>().Setup(s => s.IsSample(It.IsAny<Series>(), sampleFile, It.IsAny<bool>()))
.Returns(DetectSampleResult.Sample);
var results = Subject.ImportFiles(_localEpisode, _episodeFile, new List<string> { 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<IDiskProvider>().Setup(s => s.GetFiles(It.IsAny<string>(), SearchOption.AllDirectories))
.Returns(videoFiles);
var results = Subject.ImportFiles(_localEpisode, _episodeFile, new List<string> { subtitleFile }, true).ToList();
results.Count().Should().Be(0);
}
}
}

View File

@ -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)

View File

@ -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<IManageExtraFiles> _extraFileManagers;
private readonly Logger _logger;
public ExtraService(IMediaFileService mediaFileService,
IEpisodeService episodeService,
IDiskProvider diskProvider,
IConfigService configService,
List<IManageExtraFiles> extraFileManagers,
IEnumerable<IManageExtraFiles> 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<string>();
var hasNfo = false;
var sourceFolder = _diskProvider.GetParentFolder(localEpisode.Path);
var files = _diskProvider.GetFiles(sourceFolder, SearchOption.AllDirectories);
var managedFiles = _extraFileManagers.Select((i) => new List<string>()).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);
}
}

View File

@ -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<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile);
IEnumerable<ExtraFile> CreateAfterEpisodeFolder(Series series, string seriesFolder, string seasonFolder);
IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> 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<ExtraFile> ImportFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, List<string> files, bool isReadOnly);
}
public abstract class ExtraFileManager<TExtraFile> : IManageExtraFiles
@ -48,7 +50,8 @@ namespace NzbDrone.Core.Extras.Files
public abstract IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile);
public abstract IEnumerable<ExtraFile> CreateAfterEpisodeFolder(Series series, string seriesFolder, string seasonFolder);
public abstract IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> 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<ExtraFile> ImportFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, List<string> files, bool isReadOnly);
protected TExtraFile ImportFile(Series series, EpisodeFile episodeFile, string path, bool readOnly, string extension, string fileNameSuffix = null)
{

View File

@ -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<ExtraFile> ImportFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, List<string> files, bool isReadOnly)
{
return Enumerable.Empty<ExtraFile>();
}
private List<MetadataFile> GetMetadataFilesForConsumer(IMetadata consumer, List<MetadataFile> seriesMetadata)

View File

@ -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<OtherExtraFile>
{
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<ExtraFile> ImportFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, List<string> files, bool isReadOnly)
{
var importedFiles = new List<ExtraFile>();
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<string>();
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;
}
}
}

View File

@ -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<SubtitleFile>
{
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<ExtraFile> ImportFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, List<string> files, bool isReadOnly)
{
var importedFiles = new List<SubtitleFile>();
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<string>();
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<Tuple<string, Language, string>>();
foreach (string file in matchingFiles)
{
var language = LanguageParser.ParseSubtitleLanguage(file);
var extension = Path.GetExtension(file);
subtitleFiles.Add(new Tuple<string, Language, string>(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)

View File

@ -24,7 +24,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.Compiled);
private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?<iso_code>[a-z]{2,3})$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?<iso_code>[a-z]{2,3})([-_. ](?<tags>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))
{