New: Do not automatically import multi-season releases

Closes #8133
This commit is contained in:
Mark McDowall 2026-02-16 08:20:51 -08:00
parent d99f8b5685
commit f91ebd4c07
7 changed files with 72 additions and 28 deletions

View file

@ -80,7 +80,7 @@ private void GivenSuccessfulImport()
imported.Add(new ImportDecision(localEpisode));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), null, true, true))
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedEpisodeInfo>(), null, true, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
@ -124,7 +124,7 @@ public void should_skip_if_no_series_found()
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
Mocker.GetMock<IMakeImportDecision>()
.Verify(c => c.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<bool>(), true),
.Verify(c => c.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<bool>(), true),
Times.Never());
VerifyNoImport();
@ -175,7 +175,7 @@ public void should_not_delete_folder_if_files_were_imported_and_video_files_rema
imported.Add(new ImportDecision(localEpisode));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), null, true, true))
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedEpisodeInfo>(), null, true, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
@ -201,7 +201,7 @@ public void should_delete_folder_if_files_were_imported_and_only_sample_files_re
imported.Add(new ImportDecision(localEpisode));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), null, true, true))
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedEpisodeInfo>(), null, true, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
@ -271,7 +271,7 @@ public void should_not_delete_if_there_is_large_rar_file()
imported.Add(new ImportDecision(localEpisode));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), null, true, true))
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedEpisodeInfo>(), null, true, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
@ -322,7 +322,7 @@ public void should_use_folder_if_folder_import()
Subject.ProcessPath(fileName);
Mocker.GetMock<IMakeImportDecision>()
.Verify(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), It.Is<ParsedEpisodeInfo>(v => v.AbsoluteEpisodeNumbers.First() == 9), true), Times.Once());
.Verify(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedEpisodeInfo>(), It.Is<ParsedEpisodeInfo>(v => v.AbsoluteEpisodeNumbers.First() == 9), true), Times.Once());
}
[Test]
@ -346,7 +346,7 @@ public void should_not_use_folder_if_file_import()
var result = Subject.ProcessPath(fileName);
Mocker.GetMock<IMakeImportDecision>()
.Verify(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), null, true), Times.Once());
.Verify(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedEpisodeInfo>(), null, true), Times.Once());
}
[Test]
@ -379,7 +379,7 @@ public void should_not_delete_if_no_files_were_imported()
imported.Add(new ImportDecision(localEpisode));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), null, true, true))
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedEpisodeInfo>(), null, true, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
@ -456,7 +456,7 @@ public void should_return_rejection_if_nothing_imported_and_contains_rar_file()
var imported = new List<ImportDecision>();
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), null, true, true))
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedEpisodeInfo>(), null, true, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
@ -482,7 +482,7 @@ public void should_return_rejection_if_nothing_imported_and_contains_executable_
var imported = new List<ImportDecision>();
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), null, true, true))
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedEpisodeInfo>(), null, true, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
@ -499,6 +499,33 @@ public void should_return_rejection_if_nothing_imported_and_contains_executable_
result.First().Result.Should().Be(ImportResultType.Rejected);
}
[Test]
public void should_reject_if_download_is_multi_season()
{
GivenValidSeries();
_trackedDownload.DownloadItem.Title = "Series Title S01-S11";
var folderName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>().Setup(c => c.FolderExists(folderName))
.Returns(true);
var result = Subject.ProcessPath(folderName, ImportMode.Auto, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem);
result.Count.Should().Be(1);
result.First().Result.Should().Be(ImportResultType.Rejected);
result.First().ImportDecision.Rejections.First().Reason.Should().Be(ImportRejectionReason.MultiSeason);
Mocker.GetMock<IParsingService>().Setup(c => c.GetSeries("foldername")).Returns((Series)null);
Mocker.GetMock<IMakeImportDecision>()
.Verify(c => c.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<bool>(), true),
Times.Never());
VerifyNoImport();
}
private void VerifyNoImport()
{
Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto),

View file

@ -103,7 +103,7 @@ public void should_call_all_specifications()
GivenAugmentationSuccess();
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
Subject.GetImportDecisions(_videoFiles, _series, downloadClientItem, null, false, true);
Subject.GetImportDecisions(_videoFiles, _series, downloadClientItem, null, null, false, true);
_fail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), downloadClientItem), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), downloadClientItem), Times.Once());

View file

@ -172,6 +172,13 @@ public void Import(TrackedDownload trackedDownload)
{
return;
}
if (firstResult.ImportDecision.Rejections.FirstOrDefault()?.Reason == ImportRejectionReason.MultiSeason)
{
trackedDownload.Warn(new TrackedDownloadStatusMessage(trackedDownload.DownloadItem.Title, firstResult.Errors));
SetStateToImportBlocked(trackedDownload);
return;
}
}
var statusMessages = new List<TrackedDownloadStatusMessage>

View file

@ -187,6 +187,7 @@ private List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, ImportMode
var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name);
var videoFiles = _diskScanService.FilterPaths(directoryInfo.FullName, _diskScanService.GetVideoFiles(directoryInfo.FullName));
var downloadClientItemInfo = downloadClientItem == null ? null : Parser.Parser.ParseTitle(downloadClientItem.Title);
if (downloadClientItem == null)
{
@ -202,7 +203,17 @@ private List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, ImportMode
}
}
var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, downloadClientItem, folderInfo, true);
if (downloadClientItemInfo is { IsMultiSeason: true })
{
_logger.Debug("Download client item is marked as multi-season, not processing automatically to avoid importing incorrect files");
return new List<ImportResult>
{
RejectionResult(ImportRejectionReason.MultiSeason, "Multi-season download, unable to import automatically")
};
}
var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, downloadClientItem, downloadClientItemInfo, folderInfo, true);
var importResults = _importApprovedEpisodes.Import(decisions, true, downloadClientItem, importMode);
if (importMode == ImportMode.Auto)
@ -328,7 +339,8 @@ private List<ImportResult> ProcessFile(FileInfo fileInfo, ImportMode importMode,
}
}
var decisions = _importDecisionMaker.GetImportDecisions(new List<string>() { fileInfo.FullName }, series, downloadClientItem, null, true);
var downloadClientItemInfo = downloadClientItem == null ? null : Parser.Parser.ParseTitle(downloadClientItem.Title);
var decisions = _importDecisionMaker.GetImportDecisions(new List<string>() { fileInfo.FullName }, series, downloadClientItem, downloadClientItemInfo, null, true);
return _importApprovedEpisodes.Import(decisions, true, downloadClientItem, importMode);
}

View file

@ -16,8 +16,8 @@ public interface IMakeImportDecision
{
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, bool filterExistingFiles);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo downloadClientItemInfo, ParsedEpisodeInfo folderInfo, bool sceneSource);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo downloadClientItemInfo, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles);
ImportDecision GetDecision(LocalEpisode localEpisode, DownloadClientItem downloadClientItem);
}
@ -58,27 +58,20 @@ public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series s
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, bool filterExistingFiles)
{
return GetImportDecisions(videoFiles, series, null, null, false, filterExistingFiles);
return GetImportDecisions(videoFiles, series, null, null, null, false, filterExistingFiles);
}
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource)
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo downloadClientItemInfo, ParsedEpisodeInfo folderInfo, bool sceneSource)
{
return GetImportDecisions(videoFiles, series, downloadClientItem, folderInfo, sceneSource, true);
return GetImportDecisions(videoFiles, series, downloadClientItem, downloadClientItemInfo, folderInfo, sceneSource, true);
}
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles)
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo downloadClientItemInfo, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles)
{
var newFiles = filterExistingFiles ? _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series) : videoFiles.ToList();
_logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count);
ParsedEpisodeInfo downloadClientItemInfo = null;
if (downloadClientItem != null)
{
downloadClientItemInfo = Parser.Parser.ParseTitle(downloadClientItem.Title);
}
// If not importing from a scene source (series folder for example), then assume all files are not samples
// to avoid using media info on every file needlessly (especially if Analyse Media Files is disabled).
var nonSampleVideoFileCount = sceneSource ? GetNonSampleVideoFileCount(newFiles, series, downloadClientItemInfo, folderInfo) : videoFiles.Count;

View file

@ -37,5 +37,6 @@ public enum ImportRejectionReason
NotQualityUpgrade,
NotRevisionUpgrade,
NotCustomFormatUpgrade,
NotCustomFormatUpgradeAfterRename
NotCustomFormatUpgradeAfterRename,
MultiSeason
}

View file

@ -290,9 +290,10 @@ private List<ManualImportItem> ProcessFolder(string rootFolder, string baseFolde
return processedFiles.Concat(processedFolders).Where(i => i != null).ToList();
}
var downloadClientItemInfo = downloadClientItem == null ? null : Parser.Parser.ParseTitle(downloadClientItem.Title);
var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name);
var seriesFiles = _diskScanService.FilterPaths(rootFolder, _diskScanService.GetVideoFiles(baseFolder).ToList());
var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, downloadClientItem, folderInfo, SceneSource(series, baseFolder), filterExistingFiles);
var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, downloadClientItem, downloadClientItemInfo, folderInfo, SceneSource(series, baseFolder), filterExistingFiles);
return decisions.Select(decision => MapItem(decision, rootFolder, downloadId, directoryInfo.Name)).ToList();
}
@ -345,9 +346,12 @@ private ManualImportItem ProcessFile(string rootFolder, string baseFolder, strin
null);
}
var downloadClientItemInfo = trackedDownload?.DownloadItem == null ? null : Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title);
var importDecisions = _importDecisionMaker.GetImportDecisions(new List<string> { file },
series,
trackedDownload?.DownloadItem,
downloadClientItemInfo,
null,
SceneSource(series, baseFolder));