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.Tests/TestResults/
|
||||
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 API.Comparators;
|
||||
using API.DTOs;
|
||||
using API.Extensions;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Order;
|
||||
|
||||
@ -16,9 +17,6 @@ namespace API.Benchmark
|
||||
[RankColumn]
|
||||
public class TestBenchmark
|
||||
{
|
||||
private readonly NaturalSortComparer _naturalSortComparer = new ();
|
||||
|
||||
|
||||
private static IEnumerable<VolumeDto> GenerateVolumes(int max)
|
||||
{
|
||||
var random = new Random();
|
||||
@ -50,11 +48,11 @@ namespace API.Benchmark
|
||||
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))
|
||||
{
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,13 +8,20 @@ namespace API.Tests.Comparers
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(
|
||||
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", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
|
||||
new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"}
|
||||
)]
|
||||
public void TestLogicalComparer(string[] input, string[] expected)
|
||||
[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, nc);
|
||||
Array.Sort(input, StringLogicalComparer.Compare);
|
||||
|
||||
var i = 0;
|
||||
foreach (var s in input)
|
||||
@ -24,4 +31,4 @@ namespace API.Tests.Comparers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,11 @@ namespace API.Tests.Converters
|
||||
[InlineData("daily", "0 0 * * *")]
|
||||
[InlineData("disabled", "0 0 31 2 *")]
|
||||
[InlineData("weekly", "0 0 * * 1")]
|
||||
[InlineData("", "0 0 31 2 *")]
|
||||
[InlineData("sdfgdf", "")]
|
||||
public void ConvertTest(string input, string expected)
|
||||
{
|
||||
Assert.Equal(expected, CronConverter.ConvertToCronNotation(input));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
@ -9,7 +10,7 @@ namespace API.Tests.Extensions
|
||||
{
|
||||
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()
|
||||
{
|
||||
@ -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()
|
||||
{
|
||||
@ -28,7 +29,7 @@ namespace API.Tests.Extensions
|
||||
Format = format
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void GetAnyChapterByRange_Test_ShouldBeNull()
|
||||
{
|
||||
@ -51,11 +52,11 @@ namespace API.Tests.Extensions
|
||||
};
|
||||
|
||||
var actualChapter = chapterList.GetChapterByRange(info);
|
||||
|
||||
|
||||
Assert.NotEqual(chapterList[0], actualChapter);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void GetAnyChapterByRange_Test_ShouldBeNotNull()
|
||||
{
|
||||
@ -78,9 +79,39 @@ namespace API.Tests.Extensions
|
||||
};
|
||||
|
||||
var actualChapter = chapterList.GetChapterByRange(info);
|
||||
|
||||
|
||||
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,15 +1,12 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using API.Comparators;
|
||||
using System.Linq;
|
||||
using API.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Comparers
|
||||
{
|
||||
public class NaturalSortComparerTest
|
||||
{
|
||||
private readonly NaturalSortComparer _nc = new NaturalSortComparer();
|
||||
namespace API.Tests.Extensions;
|
||||
|
||||
[Theory]
|
||||
public class EnumerableExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(
|
||||
new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
|
||||
new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"}
|
||||
@ -40,7 +37,7 @@ namespace API.Tests.Comparers
|
||||
)]
|
||||
[InlineData(
|
||||
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(
|
||||
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"}
|
||||
)]
|
||||
[InlineData(
|
||||
new[] {"!001", "001", "002"},
|
||||
new[] {"001", "002", "!001"},
|
||||
new[] {"!001", "001", "002"}
|
||||
)]
|
||||
[InlineData(
|
||||
new[] {"001", "", null},
|
||||
new[] {"", "001", null}
|
||||
new[] {"001.jpg", "002.jpg", "!001.jpg"},
|
||||
new[] {"!001.jpg", "001.jpg", "002.jpg"}
|
||||
)]
|
||||
[InlineData(
|
||||
new[] {"001", "002", "!002"},
|
||||
new[] {"!002", "001", "002"}
|
||||
)]
|
||||
[InlineData(
|
||||
new[] {"001", ""},
|
||||
new[] {"", "001"}
|
||||
)]
|
||||
[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"},
|
||||
@ -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"}
|
||||
)]
|
||||
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);
|
||||
Assert.Equal(expected, input.OrderByNatural(x => x).ToArray());
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData(
|
||||
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",}
|
||||
)]
|
||||
[InlineData(
|
||||
new[] {"001", "002", "!001"},
|
||||
new[] {"!001", "001", "002"}
|
||||
)]
|
||||
[InlineData(
|
||||
new[] {"10/001.jpg", "10.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/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;
|
||||
foreach (var s in output)
|
||||
@ -121,5 +132,4 @@ namespace API.Tests.Comparers
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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.Extensions;
|
||||
using API.Parser;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using API.Tests.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Extensions
|
||||
@ -32,6 +36,38 @@ namespace API.Tests.Extensions
|
||||
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]
|
||||
[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)]
|
||||
|
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.Helpers;
|
||||
using API.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Helpers;
|
||||
@ -287,4 +289,5 @@ public class CacheHelperTests
|
||||
};
|
||||
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.Enums;
|
||||
using API.Entities.Metadata;
|
||||
@ -28,6 +29,7 @@ namespace API.Tests.Helpers
|
||||
return new Volume()
|
||||
{
|
||||
Name = volumeNumber,
|
||||
Number = int.Parse(volumeNumber),
|
||||
Pages = 0,
|
||||
Chapters = chapters ?? new List<Chapter>()
|
||||
};
|
||||
@ -76,4 +78,4 @@ namespace API.Tests.Helpers
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -152,7 +152,7 @@ namespace API.Tests.Parser
|
||||
[InlineData("test.jpeg", true)]
|
||||
[InlineData("test.png", true)]
|
||||
[InlineData(".test.jpg", false)]
|
||||
[InlineData("!test.jpg", false)]
|
||||
[InlineData("!test.jpg", true)]
|
||||
[InlineData("test.webp", true)]
|
||||
public void IsImageTest(string filename, bool expected)
|
||||
{
|
||||
@ -188,5 +188,17 @@ namespace API.Tests.Parser
|
||||
{
|
||||
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")]
|
||||
public void FindFolderEntry(string[] files, string expected)
|
||||
{
|
||||
var foundFile = _archiveService.FindFolderEntry(files);
|
||||
var foundFile = ArchiveService.FindFolderEntry(files);
|
||||
Assert.Equal(expected, string.IsNullOrEmpty(foundFile) ? "" : foundFile);
|
||||
}
|
||||
|
||||
@ -203,48 +203,69 @@ namespace API.Tests.Services
|
||||
[InlineData("sorting.zip", "sorting.expected.jpg")]
|
||||
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 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);
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
//Assert.Equal(expectedBytes, File.ReadAllBytes(archiveService.GetCoverImage(Path.Join(testDirectory, inputFile), Path.GetFileNameWithoutExtension(inputFile) + "_output")));
|
||||
_testOutputHelper.WriteLine($"Processed in {sw.ElapsedMilliseconds} ms");
|
||||
var actualBytes = File.ReadAllBytes(archiveService.GetCoverImage(Path.Join(testDirectory, inputFile),
|
||||
Path.GetFileNameWithoutExtension(inputFile) + "_output", outputDir));
|
||||
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("Formats/One File with DB_Supported.zip")]
|
||||
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/");
|
||||
//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();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldHaveComicInfo()
|
||||
{
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
||||
var archive = Path.Join(testDirectory, "ComicInfo.zip");
|
||||
const string summaryInfo = "By all counts, Ryouta Sakamoto is a loser when he's not holed up in his room, bombing things into oblivion in his favorite online action RPG. But his very own uneventful life is blown to pieces when he's abducted and taken to an uninhabited island, where he soon learns the hard way that he's being pitted against others just like him in a explosives-riddled death match! How could this be happening? Who's putting them up to this? And why!? The name, not to mention the objective, of this very real survival game is eerily familiar to Ryouta, who has mastered its virtual counterpart-BTOOOM! Can Ryouta still come out on top when he's playing for his life!?";
|
||||
#region ShouldHaveComicInfo
|
||||
|
||||
var comicInfo = _archiveService.GetComicInfo(archive);
|
||||
Assert.NotNull(comicInfo);
|
||||
Assert.Equal(summaryInfo, comicInfo.Summary);
|
||||
}
|
||||
[Fact]
|
||||
public void ShouldHaveComicInfo()
|
||||
{
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
||||
var archive = Path.Join(testDirectory, "ComicInfo.zip");
|
||||
const string summaryInfo = "By all counts, Ryouta Sakamoto is a loser when he's not holed up in his room, bombing things into oblivion in his favorite online action RPG. But his very own uneventful life is blown to pieces when he's abducted and taken to an uninhabited island, where he soon learns the hard way that he's being pitted against others just like him in a explosives-riddled death match! How could this be happening? Who's putting them up to this? And why!? The name, not to mention the objective, of this very real survival game is eerily familiar to Ryouta, who has mastered its virtual counterpart-BTOOOM! Can Ryouta still come out on top when he's playing for his life!?";
|
||||
|
||||
[Fact]
|
||||
public void ShouldHaveComicInfo_WithAuthors()
|
||||
{
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
||||
var archive = Path.Join(testDirectory, "ComicInfo_authors.zip");
|
||||
var comicInfo = _archiveService.GetComicInfo(archive);
|
||||
Assert.NotNull(comicInfo);
|
||||
Assert.Equal(summaryInfo, comicInfo.Summary);
|
||||
}
|
||||
|
||||
var comicInfo = _archiveService.GetComicInfo(archive);
|
||||
Assert.NotNull(comicInfo);
|
||||
Assert.Equal("Junya Inoue", comicInfo.Writer);
|
||||
}
|
||||
[Fact]
|
||||
public void ShouldHaveComicInfo_WithAuthors()
|
||||
{
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
||||
var archive = Path.Join(testDirectory, "ComicInfo_authors.zip");
|
||||
|
||||
var comicInfo = _archiveService.GetComicInfo(archive);
|
||||
Assert.NotNull(comicInfo);
|
||||
Assert.Equal("Junya Inoue", comicInfo.Writer);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CanParseComicInfo
|
||||
|
||||
[Fact]
|
||||
public void CanParseComicInfo()
|
||||
@ -268,5 +289,38 @@ namespace API.Tests.Services
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
#region BookEscaping
|
||||
|
||||
[Fact]
|
||||
public void EscapeCSSImportReferencesTest()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,10 @@ using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Metadata;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Parser;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
@ -20,6 +22,40 @@ using Xunit;
|
||||
|
||||
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
|
||||
{
|
||||
private readonly ILogger<CacheService> _logger = Substitute.For<ILogger<CacheService>>();
|
||||
@ -436,5 +472,37 @@ namespace API.Tests.Services
|
||||
|
||||
#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
|
||||
|
||||
#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>();
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
foreach (var chapter in volume.Chapters)
|
||||
{
|
||||
var userProgress = ReaderService.GetUserProgressForChapter(user, chapter);
|
||||
|
||||
if (userProgress == null) continue;
|
||||
userProgress.PagesRead = 0;
|
||||
userProgress.SeriesId = markReadDto.SeriesId;
|
||||
userProgress.VolumeId = volume.Id;
|
||||
}
|
||||
_readerService.MarkChaptersAsUnread(user, markReadDto.SeriesId, volume.Chapters);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -195,10 +196,9 @@ public class VolumeRepository : IVolumeRepository
|
||||
|
||||
private static void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
||||
{
|
||||
var sorter = new NaturalSortComparer();
|
||||
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
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the MangaFormat that is common to all the files. Unknown if files are mixed (should never happen) or no infos
|
||||
/// </summary>
|
||||
/// <param name="infos"></param>
|
||||
/// <returns></returns>
|
||||
public static MangaFormat GetFormat(this IList<ParserInfo> infos)
|
||||
{
|
||||
if (infos.Count == 0) return MangaFormat.Unknown;
|
||||
return infos.DistinctBy(x => x.Format).First().Format;
|
||||
}
|
||||
// /// <summary>
|
||||
// /// Returns the MangaFormat that is common to all the files. Unknown if files are mixed (should never happen) or no infos
|
||||
// /// </summary>
|
||||
// /// <param name="infos"></param>
|
||||
// /// <returns></returns>
|
||||
// public static MangaFormat GetFormat(this IList<ParserInfo> infos)
|
||||
// {
|
||||
// if (infos.Count == 0) return MangaFormat.Unknown;
|
||||
// return infos.DistinctBy(x => x.Format).First().Format;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ public static class PathExtensions
|
||||
{
|
||||
if (string.IsNullOrEmpty(filepath)) return filepath;
|
||||
var extension = Path.GetExtension(filepath);
|
||||
if (string.IsNullOrEmpty(extension)) return filepath;
|
||||
return Path.GetFullPath(filepath.Replace(extension, string.Empty));
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ namespace API.Extensions
|
||||
public static class SeriesExtensions
|
||||
{
|
||||
/// <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>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="list"></param>
|
||||
|
@ -12,7 +12,8 @@ namespace API.Extensions
|
||||
{
|
||||
return inBookSeries
|
||||
? 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>
|
||||
|
@ -26,16 +26,16 @@ namespace API.Helpers.Converters
|
||||
return destination;
|
||||
}
|
||||
|
||||
public static string ConvertFromCronNotation(string cronNotation)
|
||||
{
|
||||
var destination = string.Empty;
|
||||
destination = cronNotation.ToLower() switch
|
||||
{
|
||||
"0 0 31 2 *" => "disabled",
|
||||
_ => destination
|
||||
};
|
||||
|
||||
return destination;
|
||||
}
|
||||
// public static string ConvertFromCronNotation(string cronNotation)
|
||||
// {
|
||||
// var destination = string.Empty;
|
||||
// destination = cronNotation.ToLower() switch
|
||||
// {
|
||||
// "0 0 31 2 *" => "disabled",
|
||||
// _ => destination
|
||||
// };
|
||||
//
|
||||
// return destination;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -916,10 +916,9 @@ namespace API.Parser
|
||||
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 ImageRegex.IsMatch(Path.GetExtension(filePath));
|
||||
return !filePath.StartsWith(".") && ImageRegex.IsMatch(Path.GetExtension(filePath));
|
||||
}
|
||||
|
||||
public static bool IsXml(string filePath)
|
||||
@ -959,7 +958,7 @@ namespace API.Parser
|
||||
/// <returns></returns>
|
||||
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)
|
||||
@ -989,5 +988,16 @@ namespace API.Parser
|
||||
if (string.IsNullOrEmpty(author)) return string.Empty;
|
||||
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);
|
||||
bool ArchiveNeedsFlattening(ZipArchive archive);
|
||||
Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder);
|
||||
string FindCoverImageFilename(string archivePath, IList<string> entryNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -124,55 +125,27 @@ namespace API.Services
|
||||
/// </summary>
|
||||
/// <param name="entryFullNames"></param>
|
||||
/// <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
|
||||
.FirstOrDefault(x => !Path.EndsInDirectorySeparator(x) && !Parser.Parser.HasBlacklistedFolderInPath(x)
|
||||
&& Parser.Parser.IsCoverImage(x)
|
||||
&& !x.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith));
|
||||
.OrderByNatural(Path.GetFileNameWithoutExtension)
|
||||
.Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)))
|
||||
.FirstOrDefault(Parser.Parser.IsCoverImage);
|
||||
|
||||
return string.IsNullOrEmpty(result) ? null : result;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="entryFullNames"></param>
|
||||
/// <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
|
||||
// because NaturalSortComparer does not work with paths and doesn't seem 001.jpg as before chapter 1/001.jpg.
|
||||
var fullNames = entryFullNames.Where(x =>!Parser.Parser.HasBlacklistedFolderInPath(x)
|
||||
&& Parser.Parser.IsImage(x)
|
||||
&& !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();
|
||||
var result = entryFullNames
|
||||
.OrderByNatural(c => c.GetFullPathWithoutExtension())
|
||||
.Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)))
|
||||
.FirstOrDefault(path => Parser.Parser.IsImage(path));
|
||||
|
||||
return string.IsNullOrEmpty(result) ? null : result;
|
||||
}
|
||||
@ -200,25 +173,24 @@ namespace API.Services
|
||||
case ArchiveLibrary.Default:
|
||||
{
|
||||
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);
|
||||
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:
|
||||
{
|
||||
using var archive = ArchiveFactory.Open(archivePath);
|
||||
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);
|
||||
|
||||
using var stream = entry.OpenEntryStream();
|
||||
|
||||
return CreateThumbnail(archivePath + " - " + entry.Key, stream, fileName, outputDirectory);
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory);
|
||||
}
|
||||
case ArchiveLibrary.NotSupported:
|
||||
_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;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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.
|
||||
@ -282,20 +266,6 @@ namespace API.Services
|
||||
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>
|
||||
/// Test if the archive path exists and an archive
|
||||
|
@ -250,11 +250,23 @@ namespace API.Services
|
||||
var imageFile = image.Attributes["src"].Value;
|
||||
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));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
image.Attributes.Remove("src");
|
||||
|
@ -170,13 +170,11 @@ namespace API.Services
|
||||
// Calculate what chapter the page belongs to
|
||||
var path = GetCachePath(chapter.Id);
|
||||
var files = _directoryService.GetFilesWithExtension(path, Parser.Parser.ImageFileExtensions);
|
||||
using var nc = new NaturalSortComparer();
|
||||
files = files
|
||||
.AsEnumerable()
|
||||
.OrderBy(Path.GetFileNameWithoutExtension, nc)
|
||||
.OrderByNatural(Path.GetFileNameWithoutExtension)
|
||||
.ToArray();
|
||||
|
||||
|
||||
if (files.Length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
|
@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services
|
||||
@ -698,8 +699,7 @@ namespace API.Services
|
||||
{
|
||||
var fileIndex = 1;
|
||||
|
||||
using var nc = new NaturalSortComparer();
|
||||
foreach (var file in directory.EnumerateFiles().OrderBy(file => file.FullName, nc))
|
||||
foreach (var file in directory.EnumerateFiles().OrderByNatural(file => file.FullName))
|
||||
{
|
||||
if (file.Directory == null) continue;
|
||||
var paddedIndex = Parser.Parser.PadZeros(directoryIndex + "");
|
||||
@ -713,8 +713,7 @@ namespace API.Services
|
||||
directoryIndex++;
|
||||
}
|
||||
|
||||
var sort = new NaturalSortComparer();
|
||||
foreach (var subDirectory in directory.EnumerateDirectories().OrderBy(d => d.FullName, sort))
|
||||
foreach (var subDirectory in directory.EnumerateDirectories().OrderByNatural(d => d.FullName))
|
||||
{
|
||||
FlattenDirectory(root, subDirectory, ref directoryIndex);
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
@ -41,7 +43,7 @@ public class ReaderService : IReaderService
|
||||
|
||||
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>
|
||||
@ -87,34 +89,28 @@ public class ReaderService : IReaderService
|
||||
{
|
||||
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||
|
||||
if (userProgress == null)
|
||||
{
|
||||
user.Progresses.Add(new AppUserProgress
|
||||
{
|
||||
PagesRead = 0,
|
||||
VolumeId = chapter.VolumeId,
|
||||
SeriesId = seriesId,
|
||||
ChapterId = chapter.Id
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
userProgress.PagesRead = 0;
|
||||
userProgress.SeriesId = seriesId;
|
||||
userProgress.VolumeId = chapter.VolumeId;
|
||||
}
|
||||
if (userProgress == null) continue;
|
||||
|
||||
userProgress.PagesRead = 0;
|
||||
userProgress.SeriesId = seriesId;
|
||||
userProgress.VolumeId = chapter.VolumeId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="user">Must have Progresses populated</param>
|
||||
/// <param name="chapter"></param>
|
||||
/// <returns></returns>
|
||||
public static AppUserProgress GetUserProgressForChapter(AppUser user, Chapter chapter)
|
||||
private static AppUserProgress GetUserProgressForChapter(AppUser user, Chapter chapter)
|
||||
{
|
||||
AppUserProgress userProgress = null;
|
||||
|
||||
if (user.Progresses == null)
|
||||
{
|
||||
throw new KavitaException("Progresses must exist on user");
|
||||
}
|
||||
try
|
||||
{
|
||||
userProgress =
|
||||
@ -236,7 +232,7 @@ public class ReaderService : IReaderService
|
||||
if (currentVolume.Number == 0)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -287,7 +283,7 @@ public class ReaderService : IReaderService
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -177,9 +177,9 @@ namespace API.Services.Tasks
|
||||
// Search all files in bookmarks/ except bookmark files and delete those
|
||||
var bookmarkDirectory =
|
||||
(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())
|
||||
.Select(b => _directoryService.FileSystem.Path.GetFullPath(_directoryService.FileSystem.Path.Join(bookmarkDirectory,
|
||||
.Select(b => Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(bookmarkDirectory,
|
||||
b.FileName)));
|
||||
|
||||
|
||||
|
@ -44,7 +44,6 @@ public class ScannerService : IScannerService
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IReadingItemService _readingItemService;
|
||||
private readonly ICacheHelper _cacheHelper;
|
||||
private readonly NaturalSortComparer _naturalSort = new ();
|
||||
|
||||
public ScannerService(IUnitOfWork unitOfWork, ILogger<ScannerService> logger,
|
||||
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
|
||||
existingChapter.Files = existingChapter.Files
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user