mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-23 15:30:34 -04:00
* Implemented save covers as webp. Reworked screen to provide more information up front about webp and what browsers can support it. * cleaned up pages to use compact numbering and made compact numbering expand into one decimal place (20.5K) * Fixed an issue with adding new device * If a book has an invalid language set, drop the language altogether rather than reading in a corrupted entry. * Ensure genres and tags render alphabetically. Improved support for partial volumes in Comic parser. * Ensure all people, tags, collections, and genres are in alphabetical order. * Moved some code to Extensions to clean up code. * More unit tests * Cleaned up release year filter css * Tweaked some code in all series to make bulk deletes cleaner on the UI. * Trying out want to read and unread count on series detail page * Added Want to Read button for series page to make it easy to see when something is in want to read list and toggle it. Added tooltips instead of title to buttons, but they don't style correctly. Added a continue point under cover image. * Code smells
394 lines
16 KiB
C#
394 lines
16 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Data.Common;
|
|
using System.IO.Abstractions.TestingHelpers;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using API.Data;
|
|
using API.Data.Metadata;
|
|
using API.Entities;
|
|
using API.Entities.Enums;
|
|
using API.Parser;
|
|
using API.Services;
|
|
using API.Services.Tasks.Scanner;
|
|
using API.Services.Tasks.Scanner.Parser;
|
|
using API.SignalR;
|
|
using API.Tests.Helpers;
|
|
using AutoMapper;
|
|
using DotNet.Globbing;
|
|
using Flurl.Util;
|
|
using Microsoft.Data.Sqlite;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
using Microsoft.Extensions.Logging;
|
|
using NSubstitute;
|
|
using Xunit;
|
|
|
|
namespace API.Tests.Services;
|
|
|
|
internal class MockReadingItemService : IReadingItemService
|
|
{
|
|
private readonly IDefaultParser _defaultParser;
|
|
|
|
public MockReadingItemService(IDefaultParser defaultParser)
|
|
{
|
|
_defaultParser = defaultParser;
|
|
}
|
|
|
|
public ComicInfo GetComicInfo(string filePath)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public int GetNumberOfPages(string filePath, MangaFormat format)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format, bool saveAsWebP)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
public void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public ParserInfo Parse(string path, string rootPath, LibraryType type)
|
|
{
|
|
return _defaultParser.Parse(path, rootPath, type);
|
|
}
|
|
|
|
public ParserInfo ParseFile(string path, string rootPath, LibraryType type)
|
|
{
|
|
return _defaultParser.Parse(path, rootPath, type);
|
|
}
|
|
}
|
|
|
|
public class ParseScannedFilesTests
|
|
{
|
|
private readonly ILogger<ParseScannedFiles> _logger = Substitute.For<ILogger<ParseScannedFiles>>();
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
|
|
private readonly DbConnection _connection;
|
|
private readonly DataContext _context;
|
|
|
|
private const string CacheDirectory = "C:/kavita/config/cache/";
|
|
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
|
private const string BackupDirectory = "C:/kavita/config/backups/";
|
|
private const string DataDirectory = "C:/data/";
|
|
|
|
public ParseScannedFilesTests()
|
|
{
|
|
var contextOptions = new DbContextOptionsBuilder()
|
|
.UseSqlite(CreateInMemoryDatabase())
|
|
.Options;
|
|
_connection = RelationalOptionsExtension.Extract(contextOptions).Connection;
|
|
|
|
_context = new DataContext(contextOptions);
|
|
Task.Run(SeedDb).GetAwaiter().GetResult();
|
|
|
|
_unitOfWork = new UnitOfWork(_context, Substitute.For<IMapper>(), null);
|
|
|
|
// Since ProcessFile relies on _readingItemService, we can implement our own versions of _readingItemService so we have control over how the calls work
|
|
}
|
|
|
|
#region Setup
|
|
|
|
private static DbConnection CreateInMemoryDatabase()
|
|
{
|
|
var connection = new SqliteConnection("Filename=:memory:");
|
|
|
|
connection.Open();
|
|
|
|
return connection;
|
|
}
|
|
|
|
private async Task<bool> SeedDb()
|
|
{
|
|
await _context.Database.MigrateAsync();
|
|
var filesystem = CreateFileSystem();
|
|
|
|
await Seed.SeedSettings(_context, new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem));
|
|
|
|
var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync();
|
|
setting.Value = CacheDirectory;
|
|
|
|
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync();
|
|
setting.Value = BackupDirectory;
|
|
|
|
_context.ServerSetting.Update(setting);
|
|
|
|
_context.Library.Add(new Library()
|
|
{
|
|
Name = "Manga",
|
|
Folders = new List<FolderPath>()
|
|
{
|
|
new FolderPath()
|
|
{
|
|
Path = DataDirectory
|
|
}
|
|
}
|
|
});
|
|
return await _context.SaveChangesAsync() > 0;
|
|
}
|
|
|
|
private async Task ResetDB()
|
|
{
|
|
_context.Series.RemoveRange(_context.Series.ToList());
|
|
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
private static MockFileSystem CreateFileSystem()
|
|
{
|
|
var fileSystem = new MockFileSystem();
|
|
fileSystem.Directory.SetCurrentDirectory("C:/kavita/");
|
|
fileSystem.AddDirectory("C:/kavita/config/");
|
|
fileSystem.AddDirectory(CacheDirectory);
|
|
fileSystem.AddDirectory(CoverImageDirectory);
|
|
fileSystem.AddDirectory(BackupDirectory);
|
|
fileSystem.AddDirectory(DataDirectory);
|
|
|
|
return fileSystem;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region MergeName
|
|
|
|
// NOTE: I don't think I can test MergeName as it relies on Tracking Files, which is more complicated than I need
|
|
// [Fact]
|
|
// public async Task MergeName_ShouldMergeMatchingFormatAndName()
|
|
// {
|
|
// var fileSystem = new MockFileSystem();
|
|
// fileSystem.AddDirectory("C:/Data/");
|
|
// fileSystem.AddFile("C:/Data/Accel World v1.cbz", new MockFileData(string.Empty));
|
|
// fileSystem.AddFile("C:/Data/Accel World v2.cbz", new MockFileData(string.Empty));
|
|
// fileSystem.AddFile("C:/Data/Accel World v2.pdf", new MockFileData(string.Empty));
|
|
//
|
|
// var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
|
// var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
|
// new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
|
|
//
|
|
// var parsedSeries = new Dictionary<ParsedSeries, IList<ParserInfo>>();
|
|
// var parsedFiles = new ConcurrentDictionary<ParsedSeries, List<ParserInfo>>();
|
|
//
|
|
// void TrackFiles(Tuple<bool, IList<ParserInfo>> parsedInfo)
|
|
// {
|
|
// var skippedScan = parsedInfo.Item1;
|
|
// var parsedFiles = parsedInfo.Item2;
|
|
// if (parsedFiles.Count == 0) return;
|
|
//
|
|
// var foundParsedSeries = new ParsedSeries()
|
|
// {
|
|
// Name = parsedFiles.First().Series,
|
|
// NormalizedName = API.Parser.Parser.Normalize(parsedFiles.First().Series),
|
|
// Format = parsedFiles.First().Format
|
|
// };
|
|
//
|
|
// parsedSeries.Add(foundParsedSeries, parsedFiles);
|
|
// }
|
|
//
|
|
// await psf.ScanLibrariesForSeries(LibraryType.Manga, new List<string>() {"C:/Data/"}, "libraryName",
|
|
// false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), TrackFiles);
|
|
//
|
|
// Assert.Equal("Accel World",
|
|
// psf.MergeName(parsedFiles, ParserInfoFactory.CreateParsedInfo("Accel World", "1", "0", "Accel World v1.cbz", false)));
|
|
// Assert.Equal("Accel World",
|
|
// psf.MergeName(parsedFiles, ParserInfoFactory.CreateParsedInfo("accel_world", "1", "0", "Accel World v1.cbz", false)));
|
|
// Assert.Equal("Accel World",
|
|
// psf.MergeName(parsedFiles, ParserInfoFactory.CreateParsedInfo("accelworld", "1", "0", "Accel World v1.cbz", false)));
|
|
// }
|
|
//
|
|
// [Fact]
|
|
// public async Task MergeName_ShouldMerge_MismatchedFormatSameName()
|
|
// {
|
|
// var fileSystem = new MockFileSystem();
|
|
// fileSystem.AddDirectory("C:/Data/");
|
|
// fileSystem.AddFile("C:/Data/Accel World v1.cbz", new MockFileData(string.Empty));
|
|
// fileSystem.AddFile("C:/Data/Accel World v2.cbz", new MockFileData(string.Empty));
|
|
// fileSystem.AddFile("C:/Data/Accel World v2.pdf", new MockFileData(string.Empty));
|
|
//
|
|
// var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
|
// var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
|
// new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
|
|
//
|
|
//
|
|
// await psf.ScanLibrariesForSeries(LibraryType.Manga, new List<string>() {"C:/Data/"}, "libraryName");
|
|
//
|
|
// Assert.Equal("Accel World",
|
|
// psf.MergeName(ParserInfoFactory.CreateParsedInfo("Accel World", "1", "0", "Accel World v1.epub", false)));
|
|
// Assert.Equal("Accel World",
|
|
// psf.MergeName(ParserInfoFactory.CreateParsedInfo("accel_world", "1", "0", "Accel World v1.epub", false)));
|
|
// }
|
|
|
|
#endregion
|
|
|
|
#region ScanLibrariesForSeries
|
|
|
|
[Fact]
|
|
public async Task ScanLibrariesForSeries_ShouldFindFiles()
|
|
{
|
|
var fileSystem = new MockFileSystem();
|
|
fileSystem.AddDirectory("C:/Data/");
|
|
fileSystem.AddFile("C:/Data/Accel World v1.cbz", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Accel World v2.cbz", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Accel World v2.pdf", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Nothing.pdf", new MockFileData(string.Empty));
|
|
|
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
|
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
|
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
|
|
|
|
var parsedSeries = new Dictionary<ParsedSeries, IList<ParserInfo>>();
|
|
|
|
Task TrackFiles(Tuple<bool, IList<ParserInfo>> parsedInfo)
|
|
{
|
|
var skippedScan = parsedInfo.Item1;
|
|
var parsedFiles = parsedInfo.Item2;
|
|
if (parsedFiles.Count == 0) return Task.CompletedTask;
|
|
|
|
var foundParsedSeries = new ParsedSeries()
|
|
{
|
|
Name = parsedFiles.First().Series,
|
|
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize(parsedFiles.First().Series),
|
|
Format = parsedFiles.First().Format
|
|
};
|
|
|
|
parsedSeries.Add(foundParsedSeries, parsedFiles);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
await psf.ScanLibrariesForSeries(LibraryType.Manga,
|
|
new List<string>() {"C:/Data/"}, "libraryName", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), TrackFiles);
|
|
|
|
|
|
Assert.Equal(3, parsedSeries.Values.Count);
|
|
Assert.NotEmpty(parsedSeries.Keys.Where(p => p.Format == MangaFormat.Archive && p.Name.Equals("Accel World")));
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region ProcessFiles
|
|
|
|
private static MockFileSystem CreateTestFilesystem()
|
|
{
|
|
var fileSystem = new MockFileSystem();
|
|
fileSystem.AddDirectory("C:/Data/");
|
|
fileSystem.AddDirectory("C:/Data/Accel World");
|
|
fileSystem.AddDirectory("C:/Data/Accel World/Specials/");
|
|
fileSystem.AddFile("C:/Data/Accel World/Accel World v1.cbz", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Accel World/Accel World v2.cbz", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Accel World/Accel World v2.pdf", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Accel World/Specials/Accel World SP01.cbz", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Black World/Black World SP01.cbz", new MockFileData(string.Empty));
|
|
|
|
return fileSystem;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessFiles_ForLibraryMode_OnlyCallsFolderActionForEachTopLevelFolder()
|
|
{
|
|
var fileSystem = CreateTestFilesystem();
|
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
|
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
|
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
|
|
|
|
var directoriesSeen = new HashSet<string>();
|
|
await psf.ProcessFiles("C:/Data/", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),
|
|
(files, directoryPath) =>
|
|
{
|
|
directoriesSeen.Add(directoryPath);
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
Assert.Equal(2, directoriesSeen.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessFiles_ForNonLibraryMode_CallsFolderActionOnce()
|
|
{
|
|
var fileSystem = CreateTestFilesystem();
|
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
|
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
|
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
|
|
|
|
var directoriesSeen = new HashSet<string>();
|
|
await psf.ProcessFiles("C:/Data/", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),(files, directoryPath) =>
|
|
{
|
|
directoriesSeen.Add(directoryPath);
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
Assert.Single(directoriesSeen);
|
|
directoriesSeen.TryGetValue("C:/Data/", out var actual);
|
|
Assert.Equal("C:/Data/", actual);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessFiles_ShouldCallFolderActionTwice()
|
|
{
|
|
var fileSystem = new MockFileSystem();
|
|
fileSystem.AddDirectory("C:/Data/");
|
|
fileSystem.AddDirectory("C:/Data/Accel World");
|
|
fileSystem.AddDirectory("C:/Data/Accel World/Specials/");
|
|
fileSystem.AddFile("C:/Data/Accel World/Accel World v1.cbz", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Accel World/Accel World v2.cbz", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Accel World/Accel World v2.pdf", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Accel World/Specials/Accel World SP01.cbz", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Black World/Black World SP01.cbz", new MockFileData(string.Empty));
|
|
|
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
|
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
|
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
|
|
|
|
var callCount = 0;
|
|
await psf.ProcessFiles("C:/Data", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),(files, folderPath) =>
|
|
{
|
|
callCount++;
|
|
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
Assert.Equal(2, callCount);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Due to this not being a library, it's going to consider everything under C:/Data as being one folder aka a series folder
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ProcessFiles_ShouldCallFolderActionOnce()
|
|
{
|
|
var fileSystem = new MockFileSystem();
|
|
fileSystem.AddDirectory("C:/Data/");
|
|
fileSystem.AddDirectory("C:/Data/Accel World");
|
|
fileSystem.AddDirectory("C:/Data/Accel World/Specials/");
|
|
fileSystem.AddFile("C:/Data/Accel World/Accel World v1.cbz", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Accel World/Accel World v2.cbz", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Accel World/Accel World v2.pdf", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Accel World/Specials/Accel World SP01.cbz", new MockFileData(string.Empty));
|
|
fileSystem.AddFile("C:/Data/Black World/Black World SP01.cbz", new MockFileData(string.Empty));
|
|
|
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
|
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
|
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
|
|
|
|
var callCount = 0;
|
|
await psf.ProcessFiles("C:/Data", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),(files, folderPath) =>
|
|
{
|
|
callCount++;
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
Assert.Equal(1, callCount);
|
|
}
|
|
|
|
#endregion
|
|
}
|