Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Threading.Tasks;
using NLog;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Books;
using NzbDrone.Core.Books.Services;
Expand Down Expand Up @@ -189,7 +190,7 @@ private sealed class StubDiskProvider : IDiskProvider
public bool FolderWritable(string path) => throw new NotImplementedException();
public bool FolderEmpty(string path) => throw new NotImplementedException();
public IEnumerable<string> GetDirectories(string path) => throw new NotImplementedException();
public IEnumerable<string> GetFiles(string path, bool recursive) => throw new NotImplementedException();
public IEnumerable<string> GetFiles(string path, bool recursive) => Directory.Exists(path) ? Directory.GetFiles(path, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) : Array.Empty<string>();
public long GetFolderSize(string path) => throw new NotImplementedException();
public void CreateFolder(string path) => throw new NotImplementedException();
public void DeleteFile(string path) => throw new NotImplementedException();
Expand Down Expand Up @@ -2638,5 +2639,61 @@ private static Dictionary<string, List<string>> CreateAudioTags(string album, st
["TITLE"] = new List<string> { title }
};
}

private sealed class RecordingArchiveService : IArchiveService
{
public List<(string CompressedFile, string Destination)> Extractions { get; } = new();

public void Extract(string compressedFile, string destination)
{
Extractions.Add((compressedFile, destination));
}

public void CreateZip(string path, IEnumerable<string> files) => throw new NotImplementedException();
}

[Test]
public void should_extract_archives_when_folder_has_no_direct_importable_files()
{
var tempDir = Path.Combine(Path.GetTempPath(), "chaptarr-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
var zipPath = Path.Combine(tempDir, "Book.zip");
File.WriteAllBytes(zipPath, new byte[] { 1, 2, 3, 4 });

var archiveService = new RecordingArchiveService();

try
{
var service = new DownloadedBooksImportService(
new StubDiskProvider(),
new StubDiskScanService(),
new StubFileMatchingService(),
new StubMetadataTagService(),
new RecordingImportApprovedBooks(),
DispatchProxy.Create<IBookService, ThrowingProxy<IBookService>>(),
DispatchProxy.Create<IAuthorService, ThrowingProxy<IAuthorService>>(),
DispatchProxy.Create<IEditionService, ThrowingProxy<IEditionService>>(),
DispatchProxy.Create<IImportOrchestrator, ThrowingProxy<IImportOrchestrator>>(),
new StubAuthorLibraryService(),
new StubRootFolderService(),
ConfigServiceTestProxy.Create(),
DispatchProxy.Create<IHistoryService, ThrowingProxy<IHistoryService>>(),
DispatchProxy.Create<IEventAggregator, ThrowingProxy<IEventAggregator>>(),
DispatchProxy.Create<NzbDrone.Common.EnvironmentInfo.IRuntimeInfo, ThrowingProxy<NzbDrone.Common.EnvironmentInfo.IRuntimeInfo>>(),
DispatchProxy.Create<IMediaInfoExtractor, ThrowingProxy<IMediaInfoExtractor>>(),
LogManager.GetCurrentClassLogger(),
archiveService);

service.ProcessPath(tempDir, ImportMode.Auto, author: null, downloadClientItem: null);

Assert.That(archiveService.Extractions, Has.Count.EqualTo(1));
Assert.That(archiveService.Extractions[0].CompressedFile, Is.EqualTo(zipPath));
Assert.That(archiveService.Extractions[0].Destination, Is.EqualTo(tempDir));
}
finally
{
Directory.Delete(tempDir, true);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using NzbDrone.Core.MediaFiles.BookImport;
using System;
using System.Collections.Generic;
using System.IO;
Expand Down Expand Up @@ -108,6 +109,40 @@ protected override object Invoke(MethodInfo targetMethod, object[] args)
}
}

private class NullRootFolderServiceProxy : DispatchProxy
{
protected override object Invoke(MethodInfo targetMethod, object[] args)
{
if (targetMethod?.Name == nameof(IRootFolderService.GetBestRootFolder))
{
return null;
}

throw new NotImplementedException($"Test proxy does not implement IRootFolderService.{targetMethod?.Name}");
}
}

[Test]
public void should_throw_root_folder_not_found_when_best_root_folder_cannot_be_resolved()
{
var replacement = new BookFile { Id = 2, Path = "/downloads/Book.m4b" };
var author = new Author { Id = 1, Path = "/books/Author" };
var book = new Book { Id = 2, Author = author, BookFiles = new List<BookFile>() };
var localBook = new LocalBook { Author = author, Book = book, Path = replacement.Path };

var subject = new UpgradeMediaFileService(
new RecordingRecycleBinProvider(),
DispatchProxy.Create<IMediaFileService, MediaFileServiceProxy>(),
DispatchProxy.Create<IMetadataTagService, NoOpProxy<IMetadataTagService>>(),
new StubBookFileMover(),
DispatchProxy.Create<IDiskProvider, DiskProviderProxy>(),
DispatchProxy.Create<IRootFolderService, NullRootFolderServiceProxy>(),
DispatchProxy.Create<ICalibreProxy, ThrowingProxy<ICalibreProxy>>(),
LogManager.GetCurrentClassLogger());

Assert.Throws<RootFolderNotFoundException>(() => subject.UpgradeBookFile(replacement, localBook));
}

[Test]
public void should_not_delete_a_loose_path_match_while_replacing_its_stale_row()
{
Expand Down
41 changes: 40 additions & 1 deletion src/NzbDrone.Core/MediaFiles/DownloadedBooksImportService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.EnvironmentInfo;
Expand Down Expand Up @@ -52,6 +53,7 @@ public class DownloadedBooksImportService : IDownloadedBooksImportService
private readonly IEventAggregator _eventAggregator;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IMediaInfoExtractor _mediaInfoExtractor;
private readonly IArchiveService _archiveService;
private readonly Logger _logger;

public DownloadedBooksImportService(
Expand All @@ -71,7 +73,8 @@ public DownloadedBooksImportService(
IEventAggregator eventAggregator,
IRuntimeInfo runtimeInfo,
IMediaInfoExtractor mediaInfoExtractor,
Logger logger)
Logger logger,
IArchiveService archiveService = null)
{
_diskProvider = diskProvider;
_diskScanService = diskScanService;
Expand All @@ -90,6 +93,7 @@ public DownloadedBooksImportService(
_runtimeInfo = runtimeInfo;
_mediaInfoExtractor = mediaInfoExtractor;
_logger = logger;
_archiveService = archiveService ?? new ArchiveService(logger);
}

public List<ImportResult> ProcessRootFolder(IDirectoryInfo directoryInfo)
Expand Down Expand Up @@ -150,6 +154,15 @@ private List<ImportResult> ProcessFolder(IDirectoryInfo directoryInfo, ImportMod
.Where(f => MediaFileExtensions.AllExtensions.Contains(f.Extension))
.ToList();

if (!mediaFiles.Any())
{
ExtractArchivesInFolder(directoryInfo);
visibleFiles = _diskProvider.GetFileInfos(directoryInfo.FullName, true);
mediaFiles = visibleFiles
.Where(f => MediaFileExtensions.AllExtensions.Contains(f.Extension))
.ToList();
}

if (!mediaFiles.Any())
{
_logger.Debug("[DOWNLOAD-IMPORT] No media files found in: {0}", directoryInfo.FullName);
Expand Down Expand Up @@ -1411,5 +1424,31 @@ public List<ImportResult> ProcessFile(string path, ImportMode importMode = Impor
var fileInfo = _diskProvider.GetFileInfo(path);
return ProcessFile(fileInfo, importMode, author, downloadClientItem, remoteBook, requireDefaultRootFolderForMissingAuthors: requireDefaultRootFolderForMissingAuthors);
}

private void ExtractArchivesInFolder(IDirectoryInfo directoryInfo)
{
var archives = _diskProvider.GetFiles(directoryInfo.FullName, true)
.Where(IsSupportedArchive)
.ToList();

foreach (var archive in archives)
{
try
{
_archiveService.Extract(archive, directoryInfo.FullName);
}
catch (Exception e)
{
_logger.Warn(e, "Failed to extract archive during import: {0}", archive);
}
}
}

private static bool IsSupportedArchive(string path)
{
return path.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase);
}
}
}
8 changes: 6 additions & 2 deletions src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,13 @@ public BookFileMoveResult UpgradeBookFile(BookFile bookFile, LocalBook localBook
}

var rootFolder = _rootFolderService.GetBestRootFolder(rootFolderPath);
var isCalibre = rootFolder?.IsCalibreLibrary == true && rootFolder.CalibreSettings != null;
if (rootFolder == null)
{
throw new RootFolderNotFoundException($"Root folder '{rootFolderPath}' was not found.");
}

var settings = rootFolder?.CalibreSettings;
var isCalibre = rootFolder.IsCalibreLibrary && rootFolder.CalibreSettings != null;
var settings = rootFolder.CalibreSettings;

// If there are existing book files and the root folder is missing, throw, so the old file isn't left behind during the import process.
if (existingFiles != null && existingFiles.Any() && !_diskProvider.FolderExists(rootFolderPath))
Expand Down
Loading