Compare commits
5 commits
develop
...
feature/sc
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2e53987dca | ||
![]() |
4372d09ee4 | ||
![]() |
16498d4b40 | ||
![]() |
85b3187f3f | ||
![]() |
3fe5933358 |
35 changed files with 723 additions and 16 deletions
|
@ -35,7 +35,7 @@ public class ScannerHelper
|
||||||
private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests");
|
private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests");
|
||||||
private readonly string _testcasesDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/TestCases");
|
private readonly string _testcasesDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/TestCases");
|
||||||
private readonly string _imagePath = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/1x1.png");
|
private readonly string _imagePath = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/1x1.png");
|
||||||
private static readonly string[] ComicInfoExtensions = new[] { ".cbz", ".cbr", ".zip", ".rar" };
|
private static readonly string[] ComicInfoExtensions = [".cbz", ".cbr", ".zip", ".rar"];
|
||||||
|
|
||||||
public ScannerHelper(IUnitOfWork unitOfWork, ITestOutputHelper testOutputHelper)
|
public ScannerHelper(IUnitOfWork unitOfWork, ITestOutputHelper testOutputHelper)
|
||||||
{
|
{
|
||||||
|
@ -43,7 +43,7 @@ public class ScannerHelper
|
||||||
_testOutputHelper = testOutputHelper;
|
_testOutputHelper = testOutputHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Library> GenerateScannerData(string testcase, Dictionary<string, ComicInfo> comicInfos = null)
|
public async Task<Library> GenerateScannerData(string testcase, Dictionary<string, ComicInfo>? comicInfos = null)
|
||||||
{
|
{
|
||||||
var testDirectoryPath = await GenerateTestDirectory(Path.Join(_testcasesDirectory, testcase), comicInfos);
|
var testDirectoryPath = await GenerateTestDirectory(Path.Join(_testcasesDirectory, testcase), comicInfos);
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ public class ScannerHelper
|
||||||
return library;
|
return library;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ScannerService CreateServices(DirectoryService ds = null, IFileSystem fs = null)
|
public ScannerService CreateServices(DirectoryService? ds = null, IFileSystem? fs = null)
|
||||||
{
|
{
|
||||||
fs ??= new FileSystem();
|
fs ??= new FileSystem();
|
||||||
ds ??= new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs);
|
ds ??= new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs);
|
||||||
|
@ -113,7 +113,7 @@ public class ScannerHelper
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private async Task<string> GenerateTestDirectory(string mapPath, Dictionary<string, ComicInfo> comicInfos = null)
|
private async Task<string> GenerateTestDirectory(string mapPath, Dictionary<string, ComicInfo>? comicInfos = null)
|
||||||
{
|
{
|
||||||
// Read the map file
|
// Read the map file
|
||||||
var mapContent = await File.ReadAllTextAsync(mapPath);
|
var mapContent = await File.ReadAllTextAsync(mapPath);
|
||||||
|
@ -130,7 +130,7 @@ public class ScannerHelper
|
||||||
Directory.CreateDirectory(testDirectory);
|
Directory.CreateDirectory(testDirectory);
|
||||||
|
|
||||||
// Generate the files and folders
|
// Generate the files and folders
|
||||||
await Scaffold(testDirectory, filePaths, comicInfos);
|
await Scaffold(testDirectory, filePaths ?? [], comicInfos);
|
||||||
|
|
||||||
_testOutputHelper.WriteLine($"Test Directory Path: {testDirectory}");
|
_testOutputHelper.WriteLine($"Test Directory Path: {testDirectory}");
|
||||||
|
|
||||||
|
@ -138,18 +138,20 @@ public class ScannerHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task Scaffold(string testDirectory, List<string> filePaths, Dictionary<string, ComicInfo> comicInfos = null)
|
public async Task Scaffold(string testDirectory, List<string> filePaths, Dictionary<string, ComicInfo>? comicInfos = null)
|
||||||
{
|
{
|
||||||
foreach (var relativePath in filePaths)
|
foreach (var relativePath in filePaths)
|
||||||
{
|
{
|
||||||
var fullPath = Path.Combine(testDirectory, relativePath);
|
var fullPath = Path.Combine(testDirectory, relativePath);
|
||||||
var fileDir = Path.GetDirectoryName(fullPath);
|
var fileDir = Path.GetDirectoryName(fullPath);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(fileDir)) continue;
|
||||||
|
|
||||||
// Create the directory if it doesn't exist
|
// Create the directory if it doesn't exist
|
||||||
if (!Directory.Exists(fileDir))
|
if (!Directory.Exists(fileDir))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(fileDir);
|
Directory.CreateDirectory(fileDir);
|
||||||
Console.WriteLine($"Created directory: {fileDir}");
|
_testOutputHelper.WriteLine($"Created directory: {fileDir}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var ext = Path.GetExtension(fullPath).ToLower();
|
var ext = Path.GetExtension(fullPath).ToLower();
|
||||||
|
@ -161,7 +163,7 @@ public class ScannerHelper
|
||||||
{
|
{
|
||||||
// Create an empty file
|
// Create an empty file
|
||||||
await File.Create(fullPath).DisposeAsync();
|
await File.Create(fullPath).DisposeAsync();
|
||||||
Console.WriteLine($"Created empty file: {fullPath}");
|
_testOutputHelper.WriteLine($"Created empty file: {fullPath}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -188,7 +190,7 @@ public class ScannerHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Console.WriteLine($"Created minimal CBZ archive: {filePath} with{(comicInfo != null ? "" : "out")} metadata.");
|
_testOutputHelper.WriteLine($"Created minimal CBZ archive: {filePath} with{(comicInfo != null ? "" : "out")} metadata.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
156
API.Tests/Services/FileScannerTests.cs
Normal file
156
API.Tests/Services/FileScannerTests.cs
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Abstractions;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs.Internal.Scanner;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Services;
|
||||||
|
using API.Services.Tasks.Scanner;
|
||||||
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
|
using API.Tests.Helpers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
|
public class FileScannerTests : AbstractDbTest
|
||||||
|
{
|
||||||
|
private readonly FileScanner _fileScanner;
|
||||||
|
private readonly IDirectoryService _directoryService;
|
||||||
|
private readonly ScannerHelper _scannerHelper;
|
||||||
|
private readonly string _outputDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests");
|
||||||
|
private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/TestCases");
|
||||||
|
|
||||||
|
public FileScannerTests(ITestOutputHelper testOutputHelper)
|
||||||
|
{
|
||||||
|
_directoryService = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new FileSystem());
|
||||||
|
_fileScanner = new FileScanner(_directoryService, UnitOfWork);
|
||||||
|
_scannerHelper = new ScannerHelper(UnitOfWork, testOutputHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region ScanFiles - Basic Tests
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that FileTypePattern works
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task ScanFiles_ShouldIncludeOnlyArchiveTypes()
|
||||||
|
{
|
||||||
|
const string testcase = "Flat Series - Manga.json";
|
||||||
|
var library = await _scannerHelper.GenerateScannerData(testcase);
|
||||||
|
var folder = library.Folders.First().Path;
|
||||||
|
|
||||||
|
var options = new ScannerOption
|
||||||
|
{
|
||||||
|
FolderPaths = [folder],
|
||||||
|
FileTypePattern = [FileTypeGroup.Archive],
|
||||||
|
ExcludePatterns = []
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _fileScanner.ScanFiles(options);
|
||||||
|
|
||||||
|
Assert.Single(result); // One folder
|
||||||
|
var scanned = result[0];
|
||||||
|
Assert.Equal(Parser.NormalizePath(Path.Join(folder, "My Dress-Up Darling")), scanned.DirectoryPath);
|
||||||
|
Assert.All(scanned.Files, file =>
|
||||||
|
{
|
||||||
|
Assert.EndsWith(".cbz", file.FilePath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ScanFiles_ShouldIncludeMultipleTypes()
|
||||||
|
{
|
||||||
|
const string testcase = "Mixed Formats - Manga.json";
|
||||||
|
var library = await _scannerHelper.GenerateScannerData(testcase);
|
||||||
|
var folder = library.Folders.First().Path;
|
||||||
|
|
||||||
|
var options = new ScannerOption
|
||||||
|
{
|
||||||
|
FolderPaths = [folder],
|
||||||
|
FileTypePattern = [FileTypeGroup.Archive, FileTypeGroup.Epub],
|
||||||
|
ExcludePatterns = []
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _fileScanner.ScanFiles(options);
|
||||||
|
|
||||||
|
Assert.Single(result); // One folder
|
||||||
|
var scanned = result[0];
|
||||||
|
Assert.Equal(Parser.NormalizePath(Path.Join(folder, "My Dress-Up Darling")), scanned.DirectoryPath);
|
||||||
|
var validExtensions = new[] { ".cbz", ".epub" };
|
||||||
|
Assert.All(scanned.Files, file =>
|
||||||
|
{
|
||||||
|
Assert.Contains(Path.GetExtension(file.FilePath)?.ToLowerInvariant(), validExtensions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ScannFiles - Exclude Patterns
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ScanFiles_ShouldExcludeMatchingPattern()
|
||||||
|
{
|
||||||
|
const string testcase = "Flat Series - Manga.json";
|
||||||
|
var library = await _scannerHelper.GenerateScannerData(testcase);
|
||||||
|
var folder = library.Folders.First().Path;
|
||||||
|
|
||||||
|
var options = new ScannerOption
|
||||||
|
{
|
||||||
|
FolderPaths = [folder],
|
||||||
|
FileTypePattern = [FileTypeGroup.Archive],
|
||||||
|
ExcludePatterns = ["*ch 10.cbz"] // Exclude chapter 10
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _fileScanner.ScanFiles(options);
|
||||||
|
|
||||||
|
var scannedFiles = result.SelectMany(d => d.Files).ToList();
|
||||||
|
Assert.DoesNotContain(scannedFiles, f => f.FilePath.Contains("ch 10.cbz"));
|
||||||
|
Assert.Contains(scannedFiles, f => f.FilePath.Contains("v01.cbz"));
|
||||||
|
Assert.Contains(scannedFiles, f => f.FilePath.Contains("v02.cbz"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ScannFiles - Change Detection
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ScanFiles_ShouldHaveAccurateLastModifiedUtc()
|
||||||
|
{
|
||||||
|
const string testcase = "Flat Series - Manga.json";
|
||||||
|
var library = await _scannerHelper.GenerateScannerData(testcase);
|
||||||
|
var folder = library.Folders.First().Path;
|
||||||
|
|
||||||
|
var options = new ScannerOption
|
||||||
|
{
|
||||||
|
FolderPaths = [folder],
|
||||||
|
FileTypePattern = [FileTypeGroup.Archive],
|
||||||
|
ExcludePatterns = []
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _fileScanner.ScanFiles(options);
|
||||||
|
|
||||||
|
Assert.Single(result);
|
||||||
|
var scannedDir = result[0];
|
||||||
|
var file = scannedDir.Files[0];
|
||||||
|
|
||||||
|
var expected = _directoryService.GetLastWriteTime(file.FilePath).ToUniversalTime();
|
||||||
|
Assert.Equal(expected, file.LastModifiedUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
protected override async Task ResetDb()
|
||||||
|
{
|
||||||
|
Context.Series.RemoveRange(Context.Series);
|
||||||
|
Context.Library.RemoveRange(Context.Library);
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
54
API.Tests/Services/Scanner/FileSystemParserTests.cs
Normal file
54
API.Tests/Services/Scanner/FileSystemParserTests.cs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data.Repositories;
|
||||||
|
using API.Tests.Helpers;
|
||||||
|
using Hangfire;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace API.Tests.Services.Scanner;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Responsible for testing Change Detection, Exclude Patterns,
|
||||||
|
/// </summary>
|
||||||
|
public class FileSystemParserTests : AbstractDbTest
|
||||||
|
{
|
||||||
|
|
||||||
|
private readonly ITestOutputHelper _testOutputHelper;
|
||||||
|
private readonly ScannerHelper _scannerHelper;
|
||||||
|
private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests");
|
||||||
|
|
||||||
|
public FileSystemParserTests(ITestOutputHelper testOutputHelper)
|
||||||
|
{
|
||||||
|
_testOutputHelper = testOutputHelper;
|
||||||
|
|
||||||
|
// Set up Hangfire to use in-memory storage for testing
|
||||||
|
GlobalConfiguration.Configuration.UseInMemoryStorage();
|
||||||
|
_scannerHelper = new ScannerHelper(UnitOfWork, testOutputHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ResetDb()
|
||||||
|
{
|
||||||
|
Context.Library.RemoveRange(Context.Library);
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#region Validate Change Detection
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ScanLibrary_ComicVine_PublisherFolder()
|
||||||
|
{
|
||||||
|
var testcase = "Publisher - ComicVine.json";
|
||||||
|
var library = await _scannerHelper.GenerateScannerData(testcase);
|
||||||
|
var scanner = _scannerHelper.CreateServices();
|
||||||
|
await scanner.ScanLibrary(library.Id);
|
||||||
|
var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
|
||||||
|
|
||||||
|
Assert.NotNull(postLib);
|
||||||
|
Assert.Equal(4, postLib.Series.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
|
@ -18,14 +18,11 @@ namespace API.Tests.Services;
|
||||||
|
|
||||||
public class ScannerServiceTests : AbstractDbTest
|
public class ScannerServiceTests : AbstractDbTest
|
||||||
{
|
{
|
||||||
private readonly ITestOutputHelper _testOutputHelper;
|
|
||||||
private readonly ScannerHelper _scannerHelper;
|
private readonly ScannerHelper _scannerHelper;
|
||||||
private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests");
|
private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests");
|
||||||
|
|
||||||
public ScannerServiceTests(ITestOutputHelper testOutputHelper)
|
public ScannerServiceTests(ITestOutputHelper testOutputHelper)
|
||||||
{
|
{
|
||||||
_testOutputHelper = testOutputHelper;
|
|
||||||
|
|
||||||
// Set up Hangfire to use in-memory storage for testing
|
// Set up Hangfire to use in-memory storage for testing
|
||||||
GlobalConfiguration.Configuration.UseInMemoryStorage();
|
GlobalConfiguration.Configuration.UseInMemoryStorage();
|
||||||
_scannerHelper = new ScannerHelper(UnitOfWork, testOutputHelper);
|
_scannerHelper = new ScannerHelper(UnitOfWork, testOutputHelper);
|
||||||
|
@ -938,4 +935,9 @@ public class ScannerServiceTests : AbstractDbTest
|
||||||
Assert.True(sortedChapters[1].SortOrder.Is(4f));
|
Assert.True(sortedChapters[1].SortOrder.Is(4f));
|
||||||
Assert.True(sortedChapters[2].SortOrder.Is(5f));
|
Assert.True(sortedChapters[2].SortOrder.Is(5f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Scanner Overhaul
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
[
|
||||||
|
"Root 1/Series A/Series A 01.cbz",
|
||||||
|
"Root 1/Series B/Series B 01.cbz",
|
||||||
|
"Root 2/Series C/Series C 01.cbz"
|
||||||
|
]
|
|
@ -0,0 +1,6 @@
|
||||||
|
[
|
||||||
|
"Root 1/Series A/Series A/Series A 01.cbz",
|
||||||
|
"Root 1/Series A/Series A1/Series A1 01.cbz",
|
||||||
|
"Root 1/Series B/Series B 01.cbz",
|
||||||
|
"Root 2/Series C/Series C 01.cbz"
|
||||||
|
]
|
|
@ -0,0 +1,4 @@
|
||||||
|
[
|
||||||
|
"Root 1/Books/book v1.pdf",
|
||||||
|
"Root 1/Books/book v2.pdf"
|
||||||
|
]
|
|
@ -0,0 +1,5 @@
|
||||||
|
[
|
||||||
|
"Root 1/Books/Series A/ch 1/001.png",
|
||||||
|
"Root 1/Books/Series A/ch 1/002.png",
|
||||||
|
"Root 1/Books/Series A/ch 2/001.png"
|
||||||
|
]
|
|
@ -0,0 +1,5 @@
|
||||||
|
[
|
||||||
|
"Root 1/Books/Series A/vol 1 ch 1/001.png",
|
||||||
|
"Root 1/Books/Series A/vol 1 ch 2/002.png",
|
||||||
|
"Root 1/Books/Series A/vol 2/001.png"
|
||||||
|
]
|
|
@ -0,0 +1,5 @@
|
||||||
|
[
|
||||||
|
"Root 1/Books/Series A/vol 1/ch 1/001.png",
|
||||||
|
"Root 1/Books/Series A/vol 1/ch 2/002.png",
|
||||||
|
"Root 1/Books/Series A/vol 2/001.png"
|
||||||
|
]
|
|
@ -0,0 +1,4 @@
|
||||||
|
[
|
||||||
|
"Root 1/Books/Series A [Digital]/001.png",
|
||||||
|
"Root 1/Books/Series A [Digital]/002.png"
|
||||||
|
]
|
|
@ -0,0 +1,5 @@
|
||||||
|
[
|
||||||
|
"Root 1/Books/Series A/vol 1/001.png",
|
||||||
|
"Root 1/Books/Series A/vol 1/002.png",
|
||||||
|
"Root 1/Books/Series A/vol 2/001.png"
|
||||||
|
]
|
|
@ -0,0 +1,5 @@
|
||||||
|
[
|
||||||
|
"Root 1/Genre/Series A 01.cbz",
|
||||||
|
"Root 1/Genre/Series B 01.cbz",
|
||||||
|
"Root 1/Genre/Series C 01.cbz"
|
||||||
|
]
|
|
@ -0,0 +1,6 @@
|
||||||
|
[
|
||||||
|
"Root 1/Publisher 1/Series A/Series A #1.cbz",
|
||||||
|
"Root 1/Publisher 1/Series B/Series B #1.cbz",
|
||||||
|
"Root 1/Publisher 2/Series C/Series C #1.cbz",
|
||||||
|
"Root 1/Publisher 2/Series D/Series D #1.cbz"
|
||||||
|
]
|
|
@ -0,0 +1,6 @@
|
||||||
|
[
|
||||||
|
"Root 1/Series A/Series A 01.cbz",
|
||||||
|
"Root 1/Series A/Series A 02.cbz",
|
||||||
|
"Root 1/Series A/Specials/Series A - Title.cbz",
|
||||||
|
"Root 1/Series A/Specials/Title Two.cbz"
|
||||||
|
]
|
|
@ -0,0 +1,6 @@
|
||||||
|
[
|
||||||
|
"Root 1/Series A/Series A 01.cbz",
|
||||||
|
"Root 1/Series A/Series A 02.cbz",
|
||||||
|
"Root 1/Series A/Series A - Title SP01.cbz",
|
||||||
|
"Root 1/Series A/Series A - Title SP02.cbz"
|
||||||
|
]
|
|
@ -0,0 +1,6 @@
|
||||||
|
[
|
||||||
|
"Root 1/Series A/Series A 01.cbz",
|
||||||
|
"Root 1/Series B/Series B 01.cbz",
|
||||||
|
"Root 1/Series B/Series B 02.cbz",
|
||||||
|
"Root 2/Series C/Series C 01.cbz"
|
||||||
|
]
|
|
@ -0,0 +1,6 @@
|
||||||
|
[
|
||||||
|
"Root 1/Series A/Series A 01.cbz",
|
||||||
|
"Root 1/Series B/Series B 01.cbz",
|
||||||
|
"Root 1/Series B/Ignore/Series B 02.cbz",
|
||||||
|
"Root 2/Ignore/Series C/Series C 01.cbz"
|
||||||
|
]
|
|
@ -0,0 +1,6 @@
|
||||||
|
[
|
||||||
|
"Root 1/Series A/Series A 01.cbz",
|
||||||
|
"Root 1/Series B/Series B 01.cbz",
|
||||||
|
"Root 1/Series B/Ignore/Series B 02.cbz",
|
||||||
|
"Root 2/Series C/Series C 01.cbz"
|
||||||
|
]
|
|
@ -0,0 +1,8 @@
|
||||||
|
[
|
||||||
|
"My Dress-Up Darling/My Dress-Up Darling v01.cbz",
|
||||||
|
"My Dress-Up Darling/My Dress-Up Darling v02.cbz",
|
||||||
|
"My Dress-Up Darling/My Dress-Up Darling ch 10.cbz",
|
||||||
|
"My Dress-Up Darling/My Dress-Up Darling ch 11.epub",
|
||||||
|
"My Dress-Up Darling/My Dress-Up Darling ch 12.png",
|
||||||
|
"My Dress-Up Darling/My Dress-Up Darling ch 13.pdf"
|
||||||
|
]
|
|
@ -623,6 +623,15 @@ public class LibraryController : BaseApiController
|
||||||
library.ManageReadingLists = dto.ManageReadingLists;
|
library.ManageReadingLists = dto.ManageReadingLists;
|
||||||
library.AllowScrobbling = dto.AllowScrobbling;
|
library.AllowScrobbling = dto.AllowScrobbling;
|
||||||
library.AllowMetadataMatching = dto.AllowMetadataMatching;
|
library.AllowMetadataMatching = dto.AllowMetadataMatching;
|
||||||
|
|
||||||
|
if (!dto.AllowFilenameParsing && !dto.AllowMetadataParsing)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("At least one of UseFilenameParsing or UseInternalMetadataParsing must be true.");
|
||||||
|
}
|
||||||
|
|
||||||
|
library.AllowFilenameParsing = dto.AllowFilenameParsing;
|
||||||
|
library.AllowMetadataParsing = dto.AllowMetadataParsing;
|
||||||
|
|
||||||
library.LibraryFileTypes = dto.FileGroupTypes
|
library.LibraryFileTypes = dto.FileGroupTypes
|
||||||
.Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id})
|
.Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id})
|
||||||
.Distinct()
|
.Distinct()
|
||||||
|
|
12
API/DTOs/Internal/Scanner/ParsedFile.cs
Normal file
12
API/DTOs/Internal/Scanner/ParsedFile.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using API.Data.Metadata;
|
||||||
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
|
|
||||||
|
namespace API.DTOs.Internal.Scanner;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
public sealed record ParsedFile
|
||||||
|
{
|
||||||
|
public int Pages { get; set; }
|
||||||
|
public ComicInfo? Metadata { get; set; }
|
||||||
|
public ParserInfo? ParsedInformation { get; set; }
|
||||||
|
}
|
29
API/DTOs/Internal/Scanner/ScannedDirectory.cs
Normal file
29
API/DTOs/Internal/Scanner/ScannedDirectory.cs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
|
|
||||||
|
namespace API.DTOs.Internal.Scanner;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a Directory on disk and metadata information for the Scan
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ScannedDirectory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized Directory Path
|
||||||
|
/// </summary>
|
||||||
|
public required string DirectoryPath { get => _directoryPath; set => _directoryPath = Parser.NormalizePath(value); }
|
||||||
|
private string _directoryPath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Root where the directory resides
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Library Root</remarks>
|
||||||
|
public required string FolderRoot { get => _folderRoot; set => _folderRoot = Parser.NormalizePath(value); }
|
||||||
|
private string _folderRoot;
|
||||||
|
|
||||||
|
public required DateTime LastModifiedUtc { get; init; }
|
||||||
|
|
||||||
|
public List<ScannedFile> Files { get; init; } = [];
|
||||||
|
}
|
15
API/DTOs/Internal/Scanner/ScannedFile.cs
Normal file
15
API/DTOs/Internal/Scanner/ScannedFile.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using System;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
|
|
||||||
|
namespace API.DTOs.Internal.Scanner;
|
||||||
|
|
||||||
|
public sealed record ScannedFile
|
||||||
|
{
|
||||||
|
public required string FilePath { get => _filePath; set => _filePath = Parser.NormalizePath(value); }
|
||||||
|
private string _filePath;
|
||||||
|
|
||||||
|
public required DateTime LastModifiedUtc { get; set; }
|
||||||
|
public required string FolderRoot { get; set; }
|
||||||
|
public required MangaFormat Format { get; set; }
|
||||||
|
}
|
33
API/DTOs/Internal/Scanner/ScannerOption.cs
Normal file
33
API/DTOs/Internal/Scanner/ScannerOption.cs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
|
||||||
|
namespace API.DTOs.Internal.Scanner;
|
||||||
|
|
||||||
|
public sealed record ScannerOption
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A list of File Type Patterns to search files for. If empty, scan will abort
|
||||||
|
/// </summary>
|
||||||
|
public List<FileTypeGroup> FileTypePattern { get; set; } = [FileTypeGroup.Archive, FileTypeGroup.Epub, FileTypeGroup.Images, FileTypeGroup.Pdf];
|
||||||
|
/// <summary>
|
||||||
|
/// Folders to scan
|
||||||
|
/// </summary>
|
||||||
|
public List<string> FolderPaths { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Glob syntax to exclude from scan results
|
||||||
|
/// </summary>
|
||||||
|
public List<string> ExcludePatterns { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// Skip LastModified checks
|
||||||
|
/// </summary>
|
||||||
|
public bool ForceScan { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Allow use of Filename Parsing
|
||||||
|
/// </summary>
|
||||||
|
public bool UseFilenameParsing { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Allow use of Internal Metadata
|
||||||
|
/// </summary>
|
||||||
|
public bool UseInternalMetadataParsing { get; set; }
|
||||||
|
}
|
|
@ -67,4 +67,14 @@ public class LibraryDto
|
||||||
/// <remarks>This does not exclude the library from being linked to wrt Series Relationships</remarks>
|
/// <remarks>This does not exclude the library from being linked to wrt Series Relationships</remarks>
|
||||||
/// <remarks>Requires a valid LicenseKey</remarks>
|
/// <remarks>Requires a valid LicenseKey</remarks>
|
||||||
public bool AllowMetadataMatching { get; set; } = true;
|
public bool AllowMetadataMatching { get; set; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// Allow Kavita to parse Metadata from Files based on Filename
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Cannot be false if <see cref="AllowMetadataMatching"/> is false</remarks>
|
||||||
|
public bool AllowFilenameParsing { get; set; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// Allow Kavita to parse Metadata from files (ComicInfo/Epub/Pdf)
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Cannot be false if <see cref="AllowFilenameParsing"/> is false</remarks>
|
||||||
|
public bool AllowMetadataParsing { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,10 @@ public class UpdateLibraryDto
|
||||||
public bool AllowScrobbling { get; init; }
|
public bool AllowScrobbling { get; init; }
|
||||||
[Required]
|
[Required]
|
||||||
public bool AllowMetadataMatching { get; init; }
|
public bool AllowMetadataMatching { get; init; }
|
||||||
|
[Required]
|
||||||
|
public bool AllowFilenameParsing { get; set; } = true;
|
||||||
|
[Required]
|
||||||
|
public bool AllowMetadataParsing { get; set; } = true;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// What types of files to allow the scanner to pickup
|
/// What types of files to allow the scanner to pickup
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -48,6 +48,16 @@ public class Library : IEntityDate, IHasCoverImage
|
||||||
/// <remarks>This does not exclude the library from being linked to wrt Series Relationships</remarks>
|
/// <remarks>This does not exclude the library from being linked to wrt Series Relationships</remarks>
|
||||||
/// <remarks>Requires a valid LicenseKey</remarks>
|
/// <remarks>Requires a valid LicenseKey</remarks>
|
||||||
public bool AllowMetadataMatching { get; set; } = true;
|
public bool AllowMetadataMatching { get; set; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// Allow Kavita to parse Metadata from Files based on Filename
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Cannot be false if <see cref="AllowMetadataMatching"/> is false</remarks>
|
||||||
|
public bool AllowFilenameParsing { get; set; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// Allow Kavita to parse Metadata from files (ComicInfo/Epub/Pdf)
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Cannot be false if <see cref="AllowFilenameParsing"/> is false</remarks>
|
||||||
|
public bool AllowMetadataParsing { get; set; } = true;
|
||||||
|
|
||||||
|
|
||||||
public DateTime Created { get; set; }
|
public DateTime Created { get; set; }
|
||||||
|
|
|
@ -115,4 +115,16 @@ public class LibraryBuilder : IEntityBuilder<Library>
|
||||||
_library.AllowScrobbling = allowScrobbling;
|
_library.AllowScrobbling = allowScrobbling;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LibraryBuilder WithAllowFilenameParsing(bool allow)
|
||||||
|
{
|
||||||
|
_library.AllowFilenameParsing = allow;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LibraryBuilder WithAllowMetadataParsing(bool allow)
|
||||||
|
{
|
||||||
|
_library.AllowMetadataParsing = allow;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -752,6 +752,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||||
{
|
{
|
||||||
Name = w.Name,
|
Name = w.Name,
|
||||||
AniListId = ScrobblingService.ExtractId<int>(w.Url, ScrobblingService.AniListCharacterWebsite),
|
AniListId = ScrobblingService.ExtractId<int>(w.Url, ScrobblingService.AniListCharacterWebsite),
|
||||||
|
// Can I tag links to resolve favicon? Need to parse html, rewrite the urls, then have a
|
||||||
Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))),
|
Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))),
|
||||||
})
|
})
|
||||||
.Concat(series.Metadata.People
|
.Concat(series.Metadata.People
|
||||||
|
|
|
@ -633,6 +633,7 @@ public class SeriesService : ISeriesService
|
||||||
|
|
||||||
public async Task<string> FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string chapterRange, string? chapterTitle, bool withHash)
|
public async Task<string> FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string chapterRange, string? chapterTitle, bool withHash)
|
||||||
{
|
{
|
||||||
|
// TODO: Refactor so this is unit testable
|
||||||
if (string.IsNullOrEmpty(chapterTitle) && (isSpecial || libraryType == LibraryType.Book)) throw new ArgumentException("Chapter Title cannot be null");
|
if (string.IsNullOrEmpty(chapterTitle) && (isSpecial || libraryType == LibraryType.Book)) throw new ArgumentException("Chapter Title cannot be null");
|
||||||
|
|
||||||
if (isSpecial)
|
if (isSpecial)
|
||||||
|
|
125
API/Services/Tasks/Scanner/FileParser.cs
Normal file
125
API/Services/Tasks/Scanner/FileParser.cs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
using System;
|
||||||
|
using API.Data.Metadata;
|
||||||
|
using API.DTOs.Internal.Scanner;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace API.Services.Tasks.Scanner;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
public interface IFileParser
|
||||||
|
{
|
||||||
|
ParsedFile? Parse(ScannedFile file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FileParser : IFileParser
|
||||||
|
{
|
||||||
|
private readonly IArchiveService _archiveService;
|
||||||
|
private readonly IBookService _bookService;
|
||||||
|
private readonly IImageService _imageService;
|
||||||
|
private readonly ILogger<FileParser> _logger;
|
||||||
|
private readonly BasicParser _basicParser;
|
||||||
|
private readonly ComicVineParser _comicVineParser;
|
||||||
|
private readonly ImageParser _imageParser;
|
||||||
|
private readonly BookParser _bookParser;
|
||||||
|
private readonly PdfParser _pdfParser;
|
||||||
|
|
||||||
|
public FileParser(IArchiveService archiveService, IDirectoryService directoryService,
|
||||||
|
IBookService bookService, IImageService imageService, ILogger<FileParser> logger)
|
||||||
|
{
|
||||||
|
_archiveService = archiveService;
|
||||||
|
_bookService = bookService;
|
||||||
|
_imageService = imageService;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
_imageParser = new ImageParser(directoryService);
|
||||||
|
_basicParser = new BasicParser(directoryService, _imageParser);
|
||||||
|
_bookParser = new BookParser(directoryService, bookService, _basicParser);
|
||||||
|
_comicVineParser = new ComicVineParser(directoryService);
|
||||||
|
_pdfParser = new PdfParser(directoryService);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes files found during a library scan.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Path of a file</param>
|
||||||
|
/// <param name="rootPath"></param>
|
||||||
|
/// <param name="type">Library type to determine parsing to perform</param>
|
||||||
|
// public ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type)
|
||||||
|
// {
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// var info = Parse(path, rootPath, libraryRoot, type);
|
||||||
|
// if (info == null)
|
||||||
|
// {
|
||||||
|
// _logger.LogError("Unable to parse any meaningful information out of file {FilePath}", path);
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return info;
|
||||||
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// _logger.LogError(ex, "There was an exception when parsing file {FilePath}", path);
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
public ParsedFile? Parse(ScannedFile file, string folderRoot, LibraryType type)
|
||||||
|
{
|
||||||
|
var path = file.FilePath;
|
||||||
|
var rootPath = file.FolderRoot;
|
||||||
|
|
||||||
|
ParserInfo? parserInfo = null;
|
||||||
|
if (_comicVineParser.IsApplicable(path, type))
|
||||||
|
{
|
||||||
|
parserInfo = _comicVineParser.Parse(path, rootPath, folderRoot, type, GetComicInfo(path));
|
||||||
|
}
|
||||||
|
if (_imageParser.IsApplicable(path, type))
|
||||||
|
{
|
||||||
|
parserInfo = _imageParser.Parse(path, rootPath, folderRoot, type, GetComicInfo(path));
|
||||||
|
}
|
||||||
|
if (_bookParser.IsApplicable(path, type))
|
||||||
|
{
|
||||||
|
parserInfo = _bookParser.Parse(path, rootPath, folderRoot, type, GetComicInfo(path));
|
||||||
|
}
|
||||||
|
if (_pdfParser.IsApplicable(path, type))
|
||||||
|
{
|
||||||
|
parserInfo = _pdfParser.Parse(path, rootPath, folderRoot, type, GetComicInfo(path));
|
||||||
|
}
|
||||||
|
if (_basicParser.IsApplicable(path, type))
|
||||||
|
{
|
||||||
|
parserInfo = _basicParser.Parse(path, rootPath, folderRoot, type, GetComicInfo(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parserInfo == null) return null;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ComicInfo for the file if it exists. Null otherwise.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">Fully qualified path of file</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private ComicInfo? GetComicInfo(string filePath)
|
||||||
|
{
|
||||||
|
if (Parser.Parser.IsEpub(filePath) || Parser.Parser.IsPdf(filePath))
|
||||||
|
{
|
||||||
|
return _bookService.GetComicInfo(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Parser.Parser.IsComicInfoExtension(filePath))
|
||||||
|
{
|
||||||
|
return _archiveService.GetComicInfo(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
146
API/Services/Tasks/Scanner/FileScanner.cs
Normal file
146
API/Services/Tasks/Scanner/FileScanner.cs
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.Data.Repositories;
|
||||||
|
using API.DTOs.Internal.Scanner;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Extensions;
|
||||||
|
using Kavita.Common.Helpers;
|
||||||
|
|
||||||
|
namespace API.Services.Tasks.Scanner;
|
||||||
|
|
||||||
|
public interface IFileScanner
|
||||||
|
{
|
||||||
|
// TODO: Move this to the scanner service
|
||||||
|
//Task ScanLibrary(int libraryId, bool forceScan = false);
|
||||||
|
List<ScannedDirectory> ScanFiles(ScannerOption options);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class FileScanner : IFileScanner
|
||||||
|
{
|
||||||
|
private readonly IDirectoryService _directoryService;
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
|
public FileScanner(IDirectoryService directoryService, IUnitOfWork unitOfWork)
|
||||||
|
{
|
||||||
|
_directoryService = directoryService;
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// public async Task ScanLibrary(int libraryId, bool forceScan = false)
|
||||||
|
// {
|
||||||
|
// var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId,
|
||||||
|
// LibraryIncludes.Folders | LibraryIncludes.ExcludePatterns | LibraryIncludes.FileTypes);
|
||||||
|
//
|
||||||
|
// if (library == null)
|
||||||
|
// {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Create a ScannerOption
|
||||||
|
// var options = new ScannerOption()
|
||||||
|
// {
|
||||||
|
// FileTypePattern = library.LibraryFileTypes.Select(s => s.FileTypeGroup).ToList(),
|
||||||
|
// ForceScan = forceScan,
|
||||||
|
// ExcludePatterns = [.. library.LibraryExcludePatterns.Select(s => s.Pattern)],
|
||||||
|
// FolderPaths = [.. library.Folders.Select(f => Parser.Parser.NormalizePath(f.Path))]
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// // Find all the information about the directories and their files
|
||||||
|
// var files = ScanFiles(options);
|
||||||
|
//
|
||||||
|
// // Parse said information
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
public List<ScannedDirectory> ScanFiles(ScannerOption options)
|
||||||
|
{
|
||||||
|
// Validate input options
|
||||||
|
if (options == null || options.FolderPaths.Count == 0 || options.FileTypePattern.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the file extensions regex from the file type patterns
|
||||||
|
var fileExtensions = string.Join("|", options.FileTypePattern.Select(l => l.GetRegex()));
|
||||||
|
if (string.IsNullOrWhiteSpace(fileExtensions))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var matcher = BuildMatcher(options.ExcludePatterns);
|
||||||
|
var scannedDirectories = new List<ScannedDirectory>();
|
||||||
|
|
||||||
|
foreach (var folderPath in options.FolderPaths)
|
||||||
|
{
|
||||||
|
var normalizedFolderPath = Parser.Parser.NormalizePath(folderPath);
|
||||||
|
|
||||||
|
var allDirectories = _directoryService.GetAllDirectories(normalizedFolderPath, matcher)
|
||||||
|
.Select(Parser.Parser.NormalizePath)
|
||||||
|
.OrderByDescending(d => d.Length)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// TODO: Optimization: If allDirectories is large, split into Parallel tasks
|
||||||
|
|
||||||
|
foreach (var directory in allDirectories)
|
||||||
|
{
|
||||||
|
var files = _directoryService.ScanFiles(directory, fileExtensions, matcher)
|
||||||
|
.Select(filePath =>
|
||||||
|
{
|
||||||
|
// Gather metadata for each file
|
||||||
|
var lastModifiedUtc = _directoryService.GetLastWriteTime(filePath).ToUniversalTime();
|
||||||
|
var format = Parser.Parser.ParseFormat(filePath);
|
||||||
|
return new ScannedFile
|
||||||
|
{
|
||||||
|
FilePath = filePath,
|
||||||
|
LastModifiedUtc = lastModifiedUtc,
|
||||||
|
Format = format
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Skip directories with no valid files
|
||||||
|
if (files.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get directory's metadata (TODO: Replace with _directoryService.GetLastWriteTime(folder).Truncate(TimeSpan.TicksPerSecond);)
|
||||||
|
//var directoryLastModifiedUtc = files.Max(f => f.LastModifiedUtc);
|
||||||
|
var directoryLastModifiedUtc = _directoryService.GetLastWriteTime(normalizedFolderPath).Truncate(TimeSpan.TicksPerSecond);
|
||||||
|
|
||||||
|
// Add the directory and its files to the result
|
||||||
|
scannedDirectories.Add(new ScannedDirectory
|
||||||
|
{
|
||||||
|
FolderRoot = folderPath,
|
||||||
|
DirectoryPath = directory,
|
||||||
|
LastModifiedUtc = directoryLastModifiedUtc,
|
||||||
|
Files = files
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scannedDirectories;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static GlobMatcher BuildMatcher(List<string> excludePatterns)
|
||||||
|
{
|
||||||
|
var matcher = new GlobMatcher();
|
||||||
|
foreach (var pattern in excludePatterns.Where(p => !string.IsNullOrEmpty(p)))
|
||||||
|
{
|
||||||
|
matcher.AddExclude(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matcher;
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag
|
||||||
{
|
{
|
||||||
var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath);
|
var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath);
|
||||||
// TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this.
|
// TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this.
|
||||||
|
// NOTE: This may no longer be needed as we have file type group support now, thus an image wouldn't come for a Series
|
||||||
if (type != LibraryType.Image && Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null;
|
if (type != LibraryType.Image && Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null;
|
||||||
|
|
||||||
if (Parser.IsImage(filePath))
|
if (Parser.IsImage(filePath))
|
||||||
|
|
|
@ -193,10 +193,6 @@ public class ProcessSeries : IProcessSeries
|
||||||
|
|
||||||
if (seriesAdded)
|
if (seriesAdded)
|
||||||
{
|
{
|
||||||
// See if any recommendations can link up to the series and pre-fetch external metadata for the series
|
|
||||||
// BackgroundJob.Enqueue(() =>
|
|
||||||
// _externalMetadataService.FetchSeriesMetadata(series.Id, series.Library.Type));
|
|
||||||
|
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.SeriesAdded,
|
await _eventHub.SendMessageAsync(MessageFactory.SeriesAdded,
|
||||||
MessageFactory.SeriesAddedEvent(series.Id, series.Name, series.LibraryId), false);
|
MessageFactory.SeriesAddedEvent(series.Id, series.Name, series.LibraryId), false);
|
||||||
}
|
}
|
||||||
|
@ -216,6 +212,7 @@ public class ProcessSeries : IProcessSeries
|
||||||
|
|
||||||
if (seriesAdded)
|
if (seriesAdded)
|
||||||
{
|
{
|
||||||
|
// Prefetch metadata if applicable
|
||||||
await _externalMetadataService.FetchSeriesMetadata(series.Id, series.Library.Type);
|
await _externalMetadataService.FetchSeriesMetadata(series.Id, series.Library.Type);
|
||||||
}
|
}
|
||||||
await _metadataService.GenerateCoversForSeries(series.LibraryId, series.Id, false, false);
|
await _metadataService.GenerateCoversForSeries(series.LibraryId, series.Id, false, false);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue