Refactored DB to be Series->Volumes->Chapters instead. All functionality that previously worked still works. Cleanup still needed.
This commit is contained in:
parent
a42e54a078
commit
53e85317f9
33 changed files with 2084 additions and 142 deletions
|
@ -161,7 +161,7 @@ namespace API.Services
|
|||
var needsFlattening = ArchiveNeedsFlattening(archive);
|
||||
if (!archive.HasFiles() && !needsFlattening) return;
|
||||
|
||||
archive.ExtractToDirectory(extractPath);
|
||||
archive.ExtractToDirectory(extractPath, true);
|
||||
_logger.LogDebug($"Extracted archive to {extractPath} in {sw.ElapsedMilliseconds} milliseconds.");
|
||||
|
||||
if (needsFlattening)
|
||||
|
|
|
@ -18,7 +18,7 @@ namespace API.Services
|
|||
private readonly IArchiveService _archiveService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly NumericComparer _numericComparer;
|
||||
public static readonly string CacheDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../cache/"));
|
||||
public static readonly string CacheDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "cache/"));
|
||||
|
||||
public CacheService(ILogger<CacheService> logger, IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService)
|
||||
{
|
||||
|
@ -29,40 +29,36 @@ namespace API.Services
|
|||
_numericComparer = new NumericComparer();
|
||||
}
|
||||
|
||||
public bool CacheDirectoryIsAccessible()
|
||||
public void EnsureCacheDirectory()
|
||||
{
|
||||
_logger.LogDebug($"Checking if valid Cache directory: {CacheDirectory}");
|
||||
var di = new DirectoryInfo(CacheDirectory);
|
||||
return di.Exists;
|
||||
if (!di.Exists)
|
||||
{
|
||||
_logger.LogError($"Cache directory {CacheDirectory} is not accessible or does not exist. Creating...");
|
||||
Directory.CreateDirectory(CacheDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Volume> Ensure(int volumeId)
|
||||
public async Task<Chapter> Ensure(int chapterId)
|
||||
{
|
||||
if (!CacheDirectoryIsAccessible())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Volume volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
|
||||
EnsureCacheDirectory();
|
||||
Chapter chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
|
||||
|
||||
foreach (var file in volume.Files)
|
||||
foreach (var file in chapter.Files)
|
||||
{
|
||||
var extractPath = GetVolumeCachePath(volumeId, file);
|
||||
var extractPath = GetCachePath(chapterId, file);
|
||||
_archiveService.ExtractArchive(file.FilePath, extractPath);
|
||||
}
|
||||
|
||||
return volume;
|
||||
return chapter;
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
_logger.LogInformation("Performing cleanup of Cache directory");
|
||||
|
||||
if (!CacheDirectoryIsAccessible())
|
||||
{
|
||||
_logger.LogError($"Cache directory {CacheDirectory} is not accessible or does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureCacheDirectory();
|
||||
|
||||
DirectoryInfo di = new DirectoryInfo(CacheDirectory);
|
||||
|
||||
try
|
||||
|
@ -79,6 +75,7 @@ namespace API.Services
|
|||
|
||||
public void CleanupVolumes(int[] volumeIds)
|
||||
{
|
||||
// TODO: Fix this code to work with chapters
|
||||
_logger.LogInformation($"Running Cache cleanup on Volumes");
|
||||
|
||||
foreach (var volume in volumeIds)
|
||||
|
@ -96,13 +93,19 @@ namespace API.Services
|
|||
|
||||
|
||||
|
||||
public string GetVolumeCachePath(int volumeId, MangaFile file)
|
||||
/// <summary>
|
||||
/// Returns the cache path for a given Chapter. Should be cacheDirectory/{chapterId}/
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
public string GetCachePath(int chapterId, MangaFile file)
|
||||
{
|
||||
var extractPath = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), $"../cache/{volumeId}/"));
|
||||
if (file.Chapter > 0)
|
||||
{
|
||||
extractPath = Path.Join(extractPath, file.Chapter + "");
|
||||
}
|
||||
var extractPath = Path.GetFullPath(Path.Join(CacheDirectory, $"{chapterId}/"));
|
||||
// if (file.Chapter != null)
|
||||
// {
|
||||
// extractPath = Path.Join(extractPath, chapterId + "");
|
||||
// }
|
||||
return extractPath;
|
||||
}
|
||||
|
||||
|
@ -110,28 +113,29 @@ namespace API.Services
|
|||
{
|
||||
// BUG: This causes a problem because total pages on a volume assumes "specials" to be there
|
||||
//return files.OrderBy(f => f.Chapter).Where(f => f.Chapter > 0 || f.Volume.Number != 0);
|
||||
return files.OrderBy(f => f.Chapter, new ChapterSortComparer());
|
||||
return files;
|
||||
//return files.OrderBy(f => f.Chapter, new ChapterSortComparer());
|
||||
}
|
||||
|
||||
public (string path, MangaFile file) GetCachedPagePath(Volume volume, int page)
|
||||
public async Task<(string path, MangaFile file)> GetCachedPagePath(Chapter chapter, int page)
|
||||
{
|
||||
// Calculate what chapter the page belongs to
|
||||
var pagesSoFar = 0;
|
||||
// Do not allow chapters with 0, as those are specials and break ordering for reading.
|
||||
var orderedChapters = GetOrderedChapters(volume.Files);
|
||||
foreach (var mangaFile in orderedChapters)
|
||||
var chapterFiles = chapter.Files ?? await _unitOfWork.VolumeRepository.GetFilesForChapter(chapter.Id);
|
||||
foreach (var mangaFile in chapterFiles)
|
||||
{
|
||||
if (page + 1 < (mangaFile.NumberOfPages + pagesSoFar))
|
||||
{
|
||||
var path = GetVolumeCachePath(volume.Id, mangaFile);
|
||||
var path = GetCachePath(chapter.Id, mangaFile);
|
||||
var files = _directoryService.GetFiles(path);
|
||||
Array.Sort(files, _numericComparer);
|
||||
|
||||
return (files.ElementAt(page - pagesSoFar), mangaFile);
|
||||
}
|
||||
|
||||
|
||||
pagesSoFar += mangaFile.NumberOfPages;
|
||||
}
|
||||
|
||||
return ("", null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,19 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
|
||||
namespace API.Services
|
||||
{
|
||||
public class DirectoryService : IDirectoryService
|
||||
{
|
||||
private readonly ILogger<DirectoryService> _logger;
|
||||
|
||||
public DirectoryService(ILogger<DirectoryService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a set of regex search criteria, get files in the given path.
|
||||
|
@ -52,6 +59,11 @@ namespace API.Services
|
|||
|
||||
public async Task<ImageDto> ReadImageAsync(string imagePath)
|
||||
{
|
||||
if (!File.Exists(imagePath))
|
||||
{
|
||||
_logger.LogError("Image does not exist on disk.");
|
||||
return null;
|
||||
}
|
||||
using var image = Image.NewFromFile(imagePath);
|
||||
|
||||
return new ImageDto
|
||||
|
|
|
@ -61,6 +61,12 @@ namespace API.Services
|
|||
var totalFiles = 0;
|
||||
foreach (var folderPath in library.Folders)
|
||||
{
|
||||
// if (!forceUpdate && Directory.GetLastWriteTime(folderPath.Path) <= folderPath.LastScanned)
|
||||
// {
|
||||
// _logger.LogDebug($"{folderPath.Path} hasn't been updated since last scan. Skipping.");
|
||||
// continue;
|
||||
// }
|
||||
|
||||
try {
|
||||
totalFiles += DirectoryService.TraverseTreeParallelForEach(folderPath.Path, (f) =>
|
||||
{
|
||||
|
@ -88,10 +94,12 @@ namespace API.Services
|
|||
// Remove series that are no longer on disk
|
||||
RemoveSeriesNotOnDisk(allSeries, series, library);
|
||||
|
||||
foreach (var folder in library.Folders) folder.LastScanned = DateTime.Now;
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
|
||||
if (Task.Run(() => _unitOfWork.Complete()).Result)
|
||||
{
|
||||
|
||||
_logger.LogInformation($"Scan completed on {library.Name}. Parsed {series.Keys.Count()} series.");
|
||||
}
|
||||
else
|
||||
|
@ -188,10 +196,10 @@ namespace API.Services
|
|||
|
||||
private Series UpdateSeries(Series series, ParserInfo[] infos, bool forceUpdate)
|
||||
{
|
||||
var volumes = UpdateVolumes(series, infos, forceUpdate);
|
||||
var volumes = UpdateVolumesWithChapters(series, infos, forceUpdate);
|
||||
series.Volumes = volumes;
|
||||
series.Pages = volumes.Sum(v => v.Pages);
|
||||
if (series.CoverImage == null || forceUpdate)
|
||||
if (ShouldFindCoverImage(forceUpdate, series.CoverImage))
|
||||
{
|
||||
var firstCover = volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0);
|
||||
if (firstCover == null && volumes.Any())
|
||||
|
@ -213,16 +221,20 @@ namespace API.Services
|
|||
{
|
||||
_logger.LogDebug($"Creating File Entry for {info.FullFilePath}");
|
||||
|
||||
int.TryParse(info.Chapters, out var chapter);
|
||||
_logger.LogDebug($"Found Chapter: {chapter}");
|
||||
return new MangaFile()
|
||||
{
|
||||
FilePath = info.FullFilePath,
|
||||
Chapter = chapter,
|
||||
Format = info.Format,
|
||||
NumberOfPages = info.Format == MangaFormat.Archive ? _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath): 1
|
||||
};
|
||||
}
|
||||
|
||||
private bool ShouldFindCoverImage(bool forceUpdate, byte[] coverImage)
|
||||
{
|
||||
return forceUpdate || coverImage == null || !coverImage.Any();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -237,16 +249,20 @@ namespace API.Services
|
|||
{
|
||||
ICollection<Volume> volumes = new List<Volume>();
|
||||
IList<Volume> existingVolumes = _unitOfWork.SeriesRepository.GetVolumes(series.Id).ToList();
|
||||
|
||||
|
||||
//var justVolumes = infos.Select(pi => pi.Chapters == "0");
|
||||
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
var existingVolume = existingVolumes.SingleOrDefault(v => v.Name == info.Volumes);
|
||||
if (existingVolume != null)
|
||||
{
|
||||
var existingFile = existingVolume.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
|
||||
//var existingFile = existingVolume.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
|
||||
var existingFile = new MangaFile();
|
||||
if (existingFile != null)
|
||||
{
|
||||
existingFile.Chapter = Parser.Parser.MinimumNumberFromRange(info.Chapters);
|
||||
//existingFile.Chapter = Parser.Parser.MinimumNumberFromRange(info.Chapters);
|
||||
existingFile.Format = info.Format;
|
||||
existingFile.NumberOfPages = _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath);
|
||||
}
|
||||
|
@ -254,7 +270,7 @@ namespace API.Services
|
|||
{
|
||||
if (info.Format == MangaFormat.Archive)
|
||||
{
|
||||
existingVolume.Files.Add(CreateMangaFile(info));
|
||||
// existingVolume.Files.Add(CreateMangaFile(info));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -271,7 +287,7 @@ namespace API.Services
|
|||
existingVolume = volumes.SingleOrDefault(v => v.Name == info.Volumes);
|
||||
if (existingVolume != null)
|
||||
{
|
||||
existingVolume.Files.Add(CreateMangaFile(info));
|
||||
//existingVolume.Files.Add(CreateMangaFile(info));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -279,10 +295,10 @@ namespace API.Services
|
|||
{
|
||||
Name = info.Volumes,
|
||||
Number = Parser.Parser.MinimumNumberFromRange(info.Volumes),
|
||||
Files = new List<MangaFile>()
|
||||
{
|
||||
CreateMangaFile(info)
|
||||
}
|
||||
// Files = new List<MangaFile>()
|
||||
// {
|
||||
// CreateMangaFile(info)
|
||||
// }
|
||||
};
|
||||
volumes.Add(vol);
|
||||
}
|
||||
|
@ -290,23 +306,121 @@ namespace API.Services
|
|||
|
||||
_logger.LogInformation($"Adding volume {volumes.Last().Number} with File: {info.Filename}");
|
||||
}
|
||||
|
||||
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
if (forceUpdate || volume.CoverImage == null || !volume.Files.Any())
|
||||
{
|
||||
var firstFile = volume.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
||||
if (firstFile != null) volume.CoverImage = _archiveService.GetCoverImage(firstFile.FilePath, true);
|
||||
}
|
||||
|
||||
volume.Pages = volume.Files.Sum(x => x.NumberOfPages);
|
||||
// if (forceUpdate || volume.CoverImage == null || !volume.Files.Any())
|
||||
// {
|
||||
// var firstFile = volume.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
||||
// if (firstFile != null) volume.CoverImage = _archiveService.GetCoverImage(firstFile.FilePath, true);
|
||||
// }
|
||||
|
||||
//volume.Pages = volume.Files.Sum(x => x.NumberOfPages);
|
||||
}
|
||||
|
||||
|
||||
return volumes;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="volume"></param>
|
||||
/// <param name="infos"></param>
|
||||
/// <param name="forceUpdate"></param>
|
||||
/// <returns></returns>
|
||||
private ICollection<Chapter> UpdateChapters(Volume volume, IEnumerable<ParserInfo> infos, bool forceUpdate)
|
||||
{
|
||||
var chapters = new List<Chapter>();
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
volume.Chapters ??= new List<Chapter>();
|
||||
var chapter = volume.Chapters.SingleOrDefault(c => c.Range == info.Chapters) ??
|
||||
chapters.SingleOrDefault(v => v.Range == info.Chapters) ??
|
||||
new Chapter()
|
||||
{
|
||||
Number = Parser.Parser.MinimumNumberFromRange(info.Chapters) + "",
|
||||
Range = info.Chapters,
|
||||
};
|
||||
|
||||
chapter.Files ??= new List<MangaFile>();
|
||||
var existingFile = chapter?.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
|
||||
if (existingFile != null)
|
||||
{
|
||||
existingFile.Format = info.Format;
|
||||
existingFile.NumberOfPages = _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (info.Format == MangaFormat.Archive)
|
||||
{
|
||||
chapter.Files.Add(CreateMangaFile(info));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug($"Ignoring {info.Filename} as it is not an archive.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
chapter.Number = Parser.Parser.MinimumNumberFromRange(info.Chapters) + "";
|
||||
chapter.Range = info.Chapters;
|
||||
|
||||
chapters.Add(chapter);
|
||||
}
|
||||
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
chapter.Pages = chapter.Files.Sum(f => f.NumberOfPages);
|
||||
|
||||
if (ShouldFindCoverImage(forceUpdate, chapter.CoverImage))
|
||||
{
|
||||
var firstFile = chapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
||||
if (firstFile != null) chapter.CoverImage = _archiveService.GetCoverImage(firstFile.FilePath, true);
|
||||
}
|
||||
}
|
||||
|
||||
return chapters;
|
||||
}
|
||||
|
||||
|
||||
private ICollection<Volume> UpdateVolumesWithChapters(Series series, ParserInfo[] infos, bool forceUpdate)
|
||||
{
|
||||
ICollection<Volume> volumes = new List<Volume>();
|
||||
IList<Volume> existingVolumes = _unitOfWork.SeriesRepository.GetVolumes(series.Id).ToList();
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
var volume = (existingVolumes.SingleOrDefault(v => v.Name == info.Volumes) ??
|
||||
volumes.SingleOrDefault(v => v.Name == info.Volumes)) ?? new Volume
|
||||
{
|
||||
Name = info.Volumes,
|
||||
Number = Parser.Parser.MinimumNumberFromRange(info.Volumes),
|
||||
};
|
||||
|
||||
|
||||
var chapters = UpdateChapters(volume, infos.Where(pi => pi.Volumes == volume.Name).ToArray(), forceUpdate);
|
||||
volume.Chapters = chapters;
|
||||
volume.Pages = chapters.Sum(c => c.Pages);
|
||||
volumes.Add(volume);
|
||||
}
|
||||
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
if (ShouldFindCoverImage(forceUpdate, volume.CoverImage))
|
||||
{
|
||||
// TODO: Create a custom sorter for Chapters so it's consistent across the application
|
||||
var firstChapter = volume.Chapters.OrderBy(x => Double.Parse(x.Number)).FirstOrDefault();
|
||||
var firstFile = firstChapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault();
|
||||
if (firstFile != null) volume.CoverImage = _archiveService.GetCoverImage(firstFile.FilePath, true);
|
||||
}
|
||||
}
|
||||
|
||||
return volumes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void ScanSeries(int libraryId, int seriesId)
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue