Added a sorting mechanism to emulate how windows sorts files. Refactored cache to support chapter folders as well.

This commit is contained in:
Joseph Milazzo 2021-01-10 12:47:34 -06:00
parent 6020697d7d
commit f737f662df
11 changed files with 237 additions and 32 deletions

View File

@ -48,6 +48,7 @@ namespace API.Tests
[InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "")]
[InlineData("c001", "1")]
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", "12")]
[InlineData("Adding volume 1 with File: Ana Satsujin Vol. 1 Ch. 5 - Manga Box (gb).cbz", "5")]
public void ParseChaptersTest(string filename, string expected)
{
var result = ParseChapter(filename);

View File

@ -0,0 +1,28 @@
using System;
using API.Comparators;
using Xunit;
namespace API.Tests.Services
{
public class StringLogicalComparerTest
{
[Theory]
[InlineData(
new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"}
)]
public void TestLogicalComparer(string[] input, string[] expected)
{
NumericComparer nc = new NumericComparer();
Array.Sort(input, nc);
var i = 0;
foreach (var s in input)
{
Assert.Equal(s, expected[i]);
i++;
}
}
}
}

View File

@ -0,0 +1,17 @@
using System.Collections;
namespace API.Comparators
{
public class NumericComparer : IComparer
{
public int Compare(object x, object y)
{
if((x is string xs) && (y is string ys))
{
return StringLogicalComparer.Compare(xs, ys);
}
return -1;
}
}
}

View File

@ -0,0 +1,130 @@
//(c) Vasian Cepa 2005
// Version 2
// Taken from: https://www.codeproject.com/Articles/11016/Numeric-String-Sort-in-C
using System;
namespace API.Comparators
{
public static class StringLogicalComparer
{
public static int Compare(string s1, string s2)
{
//get rid of special cases
if((s1 == null) && (s2 == null)) return 0;
if(s1 == null) return -1;
if(s2 == null) return 1;
if (string.IsNullOrEmpty(s1) && string.IsNullOrEmpty(s2)) return 0;
if (string.IsNullOrEmpty(s1)) return -1;
if (string.IsNullOrEmpty(s2)) return -1;
//WE style, special case
bool sp1 = Char.IsLetterOrDigit(s1, 0);
bool sp2 = Char.IsLetterOrDigit(s2, 0);
if(sp1 && !sp2) return 1;
if(!sp1 && sp2) return -1;
int i1 = 0, i2 = 0; //current index
while(true)
{
bool c1 = Char.IsDigit(s1, i1);
bool c2 = Char.IsDigit(s2, i2);
var r = 0; // temp result
if(!c1 && !c2)
{
bool letter1 = Char.IsLetter(s1, i1);
bool letter2 = Char.IsLetter(s2, i2);
if((letter1 && letter2) || (!letter1 && !letter2))
{
if(letter1 && letter2)
{
r = Char.ToLower(s1[i1]).CompareTo(Char.ToLower(s2[i2]));
}
else
{
r = s1[i1].CompareTo(s2[i2]);
}
if(r != 0) return r;
}
else if(!letter1 && letter2) return -1;
else if(letter1 && !letter2) return 1;
}
else if(c1 && c2)
{
r = CompareNum(s1, ref i1, s2, ref i2);
if(r != 0) return r;
}
else if(c1)
{
return -1;
}
else if(c2)
{
return 1;
}
i1++;
i2++;
if((i1 >= s1.Length) && (i2 >= s2.Length))
{
return 0;
}
if(i1 >= s1.Length)
{
return -1;
}
if(i2 >= s2.Length)
{
return -1;
}
}
}
private static int CompareNum(string s1, ref int i1, string s2, ref int i2)
{
int nzStart1 = i1, nzStart2 = i2; // nz = non zero
int end1 = i1, end2 = i2;
ScanNumEnd(s1, i1, ref end1, ref nzStart1);
ScanNumEnd(s2, i2, ref end2, ref nzStart2);
var start1 = i1; i1 = end1 - 1;
var start2 = i2; i2 = end2 - 1;
var nzLength1 = end1 - nzStart1;
var nzLength2 = end2 - nzStart2;
if(nzLength1 < nzLength2) return -1;
if(nzLength1 > nzLength2) return 1;
for(int j1 = nzStart1,j2 = nzStart2; j1 <= i1; j1++,j2++)
{
var r = s1[j1].CompareTo(s2[j2]);
if(r != 0) return r;
}
// the nz parts are equal
var length1 = end1 - start1;
var length2 = end2 - start2;
if(length1 == length2) return 0;
if(length1 > length2) return -1;
return 1;
}
//lookahead
private static void ScanNumEnd(string s, int start, ref int end, ref int nzStart)
{
nzStart = start;
end = start;
bool countZeros = true;
while(Char.IsDigit(s, end))
{
if(countZeros && s[end].Equals('0'))
{
nzStart++;
}
else countZeros = false;
end++;
if(end >= s.Length) break;
}
}
}
}

View File

@ -71,6 +71,7 @@ namespace API.Controllers
if (await _userRepository.SaveAllAsync())
{
_logger.LogInformation($"Created a new library: {library.Name}");
var createdLibrary = await _libraryRepository.GetLibraryForNameAsync(library.Name);
BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(createdLibrary.Id, false));
return Ok();
@ -121,6 +122,7 @@ namespace API.Controllers
if (await _userRepository.SaveAllAsync())
{
_logger.LogInformation($"Added: {updateLibraryForUserDto.SelectedLibraries} to {updateLibraryForUserDto.Username}");
return Ok(user);
}

View File

@ -1,5 +1,9 @@
using System.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
using API.DTOs;
using API.Entities;
using API.Interfaces;
@ -12,27 +16,26 @@ namespace API.Controllers
private readonly ISeriesRepository _seriesRepository;
private readonly IDirectoryService _directoryService;
private readonly ICacheService _cacheService;
private readonly NumericComparer _numericComparer;
public ReaderController(ISeriesRepository seriesRepository, IDirectoryService directoryService, ICacheService cacheService)
{
_seriesRepository = seriesRepository;
_directoryService = directoryService;
_cacheService = cacheService;
_numericComparer = new NumericComparer();
}
[HttpGet("info")]
public async Task<ActionResult<int>> GetInformation(int volumeId)
{
// TODO: This will be refactored out. No longer needed.
Volume volume = await _seriesRepository.GetVolumeAsync(volumeId);
Volume volume = await _cacheService.Ensure(volumeId);
// Assume we always get first Manga File
if (volume == null || !volume.Files.Any())
{
// TODO: Move this into Ensure and return negative numbers for different error codes.
return BadRequest("There are no files in the volume to read.");
}
_cacheService.Ensure(volumeId);
return Ok(volume.Files.Select(x => x.NumberOfPages).Sum());
@ -42,12 +45,12 @@ namespace API.Controllers
public async Task<ActionResult<ImageDto>> GetImage(int volumeId, int page)
{
// Temp let's iterate the directory each call to get next image
_cacheService.Ensure(volumeId);
var files = _directoryService.ListFiles(_directoryService.GetExtractPath(volumeId));
var path = files.ElementAt(page);
var volume = await _cacheService.Ensure(volumeId);
var files = _directoryService.ListFiles(_cacheService.GetCachedPagePath(volume, page));
var array = files.ToArray();
Array.Sort(array, _numericComparer); // TODO: Find a way to apply numericComparer to IList.
var path = array.ElementAt(page);
var file = await _directoryService.ReadImageAsync(path);
file.Page = page;

View File

@ -1,21 +1,28 @@
using API.Entities;
using System.Threading.Tasks;
using API.Entities;
namespace API.Interfaces
{
public interface ICacheService
{
/// <summary>
/// Ensures the cache is created for the given volume and if not, will create it.
/// Ensures the cache is created for the given volume and if not, will create it. Should be called before any other
/// cache operations (except cleanup).
/// </summary>
/// <param name="volumeId"></param>
void Ensure(int volumeId);
/// <returns>Volume for the passed volumeId. Side-effect from ensuring cache.</returns>
Task<Volume> Ensure(int volumeId);
bool Cleanup(Volume volume);
//bool CleanupAll();
string GetCachePath(int volumeId);
/// <summary>
/// Returns the absolute path of a cached page.
/// </summary>
/// <param name="volume"></param>
/// <param name="page">Page number to look for</param>
/// <returns></returns>
string GetCachedPagePath(Volume volume, int page);
}
}

View File

@ -20,7 +20,7 @@ namespace API.Interfaces
/// </summary>
/// <param name="rootPath">Absolute path </param>
/// <returns>List of folder names</returns>
IEnumerable<string> ListFiles(string rootPath);
IList<string> ListFiles(string rootPath);
/// <summary>
/// Given a library id, scans folders for said library. Parses files and generates DB updates. Will overwrite

View File

@ -178,7 +178,7 @@ namespace API.Parser
}
}
return "";
return "0";
}
/// <summary>

