mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Unit Tests & New Natural Sort (#941)
* Added a lot of tests * More tests! Added a Parser.NormalizePath to normalize all paths within Kavita. * Fixed a bug where MarkChaptersAsUnread implementation wasn't consistent between different files and lead to extra row generation for no reason. * Added more unit tests * Found a better implementation for Natural Sorting. Added tests and validate it works. Next commit will swap out natural Sort for new Extension. * Replaced NaturalSortComparer with OrderByNatural. * Drastically simplified and sped up FindFirstEntry for finding cover images in archives * Initial fix for a epub bug where metadata defines key as absolute path but document uses a relative path. We now have a hack to correct for the epub.
This commit is contained in:
parent
71d42b1c8b
commit
591b574706
1
.gitignore
vendored
1
.gitignore
vendored
@ -522,3 +522,4 @@ API/config/pre-metadata/
|
|||||||
API/config/post-metadata/
|
API/config/post-metadata/
|
||||||
API.Tests/TestResults/
|
API.Tests/TestResults/
|
||||||
UI/Web/.vscode/settings.json
|
UI/Web/.vscode/settings.json
|
||||||
|
/API.Tests/Services/Test Data/ArchiveService/CoverImages/output/*
|
||||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.Extensions;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using BenchmarkDotNet.Order;
|
using BenchmarkDotNet.Order;
|
||||||
|
|
||||||
@ -16,9 +17,6 @@ namespace API.Benchmark
|
|||||||
[RankColumn]
|
[RankColumn]
|
||||||
public class TestBenchmark
|
public class TestBenchmark
|
||||||
{
|
{
|
||||||
private readonly NaturalSortComparer _naturalSortComparer = new ();
|
|
||||||
|
|
||||||
|
|
||||||
private static IEnumerable<VolumeDto> GenerateVolumes(int max)
|
private static IEnumerable<VolumeDto> GenerateVolumes(int max)
|
||||||
{
|
{
|
||||||
var random = new Random();
|
var random = new Random();
|
||||||
@ -50,11 +48,11 @@ namespace API.Benchmark
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
private static void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
||||||
{
|
{
|
||||||
foreach (var v in volumes.Where(vDto => vDto.Number == 0))
|
foreach (var v in volumes.Where(vDto => vDto.Number == 0))
|
||||||
{
|
{
|
||||||
v.Chapters = v.Chapters.OrderBy(x => x.Range, _naturalSortComparer).ToList();
|
v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs
Normal file
17
API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using API.Comparators;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Comparers;
|
||||||
|
|
||||||
|
public class ChapterSortComparerZeroFirstTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new[] {1, 2, 0}, new[] {0, 1, 2,})]
|
||||||
|
[InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})]
|
||||||
|
[InlineData(new[] {1, 0, 0}, new[] {0, 0, 1})]
|
||||||
|
public void ChapterSortComparerZeroFirstTest(int[] input, int[] expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerZeroFirst()).ToArray());
|
||||||
|
}
|
||||||
|
}
|
26
API.Tests/Comparers/NumericComparerTests.cs
Normal file
26
API.Tests/Comparers/NumericComparerTests.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using API.Comparators;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Comparers;
|
||||||
|
|
||||||
|
public class NumericComparerTests
|
||||||
|
{
|
||||||
|
[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 NumericComparerTest(string[] input, string[] expected)
|
||||||
|
{
|
||||||
|
var nc = new NumericComparer();
|
||||||
|
Array.Sort(input, nc);
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
foreach (var s in input)
|
||||||
|
{
|
||||||
|
Assert.Equal(s, expected[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,10 +11,17 @@ namespace API.Tests.Comparers
|
|||||||
new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
|
new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
|
||||||
new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"}
|
new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"}
|
||||||
)]
|
)]
|
||||||
public void TestLogicalComparer(string[] input, string[] expected)
|
[InlineData(
|
||||||
|
new[] {"a.jpg", "aaa.jpg", "1.jpg", },
|
||||||
|
new[] {"1.jpg", "a.jpg", "aaa.jpg"}
|
||||||
|
)]
|
||||||
|
[InlineData(
|
||||||
|
new[] {"a.jpg", "aaa.jpg", "1.jpg", "!cover.png"},
|
||||||
|
new[] {"!cover.png", "1.jpg", "a.jpg", "aaa.jpg"}
|
||||||
|
)]
|
||||||
|
public void StringComparer(string[] input, string[] expected)
|
||||||
{
|
{
|
||||||
NumericComparer nc = new NumericComparer();
|
Array.Sort(input, StringLogicalComparer.Compare);
|
||||||
Array.Sort(input, nc);
|
|
||||||
|
|
||||||
var i = 0;
|
var i = 0;
|
||||||
foreach (var s in input)
|
foreach (var s in input)
|
||||||
|
@ -9,6 +9,8 @@ namespace API.Tests.Converters
|
|||||||
[InlineData("daily", "0 0 * * *")]
|
[InlineData("daily", "0 0 * * *")]
|
||||||
[InlineData("disabled", "0 0 31 2 *")]
|
[InlineData("disabled", "0 0 31 2 *")]
|
||||||
[InlineData("weekly", "0 0 * * 1")]
|
[InlineData("weekly", "0 0 * * 1")]
|
||||||
|
[InlineData("", "0 0 31 2 *")]
|
||||||
|
[InlineData("sdfgdf", "")]
|
||||||
public void ConvertTest(string input, string expected)
|
public void ConvertTest(string input, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, CronConverter.ConvertToCronNotation(input));
|
Assert.Equal(expected, CronConverter.ConvertToCronNotation(input));
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
@ -9,7 +10,7 @@ namespace API.Tests.Extensions
|
|||||||
{
|
{
|
||||||
public class ChapterListExtensionsTests
|
public class ChapterListExtensionsTests
|
||||||
{
|
{
|
||||||
private Chapter CreateChapter(string range, string number, MangaFile file, bool isSpecial)
|
private static Chapter CreateChapter(string range, string number, MangaFile file, bool isSpecial)
|
||||||
{
|
{
|
||||||
return new Chapter()
|
return new Chapter()
|
||||||
{
|
{
|
||||||
@ -20,7 +21,7 @@ namespace API.Tests.Extensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private MangaFile CreateFile(string file, MangaFormat format)
|
private static MangaFile CreateFile(string file, MangaFormat format)
|
||||||
{
|
{
|
||||||
return new MangaFile()
|
return new MangaFile()
|
||||||
{
|
{
|
||||||
@ -80,7 +81,37 @@ namespace API.Tests.Extensions
|
|||||||
var actualChapter = chapterList.GetChapterByRange(info);
|
var actualChapter = chapterList.GetChapterByRange(info);
|
||||||
|
|
||||||
Assert.Equal(chapterList[0], actualChapter);
|
Assert.Equal(chapterList[0], actualChapter);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
#region GetFirstChapterWithFiles
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetFirstChapterWithFiles_ShouldReturnAllChapters()
|
||||||
|
{
|
||||||
|
var chapterList = new List<Chapter>()
|
||||||
|
{
|
||||||
|
CreateChapter("darker than black", "0", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true),
|
||||||
|
CreateChapter("darker than black", "1", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), false),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(chapterList.First(), chapterList.GetFirstChapterWithFiles());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetFirstChapterWithFiles_ShouldReturnSecondChapter()
|
||||||
|
{
|
||||||
|
var chapterList = new List<Chapter>()
|
||||||
|
{
|
||||||
|
CreateChapter("darker than black", "0", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true),
|
||||||
|
CreateChapter("darker than black", "1", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), false),
|
||||||
|
};
|
||||||
|
|
||||||
|
chapterList.First().Files = new List<MangaFile>();
|
||||||
|
|
||||||
|
Assert.Equal(chapterList.Last(), chapterList.GetFirstChapterWithFiles());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,14 +1,11 @@
|
|||||||
using System;
|
using System.Linq;
|
||||||
using System.Linq;
|
using API.Extensions;
|
||||||
using API.Comparators;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Comparers
|
namespace API.Tests.Extensions;
|
||||||
{
|
|
||||||
public class NaturalSortComparerTest
|
|
||||||
{
|
|
||||||
private readonly NaturalSortComparer _nc = new NaturalSortComparer();
|
|
||||||
|
|
||||||
|
public class EnumerableExtensionsTests
|
||||||
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(
|
[InlineData(
|
||||||
new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
|
new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
|
||||||
@ -40,7 +37,7 @@ namespace API.Tests.Comparers
|
|||||||
)]
|
)]
|
||||||
[InlineData(
|
[InlineData(
|
||||||
new[] {"3and4.cbz", "The World God Only Knows - Oneshot.cbz", "5.cbz", "1and2.cbz"},
|
new[] {"3and4.cbz", "The World God Only Knows - Oneshot.cbz", "5.cbz", "1and2.cbz"},
|
||||||
new[] {"The World God Only Knows - Oneshot.cbz", "1and2.cbz", "3and4.cbz", "5.cbz"}
|
new[] {"1and2.cbz", "3and4.cbz", "5.cbz", "The World God Only Knows - Oneshot.cbz"}
|
||||||
)]
|
)]
|
||||||
[InlineData(
|
[InlineData(
|
||||||
new[] {"Solo Leveling - c000 (v01) - p000 [Cover] [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p001 [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p002 [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p003 [dig] [Yen Press] [LuCaZ].jpg"},
|
new[] {"Solo Leveling - c000 (v01) - p000 [Cover] [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p001 [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p002 [dig] [Yen Press] [LuCaZ].jpg", "Solo Leveling - c000 (v01) - p003 [dig] [Yen Press] [LuCaZ].jpg"},
|
||||||
@ -51,12 +48,20 @@ namespace API.Tests.Comparers
|
|||||||
new[] {"Marvel2In1-7", "Marvel2In1-7-01", "Marvel2In1-7-02"}
|
new[] {"Marvel2In1-7", "Marvel2In1-7-01", "Marvel2In1-7-02"}
|
||||||
)]
|
)]
|
||||||
[InlineData(
|
[InlineData(
|
||||||
new[] {"!001", "001", "002"},
|
new[] {"001", "002", "!001"},
|
||||||
new[] {"!001", "001", "002"}
|
new[] {"!001", "001", "002"}
|
||||||
)]
|
)]
|
||||||
[InlineData(
|
[InlineData(
|
||||||
new[] {"001", "", null},
|
new[] {"001.jpg", "002.jpg", "!001.jpg"},
|
||||||
new[] {"", "001", null}
|
new[] {"!001.jpg", "001.jpg", "002.jpg"}
|
||||||
|
)]
|
||||||
|
[InlineData(
|
||||||
|
new[] {"001", "002", "!002"},
|
||||||
|
new[] {"!002", "001", "002"}
|
||||||
|
)]
|
||||||
|
[InlineData(
|
||||||
|
new[] {"001", ""},
|
||||||
|
new[] {"", "001"}
|
||||||
)]
|
)]
|
||||||
[InlineData(
|
[InlineData(
|
||||||
new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.020/001.jpg"},
|
new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.020/001.jpg"},
|
||||||
@ -66,13 +71,15 @@ namespace API.Tests.Comparers
|
|||||||
new[] {@"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\001.jpg", @"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\002.jpg"},
|
new[] {@"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\001.jpg", @"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\002.jpg"},
|
||||||
new[] {@"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\001.jpg", @"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\002.jpg"}
|
new[] {@"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\001.jpg", @"F:\/Anime_Series_Pelis/MANGA/Mangahere (EN)\Kirara Fantasia\_Ch.001\002.jpg"}
|
||||||
)]
|
)]
|
||||||
public void TestNaturalSortComparer(string[] input, string[] expected)
|
[InlineData(
|
||||||
|
new[] {"01/001.jpg", "001.jpg"},
|
||||||
|
new[] {"001.jpg", "01/001.jpg"}
|
||||||
|
)]
|
||||||
|
public void TestNaturalSort(string[] input, string[] expected)
|
||||||
{
|
{
|
||||||
Array.Sort(input, _nc);
|
Assert.Equal(expected, input.OrderByNatural(x => x).ToArray());
|
||||||
Assert.Equal(expected, input);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(
|
[InlineData(
|
||||||
new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
|
new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
|
||||||
@ -98,6 +105,10 @@ namespace API.Tests.Comparers
|
|||||||
new[] {"001.jpg", "10.jpg",},
|
new[] {"001.jpg", "10.jpg",},
|
||||||
new[] {"001.jpg", "10.jpg",}
|
new[] {"001.jpg", "10.jpg",}
|
||||||
)]
|
)]
|
||||||
|
[InlineData(
|
||||||
|
new[] {"001", "002", "!001"},
|
||||||
|
new[] {"!001", "001", "002"}
|
||||||
|
)]
|
||||||
[InlineData(
|
[InlineData(
|
||||||
new[] {"10/001.jpg", "10.jpg",},
|
new[] {"10/001.jpg", "10.jpg",},
|
||||||
new[] {"10.jpg", "10/001.jpg",}
|
new[] {"10.jpg", "10/001.jpg",}
|
||||||
@ -110,9 +121,9 @@ namespace API.Tests.Comparers
|
|||||||
new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg"},
|
new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg"},
|
||||||
new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg"}
|
new[] {"Honzuki no Gekokujou_ Part 2/_Ch.019/001.jpg", "Honzuki no Gekokujou_ Part 2/_Ch.019/002.jpg"}
|
||||||
)]
|
)]
|
||||||
public void TestNaturalSortComparerLinq(string[] input, string[] expected)
|
public void TestNaturalSortLinq(string[] input, string[] expected)
|
||||||
{
|
{
|
||||||
var output = input.OrderBy(c => c, _nc);
|
var output = input.OrderByNatural(x => x);
|
||||||
|
|
||||||
var i = 0;
|
var i = 0;
|
||||||
foreach (var s in output)
|
foreach (var s in output)
|
||||||
@ -122,4 +133,3 @@ namespace API.Tests.Comparers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
48
API.Tests/Extensions/FilterDtoExtensionsTests.cs
Normal file
48
API.Tests/Extensions/FilterDtoExtensionsTests.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using API.DTOs.Filtering;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Extensions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Extensions;
|
||||||
|
|
||||||
|
public class FilterDtoExtensionsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetSqlFilter_ShouldReturnAllFormats()
|
||||||
|
{
|
||||||
|
var filter = new FilterDto()
|
||||||
|
{
|
||||||
|
Formats = null
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(Enum.GetValues<MangaFormat>(), filter.GetSqlFilter());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetSqlFilter_ShouldReturnAllFormats2()
|
||||||
|
{
|
||||||
|
var filter = new FilterDto()
|
||||||
|
{
|
||||||
|
Formats = new List<MangaFormat>()
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(Enum.GetValues<MangaFormat>(), filter.GetSqlFilter());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetSqlFilter_ShouldReturnJust2()
|
||||||
|
{
|
||||||
|
var formats = new List<MangaFormat>()
|
||||||
|
{
|
||||||
|
MangaFormat.Archive, MangaFormat.Epub
|
||||||
|
};
|
||||||
|
var filter = new FilterDto()
|
||||||
|
{
|
||||||
|
Formats = formats
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(formats, filter.GetSqlFilter());
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,11 @@
|
|||||||
using API.Entities;
|
using System.Linq;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Parser;
|
using API.Parser;
|
||||||
|
using API.Services.Tasks.Scanner;
|
||||||
|
using API.Tests.Helpers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Extensions
|
namespace API.Tests.Extensions
|
||||||
@ -32,6 +36,38 @@ namespace API.Tests.Extensions
|
|||||||
Assert.Equal(expected, series.NameInList(list));
|
Assert.Equal(expected, series.NameInList(list));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker than Black"}, MangaFormat.Archive, true)]
|
||||||
|
[InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker_than_Black"}, MangaFormat.Archive, true)]
|
||||||
|
[InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker then Black!"}, MangaFormat.Archive, false)]
|
||||||
|
[InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"Salem's Lot"}, MangaFormat.Archive, true)]
|
||||||
|
[InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"salems lot"}, MangaFormat.Archive, true)]
|
||||||
|
[InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"salem's lot"}, MangaFormat.Archive, true)]
|
||||||
|
// Different normalizations pass as we check normalization against an on-the-fly calculation so we don't delete series just because we change how normalization works
|
||||||
|
[InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot", "salems lot"}, new [] {"salem's lot"}, MangaFormat.Archive, true)]
|
||||||
|
[InlineData(new [] {"Rent-a-Girlfriend", "Rent-a-Girlfriend", "Kanojo, Okarishimasu", "rentagirlfriend"}, new [] {"Kanojo, Okarishimasu"}, MangaFormat.Archive, true)]
|
||||||
|
public void NameInListParserInfoTest(string[] seriesInput, string[] list, MangaFormat format, bool expected)
|
||||||
|
{
|
||||||
|
var series = new Series()
|
||||||
|
{
|
||||||
|
Name = seriesInput[0],
|
||||||
|
LocalizedName = seriesInput[1],
|
||||||
|
OriginalName = seriesInput[2],
|
||||||
|
NormalizedName = seriesInput.Length == 4 ? seriesInput[3] : API.Parser.Parser.Normalize(seriesInput[0]),
|
||||||
|
Metadata = new SeriesMetadata(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var parserInfos = list.Select(s => new ParsedSeries()
|
||||||
|
{
|
||||||
|
Name = s,
|
||||||
|
NormalizedName = API.Parser.Parser.Normalize(s),
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
// This doesn't do any checks against format
|
||||||
|
Assert.Equal(expected, series.NameInList(parserInfos));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, "Darker than Black", true)]
|
[InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, "Darker than Black", true)]
|
||||||
[InlineData(new [] {"Rent-a-Girlfriend", "Rent-a-Girlfriend", "Kanojo, Okarishimasu", "rentagirlfriend"}, "Kanojo, Okarishimasu", true)]
|
[InlineData(new [] {"Rent-a-Girlfriend", "Rent-a-Girlfriend", "Kanojo, Okarishimasu", "rentagirlfriend"}, "Kanojo, Okarishimasu", true)]
|
||||||
|
177
API.Tests/Extensions/VolumeListExtensionsTests.cs
Normal file
177
API.Tests/Extensions/VolumeListExtensionsTests.cs
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Extensions;
|
||||||
|
using API.Tests.Helpers;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Extensions;
|
||||||
|
|
||||||
|
public class VolumeListExtensionsTests
|
||||||
|
{
|
||||||
|
#region FirstWithChapters
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FirstWithChapters_ReturnsVolumeWithChapters()
|
||||||
|
{
|
||||||
|
var volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("0", new List<Chapter>()),
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false),
|
||||||
|
EntityFactory.CreateChapter("2", false),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(volumes[1].Number, volumes.FirstWithChapters(false).Number);
|
||||||
|
Assert.Equal(volumes[1].Number, volumes.FirstWithChapters(true).Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FirstWithChapters_Book()
|
||||||
|
{
|
||||||
|
var volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("3", false),
|
||||||
|
EntityFactory.CreateChapter("4", false),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false),
|
||||||
|
EntityFactory.CreateChapter("0", true),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(volumes[0].Number, volumes.FirstWithChapters(true).Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FirstWithChapters_NonBook()
|
||||||
|
{
|
||||||
|
var volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("3", false),
|
||||||
|
EntityFactory.CreateChapter("4", false),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false),
|
||||||
|
EntityFactory.CreateChapter("0", true),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(volumes[0].Number, volumes.FirstWithChapters(false).Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region GetCoverImage
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCoverImage_ArchiveFormat()
|
||||||
|
{
|
||||||
|
var volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("3", false),
|
||||||
|
EntityFactory.CreateChapter("4", false),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false),
|
||||||
|
EntityFactory.CreateChapter("0", true),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(volumes[0].Number, volumes.GetCoverImage(MangaFormat.Archive).Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCoverImage_EpubFormat()
|
||||||
|
{
|
||||||
|
var volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("3", false),
|
||||||
|
EntityFactory.CreateChapter("4", false),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false),
|
||||||
|
EntityFactory.CreateChapter("0", true),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(volumes[1].Name, volumes.GetCoverImage(MangaFormat.Epub).Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCoverImage_PdfFormat()
|
||||||
|
{
|
||||||
|
var volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("3", false),
|
||||||
|
EntityFactory.CreateChapter("4", false),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false),
|
||||||
|
EntityFactory.CreateChapter("0", true),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(volumes[1].Name, volumes.GetCoverImage(MangaFormat.Pdf).Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCoverImage_ImageFormat()
|
||||||
|
{
|
||||||
|
var volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("3", false),
|
||||||
|
EntityFactory.CreateChapter("4", false),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false),
|
||||||
|
EntityFactory.CreateChapter("0", true),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(volumes[0].Name, volumes.GetCoverImage(MangaFormat.Image).Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCoverImage_ImageFormat_NoSpecials()
|
||||||
|
{
|
||||||
|
var volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("3", false),
|
||||||
|
EntityFactory.CreateChapter("4", false),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false),
|
||||||
|
EntityFactory.CreateChapter("0", true),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(volumes[1].Name, volumes.GetCoverImage(MangaFormat.Image).Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
@ -5,6 +5,8 @@ using System.IO.Abstractions.TestingHelpers;
|
|||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Helpers;
|
namespace API.Tests.Helpers;
|
||||||
@ -287,4 +289,5 @@ public class CacheHelperTests
|
|||||||
};
|
};
|
||||||
Assert.False(cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, false, file));
|
Assert.False(cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, false, file));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
@ -28,6 +29,7 @@ namespace API.Tests.Helpers
|
|||||||
return new Volume()
|
return new Volume()
|
||||||
{
|
{
|
||||||
Name = volumeNumber,
|
Name = volumeNumber,
|
||||||
|
Number = int.Parse(volumeNumber),
|
||||||
Pages = 0,
|
Pages = 0,
|
||||||
Chapters = chapters ?? new List<Chapter>()
|
Chapters = chapters ?? new List<Chapter>()
|
||||||
};
|
};
|
||||||
|
@ -152,7 +152,7 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("test.jpeg", true)]
|
[InlineData("test.jpeg", true)]
|
||||||
[InlineData("test.png", true)]
|
[InlineData("test.png", true)]
|
||||||
[InlineData(".test.jpg", false)]
|
[InlineData(".test.jpg", false)]
|
||||||
[InlineData("!test.jpg", false)]
|
[InlineData("!test.jpg", true)]
|
||||||
[InlineData("test.webp", true)]
|
[InlineData("test.webp", true)]
|
||||||
public void IsImageTest(string filename, bool expected)
|
public void IsImageTest(string filename, bool expected)
|
||||||
{
|
{
|
||||||
@ -188,5 +188,17 @@ namespace API.Tests.Parser
|
|||||||
{
|
{
|
||||||
Assert.Equal(expected, HasBlacklistedFolderInPath(inputPath));
|
Assert.Equal(expected, HasBlacklistedFolderInPath(inputPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/manga/1/1/1", "/manga/1/1/1")]
|
||||||
|
[InlineData("/manga/1/1/1.jpg", "/manga/1/1/1.jpg")]
|
||||||
|
[InlineData(@"/manga/1/1\1.jpg", @"/manga/1/1/1.jpg")]
|
||||||
|
[InlineData("/manga/1/1//1", "/manga/1/1//1")]
|
||||||
|
[InlineData("/manga/1\\1\\1", "/manga/1/1/1")]
|
||||||
|
[InlineData("C:/manga/1\\1\\1.jpg", "C:/manga/1/1/1.jpg")]
|
||||||
|
public void NormalizePathTest(string inputPath, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, NormalizePath(inputPath));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,7 +138,7 @@ namespace API.Tests.Services
|
|||||||
[InlineData(new [] {"Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c060 (v10) - p200 [Digital] [LuCaZ].jpg", "folder.jpg"}, "folder.jpg")]
|
[InlineData(new [] {"Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c060 (v10) - p200 [Digital] [LuCaZ].jpg", "folder.jpg"}, "folder.jpg")]
|
||||||
public void FindFolderEntry(string[] files, string expected)
|
public void FindFolderEntry(string[] files, string expected)
|
||||||
{
|
{
|
||||||
var foundFile = _archiveService.FindFolderEntry(files);
|
var foundFile = ArchiveService.FindFolderEntry(files);
|
||||||
Assert.Equal(expected, string.IsNullOrEmpty(foundFile) ? "" : foundFile);
|
Assert.Equal(expected, string.IsNullOrEmpty(foundFile) ? "" : foundFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,26 +203,43 @@ namespace API.Tests.Services
|
|||||||
[InlineData("sorting.zip", "sorting.expected.jpg")]
|
[InlineData("sorting.zip", "sorting.expected.jpg")]
|
||||||
public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOutputFile)
|
public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOutputFile)
|
||||||
{
|
{
|
||||||
var archiveService = Substitute.For<ArchiveService>(_logger, new DirectoryService(_directoryServiceLogger, new MockFileSystem()));
|
var imageService = new ImageService(Substitute.For<ILogger<ImageService>>(), _directoryService);
|
||||||
|
var archiveService = Substitute.For<ArchiveService>(_logger,
|
||||||
|
new DirectoryService(_directoryServiceLogger, new FileSystem()), imageService);
|
||||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages");
|
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages");
|
||||||
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
||||||
|
|
||||||
|
var outputDir = Path.Join(testDirectory, "output");
|
||||||
|
_directoryService.ClearDirectory(outputDir);
|
||||||
|
_directoryService.ExistOrCreate(outputDir);
|
||||||
|
|
||||||
archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.SharpCompress);
|
archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.SharpCompress);
|
||||||
Stopwatch sw = Stopwatch.StartNew();
|
var actualBytes = File.ReadAllBytes(archiveService.GetCoverImage(Path.Join(testDirectory, inputFile),
|
||||||
//Assert.Equal(expectedBytes, File.ReadAllBytes(archiveService.GetCoverImage(Path.Join(testDirectory, inputFile), Path.GetFileNameWithoutExtension(inputFile) + "_output")));
|
Path.GetFileNameWithoutExtension(inputFile) + "_output", outputDir));
|
||||||
_testOutputHelper.WriteLine($"Processed in {sw.ElapsedMilliseconds} ms");
|
Assert.Equal(expectedBytes, actualBytes);
|
||||||
|
|
||||||
|
_directoryService.ClearAndDeleteDirectory(outputDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is broken on GA due to DirectoryService.CoverImageDirectory
|
[Theory]
|
||||||
//[Theory]
|
|
||||||
[InlineData("Archives/macos_native.zip")]
|
[InlineData("Archives/macos_native.zip")]
|
||||||
[InlineData("Formats/One File with DB_Supported.zip")]
|
[InlineData("Formats/One File with DB_Supported.zip")]
|
||||||
public void CanParseCoverImage(string inputFile)
|
public void CanParseCoverImage(string inputFile)
|
||||||
{
|
{
|
||||||
|
var imageService = Substitute.For<IImageService>();
|
||||||
|
imageService.WriteCoverThumbnail(Arg.Any<Stream>(), Arg.Any<string>(), Arg.Any<string>()).Returns(x => "cover.jpg");
|
||||||
|
var archiveService = new ArchiveService(_logger, _directoryService, imageService);
|
||||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/");
|
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/");
|
||||||
//Assert.NotEmpty(File.ReadAllBytes(_archiveService.GetCoverImage(Path.Join(testDirectory, inputFile), Path.GetFileNameWithoutExtension(inputFile) + "_output")));
|
var inputPath = Path.GetFullPath(Path.Join(testDirectory, inputFile));
|
||||||
|
var outputPath = Path.Join(testDirectory, Path.GetFileNameWithoutExtension(inputFile) + "_output");
|
||||||
|
new DirectoryInfo(outputPath).Create();
|
||||||
|
var expectedImage = archiveService.GetCoverImage(inputPath, inputFile, outputPath);
|
||||||
|
Assert.Equal("cover.jpg", expectedImage);
|
||||||
|
new DirectoryInfo(outputPath).Delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region ShouldHaveComicInfo
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ShouldHaveComicInfo()
|
public void ShouldHaveComicInfo()
|
||||||
{
|
{
|
||||||
@ -246,6 +263,10 @@ namespace API.Tests.Services
|
|||||||
Assert.Equal("Junya Inoue", comicInfo.Writer);
|
Assert.Equal("Junya Inoue", comicInfo.Writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region CanParseComicInfo
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanParseComicInfo()
|
public void CanParseComicInfo()
|
||||||
{
|
{
|
||||||
@ -268,5 +289,38 @@ namespace API.Tests.Services
|
|||||||
|
|
||||||
Assert.NotStrictEqual(expected, actual);
|
Assert.NotStrictEqual(expected, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region FindCoverImageFilename
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new string[] {}, "", null)]
|
||||||
|
[InlineData(new [] {"001.jpg", "002.jpg"}, "Test.zip", "001.jpg")]
|
||||||
|
[InlineData(new [] {"001.jpg", "!002.jpg"}, "Test.zip", "!002.jpg")]
|
||||||
|
[InlineData(new [] {"001.jpg", "!001.jpg"}, "Test.zip", "!001.jpg")]
|
||||||
|
[InlineData(new [] {"001.jpg", "cover.jpg"}, "Test.zip", "cover.jpg")]
|
||||||
|
[InlineData(new [] {"001.jpg", "Chapter 20/cover.jpg", "Chapter 21/0001.jpg"}, "Test.zip", "Chapter 20/cover.jpg")]
|
||||||
|
[InlineData(new [] {"._/001.jpg", "._/cover.jpg", "010.jpg"}, "Test.zip", "010.jpg")]
|
||||||
|
[InlineData(new [] {"001.txt", "002.txt", "a.jpg"}, "Test.zip", "a.jpg")]
|
||||||
|
public void FindCoverImageFilename(string[] filenames, string archiveName, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, _archiveService.FindCoverImageFilename(archiveName, filenames));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region CreateZipForDownload
|
||||||
|
|
||||||
|
//[Fact]
|
||||||
|
public void CreateZipForDownloadTest()
|
||||||
|
{
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
//_archiveService.CreateZipForDownload(new []{}, outputDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,5 +52,16 @@ namespace API.Tests.Services
|
|||||||
Assert.Equal("Roger Starbuck,Junya Inoue", comicInfo.Writer);
|
Assert.Equal("Roger Starbuck,Junya Inoue", comicInfo.Writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#region BookEscaping
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EscapeCSSImportReferencesTest()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,10 @@ using System.IO.Abstractions.TestingHelpers;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
|
using API.Data.Metadata;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Parser;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
@ -20,6 +22,40 @@ using Xunit;
|
|||||||
|
|
||||||
namespace API.Tests.Services
|
namespace API.Tests.Services
|
||||||
{
|
{
|
||||||
|
internal class MockReadingItemServiceForCacheService : IReadingItemService
|
||||||
|
{
|
||||||
|
private readonly DirectoryService _directoryService;
|
||||||
|
|
||||||
|
public MockReadingItemServiceForCacheService(DirectoryService directoryService)
|
||||||
|
{
|
||||||
|
_directoryService = directoryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1)
|
||||||
|
{
|
||||||
|
throw new System.NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParserInfo Parse(string path, string rootPath, LibraryType type)
|
||||||
|
{
|
||||||
|
throw new System.NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
public class CacheServiceTests
|
public class CacheServiceTests
|
||||||
{
|
{
|
||||||
private readonly ILogger<CacheService> _logger = Substitute.For<ILogger<CacheService>>();
|
private readonly ILogger<CacheService> _logger = Substitute.For<ILogger<CacheService>>();
|
||||||
@ -436,5 +472,37 @@ namespace API.Tests.Services
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region ExtractChapterFiles
|
||||||
|
|
||||||
|
// [Fact]
|
||||||
|
// public void ExtractChapterFiles_ShouldExtractOnlyImages()
|
||||||
|
// {
|
||||||
|
// const string testDirectory = "/manga/";
|
||||||
|
// var fileSystem = new MockFileSystem();
|
||||||
|
// for (var i = 0; i < 10; i++)
|
||||||
|
// {
|
||||||
|
// fileSystem.AddFile($"{testDirectory}file_{i}.zip", new MockFileData(""));
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fileSystem.AddDirectory(CacheDirectory);
|
||||||
|
//
|
||||||
|
// var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
// var cs = new CacheService(_logger, _unitOfWork, ds,
|
||||||
|
// new MockReadingItemServiceForCacheService(ds));
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// cs.ExtractChapterFiles(CacheDirectory, new List<MangaFile>()
|
||||||
|
// {
|
||||||
|
// new MangaFile()
|
||||||
|
// {
|
||||||
|
// ChapterId = 1,
|
||||||
|
// Format = MangaFormat.Archive,
|
||||||
|
// Pages = 2,
|
||||||
|
// FilePath =
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -776,5 +776,18 @@ namespace API.Tests.Services
|
|||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region GetHumanReadableBytes
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1200, "1.17 KB")]
|
||||||
|
[InlineData(1, "1 B")]
|
||||||
|
[InlineData(10000000, "9.54 MB")]
|
||||||
|
[InlineData(10000000000, "9.31 GB")]
|
||||||
|
public void GetHumanReadableBytesTest(long bytes, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, DirectoryService.GetHumanReadableBytes(bytes));
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
814
API.Tests/Services/ReaderServiceTests.cs
Normal file
814
API.Tests/Services/ReaderServiceTests.cs
Normal file
@ -0,0 +1,814 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data.Common;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.Data.Repositories;
|
||||||
|
using API.DTOs;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Helpers;
|
||||||
|
using API.Services;
|
||||||
|
using API.SignalR;
|
||||||
|
using API.Tests.Helpers;
|
||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
|
public class ReaderServiceTests
|
||||||
|
{
|
||||||
|
private readonly ILogger<CacheService> _logger = Substitute.For<ILogger<CacheService>>();
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IHubContext<MessageHub> _messageHub = Substitute.For<IHubContext<MessageHub>>();
|
||||||
|
|
||||||
|
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 ReaderServiceTests()
|
||||||
|
{
|
||||||
|
var contextOptions = new DbContextOptionsBuilder().UseSqlite(CreateInMemoryDatabase()).Options;
|
||||||
|
_connection = RelationalOptionsExtension.Extract(contextOptions).Connection;
|
||||||
|
|
||||||
|
_context = new DataContext(contextOptions);
|
||||||
|
Task.Run(SeedDb).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
|
||||||
|
var mapper = config.CreateMapper();
|
||||||
|
_unitOfWork = new UnitOfWork(_context, mapper, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Setup
|
||||||
|
|
||||||
|
private static DbConnection CreateInMemoryDatabase()
|
||||||
|
{
|
||||||
|
var connection = new SqliteConnection("Filename=:memory:");
|
||||||
|
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => _connection.Dispose();
|
||||||
|
|
||||||
|
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 = "C:/data/"}}
|
||||||
|
});
|
||||||
|
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 FormatBookmarkFolderPath
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/manga/", 1, 1, 1, "/manga/1/1/1")]
|
||||||
|
[InlineData("C:/manga/", 1, 1, 10001, "C:/manga/1/1/10001")]
|
||||||
|
public void FormatBookmarkFolderPathTest(string baseDir, int userId, int seriesId, int chapterId, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, ReaderService.FormatBookmarkFolderPath(baseDir, userId, seriesId, chapterId));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region CapPageToChapter
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CapPageToChapterTest()
|
||||||
|
{
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Pages = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
|
||||||
|
Assert.Equal(0, await readerService.CapPageToChapter(1, -1));
|
||||||
|
Assert.Equal(1, await readerService.CapPageToChapter(1, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region SaveReadingProgress
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SaveReadingProgress_ShouldCreateNewEntity()
|
||||||
|
{
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Pages = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
|
||||||
|
var successful = await readerService.SaveReadingProgress(new ProgressDto()
|
||||||
|
{
|
||||||
|
ChapterId = 1,
|
||||||
|
PageNum = 1,
|
||||||
|
SeriesId = 1,
|
||||||
|
VolumeId = 1,
|
||||||
|
BookScrollId = null
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
Assert.True(successful);
|
||||||
|
Assert.NotNull(await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SaveReadingProgress_ShouldUpdateExisting()
|
||||||
|
{
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Pages = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
|
||||||
|
var successful = await readerService.SaveReadingProgress(new ProgressDto()
|
||||||
|
{
|
||||||
|
ChapterId = 1,
|
||||||
|
PageNum = 1,
|
||||||
|
SeriesId = 1,
|
||||||
|
VolumeId = 1,
|
||||||
|
BookScrollId = null
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
Assert.True(successful);
|
||||||
|
Assert.NotNull(await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1));
|
||||||
|
|
||||||
|
Assert.True(await readerService.SaveReadingProgress(new ProgressDto()
|
||||||
|
{
|
||||||
|
ChapterId = 1,
|
||||||
|
PageNum = 1,
|
||||||
|
SeriesId = 1,
|
||||||
|
VolumeId = 1,
|
||||||
|
BookScrollId = "/h1/"
|
||||||
|
}, 1));
|
||||||
|
|
||||||
|
Assert.Equal("/h1/", (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).BookScrollId);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region MarkChaptersAsRead
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MarkChaptersAsReadTest()
|
||||||
|
{
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Pages = 1
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Pages = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
|
||||||
|
var volumes = await _unitOfWork.VolumeRepository.GetVolumes(1);
|
||||||
|
readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region MarkChapterAsUnread
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MarkChapterAsUnreadTest()
|
||||||
|
{
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Pages = 1
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Pages = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
|
||||||
|
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList();
|
||||||
|
readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count);
|
||||||
|
|
||||||
|
readerService.MarkChaptersAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var progresses = (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses;
|
||||||
|
Assert.Equal(0, progresses.Max(p => p.PagesRead));
|
||||||
|
Assert.Equal(2, progresses.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region GetNextChapterIdAsync
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetNextChapterIdAsync_ShouldGetNextVolume()
|
||||||
|
{
|
||||||
|
// V1 -> V2
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
|
||||||
|
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 1, 1);
|
||||||
|
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||||
|
Assert.Equal("2", actualChapter.Range);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetNextChapterIdAsync_ShouldRollIntoNextVolume()
|
||||||
|
{
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
|
||||||
|
|
||||||
|
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||||
|
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||||
|
Assert.Equal("21", actualChapter.Range);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetNextChapterIdAsync_ShouldNotMoveFromVolumeToSpecial()
|
||||||
|
{
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("A.cbz", true, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("B.cbz", true, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
|
||||||
|
|
||||||
|
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||||
|
Assert.Equal(-1, nextChapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetNextChapterIdAsync_ShouldMoveFromSpecialToSpecial()
|
||||||
|
{
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("A.cbz", true, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("B.cbz", true, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
|
||||||
|
|
||||||
|
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 3, 1);
|
||||||
|
Assert.NotEqual(-1, nextChapter);
|
||||||
|
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||||
|
Assert.Equal("B.cbz", actualChapter.Range);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region GetPrevChapterIdAsync
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPrevChapterIdAsync_ShouldGetPrevVolume()
|
||||||
|
{
|
||||||
|
// V1 -> V2
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
|
||||||
|
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 2, 1);
|
||||||
|
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||||
|
Assert.Equal("1", actualChapter.Range);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPrevChapterIdAsync_ShouldRollIntoPrevVolume()
|
||||||
|
{
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
|
||||||
|
|
||||||
|
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
|
||||||
|
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||||
|
Assert.Equal("2", actualChapter.Range);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPrevChapterIdAsync_ShouldMoveFromVolumeToSpecial()
|
||||||
|
{
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("A.cbz", true, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("B.cbz", true, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
|
||||||
|
|
||||||
|
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||||
|
Assert.NotEqual(-1, prevChapter);
|
||||||
|
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||||
|
Assert.Equal("B.cbz", actualChapter.Range);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPrevChapterIdAsync_ShouldMoveFromSpecialToSpecial()
|
||||||
|
{
|
||||||
|
await ResetDB();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("A.cbz", true, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("B.cbz", true, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
|
||||||
|
|
||||||
|
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 4, 1);
|
||||||
|
Assert.NotEqual(-1, prevChapter);
|
||||||
|
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||||
|
Assert.Equal("A.cbz", actualChapter.Range);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
// #region GetNumberOfPages
|
||||||
|
//
|
||||||
|
// [Fact]
|
||||||
|
// public void GetNumberOfPages_EPUB()
|
||||||
|
// {
|
||||||
|
// const string testDirectory = "/manga/";
|
||||||
|
// var fileSystem = new MockFileSystem();
|
||||||
|
//
|
||||||
|
// var actualFile = Path.Join(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService/EPUB"), "The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub")
|
||||||
|
// fileSystem.File.WriteAllBytes("${testDirectory}test.epub", File.ReadAllBytes(actualFile));
|
||||||
|
//
|
||||||
|
// fileSystem.AddDirectory(CacheDirectory);
|
||||||
|
//
|
||||||
|
// var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
|
// var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds));
|
||||||
|
// var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), ds, cs);
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
}
|
@ -1,111 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using static System.GC;
|
|
||||||
using static System.String;
|
|
||||||
|
|
||||||
namespace API.Comparators
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Attempts to emulate Windows explorer sorting
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>This is not thread-safe</remarks>
|
|
||||||
public sealed class NaturalSortComparer : IComparer<string>, IDisposable
|
|
||||||
{
|
|
||||||
private readonly bool _isAscending;
|
|
||||||
private Dictionary<string, string[]> _table = new();
|
|
||||||
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
|
|
||||||
public NaturalSortComparer(bool inAscendingOrder = true)
|
|
||||||
{
|
|
||||||
_isAscending = inAscendingOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
int IComparer<string>.Compare(string? x, string? y)
|
|
||||||
{
|
|
||||||
if (x == y) return 0;
|
|
||||||
|
|
||||||
if (x != null && y == null) return -1;
|
|
||||||
if (x == null) return 1;
|
|
||||||
|
|
||||||
|
|
||||||
if (!_table.TryGetValue(x ?? Empty, out var x1))
|
|
||||||
{
|
|
||||||
x1 = Regex.Split(x ?? Empty, "([0-9]+)");
|
|
||||||
_table.Add(x ?? Empty, x1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_table.TryGetValue(y ?? Empty, out var y1))
|
|
||||||
{
|
|
||||||
y1 = Regex.Split(y ?? Empty, "([0-9]+)");
|
|
||||||
_table.Add(y ?? Empty, y1);
|
|
||||||
}
|
|
||||||
|
|
||||||
int returnVal;
|
|
||||||
|
|
||||||
for (var i = 0; i < x1.Length && i < y1.Length; i++)
|
|
||||||
{
|
|
||||||
if (x1[i] == y1[i]) continue;
|
|
||||||
if (x1[i] == Empty || y1[i] == Empty) continue;
|
|
||||||
returnVal = PartCompare(x1[i], y1[i]);
|
|
||||||
return _isAscending ? returnVal : -returnVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (y1.Length > x1.Length)
|
|
||||||
{
|
|
||||||
returnVal = -1;
|
|
||||||
}
|
|
||||||
else if (x1.Length > y1.Length)
|
|
||||||
{
|
|
||||||
returnVal = 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
returnVal = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return _isAscending ? returnVal : -returnVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int PartCompare(string left, string right)
|
|
||||||
{
|
|
||||||
if (!int.TryParse(left, out var x))
|
|
||||||
return Compare(left, right, StringComparison.Ordinal);
|
|
||||||
|
|
||||||
if (!int.TryParse(right, out var y))
|
|
||||||
return Compare(left, right, StringComparison.Ordinal);
|
|
||||||
|
|
||||||
return x.CompareTo(y);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (!_disposed)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
// called via myClass.Dispose().
|
|
||||||
_table.Clear();
|
|
||||||
_table = null;
|
|
||||||
}
|
|
||||||
// Release unmanaged resources.
|
|
||||||
// Set large fields to null.
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
~NaturalSortComparer() // the finalizer
|
|
||||||
{
|
|
||||||
Dispose(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -139,15 +139,7 @@ namespace API.Controllers
|
|||||||
user.Progresses ??= new List<AppUserProgress>();
|
user.Progresses ??= new List<AppUserProgress>();
|
||||||
foreach (var volume in volumes)
|
foreach (var volume in volumes)
|
||||||
{
|
{
|
||||||
foreach (var chapter in volume.Chapters)
|
_readerService.MarkChaptersAsUnread(user, markReadDto.SeriesId, volume.Chapters);
|
||||||
{
|
|
||||||
var userProgress = ReaderService.GetUserProgressForChapter(user, chapter);
|
|
||||||
|
|
||||||
if (userProgress == null) continue;
|
|
||||||
userProgress.PagesRead = 0;
|
|
||||||
userProgress.SeriesId = markReadDto.SeriesId;
|
|
||||||
userProgress.VolumeId = volume.Id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
|||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Extensions;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using AutoMapper.QueryableExtensions;
|
using AutoMapper.QueryableExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -195,10 +196,9 @@ public class VolumeRepository : IVolumeRepository
|
|||||||
|
|
||||||
private static void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
private static void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
||||||
{
|
{
|
||||||
var sorter = new NaturalSortComparer();
|
|
||||||
foreach (var v in volumes.Where(vDto => vDto.Number == 0))
|
foreach (var v in volumes.Where(vDto => vDto.Number == 0))
|
||||||
{
|
{
|
||||||
v.Chapters = v.Chapters.OrderBy(x => x.Range, sorter).ToList();
|
v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,30 @@
|
|||||||
namespace API.Extensions
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace API.Extensions
|
||||||
{
|
{
|
||||||
public static class EnumerableExtensions
|
public static class EnumerableExtensions
|
||||||
{
|
{
|
||||||
|
private static readonly Regex Regex = new Regex(@"\d+", RegexOptions.Compiled, TimeSpan.FromMilliseconds(500));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A natural sort implementation
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="items">IEnumerable to process</param>
|
||||||
|
/// <param name="selector">Function that produces a string. Does not support null values</param>
|
||||||
|
/// <param name="stringComparer">Defaults to CurrentCulture</param>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <returns>Sorted Enumerable</returns>
|
||||||
|
public static IEnumerable<T> OrderByNatural<T>(this IEnumerable<T> items, Func<T, string> selector, StringComparer stringComparer = null)
|
||||||
|
{
|
||||||
|
var maxDigits = items
|
||||||
|
.SelectMany(i => Regex.Matches(selector(i))
|
||||||
|
.Select(digitChunk => (int?)digitChunk.Value.Length))
|
||||||
|
.Max() ?? 0;
|
||||||
|
|
||||||
|
return items.OrderBy(i => Regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,15 +31,15 @@ namespace API.Extensions
|
|||||||
: infos.Any(v => v.Chapters == chapter.Range);
|
: infos.Any(v => v.Chapters == chapter.Range);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// Returns the MangaFormat that is common to all the files. Unknown if files are mixed (should never happen) or no infos
|
// /// Returns the MangaFormat that is common to all the files. Unknown if files are mixed (should never happen) or no infos
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
/// <param name="infos"></param>
|
// /// <param name="infos"></param>
|
||||||
/// <returns></returns>
|
// /// <returns></returns>
|
||||||
public static MangaFormat GetFormat(this IList<ParserInfo> infos)
|
// public static MangaFormat GetFormat(this IList<ParserInfo> infos)
|
||||||
{
|
// {
|
||||||
if (infos.Count == 0) return MangaFormat.Unknown;
|
// if (infos.Count == 0) return MangaFormat.Unknown;
|
||||||
return infos.DistinctBy(x => x.Format).First().Format;
|
// return infos.DistinctBy(x => x.Format).First().Format;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ public static class PathExtensions
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(filepath)) return filepath;
|
if (string.IsNullOrEmpty(filepath)) return filepath;
|
||||||
var extension = Path.GetExtension(filepath);
|
var extension = Path.GetExtension(filepath);
|
||||||
|
if (string.IsNullOrEmpty(extension)) return filepath;
|
||||||
return Path.GetFullPath(filepath.Replace(extension, string.Empty));
|
return Path.GetFullPath(filepath.Replace(extension, string.Empty));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ namespace API.Extensions
|
|||||||
public static class SeriesExtensions
|
public static class SeriesExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks against all the name variables of the Series if it matches anything in the list.
|
/// Checks against all the name variables of the Series if it matches anything in the list. This does not check against format.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="series"></param>
|
/// <param name="series"></param>
|
||||||
/// <param name="list"></param>
|
/// <param name="list"></param>
|
||||||
|
@ -12,7 +12,8 @@ namespace API.Extensions
|
|||||||
{
|
{
|
||||||
return inBookSeries
|
return inBookSeries
|
||||||
? volumes.FirstOrDefault(v => v.Chapters.Any())
|
? volumes.FirstOrDefault(v => v.Chapters.Any())
|
||||||
: volumes.OrderBy(v => v.Number, new ChapterSortComparer()).FirstOrDefault(v => v.Chapters.Any());
|
: volumes.OrderBy(v => v.Number, new ChapterSortComparer())
|
||||||
|
.FirstOrDefault(v => v.Chapters.Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -26,16 +26,16 @@ namespace API.Helpers.Converters
|
|||||||
return destination;
|
return destination;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ConvertFromCronNotation(string cronNotation)
|
// public static string ConvertFromCronNotation(string cronNotation)
|
||||||
{
|
// {
|
||||||
var destination = string.Empty;
|
// var destination = string.Empty;
|
||||||
destination = cronNotation.ToLower() switch
|
// destination = cronNotation.ToLower() switch
|
||||||
{
|
// {
|
||||||
"0 0 31 2 *" => "disabled",
|
// "0 0 31 2 *" => "disabled",
|
||||||
_ => destination
|
// _ => destination
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
return destination;
|
// return destination;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -916,10 +916,9 @@ namespace API.Parser
|
|||||||
return BookFileRegex.IsMatch(Path.GetExtension(filePath));
|
return BookFileRegex.IsMatch(Path.GetExtension(filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsImage(string filePath, bool suppressExtraChecks = false)
|
public static bool IsImage(string filePath)
|
||||||
{
|
{
|
||||||
if (filePath.StartsWith(".") || (!suppressExtraChecks && filePath.StartsWith("!"))) return false;
|
return !filePath.StartsWith(".") && ImageRegex.IsMatch(Path.GetExtension(filePath));
|
||||||
return ImageRegex.IsMatch(Path.GetExtension(filePath));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsXml(string filePath)
|
public static bool IsXml(string filePath)
|
||||||
@ -959,7 +958,7 @@ namespace API.Parser
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static bool IsCoverImage(string filename)
|
public static bool IsCoverImage(string filename)
|
||||||
{
|
{
|
||||||
return IsImage(filename, true) && CoverImageRegex.IsMatch(filename);
|
return IsImage(filename) && CoverImageRegex.IsMatch(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool HasBlacklistedFolderInPath(string path)
|
public static bool HasBlacklistedFolderInPath(string path)
|
||||||
@ -989,5 +988,16 @@ namespace API.Parser
|
|||||||
if (string.IsNullOrEmpty(author)) return string.Empty;
|
if (string.IsNullOrEmpty(author)) return string.Empty;
|
||||||
return author.Trim();
|
return author.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalizes the slashes in a path to be <see cref="Path.AltDirectorySeparatorChar"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <example>/manga/1\1 -> /manga/1/1</example>
|
||||||
|
/// <param name="path"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string NormalizePath(string path)
|
||||||
|
{
|
||||||
|
return path.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ namespace API.Services
|
|||||||
ArchiveLibrary CanOpen(string archivePath);
|
ArchiveLibrary CanOpen(string archivePath);
|
||||||
bool ArchiveNeedsFlattening(ZipArchive archive);
|
bool ArchiveNeedsFlattening(ZipArchive archive);
|
||||||
Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder);
|
Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder);
|
||||||
|
string FindCoverImageFilename(string archivePath, IList<string> entryNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -124,55 +125,27 @@ namespace API.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="entryFullNames"></param>
|
/// <param name="entryFullNames"></param>
|
||||||
/// <returns>Entry name of match, null if no match</returns>
|
/// <returns>Entry name of match, null if no match</returns>
|
||||||
public string FindFolderEntry(IEnumerable<string> entryFullNames)
|
public static string FindFolderEntry(IEnumerable<string> entryFullNames)
|
||||||
{
|
{
|
||||||
var result = entryFullNames
|
var result = entryFullNames
|
||||||
.FirstOrDefault(x => !Path.EndsInDirectorySeparator(x) && !Parser.Parser.HasBlacklistedFolderInPath(x)
|
.OrderByNatural(Path.GetFileNameWithoutExtension)
|
||||||
&& Parser.Parser.IsCoverImage(x)
|
.Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)))
|
||||||
&& !x.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith));
|
.FirstOrDefault(Parser.Parser.IsCoverImage);
|
||||||
|
|
||||||
return string.IsNullOrEmpty(result) ? null : result;
|
return string.IsNullOrEmpty(result) ? null : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns first entry that is an image and is not in a blacklisted folder path. Uses <see cref="NaturalSortComparer"/> for ordering files
|
/// Returns first entry that is an image and is not in a blacklisted folder path. Uses <see cref="OrderByNatural"/> for ordering files
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="entryFullNames"></param>
|
/// <param name="entryFullNames"></param>
|
||||||
/// <returns>Entry name of match, null if no match</returns>
|
/// <returns>Entry name of match, null if no match</returns>
|
||||||
public static string FirstFileEntry(IEnumerable<string> entryFullNames, string archiveName)
|
public static string? FirstFileEntry(IEnumerable<string> entryFullNames, string archiveName)
|
||||||
{
|
{
|
||||||
// First check if there are any files that are not in a nested folder before just comparing by filename. This is needed
|
var result = entryFullNames
|
||||||
// because NaturalSortComparer does not work with paths and doesn't seem 001.jpg as before chapter 1/001.jpg.
|
.OrderByNatural(c => c.GetFullPathWithoutExtension())
|
||||||
var fullNames = entryFullNames.Where(x =>!Parser.Parser.HasBlacklistedFolderInPath(x)
|
.Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)))
|
||||||
&& Parser.Parser.IsImage(x)
|
.FirstOrDefault(path => Parser.Parser.IsImage(path));
|
||||||
&& !x.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)).ToList();
|
|
||||||
if (fullNames.Count == 0) return null;
|
|
||||||
using var nc = new NaturalSortComparer();
|
|
||||||
var nonNestedFile = fullNames.Where(entry => (Path.GetDirectoryName(entry) ?? string.Empty).Equals(archiveName))
|
|
||||||
.OrderBy(f => f.GetFullPathWithoutExtension(), nc) // BUG: This shouldn't take into account extension
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(nonNestedFile)) return nonNestedFile;
|
|
||||||
|
|
||||||
// Check the first folder and sort within that to see if we can find a file, else fallback to first file with basic sort.
|
|
||||||
// Get first folder, then sort within that
|
|
||||||
var firstDirectoryFile = fullNames.OrderBy(Path.GetDirectoryName, nc).FirstOrDefault();
|
|
||||||
if (!string.IsNullOrEmpty(firstDirectoryFile))
|
|
||||||
{
|
|
||||||
var firstDirectory = Path.GetDirectoryName(firstDirectoryFile);
|
|
||||||
if (!string.IsNullOrEmpty(firstDirectory))
|
|
||||||
{
|
|
||||||
var firstDirectoryResult = fullNames.Where(f => firstDirectory.Equals(Path.GetDirectoryName(f)))
|
|
||||||
.OrderBy(Path.GetFileNameWithoutExtension, nc)
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(firstDirectoryResult)) return firstDirectoryResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = fullNames
|
|
||||||
.OrderBy(Path.GetFileNameWithoutExtension, nc)
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
return string.IsNullOrEmpty(result) ? null : result;
|
return string.IsNullOrEmpty(result) ? null : result;
|
||||||
}
|
}
|
||||||
@ -200,25 +173,24 @@ namespace API.Services
|
|||||||
case ArchiveLibrary.Default:
|
case ArchiveLibrary.Default:
|
||||||
{
|
{
|
||||||
using var archive = ZipFile.OpenRead(archivePath);
|
using var archive = ZipFile.OpenRead(archivePath);
|
||||||
var entryNames = archive.Entries.Select(e => e.FullName).ToArray();
|
var entryNames = archive.Entries.Select(e => e.FullName).ToList();
|
||||||
|
|
||||||
var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames, Path.GetFileName(archivePath));
|
var entryName = FindCoverImageFilename(archivePath, entryNames);
|
||||||
var entry = archive.Entries.Single(e => e.FullName == entryName);
|
var entry = archive.Entries.Single(e => e.FullName == entryName);
|
||||||
using var stream = entry.Open();
|
|
||||||
|
|
||||||
return CreateThumbnail(archivePath + " - " + entry.FullName, stream, fileName, outputDirectory);
|
using var stream = entry.Open();
|
||||||
|
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory);
|
||||||
}
|
}
|
||||||
case ArchiveLibrary.SharpCompress:
|
case ArchiveLibrary.SharpCompress:
|
||||||
{
|
{
|
||||||
using var archive = ArchiveFactory.Open(archivePath);
|
using var archive = ArchiveFactory.Open(archivePath);
|
||||||
var entryNames = archive.Entries.Where(archiveEntry => !archiveEntry.IsDirectory).Select(e => e.Key).ToList();
|
var entryNames = archive.Entries.Where(archiveEntry => !archiveEntry.IsDirectory).Select(e => e.Key).ToList();
|
||||||
|
|
||||||
var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames, Path.GetFileName(archivePath));
|
var entryName = FindCoverImageFilename(archivePath, entryNames);
|
||||||
var entry = archive.Entries.Single(e => e.Key == entryName);
|
var entry = archive.Entries.Single(e => e.Key == entryName);
|
||||||
|
|
||||||
using var stream = entry.OpenEntryStream();
|
using var stream = entry.OpenEntryStream();
|
||||||
|
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory);
|
||||||
return CreateThumbnail(archivePath + " - " + entry.Key, stream, fileName, outputDirectory);
|
|
||||||
}
|
}
|
||||||
case ArchiveLibrary.NotSupported:
|
case ArchiveLibrary.NotSupported:
|
||||||
_logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath);
|
_logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath);
|
||||||
@ -236,6 +208,18 @@ namespace API.Services
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given a list of image paths (assume within an archive), find the filename that corresponds to the cover
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="archivePath"></param>
|
||||||
|
/// <param name="entryNames"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string FindCoverImageFilename(string archivePath, IList<string> entryNames)
|
||||||
|
{
|
||||||
|
var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames, Path.GetFileName(archivePath));
|
||||||
|
return entryName;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given an archive stream, will assess whether directory needs to be flattened so that the extracted archive files are directly
|
/// Given an archive stream, will assess whether directory needs to be flattened so that the extracted archive files are directly
|
||||||
/// under extract path and not nested in subfolders. See <see cref="DirectoryInfoExtensions"/> Flatten method.
|
/// under extract path and not nested in subfolders. See <see cref="DirectoryInfoExtensions"/> Flatten method.
|
||||||
@ -282,20 +266,6 @@ namespace API.Services
|
|||||||
return Tuple.Create(fileBytes, zipPath);
|
return Tuple.Create(fileBytes, zipPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string CreateThumbnail(string entryName, Stream stream, string fileName, string outputDirectory)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// NOTE: I can just let this bubble up
|
|
||||||
_logger.LogWarning(ex, "[GetCoverImage] There was an error and prevented thumbnail generation on {EntryName}. Defaulting to no cover image", entryName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Test if the archive path exists and an archive
|
/// Test if the archive path exists and an archive
|
||||||
|
@ -250,13 +250,25 @@ namespace API.Services
|
|||||||
var imageFile = image.Attributes["src"].Value;
|
var imageFile = image.Attributes["src"].Value;
|
||||||
if (!book.Content.Images.ContainsKey(imageFile))
|
if (!book.Content.Images.ContainsKey(imageFile))
|
||||||
{
|
{
|
||||||
|
// TODO: Refactor the Key code to a method to allow the hacks to be tested
|
||||||
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
|
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
|
||||||
if (correctedKey != null)
|
if (correctedKey != null)
|
||||||
|
{
|
||||||
|
imageFile = correctedKey;
|
||||||
|
} else if (imageFile.StartsWith(".."))
|
||||||
|
{
|
||||||
|
// There are cases where the key is defined static like OEBPS/Images/1-4.jpg but reference is ../Images/1-4.jpg
|
||||||
|
correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile.Replace("..", string.Empty)));
|
||||||
|
if (correctedKey != null)
|
||||||
{
|
{
|
||||||
imageFile = correctedKey;
|
imageFile = correctedKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
image.Attributes.Remove("src");
|
image.Attributes.Remove("src");
|
||||||
image.Attributes.Add("src", $"{apiBase}" + imageFile);
|
image.Attributes.Add("src", $"{apiBase}" + imageFile);
|
||||||
}
|
}
|
||||||
|
@ -170,13 +170,11 @@ namespace API.Services
|
|||||||
// Calculate what chapter the page belongs to
|
// Calculate what chapter the page belongs to
|
||||||
var path = GetCachePath(chapter.Id);
|
var path = GetCachePath(chapter.Id);
|
||||||
var files = _directoryService.GetFilesWithExtension(path, Parser.Parser.ImageFileExtensions);
|
var files = _directoryService.GetFilesWithExtension(path, Parser.Parser.ImageFileExtensions);
|
||||||
using var nc = new NaturalSortComparer();
|
|
||||||
files = files
|
files = files
|
||||||
.AsEnumerable()
|
.AsEnumerable()
|
||||||
.OrderBy(Path.GetFileNameWithoutExtension, nc)
|
.OrderByNatural(Path.GetFileNameWithoutExtension)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
|
|
||||||
if (files.Length == 0)
|
if (files.Length == 0)
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
@ -8,6 +8,7 @@ using System.Linq;
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
|
using API.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Services
|
namespace API.Services
|
||||||
@ -698,8 +699,7 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
var fileIndex = 1;
|
var fileIndex = 1;
|
||||||
|
|
||||||
using var nc = new NaturalSortComparer();
|
foreach (var file in directory.EnumerateFiles().OrderByNatural(file => file.FullName))
|
||||||
foreach (var file in directory.EnumerateFiles().OrderBy(file => file.FullName, nc))
|
|
||||||
{
|
{
|
||||||
if (file.Directory == null) continue;
|
if (file.Directory == null) continue;
|
||||||
var paddedIndex = Parser.Parser.PadZeros(directoryIndex + "");
|
var paddedIndex = Parser.Parser.PadZeros(directoryIndex + "");
|
||||||
@ -713,8 +713,7 @@ namespace API.Services
|
|||||||
directoryIndex++;
|
directoryIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sort = new NaturalSortComparer();
|
foreach (var subDirectory in directory.EnumerateDirectories().OrderByNatural(d => d.FullName))
|
||||||
foreach (var subDirectory in directory.EnumerateDirectories().OrderBy(d => d.FullName, sort))
|
|
||||||
{
|
{
|
||||||
FlattenDirectory(root, subDirectory, ref directoryIndex);
|
FlattenDirectory(root, subDirectory, ref directoryIndex);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ using API.Data;
|
|||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Extensions;
|
||||||
|
using Kavita.Common;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Services;
|
namespace API.Services;
|
||||||
@ -41,7 +43,7 @@ public class ReaderService : IReaderService
|
|||||||
|
|
||||||
public static string FormatBookmarkFolderPath(string baseDirectory, int userId, int seriesId, int chapterId)
|
public static string FormatBookmarkFolderPath(string baseDirectory, int userId, int seriesId, int chapterId)
|
||||||
{
|
{
|
||||||
return Path.Join(baseDirectory, $"{userId}", $"{seriesId}", $"{chapterId}");
|
return Parser.Parser.NormalizePath(Path.Join(baseDirectory, $"{userId}", $"{seriesId}", $"{chapterId}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -87,34 +89,28 @@ public class ReaderService : IReaderService
|
|||||||
{
|
{
|
||||||
var userProgress = GetUserProgressForChapter(user, chapter);
|
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||||
|
|
||||||
if (userProgress == null)
|
if (userProgress == null) continue;
|
||||||
{
|
|
||||||
user.Progresses.Add(new AppUserProgress
|
|
||||||
{
|
|
||||||
PagesRead = 0,
|
|
||||||
VolumeId = chapter.VolumeId,
|
|
||||||
SeriesId = seriesId,
|
|
||||||
ChapterId = chapter.Id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
userProgress.PagesRead = 0;
|
userProgress.PagesRead = 0;
|
||||||
userProgress.SeriesId = seriesId;
|
userProgress.SeriesId = seriesId;
|
||||||
userProgress.VolumeId = chapter.VolumeId;
|
userProgress.VolumeId = chapter.VolumeId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the User Progress for a given Chapter. This will handle any duplicates that might have occured in past versions and will delete them. Does not commit.
|
/// Gets the User Progress for a given Chapter. This will handle any duplicates that might have occured in past versions and will delete them. Does not commit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user"></param>
|
/// <param name="user">Must have Progresses populated</param>
|
||||||
/// <param name="chapter"></param>
|
/// <param name="chapter"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static AppUserProgress GetUserProgressForChapter(AppUser user, Chapter chapter)
|
private static AppUserProgress GetUserProgressForChapter(AppUser user, Chapter chapter)
|
||||||
{
|
{
|
||||||
AppUserProgress userProgress = null;
|
AppUserProgress userProgress = null;
|
||||||
|
|
||||||
|
if (user.Progresses == null)
|
||||||
|
{
|
||||||
|
throw new KavitaException("Progresses must exist on user");
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
userProgress =
|
userProgress =
|
||||||
@ -236,7 +232,7 @@ public class ReaderService : IReaderService
|
|||||||
if (currentVolume.Number == 0)
|
if (currentVolume.Number == 0)
|
||||||
{
|
{
|
||||||
// Handle specials by sorting on their Filename aka Range
|
// Handle specials by sorting on their Filename aka Range
|
||||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Range, new NaturalSortComparer()), currentChapter.Number);
|
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range), currentChapter.Number);
|
||||||
if (chapterId > 0) return chapterId;
|
if (chapterId > 0) return chapterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,7 +283,7 @@ public class ReaderService : IReaderService
|
|||||||
|
|
||||||
if (currentVolume.Number == 0)
|
if (currentVolume.Number == 0)
|
||||||
{
|
{
|
||||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Range, new NaturalSortComparer()).Reverse(), currentChapter.Number);
|
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range).Reverse(), currentChapter.Number);
|
||||||
if (chapterId > 0) return chapterId;
|
if (chapterId > 0) return chapterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,9 +177,9 @@ namespace API.Services.Tasks
|
|||||||
// Search all files in bookmarks/ except bookmark files and delete those
|
// Search all files in bookmarks/ except bookmark files and delete those
|
||||||
var bookmarkDirectory =
|
var bookmarkDirectory =
|
||||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||||
var allBookmarkFiles = _directoryService.GetFiles(bookmarkDirectory, searchOption: SearchOption.AllDirectories).Select(f => _directoryService.FileSystem.Path.GetFullPath(f));
|
var allBookmarkFiles = _directoryService.GetFiles(bookmarkDirectory, searchOption: SearchOption.AllDirectories).Select(Parser.Parser.NormalizePath);
|
||||||
var bookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync())
|
var bookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync())
|
||||||
.Select(b => _directoryService.FileSystem.Path.GetFullPath(_directoryService.FileSystem.Path.Join(bookmarkDirectory,
|
.Select(b => Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(bookmarkDirectory,
|
||||||
b.FileName)));
|
b.FileName)));
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,7 +44,6 @@ public class ScannerService : IScannerService
|
|||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
private readonly IReadingItemService _readingItemService;
|
private readonly IReadingItemService _readingItemService;
|
||||||
private readonly ICacheHelper _cacheHelper;
|
private readonly ICacheHelper _cacheHelper;
|
||||||
private readonly NaturalSortComparer _naturalSort = new ();
|
|
||||||
|
|
||||||
public ScannerService(IUnitOfWork unitOfWork, ILogger<ScannerService> logger,
|
public ScannerService(IUnitOfWork unitOfWork, ILogger<ScannerService> logger,
|
||||||
IMetadataService metadataService, ICacheService cacheService, IHubContext<MessageHub> messageHub,
|
IMetadataService metadataService, ICacheService cacheService, IHubContext<MessageHub> messageHub,
|
||||||
@ -709,7 +708,7 @@ public class ScannerService : IScannerService
|
|||||||
// Ensure we remove any files that no longer exist AND order
|
// Ensure we remove any files that no longer exist AND order
|
||||||
existingChapter.Files = existingChapter.Files
|
existingChapter.Files = existingChapter.Files
|
||||||
.Where(f => parsedInfos.Any(p => p.FullFilePath == f.FilePath))
|
.Where(f => parsedInfos.Any(p => p.FullFilePath == f.FilePath))
|
||||||
.OrderBy(f => f.FilePath, _naturalSort).ToList();
|
.OrderByNatural(f => f.FilePath).ToList();
|
||||||
existingChapter.Pages = existingChapter.Files.Sum(f => f.Pages);
|
existingChapter.Pages = existingChapter.Files.Sum(f => f.Pages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user