diff --git a/API.Tests/ParserTest.cs b/API.Tests/ParserTest.cs index 320856c32..2a70ee429 100644 --- a/API.Tests/ParserTest.cs +++ b/API.Tests/ParserTest.cs @@ -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); diff --git a/API.Tests/Services/StringLogicalComparerTest.cs b/API.Tests/Services/StringLogicalComparerTest.cs new file mode 100644 index 000000000..25c5d3b2f --- /dev/null +++ b/API.Tests/Services/StringLogicalComparerTest.cs @@ -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++; + } + } + } +} \ No newline at end of file diff --git a/API/Comparators/NumericComparer.cs b/API/Comparators/NumericComparer.cs new file mode 100644 index 000000000..b40e33e0a --- /dev/null +++ b/API/Comparators/NumericComparer.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/API/Comparators/StringLogicalComparer.cs b/API/Comparators/StringLogicalComparer.cs new file mode 100644 index 000000000..957921c4c --- /dev/null +++ b/API/Comparators/StringLogicalComparer.cs @@ -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; + } + } + } +} \ No newline at end of file diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index baa445d43..a51a81e0f 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -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); } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index e426e87a8..b15a58496 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -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> 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> 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; diff --git a/API/Interfaces/ICacheService.cs b/API/Interfaces/ICacheService.cs index 809733050..6083c4810 100644 --- a/API/Interfaces/ICacheService.cs +++ b/API/Interfaces/ICacheService.cs @@ -1,21 +1,28 @@ -using API.Entities; +using System.Threading.Tasks; +using API.Entities; namespace API.Interfaces { public interface ICacheService { /// - /// 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). /// /// - void Ensure(int volumeId); + /// Volume for the passed volumeId. Side-effect from ensuring cache. + Task Ensure(int volumeId); bool Cleanup(Volume volume); //bool CleanupAll(); - string GetCachePath(int volumeId); - - + /// + /// Returns the absolute path of a cached page. + /// + /// + /// Page number to look for + /// + string GetCachedPagePath(Volume volume, int page); } } \ No newline at end of file diff --git a/API/Interfaces/IDirectoryService.cs b/API/Interfaces/IDirectoryService.cs index 95db809c3..beeb7cfd0 100644 --- a/API/Interfaces/IDirectoryService.cs +++ b/API/Interfaces/IDirectoryService.cs @@ -20,7 +20,7 @@ namespace API.Interfaces /// /// Absolute path /// List of folder names - IEnumerable ListFiles(string rootPath); + IList ListFiles(string rootPath); /// /// Given a library id, scans folders for said library. Parses files and generates DB updates. Will overwrite diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index 7729f2174..6f2d4eaf2 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -178,7 +178,7 @@ namespace API.Parser } } - return ""; + return "0"; } /// diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 874a1deb8..714fd1f45 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -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 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 ""; } - - } } \ No newline at end of file diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 7809aee7a..226703032 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -67,7 +67,7 @@ namespace API.Services return dirs; } - public IEnumerable ListFiles(string rootPath) + public IList ListFiles(string rootPath) { if (!Directory.Exists(rootPath)) return ImmutableList.Empty; return Directory.GetFiles(rootPath);