View File

@ -1,4 +1,6 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Interfaces;
@ -15,31 +17,46 @@ namespace API.Services
_seriesRepository = seriesRepository;
}
public async void Ensure(int volumeId)
public async Task<Volume> Ensure(int volumeId)
{
Volume volume = await _seriesRepository.GetVolumeAsync(volumeId);
foreach (var file in volume.Files)
{
var extractPath = GetCachePath(volumeId);
if (file.Chapter > 0)
{
extractPath = Path.Join(extractPath, file.Chapter + "");
}
var extractPath = GetVolumeCachePath(volumeId, file);
_directoryService.ExtractArchive(file.FilePath, extractPath);
}
return volume;
}
public bool Cleanup(Volume volume)
{
throw new System.NotImplementedException();
}
public string GetCachePath(int volumeId)
private string GetVolumeCachePath(int volumeId, MangaFile file)
{
return Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), $"../cache/{volumeId}/"));
var extractPath = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), $"../cache/{volumeId}/"));
if (file.Chapter > 0)
{
extractPath = Path.Join(extractPath, file.Chapter + "");
}
return extractPath;
}
public string GetCachedPagePath(Volume volume, int page)
{
// Calculate what chapter the page belongs to
foreach (var mangaFile in volume.Files.OrderBy(f => f.Chapter))
{
if (page + 1 < mangaFile.NumberOfPages)
{
return GetVolumeCachePath(volume.Id, mangaFile);
}
}
return "";
}
}
}

View File

@ -67,7 +67,7 @@ namespace API.Services
return dirs;
}
public IEnumerable<string> ListFiles(string rootPath)
public IList<string> ListFiles(string rootPath)
{
if (!Directory.Exists(rootPath)) return ImmutableList<string>.Empty;
return Directory.GetFiles(rootPath);