mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
v0.4.6 Release (#599)
* v0.4.5 Release (#539) * Bump versions by dotnet-bump-version. * Versioning Fix & Improvement (#553) * Fix UpdaterService versioning (#544) * Add "Available" to newer, non-installed updates * Add if...else logic to Available/Installed badge * Change substring to account for bigger versions (#544) * Cache BuildInfo.version into local variable (#544) * Cache BuildInfo.Version.ToString() into local variable (#544) Co-authored-by: Yovarni Yearwood <yovarni@hawser.org> * Bump versions by dotnet-bump-version. * Incognito Reader (#560) * Hooked in incognito mode into the manga reader * Bump versions by dotnet-bump-version. * Reading Lists & More (#564) * Added continous reading to the book reader. Clicking on the max pages to right of progress bar will now go to last page. * Forgot a file for continous book reading * Fixed up some code regarding transitioning between chapters. Arrows now show to represent a chapter transition. * Laid the foundation for reading lists * All foundation is laid out. Actions are wired in the UI. Backend repository is setup. Redid the migration to have ReadingList track modification so we can order them for the user. * Updated add modal to have basic skeleton * Hooked up ability to fetch reading lists from backend * Made a huge performance improvement to GetChapterIdsForSeriesAsync() by reducing a JOIN and an iteration loop. Improvement went from 2 seconds -> 200 ms. * Implemented the ability to add all chapters in a series to a reading list. * Fixed issue with adding new items to reading list not being in a logical order. Lots of work on getting all the information around the reading list view. Added some foreign keys back to chapter so delete should clean up after itself. * Added ability to open directly the series * Reading List Items now have progress attached * Hooked up list deletion and added a case where if doesn't exist on load, then redirect to library. * Lots of changes. Introduced a dashboard component for the main app. This will sit on libraries route for now and will have 3 tabs to show different sections. Moved libraries reel down to bottom as people are more likely to access recently added or in progress than explore their whole library. Note: Bundles are messed up, they need to be reoptimized and routes need to be updated. * Added pagination to the reading lists api and implemented a page to show all lists * Cleaned up old code from all-collections component so now it only handles all collections and doesn't have the old code for an individual collection * Hooked in actions and navigation on reading lists * When the user re-arranges items, they are now persisted * Implemented remove read, but performance is pretty poor. Needs to be optimized. * Lots of API fixes for adding items to a series, returning items, etc. Committing before fixing incorrect fetches of items for a readingListId. * Rewrote the joins for GetReadingListItemDtosByIdAsync() to not return extra records. * Remove bug marker now that it is fixed * Refactor update-by-series to move more of the code to a re-usable function for update-by-volume/chapter APIs * Implemented the ability to add via series, volume or chapter. * Added OPDS support for reading lists. This included adding VolumeId to the ReadingListDto. * Fixed a bug with deleting items * After we create a library inform user that a scan has started * Added some extra help information for users on directory picker, since linux users were getting confused. * Setup for the reading functionality * Fixed an issue where opening the edit series modal and pressing save without doing anything would empty collection tags. Would happen often when editing cover images. * Fixed get-next-chapter for reading list. Refactored all methods to use the new GetUserIdByUsernameAsync(), which is much faster and uses less memory. * Hooked in prev chapter for continuous reading with reading list * Hooked up the read code for manga reader and book reader to have list id passed * Manga reader now functions completely with reading lists * Implemented reading list and incognito mode into book reader * Refactored some common reading code into reader service * Added support for "Series - - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz" format that can occur with FMD2. * Implemented continuous reading with a reading list between different readers. This incurs a 3x performance hit on the book info api. * style changes. Don't emit an event if position of draggable item hasn't changed * Styling and added the edit reading list flow. * Cleaned up some extra spaces when actionables isn't shown. Lots of cleanup for promoted lists. * Refactored some filter code to a common service * Added an RBS check in getting Items for a given user. * Code smells * More smells * Bump versions by dotnet-bump-version. * Cleanup bookmarks and Reading List Items (#567) * Removed directives, ensured we delete bookmarks and reading list items when chapters are deleted. * Added parsing support for "Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 11-10" * Bump versions by dotnet-bump-version. * Performance Improvements (#568) * Refactored the performance of GetChapter/BookInfo API to have a 10x speed improvement and to use common code, rather than duplicating code. Removed an api param that is no longer needed. * Book reader now has dedicated buttons to jump to next/prev chapter as well as through page buttons * Bump versions by dotnet-bump-version. * Fixed an issue from perf tuning where I forgot to send Pages to frontend, breaking reader. (#569) * Bump versions by dotnet-bump-version. * Fixed scaling issue (#573) * Bump versions by dotnet-bump-version. * Continuous Reading for Webtoons & I Just Couldn't Stop Coding (#574) * Fixed an issue from perf tuning where I forgot to send Pages to frontend, breaking reader. * Built out continuous reading for webtoon reader. Still has some issues with triggering. * Refactored GetUserByUsernameAsync to have a new flavor and allow the caller to pass in bitwise flags for what to include. This has a get by username or id variant. Code is much cleaner and snappier as we avoid many extra joins when not needed. * Cleanup old code from UserRepository.cs * Refactored OPDS to use faster API lookups for User * Refactored more code to be cleaner and faster. * Refactored GetNext/Prev ChapterIds to ReaderService. * Refactored Repository methods to their correct entity repos. * Refactored DTOs and overall cleanup of the code. * Added ability to press 'b' to bookmark a page * On hitting last page, save progress forcing last page to be read. Adjusted logic for the top and bottom spacers for triggering next/prev chapter load * When at top or moving between chapters, scrolling down then up will now trigger page load. Show a toastr to inform the user of a change in chapter (it can be really fast to switch) * Cleaned up scroll code * Fixed an issue where loading a chapter with last page bookmarked, we'd load lastpage - 1 * Fixed last page of webtoon reader not being resumed on loading said chapter due to a difference in how max page is handled between infinite scroller and manga reader. * Removed some comments * Book reader shouldn't look at left/right tap to paginate elems for position bookmarking. Missed a few areas for saving while in incognito mode * Added a benchmark to test out a sort code * Updated the read status on reading list to use same style as other places * Refactored GetNextChapterId to bring the average response time from 1.2 seconds to 400ms. * Added a filter to add to list when there are more than 5 reading lists * Added download reading list (will be removed, just saving for later). Fixes around styling on reading lists * Removed ability to download reading lists * Tweaked the logic for infinite scroller to be much smoother loading next/prev chapter. Added a bug marker for a concurrency bug. * Updated the top spacer so that when you hit the top, you stay at the page height and can now just scroll up. * Got the logic for scrolling up. Now just need the CSS then cont infinite scroller will be working * More polishing on infinite scroller * Removed IsSpecial on volumeDto, which is not used anywhere. * Cont Reading inf scroller edition is done. * Code smells and fixed package.json explore script * Bump versions by dotnet-bump-version. * Bump versions by dotnet-bump-version. * Bugfix/scanner issue (#576) * Refactored the scanner to hopefully fix a hard to reproduce KeyNotFoundException from GetInfosByName. * Added parsing support for "A Compendium of Ghosts - 031 - The Third Story_ Part 12 (Digital) (Cobalt001)" * Bump versions by dotnet-bump-version. * Bugs, Enhancements, and Performance (#580) * Added parser case for "The Duke of Death and His Black Maid - Ch. 177 - The Ball (3).cbz" * Removed a file that is created and modified every test run. * Fixed a bad parser case for "Batman Beyond 02 (of 6) (1999)" which was consuming too many characters * Removed a lot of "Volume" parsing for Comics that don't make sense. This is prep work for the upcoming Comic Rework release. * Reworked a lot of parsing cases for comics based on naming conventions observed from releases found online. * Added a way for external scripts to use a user api key to authenticate * Fixed an issue if the manga only had one page, the bottom menu would be missing page and chapter controls. * Fixed a bug where on small phones, nav bar could overflow due to scroll to top * Tweaked a lot of regex for manga parsing to handle some cases where poorly named files, like "Vol. 03 Ch. 21" would end up parsing as Series "Vol. 03". * Even more handling of parser cases. Manga parser should be as it was but more robust to handle bad naming. * Fixed: Don't force metadata refresh on Scan Series, only on refresh metadata * Implemented the ability to automatically refresh after a series scan based on when server finishes. Remove a duplicate API call from series detail. * Removed another API call for series metadata that isn't needed. * Refactored Message creation to a factory, hardcoded strings are centralized, and RefreshSeriesMetadata sends an event and is refactored to be async. * Fixed a bug when really poorly named files are within a folder that contains the series name, fallback couldn't occur due to it being taken as root folder. Now we detect said condition and will go one level higher, resulting in potentially more I/O, but the series will not be deleted. * Added the Read in Incognito context item for Chapter cards * Skip an additional check for series summary for series that aren't EPUB or Archive formats. * Fixed an issue where cover image generation could occur due to a bad check on LastWriteTime on the underlying file. * Added some extra comic parser tests * Added a ScanLibrary event (not hooked up in UI) * Performance improvement on metadata service. Now when we scan for cover image changes, we emit when a change occurs and only then do we update parent entities (array copy). * Removed an hr from series detail and ensure we update the cover image for series when scan series finishes. * Updated the infinite scroller to use a Flags pattern for the debug mode. Updated a few logical conditions for mobile. * Removed the concurrency check on row progress as if too many calls hit the DB, it will throw, but it doesn't matter. Fixed a bad logic code which could cause scrolling after hitting the bottom of the chapter. * Ensure prefetching uses totalPages + 1 since we pass in totalPages as - 1 from manga reader * Fixed issue where last page of webtoon wouldn't be prefetched due to a < instead of <= on prefetching code * Implemented ability to send images from archives to the UI without incurring any extra memory pressure. * Dropdown menus now have a darker background * Webtoon reader now works on mobile. * Fixed how keyboard presses for up/down/left/right work with MANGA_UD reading mode. See issue #579 * Fixed cont reader for webtoons on mobile * Fixed a small issue where top spacer would too quickly switch to prev chapter * Updated user preferences to use same slider style. Removed some css that is not used. * Added comic parser case for "Saga 001 (2012) (Digital) (Empire-Zone)" * Added accessibility toggle to reading list order and aligned sliders to all use the same style. * Removed a todo for checking on new image serving code. It works great. * Fixed a missing await * Auth guard will now check if an existing toast is present giving same message before poping the toast. * Fixed alignment on phones for reading lists * Moved sorters so they aren't resused between multiple threads. Slightly higher memory footprint. * Fixed a broken unit test * Code smells * More unit test fixing * Bump versions by dotnet-bump-version. * WebP Support (#581) * Added trackby so when series scan event comes through, cards can update too * Added chapter boundary toasts on book reader * Handle closing the reader when in a reading list * Somehow the trackby save didn't happen * Fixed an issue where after opening a chapter info modal, then trying to open another in specials tab it would fail due to a pass by reference issue with our factory. * When a series update occurs, if we loose specials tab, but we were on it, reselect volumes/chapters tab * Fixed an issue where older releases would show as available, even though they were already installed. * Converted tabs within modals to use vertical orientation (except on mobile) * Implemented webp support. Only Safari does not support this format natively. MacOS users can use an alternative browser. * Refactored ScannerService and MetadataService to be fully async * Bump versions by dotnet-bump-version. * Bugfix/manga reader (#582) * Fixed an issue where you could change paging direction then switch to Up/Down mode and paging direction would still be present * Removed some code for a feature I'm not implementing anymore * Bump versions by dotnet-bump-version. * High Res images breaking due to Canvas limits (#587) * Fixed an issue where on Safari with high resolution images, the canvas wouldn't be able to render them. Now we detect high res that might break canvas on different browsers and scale them. * Removed some code no longer needed * Bump versions by dotnet-bump-version. * Foundational Cover Image Rework (#584) * Updating wording on card item when total pages is 0, to be just "Cannot Read" since it could be a non-archive file * Refactored cover images to be stored on disk. This first commit has the extraction to disk and the metadata service to handle updating when applicable. * Refactored code to have the actual save to cover image directory done by ImageService. * Implemented the ability to override cover images. * Some cleanup on Image service * Implemented the ability to cleanup old covers nightly * Added a migration to migrate existing covers to new cover image format (files). * Refactored all CoverImages to just be the filename, leaving the Join with Cover directory to higher level code. * Ensure when getting user progress, we pick the first. * Added cleanup cover images for deleted tags. Don't pull any cover images that are blank. * After series update, clear out cover image. No change on UI, but just keeps things clear before metadata refresh hits * Refactored image formats for covers to ImageService. * Fixed an issue where after refactoring how images were stored, the cleanup service was deleting them after each scan. * Changed how ShouldUpdateCoverImage works to check if file exists or not even if cover image is locked. * Fixed unit tests * Added caching back to cover images. * Caching on series as well * Code Cleanup items * Ensure when checking if a file exists in MetadataService, that we join for cover image directory. After we scan library, do one last filter to delete any series that have 0 pages total. * Catch exceptions so we don't run cover migration if this is first time run. * After a scan, only clear out the cache directory and not do a deep clean. * Implemented the ability to backup custom locked covers only. * Fixed unit tests * Trying to figure out why GA crashes when running MetadataServiceTests.cs * Some debugging on GA tests not running * Commented out tests that were causing issues in GA. * Fixed an issue where series cover images wouldn't migrate * Fixed the updating of links to actually do all series and not just locked * Bump versions by dotnet-bump-version. * Feature/image rework cleanup (#589) * Added volume migrations. Added parser case for "Chapter 63 - The Promise Made for 520 Cenz.cbr" * Added some info statements for when full library scans occur. For image apis, return the name of the file to aid in caching. * When managing users, show the current logged in user at the top of the list. Added a message when no libraries have been setup but you are trying to add a user to a library. * Bump versions by dotnet-bump-version. * Additional Memory Pressure Enhancements (#590) * Added volume migrations. Added parser case for "Chapter 63 - The Promise Made for 520 Cenz.cbr" * Added some info statements for when full library scans occur. For image apis, return the name of the file to aid in caching. * When managing users, show the current logged in user at the top of the list. Added a message when no libraries have been setup but you are trying to add a user to a library. * Removed an extra stream operation from SharpCompress cover image work. Removed an extra ToArray() from Book Reader for extracting PDF pages. * Removed the left over comment * Added parsing case for "Batman Beyond 04 (of 6) (1999)" * Removed dead code * Bump versions by dotnet-bump-version. * Added ios detection (#591) https://stackoverflow.com/questions/9038625/detect-if-device-is-ios/9039885#9039885 *Note* navigator.platform is deprecated but still seems to work for now. * Bump versions by dotnet-bump-version. * Added entry for persistent covers folder * Bump versions by dotnet-bump-version. * Bulk Operations (#596) * Implemented the ability to perform multi-selections on cards. Basic selection code is done, CSS needed and exposing actions. * Implemented a bulk selection bar. Added logic to the card on when to force show checkboxes. * Fixed some bad parsing groups and cases for Comic Chapters. * Hooked up some bulk actions on series detail page. Not hooked up to backend yet. * Fixes #593. URI Enocde library names as sometimes they can have & in them. * Implemented the ability to mark volume/chapters as read/unread. * Hooked up mark as unread with specials as well. * Add to reading list hooked up for Series Detail * Implemented ability to add multiple series to a reading list. * Implemented bulk selection for series cards * Added comments to the new code in ReaderService.cs * Implemented proper styling on bulk operation bar and integrated for collections. * Fixed an issue with shift clicking * Cleaned up css of bulk operations bar * Code cleanup * Bump versions by dotnet-bump-version. * Feature/release cleanup (#597) * Deselect all after bulk operations complete. * Implemented a way to trigger selection code on mobile. * When selection mode is active, make the clickable area the whole image. * Series detail shouldn't use gutters for card layout as it can cause skewing on mobile * Long press on card items to trigger the selection on mobile * Code cleanup * One more * Misread the code issue * Bump versions by dotnet-bump-version. * v0.4.6 Release (#598) * Bump versions by dotnet-bump-version. * Bump versions by dotnet-bump-version. Co-authored-by: Yovarni Yearwood <yyearwood@endlessgalaxy.dev> Co-authored-by: Yovarni Yearwood <yovarni@hawser.org> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: Chris Plaatjes <kizaing@gmail.com>
This commit is contained in:
parent
5cd95dd1da
commit
9aa6c87b72
1
.gitignore
vendored
1
.gitignore
vendored
@ -500,3 +500,4 @@ _output/
|
||||
API/stats/
|
||||
UI/Web/dist/
|
||||
/API.Tests/Extensions/Test Data/modified on run.txt
|
||||
/API/covers/
|
||||
|
8
API.Benchmark/ArchiveSerivceBenchmark.cs
Normal file
8
API.Benchmark/ArchiveSerivceBenchmark.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace API.Benchmark
|
||||
{
|
||||
public class ArchiveSerivceBenchmark
|
||||
{
|
||||
// Benchmark to test default GetNumberOfPages from archive
|
||||
// vs a new method where I try to open the archive and return said stream
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using API.Data;
|
||||
using API.Entities.Enums;
|
||||
using API.Interfaces.Services;
|
||||
using API.Parser;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
@ -14,7 +16,7 @@ namespace API.Benchmark
|
||||
[MemoryDiagnoser]
|
||||
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||
[RankColumn]
|
||||
[SimpleJob(launchCount: 1, warmupCount: 3, targetCount: 5, invocationCount: 100, id: "Test"), ShortRunJob]
|
||||
//[SimpleJob(launchCount: 1, warmupCount: 3, targetCount: 5, invocationCount: 100, id: "Test"), ShortRunJob]
|
||||
public class ParseScannedFilesBenchmarks
|
||||
{
|
||||
private readonly ParseScannedFiles _parseScannedFiles;
|
||||
@ -27,14 +29,37 @@ namespace API.Benchmark
|
||||
_parseScannedFiles = new ParseScannedFiles(bookService, _logger);
|
||||
}
|
||||
|
||||
// [Benchmark]
|
||||
// public void Test()
|
||||
// {
|
||||
// var libraryPath = Path.Join(Directory.GetCurrentDirectory(),
|
||||
// "../../../Services/Test Data/ScannerService/Manga");
|
||||
// var parsedSeries = _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new string[] {libraryPath},
|
||||
// out var totalFiles, out var scanElapsedTime);
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Generate a list of Series and another list with
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public void Test()
|
||||
public void MergeName()
|
||||
{
|
||||
var libraryPath = Path.Join(Directory.GetCurrentDirectory(),
|
||||
"../../../Services/Test Data/ScannerService/Manga");
|
||||
var p1 = new ParserInfo()
|
||||
{
|
||||
Chapters = "0",
|
||||
Edition = "",
|
||||
Format = MangaFormat.Archive,
|
||||
FullFilePath = Path.Join(libraryPath, "A Town Where You Live", "A_Town_Where_You_Live_v01.zip"),
|
||||
IsSpecial = false,
|
||||
Series = "A Town Where You Live",
|
||||
Title = "A Town Where You Live",
|
||||
Volumes = "1"
|
||||
};
|
||||
var parsedSeries = _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new string[] {libraryPath},
|
||||
out var totalFiles, out var scanElapsedTime);
|
||||
_parseScannedFiles.MergeName(p1);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ namespace API.Benchmark
|
||||
static void Main(string[] args)
|
||||
{
|
||||
BenchmarkRunner.Run<ParseScannedFilesBenchmarks>();
|
||||
//BenchmarkRunner.Run<TestBenchmark>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
69
API.Benchmark/TestBenchmark.cs
Normal file
69
API.Benchmark/TestBenchmark.cs
Normal file
@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Comparators;
|
||||
using API.DTOs;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Order;
|
||||
|
||||
namespace API.Benchmark
|
||||
{
|
||||
/// <summary>
|
||||
/// This is used as a scratchpad for testing
|
||||
/// </summary>
|
||||
[MemoryDiagnoser]
|
||||
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||
[RankColumn]
|
||||
public class TestBenchmark
|
||||
{
|
||||
private readonly NaturalSortComparer _naturalSortComparer = new ();
|
||||
|
||||
|
||||
private static IEnumerable<VolumeDto> GenerateVolumes(int max)
|
||||
{
|
||||
var random = new Random();
|
||||
var maxIterations = random.Next(max) + 1;
|
||||
var list = new List<VolumeDto>();
|
||||
for (var i = 0; i < maxIterations; i++)
|
||||
{
|
||||
list.Add(new VolumeDto()
|
||||
{
|
||||
Number = random.Next(10) > 5 ? 1 : 0,
|
||||
Chapters = GenerateChapters()
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static List<ChapterDto> GenerateChapters()
|
||||
{
|
||||
var list = new List<ChapterDto>();
|
||||
for (var i = 1; i < 40; i++)
|
||||
{
|
||||
list.Add(new ChapterDto()
|
||||
{
|
||||
Range = i + string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private 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();
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void TestSortSpecialChapters()
|
||||
{
|
||||
var volumes = GenerateVolumes(10);
|
||||
SortSpecialChapters(volumes);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -30,4 +30,8 @@
|
||||
<Folder Include="Services\Test Data\ScannerService\Manga" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Extensions\Test Data\modified on run.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -1,3 +0,0 @@
|
||||
This file should be modified by the unit test08/20/2021 10:26:03
|
||||
08/20/2021 10:26:29
|
||||
08/22/2021 12:39:58
|
@ -23,49 +23,63 @@ namespace API.Tests.Parser
|
||||
[InlineData("Amazing Man Comics chapter 25", "Amazing Man Comics")]
|
||||
[InlineData("Amazing Man Comics issue #25", "Amazing Man Comics")]
|
||||
[InlineData("Teen Titans v1 038 (1972) (c2c).cbr", "Teen Titans")]
|
||||
[InlineData("Batman Beyond 02 (of 6) (1999)", "Batman Beyond")]
|
||||
[InlineData("Batman Beyond - Return of the Joker (2001)", "Batman Beyond - Return of the Joker")]
|
||||
[InlineData("Invincible 033.5 - Marvel Team-Up 14 (2006) (digital) (Minutemen-Slayer)", "Invincible")]
|
||||
[InlineData("Batman Wayne Family Adventures - Ep. 001 - Moving In", "Batman Wayne Family Adventures")]
|
||||
[InlineData("Saga 001 (2012) (Digital) (Empire-Zone).cbr", "Saga")]
|
||||
[InlineData("Batman Beyond 04 (of 6) (1999)", "Batman Beyond")]
|
||||
public void ParseComicSeriesTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Parser.Parser.ParseComicSeries(filename));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("01 Spider-Man & Wolverine 01.cbr", "1")]
|
||||
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "4")]
|
||||
[InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "0")]
|
||||
[InlineData("Batman & Catwoman - Trail of the Gun 01", "1")]
|
||||
[InlineData("Batman & Daredevil - King of New York", "0")]
|
||||
[InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "1")]
|
||||
[InlineData("Batman & Robin the Teen Wonder #0", "0")]
|
||||
[InlineData("Batman & Wildcat (1 of 3)", "0")]
|
||||
[InlineData("Batman And Superman World's Finest #01", "1")]
|
||||
[InlineData("Babe 01", "1")]
|
||||
[InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", "1")]
|
||||
[InlineData("Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "1")]
|
||||
[InlineData("Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005)", "2")]
|
||||
[InlineData("Superman v1 024 (09-10 1943)", "1")]
|
||||
[InlineData("Amazing Man Comics chapter 25", "0")]
|
||||
public void ParseComicVolumeTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Parser.Parser.ParseComicVolume(filename));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("01 Spider-Man & Wolverine 01.cbr", "0")]
|
||||
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "0")]
|
||||
[InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "0")]
|
||||
[InlineData("Batman & Catwoman - Trail of the Gun 01", "0")]
|
||||
[InlineData("Batman & Daredevil - King of New York", "0")]
|
||||
[InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "0")]
|
||||
[InlineData("Batman & Robin the Teen Wonder #0", "0")]
|
||||
[InlineData("Batman & Wildcat (1 of 3)", "0")]
|
||||
[InlineData("Batman And Superman World's Finest #01", "0")]
|
||||
[InlineData("Babe 01", "0")]
|
||||
[InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", "0")]
|
||||
[InlineData("Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "1")]
|
||||
[InlineData("Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005)", "0")]
|
||||
[InlineData("Superman v1 024 (09-10 1943)", "1")]
|
||||
[InlineData("Amazing Man Comics chapter 25", "0")]
|
||||
[InlineData("Invincible 033.5 - Marvel Team-Up 14 (2006) (digital) (Minutemen-Slayer)", "0")]
|
||||
[InlineData("Cyberpunk 2077 - Trauma Team 04.cbz", "0")]
|
||||
public void ParseComicVolumeTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Parser.Parser.ParseComicVolume(filename));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("01 Spider-Man & Wolverine 01.cbr", "1")]
|
||||
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "0")]
|
||||
[InlineData("The First Asterix Frieze (WebP by Doc MaKS)", "0")]
|
||||
[InlineData("Batman & Catwoman - Trail of the Gun 01", "1")]
|
||||
[InlineData("Batman & Daredevil - King of New York", "0")]
|
||||
[InlineData("Batman & Grendel (1996) 01 - Devil's Bones", "1")]
|
||||
[InlineData("Batman & Robin the Teen Wonder #0", "0")]
|
||||
[InlineData("Batman & Wildcat (1 of 3)", "1")]
|
||||
[InlineData("Batman & Wildcat (2 of 3)", "2")]
|
||||
[InlineData("Batman And Superman World's Finest #01", "0")]
|
||||
[InlineData("Babe 01", "0")]
|
||||
[InlineData("Batman And Superman World's Finest #01", "1")]
|
||||
[InlineData("Babe 01", "1")]
|
||||
[InlineData("Scott Pilgrim 01 - Scott Pilgrim's Precious Little Life (2004)", "1")]
|
||||
[InlineData("Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "1")]
|
||||
[InlineData("Superman v1 024 (09-10 1943)", "24")]
|
||||
[InlineData("Invincible 070.5 - Invincible Returns 1 (2010) (digital) (Minutemen-InnerDemons).cbr", "70.5")]
|
||||
[InlineData("Amazing Man Comics chapter 25", "25")]
|
||||
[InlineData("Invincible 033.5 - Marvel Team-Up 14 (2006) (digital) (Minutemen-Slayer)", "33.5")]
|
||||
[InlineData("Batman Wayne Family Adventures - Ep. 014 - Moving In", "14")]
|
||||
[InlineData("Saga 001 (2012) (Digital) (Empire-Zone)", "1")]
|
||||
[InlineData("Batman Beyond 04 (of 6) (1999)", "4")]
|
||||
[InlineData("Invincible 052 (c2c) (2008) (Minutemen-TheCouple)", "52")]
|
||||
[InlineData("Y - The Last Man #001", "1")]
|
||||
public void ParseComicChapterTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Parser.Parser.ParseComicChapter(filename));
|
||||
|
@ -66,6 +66,7 @@ namespace API.Tests.Parser
|
||||
[InlineData("Noblesse - Episode 406 (52 Pages).7z", "0")]
|
||||
[InlineData("X-Men v1 #201 (September 2007).cbz", "1")]
|
||||
[InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "6")]
|
||||
[InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz", "3")]
|
||||
public void ParseVolumeTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Parser.Parser.ParseVolume(filename));
|
||||
@ -128,7 +129,6 @@ namespace API.Tests.Parser
|
||||
[InlineData("Fullmetal Alchemist chapters 101-108.cbz", "Fullmetal Alchemist")]
|
||||
[InlineData("To Love Ru v09 Uncensored (Ch.071-079).cbz", "To Love Ru")]
|
||||
[InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "One Piece - Digital Colored Comics")]
|
||||
//[InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter", "Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U")]
|
||||
[InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Chapter 01", "Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U")]
|
||||
[InlineData("Vol03_ch15-22.rar", "")]
|
||||
[InlineData("Love Hina - Special.cbz", "")] // This has to be a fallback case
|
||||
@ -157,6 +157,15 @@ namespace API.Tests.Parser
|
||||
[InlineData("Killing Bites - Vol 11 Chapter 050 Save Me, Nunupi!.cbz", "Killing Bites")]
|
||||
[InlineData("Mad Chimera World - Volume 005 - Chapter 026.cbz", "Mad Chimera World")]
|
||||
[InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "Hentai Ouji to Warawanai Neko.")]
|
||||
[InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz", "The 100 Girlfriends Who Really, Really, Really, Really, Really Love You")]
|
||||
[InlineData("Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 1-10", "Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo")]
|
||||
[InlineData("The Duke of Death and His Black Maid - Ch. 177 - The Ball (3).cbz", "The Duke of Death and His Black Maid")]
|
||||
[InlineData("A Compendium of Ghosts - 031 - The Third Story_ Part 12 (Digital) (Cobalt001)", "A Compendium of Ghosts")]
|
||||
[InlineData("The Duke of Death and His Black Maid - Vol. 04 Ch. 054.5 - V4 Omake", "The Duke of Death and His Black Maid")]
|
||||
[InlineData("Vol. 04 Ch. 054.5", "")]
|
||||
[InlineData("Great_Teacher_Onizuka_v16[TheSpectrum]", "Great Teacher Onizuka")]
|
||||
[InlineData("[Renzokusei]_Kimi_wa_Midara_na_Boku_no_Joou_Ch5_Final_Chapter", "Kimi wa Midara na Boku no Joou")]
|
||||
[InlineData("Battle Royale, v01 (2000) [TokyoPop] [Manga-Sketchbook]", "Battle Royale")]
|
||||
public void ParseSeriesTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename));
|
||||
@ -226,6 +235,9 @@ namespace API.Tests.Parser
|
||||
[InlineData("Ijousha No Ai - Vol.01 Chapter 029 8 Years Ago", "29")]
|
||||
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz", "9")]
|
||||
[InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "34.5")]
|
||||
[InlineData("Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 1-10", "1-10")]
|
||||
[InlineData("Deku_&_Bakugo_-_Rising_v1_c1.1.cbz", "1.1")]
|
||||
[InlineData("Chapter 63 - The Promise Made for 520 Cenz.cbr", "63")]
|
||||
public void ParseChaptersTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Parser.Parser.ParseChapter(filename));
|
||||
@ -408,6 +420,22 @@ namespace API.Tests.Parser
|
||||
FullFilePath = filepath, IsSpecial = false
|
||||
});
|
||||
|
||||
filepath = @"E:\Manga\Kono Subarashii Sekai ni Bakuen wo!\Vol. 00 Ch. 000.cbz";
|
||||
expected.Add(filepath, new ParserInfo
|
||||
{
|
||||
Series = "Kono Subarashii Sekai ni Bakuen wo!", Volumes = "0", Edition = "",
|
||||
Chapters = "0", Filename = "Vol. 00 Ch. 000.cbz", Format = MangaFormat.Archive,
|
||||
FullFilePath = filepath, IsSpecial = false
|
||||
});
|
||||
|
||||
filepath = @"E:\Manga\Toukyou Akazukin\Vol. 01 Ch. 001.cbz";
|
||||
expected.Add(filepath, new ParserInfo
|
||||
{
|
||||
Series = "Toukyou Akazukin", Volumes = "1", Edition = "",
|
||||
Chapters = "1", Filename = "Vol. 01 Ch. 001.cbz", Format = MangaFormat.Archive,
|
||||
FullFilePath = filepath, IsSpecial = false
|
||||
});
|
||||
|
||||
// If an image is cover exclusively, ignore it
|
||||
filepath = @"E:\Manga\Seraph of the End\cover.png";
|
||||
expected.Add(filepath, null);
|
||||
|
@ -156,6 +156,7 @@ namespace API.Tests.Parser
|
||||
[InlineData("test.png", true)]
|
||||
[InlineData(".test.jpg", false)]
|
||||
[InlineData("!test.jpg", false)]
|
||||
[InlineData("test.webp", true)]
|
||||
public void IsImageTest(string filename, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, IsImage(filename));
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using API.Archive;
|
||||
using API.Interfaces.Services;
|
||||
using API.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
@ -17,11 +18,12 @@ namespace API.Tests.Services
|
||||
private readonly ArchiveService _archiveService;
|
||||
private readonly ILogger<ArchiveService> _logger = Substitute.For<ILogger<ArchiveService>>();
|
||||
private readonly ILogger<DirectoryService> _directoryServiceLogger = Substitute.For<ILogger<DirectoryService>>();
|
||||
private readonly IDirectoryService _directoryService = new DirectoryService(Substitute.For<ILogger<DirectoryService>>());
|
||||
|
||||
public ArchiveServiceTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_testOutputHelper = testOutputHelper;
|
||||
_archiveService = new ArchiveService(_logger, new DirectoryService(_directoryServiceLogger));
|
||||
_archiveService = new ArchiveService(_logger, _directoryService);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -50,7 +52,7 @@ namespace API.Tests.Services
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives");
|
||||
Assert.Equal(expected, _archiveService.IsValidArchive(Path.Join(testDirectory, archivePath)));
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData("non existent file.zip", 0)]
|
||||
[InlineData("winrar.rar", 0)]
|
||||
@ -69,7 +71,7 @@ namespace API.Tests.Services
|
||||
Assert.Equal(expected, _archiveService.GetNumberOfPagesFromArchive(Path.Join(testDirectory, archivePath)));
|
||||
_testOutputHelper.WriteLine($"Processed Original in {sw.ElapsedMilliseconds} ms");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[Theory]
|
||||
@ -84,12 +86,12 @@ namespace API.Tests.Services
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives");
|
||||
|
||||
|
||||
Assert.Equal(expected, _archiveService.CanOpen(Path.Join(testDirectory, archivePath)));
|
||||
_testOutputHelper.WriteLine($"Processed Original in {sw.ElapsedMilliseconds} ms");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData("non existent file.zip", 0)]
|
||||
[InlineData("winrar.rar", 0)]
|
||||
@ -100,18 +102,18 @@ namespace API.Tests.Services
|
||||
[InlineData("file in folder_alt.zip", 1)]
|
||||
public void CanExtractArchive(string archivePath, int expectedFileCount)
|
||||
{
|
||||
|
||||
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives");
|
||||
var extractDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives/Extraction");
|
||||
|
||||
DirectoryService.ClearAndDeleteDirectory(extractDirectory);
|
||||
|
||||
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
_archiveService.ExtractArchive(Path.Join(testDirectory, archivePath), extractDirectory);
|
||||
var di1 = new DirectoryInfo(extractDirectory);
|
||||
Assert.Equal(expectedFileCount, di1.Exists ? di1.GetFiles().Length : 0);
|
||||
_testOutputHelper.WriteLine($"Processed in {sw.ElapsedMilliseconds} ms");
|
||||
|
||||
|
||||
DirectoryService.ClearAndDeleteDirectory(extractDirectory);
|
||||
}
|
||||
|
||||
@ -142,14 +144,14 @@ namespace API.Tests.Services
|
||||
var foundFile = _archiveService.FirstFileEntry(files);
|
||||
Assert.Equal(expected, string.IsNullOrEmpty(foundFile) ? "" : foundFile);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Theory]
|
||||
|
||||
|
||||
|
||||
// TODO: This is broken on GA due to DirectoryService.CoverImageDirectory
|
||||
//[Theory]
|
||||
[InlineData("v10.cbz", "v10.expected.jpg")]
|
||||
[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.jpg")]
|
||||
[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.jpg")]
|
||||
//[InlineData("png.zip", "png.PNG")]
|
||||
[InlineData("macos_native.zip", "macos_native.jpg")]
|
||||
[InlineData("v10 - duplicate covers.cbz", "v10 - duplicate covers.expected.jpg")]
|
||||
[InlineData("sorting.zip", "sorting.expected.jpg")]
|
||||
@ -159,17 +161,29 @@ namespace API.Tests.Services
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages");
|
||||
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
||||
archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.Default);
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
Assert.Equal(expectedBytes, archiveService.GetCoverImage(Path.Join(testDirectory, inputFile)));
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
var outputDir = Path.Join(testDirectory, "output");
|
||||
DirectoryService.ClearAndDeleteDirectory(outputDir);
|
||||
DirectoryService.ExistOrCreate(outputDir);
|
||||
|
||||
|
||||
var coverImagePath = archiveService.GetCoverImage(Path.Join(testDirectory, inputFile),
|
||||
Path.GetFileNameWithoutExtension(inputFile) + "_output");
|
||||
var actual = File.ReadAllBytes(coverImagePath);
|
||||
|
||||
|
||||
Assert.Equal(expectedBytes, actual);
|
||||
_testOutputHelper.WriteLine($"Processed in {sw.ElapsedMilliseconds} ms");
|
||||
DirectoryService.ClearAndDeleteDirectory(outputDir);
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
|
||||
|
||||
// TODO: This is broken on GA due to DirectoryService.CoverImageDirectory
|
||||
//[Theory]
|
||||
[InlineData("v10.cbz", "v10.expected.jpg")]
|
||||
[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.jpg")]
|
||||
[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.jpg")]
|
||||
//[InlineData("png.zip", "png.PNG")]
|
||||
[InlineData("macos_native.zip", "macos_native.jpg")]
|
||||
[InlineData("v10 - duplicate covers.cbz", "v10 - duplicate covers.expected.jpg")]
|
||||
[InlineData("sorting.zip", "sorting.expected.jpg")]
|
||||
@ -178,20 +192,21 @@ namespace API.Tests.Services
|
||||
var archiveService = Substitute.For<ArchiveService>(_logger, new DirectoryService(_directoryServiceLogger));
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages");
|
||||
var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile));
|
||||
|
||||
|
||||
archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.SharpCompress);
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
Assert.Equal(expectedBytes, archiveService.GetCoverImage(Path.Join(testDirectory, inputFile)));
|
||||
Assert.Equal(expectedBytes, File.ReadAllBytes(archiveService.GetCoverImage(Path.Join(testDirectory, inputFile), Path.GetFileNameWithoutExtension(inputFile) + "_output")));
|
||||
_testOutputHelper.WriteLine($"Processed in {sw.ElapsedMilliseconds} ms");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// TODO: This is broken on GA due to DirectoryService.CoverImageDirectory
|
||||
//[Theory]
|
||||
[InlineData("Archives/macos_native.zip")]
|
||||
[InlineData("Formats/One File with DB_Supported.zip")]
|
||||
public void CanParseCoverImage(string inputFile)
|
||||
{
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/");
|
||||
Assert.NotEmpty(_archiveService.GetCoverImage(Path.Join(testDirectory, inputFile)));
|
||||
Assert.NotEmpty(File.ReadAllBytes(_archiveService.GetCoverImage(Path.Join(testDirectory, inputFile), Path.GetFileNameWithoutExtension(inputFile) + "_output")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -200,9 +215,9 @@ namespace API.Tests.Services
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
||||
var archive = Path.Join(testDirectory, "file in folder.zip");
|
||||
var 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!?";
|
||||
|
||||
|
||||
Assert.Equal(summaryInfo, _archiveService.GetSummaryInfo(archive));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +89,15 @@ namespace API.Tests.Services
|
||||
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new string[] {"C:/Manga/"}, new string[] {"C:/Manga/Love Hina/Vol. 01.cbz"}, "C:/Manga/Love Hina")]
|
||||
public void FindHighestDirectoriesFromFilesTest(string[] rootDirectories, string[] folders, string expectedDirectory)
|
||||
{
|
||||
var actual = DirectoryService.FindHighestDirectoriesFromFiles(rootDirectories, folders);
|
||||
var expected = new Dictionary<string, string> {{expectedDirectory, ""}};
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("C:/Manga/", "C:/Manga/Love Hina/Specials/Omake/", "Omake,Specials,Love Hina")]
|
||||
[InlineData("C:/Manga/", "C:/Manga/Love Hina/Specials/Omake", "Omake,Specials,Love Hina")]
|
||||
@ -102,6 +111,7 @@ namespace API.Tests.Services
|
||||
[InlineData(@"C:/", @"C://Btooom!/Vol.1 Chapter 2/1.cbz", "Vol.1 Chapter 2,Btooom!")]
|
||||
[InlineData(@"C:\\", @"C://Btooom!/Vol.1 Chapter 2/1.cbz", "Vol.1 Chapter 2,Btooom!")]
|
||||
[InlineData(@"C://mount/gdrive/Library/Test Library/Comics", @"C://mount/gdrive/Library/Test Library/Comics/Dragon Age/Test", "Test,Dragon Age")]
|
||||
[InlineData(@"M:\", @"M:\Toukyou Akazukin\Vol. 01 Ch. 005.cbz", @"Toukyou Akazukin")]
|
||||
public void GetFoldersTillRoot_Test(string rootPath, string fullpath, string expectedArray)
|
||||
{
|
||||
var expected = expectedArray.Split(",");
|
||||
|
@ -4,6 +4,8 @@ using API.Entities;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Services;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
@ -13,16 +15,19 @@ namespace API.Tests.Services
|
||||
public class MetadataServiceTests
|
||||
{
|
||||
private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives");
|
||||
private readonly MetadataService _metadataService;
|
||||
private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
|
||||
private readonly IImageService _imageService = Substitute.For<IImageService>();
|
||||
private readonly IBookService _bookService = Substitute.For<IBookService>();
|
||||
private readonly IArchiveService _archiveService = Substitute.For<IArchiveService>();
|
||||
private readonly ILogger<MetadataService> _logger = Substitute.For<ILogger<MetadataService>>();
|
||||
private const string TestCoverImageFile = "thumbnail.jpg";
|
||||
private readonly string _testCoverImageDirectory = Path.Join(Directory.GetCurrentDirectory(), @"../../../Services/Test Data/ArchiveService/CoverImages");
|
||||
//private readonly MetadataService _metadataService;
|
||||
// private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
|
||||
// private readonly IImageService _imageService = Substitute.For<IImageService>();
|
||||
// private readonly IBookService _bookService = Substitute.For<IBookService>();
|
||||
// private readonly IArchiveService _archiveService = Substitute.For<IArchiveService>();
|
||||
// private readonly ILogger<MetadataService> _logger = Substitute.For<ILogger<MetadataService>>();
|
||||
// private readonly IHubContext<MessageHub> _messageHub = Substitute.For<IHubContext<MessageHub>>();
|
||||
|
||||
public MetadataServiceTests()
|
||||
{
|
||||
_metadataService = new MetadataService(_unitOfWork, _logger, _archiveService, _bookService, _imageService);
|
||||
//_metadataService = new MetadataService(_unitOfWork, _logger, _archiveService, _bookService, _imageService, _messageHub);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -44,7 +49,7 @@ namespace API.Tests.Services
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldUpdateCoverImage_OnSecondRun_FileModified()
|
||||
public void ShouldUpdateCoverImage_OnFirstRun_FileModified()
|
||||
{
|
||||
// Represents first run
|
||||
Assert.True(MetadataService.ShouldUpdateCoverImage(null, new MangaFile()
|
||||
@ -55,10 +60,10 @@ namespace API.Tests.Services
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldUpdateCoverImage_OnSecondRun_CoverImageLocked()
|
||||
public void ShouldUpdateCoverImage_OnFirstRun_CoverImageLocked()
|
||||
{
|
||||
// Represents first run
|
||||
Assert.False(MetadataService.ShouldUpdateCoverImage(null, new MangaFile()
|
||||
Assert.True(MetadataService.ShouldUpdateCoverImage(null, new MangaFile()
|
||||
{
|
||||
FilePath = Path.Join(_testDirectory, "file in folder.zip"),
|
||||
LastModified = new FileInfo(Path.Join(_testDirectory, "file in folder.zip")).LastWriteTime
|
||||
@ -99,14 +104,36 @@ namespace API.Tests.Services
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldUpdateCoverImage_OnSecondRun_CoverImageSet()
|
||||
public void ShouldNotUpdateCoverImage_OnSecondRun_CoverImageSet()
|
||||
{
|
||||
// Represents first run
|
||||
Assert.False(MetadataService.ShouldUpdateCoverImage(new byte[] {1}, new MangaFile()
|
||||
Assert.False(MetadataService.ShouldUpdateCoverImage(TestCoverImageFile, new MangaFile()
|
||||
{
|
||||
FilePath = Path.Join(_testDirectory, "file in folder.zip"),
|
||||
LastModified = new FileInfo(Path.Join(_testDirectory, "file in folder.zip")).LastWriteTime
|
||||
}, false, false));
|
||||
}, false, false, _testCoverImageDirectory));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldNotUpdateCoverImage_OnSecondRun_HasCoverImage_NoForceUpdate_NoLock()
|
||||
{
|
||||
|
||||
Assert.False(MetadataService.ShouldUpdateCoverImage(TestCoverImageFile, new MangaFile()
|
||||
{
|
||||
FilePath = Path.Join(_testDirectory, "file in folder.zip"),
|
||||
LastModified = DateTime.Now
|
||||
}, false, false, _testCoverImageDirectory));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldUpdateCoverImage_OnSecondRun_HasCoverImage_NoForceUpdate_HasLock_CoverImageDoesntExist()
|
||||
{
|
||||
|
||||
Assert.True(MetadataService.ShouldUpdateCoverImage(@"doesn't_exist.jpg", new MangaFile()
|
||||
{
|
||||
FilePath = Path.Join(_testDirectory, "file in folder.zip"),
|
||||
LastModified = DateTime.Now
|
||||
}, false, true, _testCoverImageDirectory));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,10 @@ using API.Parser;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@ -34,6 +36,7 @@ namespace API.Tests.Services
|
||||
private readonly IImageService _imageService = Substitute.For<IImageService>();
|
||||
private readonly ILogger<MetadataService> _metadataLogger = Substitute.For<ILogger<MetadataService>>();
|
||||
private readonly ICacheService _cacheService = Substitute.For<ICacheService>();
|
||||
private readonly IHubContext<MessageHub> _messageHub = Substitute.For<IHubContext<MessageHub>>();
|
||||
|
||||
private readonly DbConnection _connection;
|
||||
private readonly DataContext _context;
|
||||
@ -52,8 +55,8 @@ namespace API.Tests.Services
|
||||
IUnitOfWork unitOfWork = new UnitOfWork(_context, Substitute.For<IMapper>(), null);
|
||||
|
||||
|
||||
IMetadataService metadataService = Substitute.For<MetadataService>(unitOfWork, _metadataLogger, _archiveService, _bookService, _imageService);
|
||||
_scannerService = new ScannerService(unitOfWork, _logger, _archiveService, metadataService, _bookService, _cacheService);
|
||||
IMetadataService metadataService = Substitute.For<MetadataService>(unitOfWork, _metadataLogger, _archiveService, _bookService, _imageService, _messageHub);
|
||||
_scannerService = new ScannerService(unitOfWork, _logger, _archiveService, metadataService, _bookService, _cacheService, _messageHub);
|
||||
}
|
||||
|
||||
private async Task<bool> SeedDb()
|
||||
@ -111,6 +114,7 @@ namespace API.Tests.Services
|
||||
Assert.Empty(_scannerService.FindSeriesNotOnDisk(existingSeries, infos));
|
||||
}
|
||||
|
||||
|
||||
// TODO: Figure out how to do this with ParseScannedFiles
|
||||
// [Theory]
|
||||
// [InlineData(new [] {"Darker than Black"}, "Darker than Black", "Darker than Black")]
|
||||
|
BIN
API.Tests/Services/Test Data/ImageService/cover.expected.jpg
Normal file
BIN
API.Tests/Services/Test Data/ImageService/cover.expected.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 462 KiB |
@ -22,8 +22,6 @@
|
||||
<Company>kareadita.github.io</Company>
|
||||
<Copyright>Copyright 2020-$([System.DateTime]::Now.ToString('yyyy')) kavitareader.com (GNU General Public v3)</Copyright>
|
||||
|
||||
<!-- Should be replaced by CI -->
|
||||
<AssemblyVersion>0.4.1</AssemblyVersion>
|
||||
<AssemblyConfiguration>$(Configuration)-dev</AssemblyConfiguration>
|
||||
|
||||
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
|
||||
@ -60,7 +58,7 @@
|
||||
<PackageReference Include="NetVips.Native" Version="8.11.0" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.1.2" />
|
||||
<PackageReference Include="Sentry.AspNetCore" Version="3.8.3" />
|
||||
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
||||
<PackageReference Include="SharpCompress" Version="0.29.0" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.27.0.35380">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@ -75,42 +73,41 @@
|
||||
<ProjectReference Include="..\Kavita.Common\Kavita.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Hangfire-log.db" />
|
||||
<None Remove="obj\**" />
|
||||
<None Remove="wwwroot\**" />
|
||||
<None Remove="cache\**" />
|
||||
<None Remove="backups\**" />
|
||||
<None Remove="logs\**" />
|
||||
<None Remove="temp\**" />
|
||||
<None Remove="kavita.log" />
|
||||
<None Remove="kavita.db" />
|
||||
<None Remove="covers\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Interfaces\IMetadataService.cs" />
|
||||
<Compile Remove="obj\**" />
|
||||
<Compile Remove="wwwroot\**" />
|
||||
<Compile Remove="cache\**" />
|
||||
<Compile Remove="backups\**" />
|
||||
<Compile Remove="logs\**" />
|
||||
<Compile Remove="temp\**" />
|
||||
<Compile Remove="covers\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Remove="obj\**" />
|
||||
<EmbeddedResource Remove="wwwroot\**" />
|
||||
<EmbeddedResource Remove="cache\**" />
|
||||
<EmbeddedResource Remove="backups\**" />
|
||||
<EmbeddedResource Remove="logs\**" />
|
||||
<EmbeddedResource Remove="temp\**" />
|
||||
<EmbeddedResource Remove="covers\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="obj\**" />
|
||||
<Content Remove="wwwroot\**" />
|
||||
<Content Remove="cache\**" />
|
||||
<Content Remove="backups\**" />
|
||||
<Content Remove="logs\**" />
|
||||
@ -120,6 +117,7 @@
|
||||
<Content Update="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Remove="covers\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -244,6 +242,48 @@
|
||||
<_ContentIncludedByDefault Remove="wwwroot\styles.4bd902bb3037f36f2c64.css.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\vendor.6b2a0912ae80e6fd297f.js" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\vendor.6b2a0912ae80e6fd297f.js.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\10.b727db78581442412e9a.js" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\10.b727db78581442412e9a.js.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\2.fcc031071e80d6837012.js" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\2.fcc031071e80d6837012.js.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\7.c30da7d2e809fa05d1e3.js" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\7.c30da7d2e809fa05d1e3.js.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\8.d4c77a90c95e9861656a.js" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\8.d4c77a90c95e9861656a.js.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\9.489b177dd1a6beeb35ad.js" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\9.489b177dd1a6beeb35ad.js.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\fonts\Spartan\OFL.txt" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\fonts\Spartan\Spartan-VariableFont_wght.ttf" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\icons\android-chrome-192x192.png" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\icons\android-chrome-256x256.png" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\icons\apple-touch-icon.png" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\icons\browserconfig.xml" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\icons\favicon-16x16.png" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\icons\favicon-32x32.png" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\icons\favicon.ico" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\icons\mstile-150x150.png" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\images\image-reset-cover-min.png" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\images\image-reset-cover.png" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\images\kavita-book-cropped.png" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\images\login-bg.jpg" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\assets\images\logo.png" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\common.fbf71de364f5a1f37413.js" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\common.fbf71de364f5a1f37413.js.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\login-bg.8860e6ff9d2a3598539c.jpg" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\main.a3a1e647a39145accff3.js" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\main.a3a1e647a39145accff3.js.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\polyfills.3dda3bf3d087e5d131ba.js" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\polyfills.3dda3bf3d087e5d131ba.js.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\runtime.b9818dfc90f418b3f0a7.js" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\runtime.b9818dfc90f418b3f0a7.js.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\scripts.7d1c78b2763c483bb699.js" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\scripts.7d1c78b2763c483bb699.js.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\site.webmanifest" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\Spartan-VariableFont_wght.0427aac0d980a12ae8ba.ttf" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\styles.85a58cb3e4a4b1add864.css" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\styles.85a58cb3e4a4b1add864.css.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\vendor.54bf44a9aa720ff8881d.js" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\vendor.54bf44a9aa720ff8881d.js.map" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
2
API/API.csproj.DotSettings
Normal file
2
API/API.csproj.DotSettings
Normal file
@ -0,0 +1,2 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=covers/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
@ -2,8 +2,17 @@
|
||||
|
||||
namespace API.Comparators
|
||||
{
|
||||
/// <summary>
|
||||
/// Sorts chapters based on their Number. Uses natural ordering of doubles.
|
||||
/// </summary>
|
||||
public class ChapterSortComparer : IComparer<double>
|
||||
{
|
||||
/// <summary>
|
||||
/// Normal sort for 2 doubles. 0 always comes before anything else
|
||||
/// </summary>
|
||||
/// <param name="x"></param>
|
||||
/// <param name="y"></param>
|
||||
/// <returns></returns>
|
||||
public int Compare(double x, double y)
|
||||
{
|
||||
if (x == 0.0 && y == 0.0) return 0;
|
||||
|
@ -10,7 +10,7 @@ namespace API.Comparators
|
||||
{
|
||||
private readonly bool _isAscending;
|
||||
private Dictionary<string, string[]> _table = new();
|
||||
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
|
||||
@ -23,9 +23,9 @@ namespace API.Comparators
|
||||
{
|
||||
if (x == y) return 0;
|
||||
|
||||
// Should be fixed: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.
|
||||
if (!_table.TryGetValue(x ?? Empty, out var x1))
|
||||
{
|
||||
// .Replace(" ", Empty)
|
||||
x1 = Regex.Split(x ?? Empty, "([0-9]+)");
|
||||
_table.Add(x ?? Empty, x1);
|
||||
}
|
||||
@ -33,6 +33,7 @@ namespace API.Comparators
|
||||
if (!_table.TryGetValue(y ?? Empty, out var y1))
|
||||
{
|
||||
y1 = Regex.Split(y ?? Empty, "([0-9]+)");
|
||||
// Should be fixed: EXCEPTION: An item with the same key has already been added. Key: M:\Girls of the Wild's\Girls of the Wild's - Ep. 083 (Season 1) [LINE Webtoon].cbz
|
||||
_table.Add(y ?? Empty, y1);
|
||||
}
|
||||
|
||||
@ -50,8 +51,8 @@ namespace API.Comparators
|
||||
returnVal = 1;
|
||||
}
|
||||
else if (x1.Length > y1.Length)
|
||||
{
|
||||
returnVal = -1;
|
||||
{
|
||||
returnVal = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -78,12 +79,12 @@ namespace API.Comparators
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// called via myClass.Dispose().
|
||||
// called via myClass.Dispose().
|
||||
_table.Clear();
|
||||
_table = null;
|
||||
}
|
||||
// Release unmanaged resources.
|
||||
// Set large fields to null.
|
||||
// Set large fields to null.
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
@ -93,10 +94,10 @@ namespace API.Comparators
|
||||
Dispose(true);
|
||||
SuppressFinalize(this);
|
||||
}
|
||||
|
||||
|
||||
~NaturalSortComparer() // the finalizer
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Account;
|
||||
using API.Entities;
|
||||
using API.Errors;
|
||||
using API.Extensions;
|
||||
|
@ -2,6 +2,8 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Services;
|
||||
@ -31,18 +33,36 @@ namespace API.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("{chapterId}/book-info")]
|
||||
public async Task<ActionResult<string>> GetBookInfo(int chapterId)
|
||||
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
|
||||
{
|
||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
|
||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||
var bookTitle = string.Empty;
|
||||
if (dto.SeriesFormat == MangaFormat.Epub)
|
||||
{
|
||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath);
|
||||
bookTitle = book.Title;
|
||||
}
|
||||
|
||||
return book.Title;
|
||||
return Ok(new BookInfoDto()
|
||||
{
|
||||
ChapterNumber = dto.ChapterNumber,
|
||||
VolumeNumber = dto.VolumeNumber,
|
||||
VolumeId = dto.VolumeId,
|
||||
BookTitle = bookTitle,
|
||||
SeriesName = dto.SeriesName,
|
||||
SeriesFormat = dto.SeriesFormat,
|
||||
SeriesId = dto.SeriesId,
|
||||
LibraryId = dto.LibraryId,
|
||||
IsSpecial = dto.IsSpecial,
|
||||
Pages = dto.Pages,
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{chapterId}/book-resources")]
|
||||
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
|
||||
{
|
||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
|
||||
|
||||
var key = BookService.CleanContentKeys(file);
|
||||
@ -61,7 +81,7 @@ namespace API.Controllers
|
||||
{
|
||||
// This will return a list of mappings from ID -> pagenum. ID will be the xhtml key and pagenum will be the reading order
|
||||
// this is used to rewrite anchors in the book text so that we always load properly in FE
|
||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
|
||||
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
||||
|
||||
|
@ -121,7 +121,7 @@ namespace API.Controllers
|
||||
if (!updateSeriesForTagDto.Tag.CoverImageLocked)
|
||||
{
|
||||
tag.CoverImageLocked = false;
|
||||
tag.CoverImage = Array.Empty<byte>();
|
||||
tag.CoverImage = string.Empty;
|
||||
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,6 @@ using API.Services;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
|
||||
namespace API.Controllers
|
||||
{
|
||||
@ -49,7 +48,7 @@ namespace API.Controllers
|
||||
[HttpGet("chapter-size")]
|
||||
public async Task<ActionResult<long>> GetChapterSize(int chapterId)
|
||||
{
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
return Ok(DirectoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||
}
|
||||
|
||||
@ -91,8 +90,8 @@ namespace API.Controllers
|
||||
[HttpGet("chapter")]
|
||||
public async Task<ActionResult> DownloadChapter(int chapterId)
|
||||
{
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(chapterId);
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
var volume = await _unitOfWork.SeriesRepository.GetVolumeByIdAsync(chapter.VolumeId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
try
|
||||
@ -155,7 +154,7 @@ namespace API.Controllers
|
||||
var chapterExtractPath = Path.Join(fullExtractPath, $"{series.Id}_bookmark_{chapterId}");
|
||||
var chapterPages = downloadBookmarkDto.Bookmarks.Where(b => b.ChapterId == chapterId)
|
||||
.Select(b => b.Page).ToList();
|
||||
var mangaFiles = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||
var mangaFiles = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
switch (series.Format)
|
||||
{
|
||||
case MangaFormat.Image:
|
||||
|
@ -1,7 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using API.Extensions;
|
||||
using API.Interfaces;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace API.Controllers
|
||||
{
|
||||
@ -10,7 +15,6 @@ namespace API.Controllers
|
||||
/// </summary>
|
||||
public class ImageController : BaseApiController
|
||||
{
|
||||
private const string Format = "jpeg";
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -27,11 +31,12 @@ namespace API.Controllers
|
||||
[HttpGet("chapter-cover")]
|
||||
public async Task<ActionResult> GetChapterCoverImage(int chapterId)
|
||||
{
|
||||
var content = await _unitOfWork.VolumeRepository.GetChapterCoverImageAsync(chapterId);
|
||||
if (content == null) return BadRequest("No cover image");
|
||||
var path = Path.Join(DirectoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
Response.AddCacheHeader(content);
|
||||
return File(content, "image/" + Format, $"{chapterId}");
|
||||
Response.AddCacheHeader(path);
|
||||
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -42,11 +47,12 @@ namespace API.Controllers
|
||||
[HttpGet("volume-cover")]
|
||||
public async Task<ActionResult> GetVolumeCoverImage(int volumeId)
|
||||
{
|
||||
var content = await _unitOfWork.SeriesRepository.GetVolumeCoverImageAsync(volumeId);
|
||||
if (content == null) return BadRequest("No cover image");
|
||||
var path = Path.Join(DirectoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
Response.AddCacheHeader(content);
|
||||
return File(content, "image/" + Format, $"{volumeId}");
|
||||
Response.AddCacheHeader(path);
|
||||
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -57,11 +63,12 @@ namespace API.Controllers
|
||||
[HttpGet("series-cover")]
|
||||
public async Task<ActionResult> GetSeriesCoverImage(int seriesId)
|
||||
{
|
||||
var content = await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId);
|
||||
if (content == null) return BadRequest("No cover image");
|
||||
var path = Path.Join(DirectoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId));
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
Response.AddCacheHeader(content);
|
||||
return File(content, "image/" + Format, $"{seriesId}");
|
||||
Response.AddCacheHeader(path);
|
||||
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -72,11 +79,12 @@ namespace API.Controllers
|
||||
[HttpGet("collection-cover")]
|
||||
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId)
|
||||
{
|
||||
var content = await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId);
|
||||
if (content == null) return BadRequest("No cover image");
|
||||
var path = Path.Join(DirectoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
Response.AddCacheHeader(content);
|
||||
return File(content, "image/" + Format, $"{collectionTagId}");
|
||||
Response.AddCacheHeader(path);
|
||||
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -225,11 +225,11 @@ namespace API.Controllers
|
||||
[HttpGet("search")]
|
||||
public async Task<ActionResult<IEnumerable<SearchResultDto>>> Search(string queryString)
|
||||
{
|
||||
queryString = queryString.Replace(@"%", "");
|
||||
queryString = queryString.Trim().Replace(@"%", "");
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
// Get libraries user has access to
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList();
|
||||
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
|
||||
|
@ -93,6 +93,19 @@ namespace API.Controllers
|
||||
}
|
||||
});
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "readingList",
|
||||
Title = "Reading Lists",
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = "Browse by Reading Lists"
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/reading-list"),
|
||||
}
|
||||
});
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "allLibraries",
|
||||
Title = "All Libraries",
|
||||
@ -128,8 +141,8 @@ namespace API.Controllers
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var user = await GetUser(apiKey);
|
||||
var libraries = await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id);
|
||||
var userId = await GetUser(apiKey);
|
||||
var libraries = await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId);
|
||||
|
||||
var feed = CreateFeed("All Libraries", $"{apiKey}/libraries", apiKey);
|
||||
|
||||
@ -155,7 +168,8 @@ namespace API.Controllers
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var user = await GetUser(apiKey);
|
||||
var userId = await GetUser(apiKey);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
|
||||
|
||||
IEnumerable <CollectionTagDto> tags;
|
||||
@ -190,13 +204,15 @@ namespace API.Controllers
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("{apiKey}/collections/{collectionId}")]
|
||||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetCollection(int collectionId, string apiKey, [FromQuery] int pageNumber = 0)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var user = await GetUser(apiKey);
|
||||
var userId = await GetUser(apiKey);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
var isAdmin = await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
|
||||
|
||||
IEnumerable <CollectionTagDto> tags;
|
||||
@ -215,7 +231,7 @@ namespace API.Controllers
|
||||
return BadRequest("Collection does not exist or you don't have access");
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, user.Id, new UserParams()
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, new UserParams()
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
PageSize = 20
|
||||
@ -230,6 +246,77 @@ namespace API.Controllers
|
||||
}
|
||||
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/reading-list")]
|
||||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetReadingLists(string apiKey, [FromQuery] int pageNumber = 0)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var userId = await GetUser(apiKey);
|
||||
|
||||
var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, true, new UserParams()
|
||||
{
|
||||
PageNumber = pageNumber
|
||||
});
|
||||
|
||||
|
||||
var feed = CreateFeed("All Reading Lists", $"{apiKey}/reading-list", apiKey);
|
||||
|
||||
foreach (var readingListDto in readingLists)
|
||||
{
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = readingListDto.Id.ToString(),
|
||||
Title = readingListDto.Title,
|
||||
Summary = readingListDto.Summary,
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/reading-list/{readingListDto.Id}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
||||
[HttpGet("{apiKey}/reading-list/{readingListId}")]
|
||||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetReadingListItems(int readingListId, string apiKey)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var userId = await GetUser(apiKey);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
|
||||
var userWithLists = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(user.UserName);
|
||||
var readingList = userWithLists.ReadingLists.SingleOrDefault(t => t.Id == readingListId);
|
||||
if (readingList == null)
|
||||
{
|
||||
return BadRequest("Reading list does not exist or you don't have access");
|
||||
}
|
||||
|
||||
var feed = CreateFeed(readingList.Title + " Reading List", $"{apiKey}/reading-list/{readingListId}", apiKey);
|
||||
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
|
||||
foreach (var item in items)
|
||||
{
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = item.ChapterId.ToString(),
|
||||
Title = "Chapter " + item.ChapterNumber,
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{item.SeriesId}/volume/{item.VolumeId}/chapter/{item.ChapterId}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={item.ChapterId}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
||||
@ -239,16 +326,16 @@ namespace API.Controllers
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var user = await GetUser(apiKey);
|
||||
var userId = await GetUser(apiKey);
|
||||
var library =
|
||||
(await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).SingleOrDefault(l =>
|
||||
(await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).SingleOrDefault(l =>
|
||||
l.Id == libraryId);
|
||||
if (library == null)
|
||||
{
|
||||
return BadRequest("User does not have access to this library");
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id, new UserParams()
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, new UserParams()
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
PageSize = 20
|
||||
@ -271,8 +358,8 @@ namespace API.Controllers
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var user = await GetUser(apiKey);
|
||||
var recentlyAdded = await _unitOfWork.SeriesRepository.GetRecentlyAdded(0, user.Id, new UserParams()
|
||||
var userId = await GetUser(apiKey);
|
||||
var recentlyAdded = await _unitOfWork.SeriesRepository.GetRecentlyAdded(0, userId, new UserParams()
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
PageSize = 20
|
||||
@ -296,13 +383,13 @@ namespace API.Controllers
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var user = await GetUser(apiKey);
|
||||
var userId = await GetUser(apiKey);
|
||||
var userParams = new UserParams()
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
PageSize = 20
|
||||
};
|
||||
var results = await _unitOfWork.SeriesRepository.GetInProgress(user.Id, 0, userParams, _filterDto);
|
||||
var results = await _unitOfWork.SeriesRepository.GetInProgress(userId, 0, userParams, _filterDto);
|
||||
var listResults = results.DistinctBy(s => s.Name).Skip((userParams.PageNumber - 1) * userParams.PageSize)
|
||||
.Take(userParams.PageSize).ToList();
|
||||
var pagedList = new PagedList<SeriesDto>(listResults, listResults.Count, userParams.PageNumber, userParams.PageSize);
|
||||
@ -326,14 +413,14 @@ namespace API.Controllers
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var user = await GetUser(apiKey);
|
||||
var userId = await GetUser(apiKey);
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
return BadRequest("You must pass a query parameter");
|
||||
}
|
||||
query = query.Replace(@"%", "");
|
||||
// Get libraries user has access to
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList();
|
||||
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
|
||||
@ -378,9 +465,9 @@ namespace API.Controllers
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var user = await GetUser(apiKey);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, user.Id);
|
||||
var volumes = await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, user.Id);
|
||||
var userId = await GetUser(apiKey);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var volumes = await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, userId);
|
||||
var feed = CreateFeed(series.Name + " - Volumes", $"{apiKey}/series/{series.Id}", apiKey);
|
||||
feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/series-cover?seriesId={seriesId}"));
|
||||
foreach (var volumeDto in volumes)
|
||||
@ -397,11 +484,11 @@ namespace API.Controllers
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var user = await GetUser(apiKey);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, user.Id);
|
||||
var userId = await GetUser(apiKey);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
|
||||
var chapters =
|
||||
(await _unitOfWork.VolumeRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number),
|
||||
(await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number),
|
||||
_chapterSortComparer);
|
||||
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume.Name + " - Chapters ", $"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey);
|
||||
@ -428,11 +515,11 @@ namespace API.Controllers
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var user = await GetUser(apiKey);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, user.Id);
|
||||
var userId = await GetUser(apiKey);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
|
||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterDtoAsync(chapterId);
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId);
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume.Name + " - Chapters ", $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey);
|
||||
foreach (var mangaFile in files)
|
||||
@ -456,7 +543,7 @@ namespace API.Controllers
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId);
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
var (bytes, contentType, fileDownloadName) = await _downloadService.GetFirstFileDownload(files);
|
||||
return File(bytes, contentType, fileDownloadName);
|
||||
}
|
||||
@ -544,7 +631,7 @@ namespace API.Controllers
|
||||
return new FeedEntry()
|
||||
{
|
||||
Id = volumeDto.Id.ToString(),
|
||||
Title = volumeDto.IsSpecial ? "Specials" : "Volume " + volumeDto.Name,
|
||||
Title = "Volume " + volumeDto.Name,
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{seriesId}/volume/{volumeDto.Id}"),
|
||||
@ -639,15 +726,18 @@ namespace API.Controllers
|
||||
/// Gets the user from the API key
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task<AppUser> GetUser(string apiKey)
|
||||
private async Task<int> GetUser(string apiKey)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByApiKeyAsync(apiKey);
|
||||
if (user == null)
|
||||
try
|
||||
{
|
||||
throw new KavitaException("User does not exist");
|
||||
var user = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
return user;
|
||||
}
|
||||
|
||||
return user;
|
||||
catch
|
||||
{
|
||||
/* Do nothing */
|
||||
}
|
||||
throw new KavitaException("User does not exist");
|
||||
}
|
||||
|
||||
private static FeedLink CreatePageStreamLink(int seriesId, int volumeId, int chapterId, MangaFile mangaFile, string apiKey)
|
||||
|
45
API/Controllers/PluginController.cs
Normal file
45
API/Controllers/PluginController.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers
|
||||
{
|
||||
public class PluginController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly ILogger<PluginController> _logger;
|
||||
|
||||
public PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger<PluginController> logger)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_tokenService = tokenService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticate with the Server given an apiKey. This will log you in by returning the user object and the JWT token.
|
||||
/// </summary>
|
||||
/// <param name="apiKey"></param>
|
||||
/// <param name="pluginName">Name of the Plugin</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("authenticate")]
|
||||
public async Task<ActionResult<UserDto>> Authenticate(string apiKey, string pluginName)
|
||||
{
|
||||
// NOTE: In order to log information about plugins, we need some Plugin Description information for each request
|
||||
// Should log into access table so we can tell the user
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
_logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName, user.UserName, userId);
|
||||
return new UserDto
|
||||
{
|
||||
Username = user.UserName,
|
||||
Token = await _tokenService.CreateToken(user),
|
||||
ApiKey = user.ApiKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
@ -20,20 +20,15 @@ namespace API.Controllers
|
||||
/// </summary>
|
||||
public class ReaderController : BaseApiController
|
||||
{
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<ReaderController> _logger;
|
||||
private readonly IReaderService _readerService;
|
||||
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||
private readonly NaturalSortComparer _naturalSortComparer = new NaturalSortComparer();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReaderController(IDirectoryService directoryService, ICacheService cacheService,
|
||||
public ReaderController(ICacheService cacheService,
|
||||
IUnitOfWork unitOfWork, ILogger<ReaderController> logger, IReaderService readerService)
|
||||
{
|
||||
_directoryService = directoryService;
|
||||
_cacheService = cacheService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
@ -49,7 +44,7 @@ namespace API.Controllers
|
||||
[HttpGet("image")]
|
||||
public async Task<ActionResult> GetImage(int chapterId, int page)
|
||||
{
|
||||
if (page < 0) return BadRequest("Page cannot be less than 0");
|
||||
if (page < 0) page = 0;
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
if (chapter == null) return BadRequest("There was an issue finding image file for reading");
|
||||
|
||||
@ -57,14 +52,9 @@ namespace API.Controllers
|
||||
{
|
||||
var (path, _) = await _cacheService.GetCachedPagePath(chapter, page);
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
|
||||
|
||||
var content = await _directoryService.ReadFileAsync(path);
|
||||
var format = Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
// Calculates SHA1 Hash for byte[]
|
||||
Response.AddCacheHeader(content);
|
||||
|
||||
return File(content, "image/" + format);
|
||||
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@ -76,30 +66,29 @@ namespace API.Controllers
|
||||
/// <summary>
|
||||
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter-info")]
|
||||
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int seriesId, int chapterId)
|
||||
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int chapterId)
|
||||
{
|
||||
// PERF: Write this in one DB call
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
||||
|
||||
var volume = await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(chapter.VolumeId);
|
||||
if (volume == null) return BadRequest("Could not find Volume");
|
||||
var mangaFile = (await _unitOfWork.VolumeRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
|
||||
return Ok(new ChapterInfoDto()
|
||||
{
|
||||
ChapterNumber = chapter.Range,
|
||||
VolumeNumber = volume.Number + string.Empty,
|
||||
VolumeId = volume.Id,
|
||||
ChapterNumber = dto.ChapterNumber,
|
||||
VolumeNumber = dto.VolumeNumber,
|
||||
VolumeId = dto.VolumeId,
|
||||
FileName = Path.GetFileName(mangaFile.FilePath),
|
||||
SeriesName = series?.Name,
|
||||
IsSpecial = chapter.IsSpecial,
|
||||
Pages = chapter.Pages,
|
||||
SeriesName = dto.SeriesName,
|
||||
SeriesFormat = dto.SeriesFormat,
|
||||
SeriesId = dto.SeriesId,
|
||||
LibraryId = dto.LibraryId,
|
||||
IsSpecial = dto.IsSpecial,
|
||||
Pages = dto.Pages,
|
||||
});
|
||||
}
|
||||
|
||||
@ -107,32 +96,12 @@ namespace API.Controllers
|
||||
[HttpPost("mark-read")]
|
||||
public async Task<ActionResult> MarkRead(MarkReadDto markReadDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
var volumes = await _unitOfWork.SeriesRepository.GetVolumes(markReadDto.SeriesId);
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
foreach (var chapter in volume.Chapters)
|
||||
{
|
||||
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||
|
||||
if (userProgress == null)
|
||||
{
|
||||
user.Progresses.Add(new AppUserProgress
|
||||
{
|
||||
PagesRead = chapter.Pages,
|
||||
VolumeId = volume.Id,
|
||||
SeriesId = markReadDto.SeriesId,
|
||||
ChapterId = chapter.Id
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
userProgress.PagesRead = chapter.Pages;
|
||||
userProgress.SeriesId = markReadDto.SeriesId;
|
||||
userProgress.VolumeId = volume.Id;
|
||||
}
|
||||
}
|
||||
_readerService.MarkChaptersAsRead(user, markReadDto.SeriesId, volume.Chapters);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
@ -146,47 +115,23 @@ namespace API.Controllers
|
||||
return BadRequest("There was an issue saving progress");
|
||||
}
|
||||
|
||||
private static AppUserProgress GetUserProgressForChapter(AppUser user, Chapter chapter)
|
||||
{
|
||||
AppUserProgress userProgress = null;
|
||||
try
|
||||
{
|
||||
userProgress =
|
||||
user.Progresses.SingleOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// There is a very rare chance that user progress will duplicate current row. If that happens delete one with less pages
|
||||
var progresses = user.Progresses.Where(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id).ToList();
|
||||
if (progresses.Count > 1)
|
||||
{
|
||||
user.Progresses = new List<AppUserProgress>()
|
||||
{
|
||||
user.Progresses.First()
|
||||
};
|
||||
userProgress = user.Progresses.First();
|
||||
}
|
||||
}
|
||||
|
||||
return userProgress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a Chapter as Unread (progress)
|
||||
/// Marks a Series as Unread (progress)
|
||||
/// </summary>
|
||||
/// <param name="markReadDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("mark-unread")]
|
||||
public async Task<ActionResult> MarkUnread(MarkReadDto markReadDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
var volumes = await _unitOfWork.SeriesRepository.GetVolumes(markReadDto.SeriesId);
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
foreach (var chapter in volume.Chapters)
|
||||
{
|
||||
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||
var userProgress = ReaderService.GetUserProgressForChapter(user, chapter);
|
||||
|
||||
if (userProgress == null) continue;
|
||||
userProgress.PagesRead = 0;
|
||||
@ -206,6 +151,29 @@ namespace API.Controllers
|
||||
return BadRequest("There was an issue saving progress");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks all chapters within a volume as unread
|
||||
/// </summary>
|
||||
/// <param name="markVolumeReadDto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("mark-volume-unread")]
|
||||
public async Task<ActionResult> MarkVolumeAsUnread(MarkVolumeReadDto markVolumeReadDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
|
||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||
_readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters);
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks all chapters within a volume as Read
|
||||
/// </summary>
|
||||
@ -214,30 +182,122 @@ namespace API.Controllers
|
||||
[HttpPost("mark-volume-read")]
|
||||
public async Task<ActionResult> MarkVolumeAsRead(MarkVolumeReadDto markVolumeReadDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
|
||||
var chapters = await _unitOfWork.VolumeRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||
foreach (var chapter in chapters)
|
||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||
_readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
var userProgress = user.Progresses.FirstOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
if (userProgress == null)
|
||||
{
|
||||
user.Progresses.Add(new AppUserProgress
|
||||
{
|
||||
PagesRead = chapter.Pages,
|
||||
VolumeId = markVolumeReadDto.VolumeId,
|
||||
SeriesId = markVolumeReadDto.SeriesId,
|
||||
ChapterId = chapter.Id
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
userProgress.PagesRead = chapter.Pages;
|
||||
userProgress.SeriesId = markVolumeReadDto.SeriesId;
|
||||
userProgress.VolumeId = markVolumeReadDto.VolumeId;
|
||||
}
|
||||
return BadRequest("Could not save progress");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Marks all chapters within a list of volumes as Read. All volumes must belong to the same Series.
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("mark-multiple-read")]
|
||||
public async Task<ActionResult> MarkMultipleAsRead(MarkVolumesReadDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
|
||||
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
|
||||
foreach (var chapterId in dto.ChapterIds)
|
||||
{
|
||||
chapterIds.Add(chapterId);
|
||||
}
|
||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
||||
_readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters);
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks all chapters within a list of volumes as Unread. All volumes must belong to the same Series.
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("mark-multiple-unread")]
|
||||
public async Task<ActionResult> MarkMultipleAsUnread(MarkVolumesReadDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
|
||||
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
|
||||
foreach (var chapterId in dto.ChapterIds)
|
||||
{
|
||||
chapterIds.Add(chapterId);
|
||||
}
|
||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
||||
_readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters);
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks all chapters within a list of series as Read.
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("mark-multiple-series-read")]
|
||||
public async Task<ActionResult> MarkMultipleSeriesAsRead(MarkMultipleSeriesAsReadDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
|
||||
var volumes = await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
_readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks all chapters within a list of series as Unread.
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("mark-multiple-series-unread")]
|
||||
public async Task<ActionResult> MarkMultipleSeriesAsUnread(MarkMultipleSeriesAsReadDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
|
||||
var volumes = await _unitOfWork.SeriesRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
_readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
@ -258,7 +318,7 @@ namespace API.Controllers
|
||||
[HttpGet("get-progress")]
|
||||
public async Task<ActionResult<ProgressDto>> GetProgress(int chapterId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
var progressBookmark = new ProgressDto()
|
||||
{
|
||||
PageNum = 0,
|
||||
@ -267,7 +327,7 @@ namespace API.Controllers
|
||||
SeriesId = 0
|
||||
};
|
||||
if (user.Progresses == null) return Ok(progressBookmark);
|
||||
var progress = user.Progresses.SingleOrDefault(x => x.AppUserId == user.Id && x.ChapterId == chapterId);
|
||||
var progress = user.Progresses.FirstOrDefault(x => x.AppUserId == user.Id && x.ChapterId == chapterId);
|
||||
|
||||
if (progress != null)
|
||||
{
|
||||
@ -288,7 +348,8 @@ namespace API.Controllers
|
||||
public async Task<ActionResult> BookmarkProgress(ProgressDto progressDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (await _readerService.SaveReadingProgress(progressDto, user)) return Ok(true);
|
||||
|
||||
if (await _readerService.SaveReadingProgress(progressDto, user.Id)) return Ok(true);
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
}
|
||||
@ -301,7 +362,7 @@ namespace API.Controllers
|
||||
[HttpGet("get-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(user.Id, chapterId));
|
||||
}
|
||||
@ -313,7 +374,7 @@ namespace API.Controllers
|
||||
[HttpGet("get-all-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks()
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id));
|
||||
}
|
||||
@ -326,7 +387,7 @@ namespace API.Controllers
|
||||
[HttpPost("remove-bookmarks")]
|
||||
public async Task<ActionResult> RemoveBookmarks(RemoveBookmarkForSeriesDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user.Bookmarks == null) return Ok("Nothing to remove");
|
||||
try
|
||||
{
|
||||
@ -356,7 +417,7 @@ namespace API.Controllers
|
||||
[HttpGet("get-volume-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(user.Id, volumeId));
|
||||
}
|
||||
@ -369,7 +430,7 @@ namespace API.Controllers
|
||||
[HttpGet("get-series-bookmarks")]
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(user.Id, seriesId));
|
||||
@ -383,45 +444,28 @@ namespace API.Controllers
|
||||
[HttpPost("bookmark")]
|
||||
public async Task<ActionResult> BookmarkPage(BookmarkDto bookmarkDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
|
||||
// Don't let user save past total pages.
|
||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(bookmarkDto.ChapterId);
|
||||
if (bookmarkDto.Page > chapter.Pages)
|
||||
{
|
||||
bookmarkDto.Page = chapter.Pages;
|
||||
}
|
||||
|
||||
if (bookmarkDto.Page < 0)
|
||||
{
|
||||
bookmarkDto.Page = 0;
|
||||
}
|
||||
|
||||
bookmarkDto.Page = await _readerService.CapPageToChapter(bookmarkDto.ChapterId, bookmarkDto.Page);
|
||||
|
||||
try
|
||||
{
|
||||
user.Bookmarks ??= new List<AppUserBookmark>();
|
||||
var userBookmark =
|
||||
user.Bookmarks.SingleOrDefault(x => x.ChapterId == bookmarkDto.ChapterId && x.AppUserId == user.Id && x.Page == bookmarkDto.Page);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
var userBookmark =
|
||||
await _unitOfWork.UserRepository.GetBookmarkForPage(bookmarkDto.Page, bookmarkDto.ChapterId, user.Id);
|
||||
|
||||
if (userBookmark == null)
|
||||
{
|
||||
user.Bookmarks.Add(new AppUserBookmark()
|
||||
{
|
||||
Page = bookmarkDto.Page,
|
||||
VolumeId = bookmarkDto.VolumeId,
|
||||
SeriesId = bookmarkDto.SeriesId,
|
||||
ChapterId = bookmarkDto.ChapterId,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
userBookmark.Page = bookmarkDto.Page;
|
||||
userBookmark.SeriesId = bookmarkDto.SeriesId;
|
||||
userBookmark.VolumeId = bookmarkDto.VolumeId;
|
||||
user.Bookmarks ??= new List<AppUserBookmark>();
|
||||
user.Bookmarks.Add(new AppUserBookmark()
|
||||
{
|
||||
Page = bookmarkDto.Page,
|
||||
VolumeId = bookmarkDto.VolumeId,
|
||||
SeriesId = bookmarkDto.SeriesId,
|
||||
ChapterId = bookmarkDto.ChapterId,
|
||||
});
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
@ -444,7 +488,7 @@ namespace API.Controllers
|
||||
[HttpPost("unbookmark")]
|
||||
public async Task<ActionResult> UnBookmarkPage(BookmarkDto bookmarkDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
|
||||
if (user.Bookmarks == null) return Ok();
|
||||
try {
|
||||
@ -453,7 +497,6 @@ namespace API.Controllers
|
||||
&& x.AppUserId == user.Id
|
||||
&& x.Page != bookmarkDto.Page).ToList();
|
||||
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
@ -482,58 +525,10 @@ namespace API.Controllers
|
||||
[HttpGet("next-chapter")]
|
||||
public async Task<ActionResult<int>> GetNextChapter(int seriesId, int volumeId, int currentChapterId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var volumes = await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, user.Id);
|
||||
var currentVolume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
|
||||
var currentChapter = await _unitOfWork.VolumeRepository.GetChapterAsync(currentChapterId);
|
||||
if (currentVolume.Number == 0)
|
||||
{
|
||||
// Handle specials by sorting on their Filename aka Range
|
||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Range, _naturalSortComparer), currentChapter.Number);
|
||||
if (chapterId > 0) return Ok(chapterId);
|
||||
}
|
||||
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
if (volume.Number == currentVolume.Number && volume.Chapters.Count > 1)
|
||||
{
|
||||
// Handle Chapters within current Volume
|
||||
// In this case, i need 0 first because 0 represents a full volume file.
|
||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting), currentChapter.Number);
|
||||
if (chapterId > 0) return Ok(chapterId);
|
||||
}
|
||||
|
||||
if (volume.Number == currentVolume.Number + 1)
|
||||
{
|
||||
// Handle Chapters within next Volume
|
||||
// ! When selecting the chapter for the next volume, we need to make sure a c0 comes before a c1+
|
||||
var chapters = volume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer).ToList();
|
||||
if (currentChapter.Number.Equals("0") && chapters.Last().Number.Equals("0"))
|
||||
{
|
||||
return chapters.Last().Id;
|
||||
}
|
||||
|
||||
return Ok(chapters.FirstOrDefault()?.Id);
|
||||
}
|
||||
}
|
||||
return Ok(-1);
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return await _readerService.GetNextChapterIdAsync(seriesId, volumeId, currentChapterId, userId);
|
||||
}
|
||||
|
||||
private static int GetNextChapterId(IEnumerable<Chapter> chapters, string currentChapterNumber)
|
||||
{
|
||||
var next = false;
|
||||
var chaptersList = chapters.ToList();
|
||||
foreach (var chapter in chaptersList)
|
||||
{
|
||||
if (next)
|
||||
{
|
||||
return chapter.Id;
|
||||
}
|
||||
if (currentChapterNumber.Equals(chapter.Number)) next = true;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the previous logical chapter from the series.
|
||||
@ -548,30 +543,8 @@ namespace API.Controllers
|
||||
[HttpGet("prev-chapter")]
|
||||
public async Task<ActionResult<int>> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var volumes = await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, user.Id);
|
||||
var currentVolume = await _unitOfWork.SeriesRepository.GetVolumeAsync(volumeId);
|
||||
var currentChapter = await _unitOfWork.VolumeRepository.GetChapterAsync(currentChapterId);
|
||||
|
||||
if (currentVolume.Number == 0)
|
||||
{
|
||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Range, _naturalSortComparer).Reverse(), currentChapter.Number);
|
||||
if (chapterId > 0) return Ok(chapterId);
|
||||
}
|
||||
|
||||
foreach (var volume in volumes.Reverse())
|
||||
{
|
||||
if (volume.Number == currentVolume.Number)
|
||||
{
|
||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting).Reverse(), currentChapter.Number);
|
||||
if (chapterId > 0) return Ok(chapterId);
|
||||
}
|
||||
if (volume.Number == currentVolume.Number - 1)
|
||||
{
|
||||
return Ok(volume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting).LastOrDefault()?.Id);
|
||||
}
|
||||
}
|
||||
return Ok(-1);
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return await _readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, userId);
|
||||
}
|
||||
|
||||
}
|
||||
|
503
API/Controllers/ReadingListController.cs
Normal file
503
API/Controllers/ReadingListController.cs
Normal file
@ -0,0 +1,503 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers
|
||||
{
|
||||
public class ReadingListController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||
|
||||
public ReadingListController(IUnitOfWork unitOfWork)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a single Reading List
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetList(int readingListId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns reading lists (paginated) for a given user.
|
||||
/// </summary>
|
||||
/// <param name="includePromoted">Defaults to true</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("lists")]
|
||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams, [FromQuery] bool includePromoted = true)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, includePromoted,
|
||||
userParams);
|
||||
Response.AddPaginationHeader(items.CurrentPage, items.PageSize, items.TotalCount, items.TotalPages);
|
||||
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all reading list items for a given list including rich metadata around series, volume, chapters, and progress
|
||||
/// </summary>
|
||||
/// <remarks>This call is expensive</remarks>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("items")]
|
||||
public async Task<ActionResult<IEnumerable<ReadingListItemDto>>> GetListForUser(int readingListId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
|
||||
|
||||
return Ok(await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an items position
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-position")]
|
||||
public async Task<ActionResult> UpdateListItemPosition(UpdateReadingListPosition dto)
|
||||
{
|
||||
// Make sure UI buffers events
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList();
|
||||
var item = items.Find(r => r.Id == dto.ReadingListItemId);
|
||||
items.Remove(item);
|
||||
items.Insert(dto.ToPosition, item);
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
items[i].Order = i;
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok("Updated");
|
||||
}
|
||||
|
||||
return BadRequest("Couldn't update position");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a list item from the list. Will reorder all item positions afterwards
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("delete-item")]
|
||||
public async Task<ActionResult> DeleteListItem(UpdateReadingListPosition dto)
|
||||
{
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList();
|
||||
var item = items.Find(r => r.Id == dto.ReadingListItemId);
|
||||
items.Remove(item);
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
items[i].Order = i;
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok("Updated");
|
||||
}
|
||||
|
||||
return BadRequest("Couldn't delete item");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all entries that are fully read from the reading list
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("remove-read")]
|
||||
public async Task<ActionResult> DeleteReadFromList([FromQuery] int readingListId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
|
||||
items = await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList());
|
||||
|
||||
// Collect all Ids to remove
|
||||
var itemIdsToRemove = items.Where(item => item.PagesRead == item.PagesTotal).Select(item => item.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var listItems =
|
||||
(await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).Where(r =>
|
||||
itemIdsToRemove.Contains(r.Id));
|
||||
_unitOfWork.ReadingListRepository.BulkRemove(listItems);
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok("Nothing to remove");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Could not remove read items");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a reading list
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult> DeleteList([FromQuery] int readingListId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
|
||||
var readingList = user.ReadingLists.SingleOrDefault(r => r.Id == readingListId);
|
||||
if (readingList == null)
|
||||
{
|
||||
return BadRequest("User is not associated with this reading list");
|
||||
}
|
||||
|
||||
user.ReadingLists.Remove(readingList);
|
||||
|
||||
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok("Deleted");
|
||||
}
|
||||
|
||||
return BadRequest("There was an issue deleting reading list");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new List with a unique title. Returns the new ReadingList back
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("create")]
|
||||
public async Task<ActionResult<ReadingListDto>> CreateList(CreateReadingListDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
|
||||
|
||||
// When creating, we need to make sure Title is unique
|
||||
var hasExisting = user.ReadingLists.Any(l => l.Title.Equals(dto.Title));
|
||||
if (hasExisting)
|
||||
{
|
||||
return BadRequest("A list of this name already exists");
|
||||
}
|
||||
user.ReadingLists.Add(new ReadingList()
|
||||
{
|
||||
Promoted = false,
|
||||
Title = dto.Title,
|
||||
Summary = string.Empty
|
||||
});
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return BadRequest("There was a problem creating list");
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(dto.Title));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the properites (title, summary) of a reading list
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> UpdateList(UpdateReadingListDto dto)
|
||||
{
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("List does not exist");
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.Title))
|
||||
{
|
||||
readingList.Title = dto.Title; // Should I check if this is unique?
|
||||
}
|
||||
if (!string.IsNullOrEmpty(dto.Title))
|
||||
{
|
||||
readingList.Summary = dto.Summary;
|
||||
}
|
||||
|
||||
readingList.Promoted = dto.Promoted;
|
||||
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok("Updated");
|
||||
}
|
||||
return BadRequest("Could not update reading list");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all chapters from a Series to a reading list
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-by-series")]
|
||||
public async Task<ActionResult> UpdateListBySeries(UpdateReadingListBySeriesDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
var chapterIdsForSeries =
|
||||
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new [] {dto.SeriesId});
|
||||
|
||||
// If there are adds, tell tracking this has been modified
|
||||
if (await AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList))
|
||||
{
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds all chapters from a list of volumes and chapters to a reading list
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-by-multiple")]
|
||||
public async Task<ActionResult> UpdateListByMultiple(UpdateReadingListByMultipleDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
|
||||
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
|
||||
foreach (var chapterId in dto.ChapterIds)
|
||||
{
|
||||
chapterIds.Add(chapterId);
|
||||
}
|
||||
|
||||
// If there are adds, tell tracking this has been modified
|
||||
if (await AddChaptersToReadingList(dto.SeriesId, chapterIds, readingList))
|
||||
{
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all chapters from a list of series to a reading list
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("update-by-multiple-series")]
|
||||
public async Task<ActionResult> UpdateListByMultipleSeries(UpdateReadingListByMultipleSeriesDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
|
||||
var ids = await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray());
|
||||
|
||||
foreach (var seriesId in ids.Keys)
|
||||
{
|
||||
// If there are adds, tell tracking this has been modified
|
||||
if (await AddChaptersToReadingList(seriesId, ids[seriesId], readingList))
|
||||
{
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
}
|
||||
|
||||
[HttpPost("update-by-volume")]
|
||||
public async Task<ActionResult> UpdateListByVolume(UpdateReadingListByVolumeDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
var chapterIdsForVolume =
|
||||
(await _unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList();
|
||||
|
||||
// If there are adds, tell tracking this has been modified
|
||||
if (await AddChaptersToReadingList(dto.SeriesId, chapterIdsForVolume, readingList))
|
||||
{
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
}
|
||||
|
||||
[HttpPost("update-by-chapter")]
|
||||
public async Task<ActionResult> UpdateListByChapter(UpdateReadingListByChapterDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
|
||||
// If there are adds, tell tracking this has been modified
|
||||
if (await AddChaptersToReadingList(dto.SeriesId, new List<int>() { dto.ChapterId }, readingList))
|
||||
{
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a list of Chapters as reading list items to the passed reading list.
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="chapterIds"></param>
|
||||
/// <param name="readingList"></param>
|
||||
/// <returns>True if new chapters were added</returns>
|
||||
private async Task<bool> AddChaptersToReadingList(int seriesId, IList<int> chapterIds,
|
||||
ReadingList readingList)
|
||||
{
|
||||
readingList.Items ??= new List<ReadingListItem>();
|
||||
var lastOrder = 0;
|
||||
if (readingList.Items.Any())
|
||||
{
|
||||
lastOrder = readingList.Items.DefaultIfEmpty().Max(rli => rli.Order);
|
||||
}
|
||||
|
||||
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
||||
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds))
|
||||
.OrderBy(c => int.Parse(c.Volume.Name))
|
||||
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
|
||||
|
||||
var index = lastOrder + 1;
|
||||
foreach (var chapter in chaptersForSeries)
|
||||
{
|
||||
if (existingChapterExists.Contains(chapter.Id)) continue;
|
||||
|
||||
readingList.Items.Add(new ReadingListItem()
|
||||
{
|
||||
Order = index,
|
||||
ChapterId = chapter.Id,
|
||||
SeriesId = seriesId,
|
||||
VolumeId = chapter.VolumeId
|
||||
});
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return index > lastOrder + 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next chapter within the reading list
|
||||
/// </summary>
|
||||
/// <param name="currentChapterId"></param>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns>Chapter Id for next item, -1 if nothing exists</returns>
|
||||
[HttpGet("next-chapter")]
|
||||
public async Task<ActionResult<int>> GetNextChapter(int currentChapterId, int readingListId)
|
||||
{
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
|
||||
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
|
||||
if (readingListItem == null) return BadRequest("Id does not exist");
|
||||
var index = items.IndexOf(readingListItem) + 1;
|
||||
if (items.Count > index)
|
||||
{
|
||||
return items[index].ChapterId;
|
||||
}
|
||||
|
||||
return Ok(-1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the prev chapter within the reading list
|
||||
/// </summary>
|
||||
/// <param name="currentChapterId"></param>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns>Chapter Id for next item, -1 if nothing exists</returns>
|
||||
[HttpGet("prev-chapter")]
|
||||
public async Task<ActionResult<int>> GetPrevChapter(int currentChapterId, int readingListId)
|
||||
{
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
|
||||
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
|
||||
if (readingListItem == null) return BadRequest("Id does not exist");
|
||||
var index = items.IndexOf(readingListItem) - 1;
|
||||
if (0 <= index)
|
||||
{
|
||||
return items[index].ChapterId;
|
||||
}
|
||||
|
||||
return Ok(-1);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.Entities;
|
||||
@ -32,14 +33,14 @@ namespace API.Controllers
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams, [FromBody] FilterDto filterDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, user.Id, userParams, filterDto);
|
||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest("Could not get series for library");
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series);
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
@ -55,10 +56,10 @@ namespace API.Controllers
|
||||
[HttpGet("{seriesId}")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
try
|
||||
{
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, user.Id));
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -95,30 +96,28 @@ namespace API.Controllers
|
||||
[HttpGet("volumes")]
|
||||
public async Task<ActionResult<IEnumerable<VolumeDto>>> GetVolumes(int seriesId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, user.Id));
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetVolumesDtoAsync(seriesId, userId));
|
||||
}
|
||||
|
||||
[HttpGet("volume")]
|
||||
public async Task<ActionResult<VolumeDto>> GetVolume(int volumeId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(volumeId, user.Id));
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetVolumeDtoAsync(volumeId, userId));
|
||||
}
|
||||
|
||||
[HttpGet("chapter")]
|
||||
public async Task<ActionResult<VolumeDto>> GetChapter(int chapterId)
|
||||
{
|
||||
return Ok(await _unitOfWork.VolumeRepository.GetChapterDtoAsync(chapterId));
|
||||
return Ok(await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[HttpPost("update-rating")]
|
||||
public async Task<ActionResult> UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Ratings);
|
||||
var userRating = await _unitOfWork.UserRepository.GetUserRating(updateSeriesRatingDto.SeriesId, user.Id) ??
|
||||
new AppUserRating();
|
||||
|
||||
@ -158,10 +157,12 @@ namespace API.Controllers
|
||||
series.Summary = updateSeries.Summary?.Trim();
|
||||
|
||||
var needsRefreshMetadata = false;
|
||||
// This is when you hit Reset
|
||||
if (series.CoverImageLocked && !updateSeries.CoverImageLocked)
|
||||
{
|
||||
// Trigger a refresh when we are moving from a locked image to a non-locked
|
||||
needsRefreshMetadata = true;
|
||||
series.CoverImage = string.Empty;
|
||||
series.CoverImageLocked = updateSeries.CoverImageLocked;
|
||||
}
|
||||
|
||||
@ -182,14 +183,14 @@ namespace API.Controllers
|
||||
[HttpPost("recently-added")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, user.Id, userParams, filterDto);
|
||||
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest("Could not get series");
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series);
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
@ -200,8 +201,8 @@ namespace API.Controllers
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetInProgress(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
// NOTE: This has to be done manually like this due to the DistinctBy requirement
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var results = await _unitOfWork.SeriesRepository.GetInProgress(user.Id, libraryId, userParams, filterDto);
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var results = await _unitOfWork.SeriesRepository.GetInProgress(userId, libraryId, userParams, filterDto);
|
||||
|
||||
var listResults = results.DistinctBy(s => s.Name).Skip((userParams.PageNumber - 1) * userParams.PageSize)
|
||||
.Take(userParams.PageSize).ToList();
|
||||
@ -316,14 +317,14 @@ namespace API.Controllers
|
||||
[HttpGet("series-by-collection")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetSeriesByCollectionTag(int collectionId, [FromQuery] UserParams userParams)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, user.Id, userParams);
|
||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest("Could not get series for collection");
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series);
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
|
||||
@ -339,8 +340,8 @@ namespace API.Controllers
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto)
|
||||
{
|
||||
if (dto.SeriesIds == null) return BadRequest("Must pass seriesIds");
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, user.Id));
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, userId));
|
||||
}
|
||||
|
||||
|
||||
|
@ -103,7 +103,7 @@ namespace API.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
_taskScheduler.ScheduleStatsTasks();
|
||||
await _taskScheduler.ScheduleStatsTasks();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,11 @@ using System.Threading.Tasks;
|
||||
using API.DTOs.Uploads;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Services;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
|
||||
namespace API.Controllers
|
||||
{
|
||||
@ -48,12 +50,12 @@ namespace API.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url);
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, ImageService.GetSeriesFormat(uploadFileDto.Id));
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
|
||||
|
||||
if (bytes.Length > 0)
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
series.CoverImage = bytes;
|
||||
series.CoverImage = filePath;
|
||||
series.CoverImageLocked = true;
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
}
|
||||
@ -93,12 +95,12 @@ namespace API.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url);
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id);
|
||||
|
||||
if (bytes.Length > 0)
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
tag.CoverImage = bytes;
|
||||
tag.CoverImage = filePath;
|
||||
tag.CoverImageLocked = true;
|
||||
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||
}
|
||||
@ -138,12 +140,12 @@ namespace API.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
|
||||
|
||||
if (bytes.Length > 0)
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
chapter.CoverImage = bytes;
|
||||
chapter.CoverImage = filePath;
|
||||
chapter.CoverImageLocked = true;
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(chapter.VolumeId);
|
||||
@ -178,8 +180,9 @@ namespace API.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
var chapter = await _unitOfWork.VolumeRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
chapter.CoverImage = Array.Empty<byte>();
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
var originalFile = chapter.CoverImage;
|
||||
chapter.CoverImage = string.Empty;
|
||||
chapter.CoverImageLocked = false;
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
var volume = await _unitOfWork.SeriesRepository.GetVolumeAsync(chapter.VolumeId);
|
||||
@ -190,7 +193,8 @@ namespace API.Controllers
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id);
|
||||
System.IO.File.Delete(originalFile);
|
||||
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ namespace API.Controllers
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpDelete("delete-user")]
|
||||
public async Task<ActionResult> DeleteUser(string username)
|
||||
@ -30,7 +30,7 @@ namespace API.Controllers
|
||||
|
||||
return BadRequest("Could not delete the user.");
|
||||
}
|
||||
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<MemberDto>>> GetUsers()
|
||||
@ -42,8 +42,8 @@ namespace API.Controllers
|
||||
public async Task<ActionResult<bool>> HasReadingProgress(int libraryId)
|
||||
{
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, user.Id));
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
|
||||
}
|
||||
|
||||
[HttpGet("has-library-access")]
|
||||
@ -77,8 +77,8 @@ namespace API.Controllers
|
||||
{
|
||||
return Ok(preferencesDto);
|
||||
}
|
||||
|
||||
|
||||
return BadRequest("There was an issue saving preferences.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs
|
||||
namespace API.DTOs.Account
|
||||
{
|
||||
public class ResetPasswordDto
|
||||
{
|
||||
@ -10,4 +10,4 @@ namespace API.DTOs
|
||||
[StringLength(32, MinimumLength = 6)]
|
||||
public string Password { get; init; }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using API.DTOs.Reader;
|
||||
|
||||
namespace API.DTOs.Downloads
|
||||
{
|
||||
|
@ -1,15 +0,0 @@
|
||||
namespace API.DTOs
|
||||
{
|
||||
public class ImageDto
|
||||
{
|
||||
public int Page { get; init; }
|
||||
public string Filename { get; init; }
|
||||
public string FullPath { get; init; }
|
||||
public int Width { get; init; }
|
||||
public int Height { get; init; }
|
||||
public string Format { get; init; }
|
||||
public byte[] Content { get; init; }
|
||||
public string MangaFileName { get; init; }
|
||||
public bool NeedsSplitting { get; init; }
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
namespace API.DTOs
|
||||
{
|
||||
public class InProgressChapterDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
/// <summary>
|
||||
/// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2".
|
||||
/// </summary>
|
||||
public string Range { get; init; }
|
||||
/// <summary>
|
||||
/// Smallest number of the Range.
|
||||
/// </summary>
|
||||
public string Number { get; init; }
|
||||
/// <summary>
|
||||
/// Total number of pages in all MangaFiles
|
||||
/// </summary>
|
||||
public int Pages { get; init; }
|
||||
public int SeriesId { get; init; }
|
||||
public int LibraryId { get; init; }
|
||||
public string SeriesName { get; init; }
|
||||
public int VolumeId { get; init; }
|
||||
|
||||
}
|
||||
}
|
18
API/DTOs/Reader/BookInfoDto.cs
Normal file
18
API/DTOs/Reader/BookInfoDto.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.Reader
|
||||
{
|
||||
public class BookInfoDto : IChapterInfoDto
|
||||
{
|
||||
public string BookTitle { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public int VolumeId { get; set; }
|
||||
public MangaFormat SeriesFormat { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public string ChapterNumber { get; set; }
|
||||
public string VolumeNumber { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
public int Pages { get; set; }
|
||||
public bool IsSpecial { get; set; }
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
namespace API.DTOs
|
||||
namespace API.DTOs.Reader
|
||||
{
|
||||
public class BookmarkDto
|
||||
{
|
@ -1,16 +1,21 @@
|
||||
namespace API.DTOs.Reader
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.Reader
|
||||
{
|
||||
public class ChapterInfoDto
|
||||
public class ChapterInfoDto : IChapterInfoDto
|
||||
{
|
||||
|
||||
|
||||
public string ChapterNumber { get; set; }
|
||||
public string VolumeNumber { get; set; }
|
||||
public int VolumeId { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public MangaFormat SeriesFormat { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
public string ChapterTitle { get; set; } = "";
|
||||
public int Pages { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public bool IsSpecial { get; set; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
19
API/DTOs/Reader/IChapterInfoDto.cs
Normal file
19
API/DTOs/Reader/IChapterInfoDto.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using API.Entities.Enums;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.DTOs.Reader
|
||||
{
|
||||
public interface IChapterInfoDto
|
||||
{
|
||||
public int SeriesId { get; set; }
|
||||
public int VolumeId { get; set; }
|
||||
public MangaFormat SeriesFormat { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public string ChapterNumber { get; set; }
|
||||
public string VolumeNumber { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
public int Pages { get; set; }
|
||||
public bool IsSpecial { get; set; }
|
||||
|
||||
}
|
||||
}
|
9
API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs
Normal file
9
API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs.Reader
|
||||
{
|
||||
public class MarkMultipleSeriesAsReadDto
|
||||
{
|
||||
public IReadOnlyList<int> SeriesIds { get; init; }
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
namespace API.DTOs
|
||||
namespace API.DTOs.Reader
|
||||
{
|
||||
public class MarkReadDto
|
||||
{
|
||||
public int SeriesId { get; init; }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
namespace API.DTOs
|
||||
namespace API.DTOs.Reader
|
||||
{
|
||||
public class MarkVolumeReadDto
|
||||
{
|
||||
public int SeriesId { get; init; }
|
||||
public int VolumeId { get; init; }
|
||||
}
|
||||
}
|
||||
}
|
20
API/DTOs/Reader/MarkVolumesReadDto.cs
Normal file
20
API/DTOs/Reader/MarkVolumesReadDto.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs.Reader
|
||||
{
|
||||
/// <summary>
|
||||
/// This is used for bulk updating a set of volume and or chapters in one go
|
||||
/// </summary>
|
||||
public class MarkVolumesReadDto
|
||||
{
|
||||
public int SeriesId { get; set; }
|
||||
/// <summary>
|
||||
/// A list of Volumes to mark read
|
||||
/// </summary>
|
||||
public IReadOnlyList<int> VolumeIds { get; set; }
|
||||
/// <summary>
|
||||
/// A list of additional Chapters to mark as read
|
||||
/// </summary>
|
||||
public IReadOnlyList<int> ChapterIds { get; set; }
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
namespace API.DTOs
|
||||
namespace API.DTOs.Reader
|
||||
{
|
||||
public class RemoveBookmarkForSeriesDto
|
||||
{
|
7
API/DTOs/ReadingLists/CreateReadingListDto.cs
Normal file
7
API/DTOs/ReadingLists/CreateReadingListDto.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace API.DTOs.ReadingLists
|
||||
{
|
||||
public class CreateReadingListDto
|
||||
{
|
||||
public string Title { get; init; }
|
||||
}
|
||||
}
|
13
API/DTOs/ReadingLists/ReadingListDto.cs
Normal file
13
API/DTOs/ReadingLists/ReadingListDto.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace API.DTOs.ReadingLists
|
||||
{
|
||||
public class ReadingListDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string Title { get; set; }
|
||||
public string Summary { get; set; }
|
||||
/// <summary>
|
||||
/// Reading lists that are promoted are only done by admins
|
||||
/// </summary>
|
||||
public bool Promoted { get; set; }
|
||||
}
|
||||
}
|
25
API/DTOs/ReadingLists/ReadingListItemDto.cs
Normal file
25
API/DTOs/ReadingLists/ReadingListItemDto.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.ReadingLists
|
||||
{
|
||||
public class ReadingListItemDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public int Order { get; init; }
|
||||
public int ChapterId { get; init; }
|
||||
public int SeriesId { get; init; }
|
||||
public string SeriesName { get; set; }
|
||||
public MangaFormat SeriesFormat { get; set; }
|
||||
public int PagesRead { get; set; }
|
||||
public int PagesTotal { get; set; }
|
||||
public string ChapterNumber { get; set; }
|
||||
public string VolumeNumber { get; set; }
|
||||
public int VolumeId { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
public string Title { get; set; }
|
||||
/// <summary>
|
||||
/// Used internally only
|
||||
/// </summary>
|
||||
public int ReadingListId { get; set; }
|
||||
}
|
||||
}
|
9
API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs
Normal file
9
API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace API.DTOs.ReadingLists
|
||||
{
|
||||
public class UpdateReadingListByChapterDto
|
||||
{
|
||||
public int ChapterId { get; init; }
|
||||
public int SeriesId { get; init; }
|
||||
public int ReadingListId { get; init; }
|
||||
}
|
||||
}
|
12
API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs
Normal file
12
API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs.ReadingLists
|
||||
{
|
||||
public class UpdateReadingListByMultipleDto
|
||||
{
|
||||
public int SeriesId { get; init; }
|
||||
public int ReadingListId { get; init; }
|
||||
public IReadOnlyList<int> VolumeIds { get; init; }
|
||||
public IReadOnlyList<int> ChapterIds { get; init; }
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs.ReadingLists
|
||||
{
|
||||
public class UpdateReadingListByMultipleSeriesDto
|
||||
{
|
||||
public int ReadingListId { get; init; }
|
||||
public IReadOnlyList<int> SeriesIds { get; init; }
|
||||
}
|
||||
}
|
8
API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs
Normal file
8
API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace API.DTOs.ReadingLists
|
||||
{
|
||||
public class UpdateReadingListBySeriesDto
|
||||
{
|
||||
public int SeriesId { get; init; }
|
||||
public int ReadingListId { get; init; }
|
||||
}
|
||||
}
|
9
API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs
Normal file
9
API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace API.DTOs.ReadingLists
|
||||
{
|
||||
public class UpdateReadingListByVolumeDto
|
||||
{
|
||||
public int VolumeId { get; init; }
|
||||
public int SeriesId { get; init; }
|
||||
public int ReadingListId { get; init; }
|
||||
}
|
||||
}
|
10
API/DTOs/ReadingLists/UpdateReadingListDto.cs
Normal file
10
API/DTOs/ReadingLists/UpdateReadingListDto.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace API.DTOs.ReadingLists
|
||||
{
|
||||
public class UpdateReadingListDto
|
||||
{
|
||||
public int ReadingListId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Summary { get; set; }
|
||||
public bool Promoted { get; set; }
|
||||
}
|
||||
}
|
10
API/DTOs/ReadingLists/UpdateReadingListPosition.cs
Normal file
10
API/DTOs/ReadingLists/UpdateReadingListPosition.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace API.DTOs.ReadingLists
|
||||
{
|
||||
public class UpdateReadingListPosition
|
||||
{
|
||||
public int ReadingListId { get; set; }
|
||||
public int ReadingListItemId { get; set; }
|
||||
public int FromPosition { get; set; }
|
||||
public int ToPosition { get; set; }
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ namespace API.DTOs
|
||||
public int PagesRead { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public bool IsSpecial { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public ICollection<ChapterDto> Chapters { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
namespace API.Data
|
||||
{
|
||||
public class BookmarkRepository
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
using API.Entities;
|
||||
using API.Interfaces.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data
|
||||
{
|
||||
public class ChapterRepository : IChapterRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
|
||||
public ChapterRepository(DataContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public void Update(Chapter chapter)
|
||||
{
|
||||
_context.Entry(chapter).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
// TODO: Move over Chapter based queries here
|
||||
}
|
||||
}
|
@ -35,6 +35,9 @@ namespace API.Data
|
||||
public DbSet<SeriesMetadata> SeriesMetadata { get; set; }
|
||||
public DbSet<CollectionTag> CollectionTag { get; set; }
|
||||
public DbSet<AppUserBookmark> AppUserBookmark { get; set; }
|
||||
public DbSet<ReadingList> ReadingList { get; set; }
|
||||
public DbSet<ReadingListItem> ReadingListItem { get; set; }
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
|
180
API/Data/MigrateCoverImages.cs
Normal file
180
API/Data/MigrateCoverImages.cs
Normal file
@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// A data structure to migrate Cover Images from byte[] to files.
|
||||
/// </summary>
|
||||
internal class CoverMigration
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public byte[] CoverImage { get; set; }
|
||||
public string ParentId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In v0.4.6, Cover Images were migrated from byte[] in the DB to external files. This migration handles that work.
|
||||
/// </summary>
|
||||
public static class MigrateCoverImages
|
||||
{
|
||||
private static readonly ChapterSortComparerZeroFirst ChapterSortComparerForInChapterSorting = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Run first. Will extract byte[]s from DB and write them to the cover directory.
|
||||
/// </summary>
|
||||
public static void ExtractToImages(DbContext context)
|
||||
{
|
||||
Console.WriteLine("Migrating Cover Images to disk. Expect delay.");
|
||||
DirectoryService.ExistOrCreate(DirectoryService.CoverImageDirectory);
|
||||
|
||||
Console.WriteLine("Extracting cover images for Series");
|
||||
var lockedSeries = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From Series Where CoverImage IS NOT NULL", x =>
|
||||
new CoverMigration()
|
||||
{
|
||||
Id = x[0] + string.Empty,
|
||||
CoverImage = (byte[]) x[1],
|
||||
ParentId = "0"
|
||||
});
|
||||
foreach (var series in lockedSeries)
|
||||
{
|
||||
if (series.CoverImage == null || !series.CoverImage.Any()) continue;
|
||||
if (File.Exists(Path.Join(DirectoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetSeriesFormat(int.Parse(series.Id))}.png"))) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var stream = new MemoryStream(series.CoverImage);
|
||||
stream.Position = 0;
|
||||
ImageService.WriteCoverThumbnail(stream, ImageService.GetSeriesFormat(int.Parse(series.Id)));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Extracting cover images for Chapters");
|
||||
var chapters = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage, VolumeId From Chapter Where CoverImage IS NOT NULL;", x =>
|
||||
new CoverMigration()
|
||||
{
|
||||
Id = x[0] + string.Empty,
|
||||
CoverImage = (byte[]) x[1],
|
||||
ParentId = x[2] + string.Empty
|
||||
});
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
if (chapter.CoverImage == null || !chapter.CoverImage.Any()) continue;
|
||||
if (File.Exists(Path.Join(DirectoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}.png"))) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var stream = new MemoryStream(chapter.CoverImage);
|
||||
stream.Position = 0;
|
||||
ImageService.WriteCoverThumbnail(stream, $"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Extracting cover images for Collection Tags");
|
||||
var tags = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From CollectionTag Where CoverImage IS NOT NULL;", x =>
|
||||
new CoverMigration()
|
||||
{
|
||||
Id = x[0] + string.Empty,
|
||||
CoverImage = (byte[]) x[1] ,
|
||||
ParentId = "0"
|
||||
});
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (tag.CoverImage == null || !tag.CoverImage.Any()) continue;
|
||||
if (File.Exists(Path.Join(DirectoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}.png"))) continue;
|
||||
try
|
||||
{
|
||||
var stream = new MemoryStream(tag.CoverImage);
|
||||
stream.Position = 0;
|
||||
ImageService.WriteCoverThumbnail(stream, $"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run after <see cref="ExtractToImages"/>. Will update the DB with names of files that were extracted.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
public static async Task UpdateDatabaseWithImages(DataContext context)
|
||||
{
|
||||
Console.WriteLine("Updating Series entities");
|
||||
var seriesCovers = await context.Series.Where(s => !string.IsNullOrEmpty(s.CoverImage)).ToListAsync();
|
||||
foreach (var series in seriesCovers)
|
||||
{
|
||||
if (!File.Exists(Path.Join(DirectoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetSeriesFormat(series.Id)}.png"))) continue;
|
||||
series.CoverImage = $"{ImageService.GetSeriesFormat(series.Id)}.png";
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("Updating Chapter entities");
|
||||
var chapters = await context.Chapter.ToListAsync();
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
if (File.Exists(Path.Join(DirectoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png")))
|
||||
{
|
||||
chapter.CoverImage = $"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("Updating Volume entities");
|
||||
var volumes = await context.Volume.Include(v => v.Chapters).ToListAsync();
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
var firstChapter = volume.Chapters.OrderBy(x => double.Parse(x.Number), ChapterSortComparerForInChapterSorting).FirstOrDefault();
|
||||
if (firstChapter == null) continue;
|
||||
if (File.Exists(Path.Join(DirectoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png")))
|
||||
{
|
||||
volume.CoverImage = $"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("Updating Collection Tag entities");
|
||||
var tags = await context.CollectionTag.ToListAsync();
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (File.Exists(Path.Join(DirectoryService.CoverImageDirectory,
|
||||
$"{ImageService.GetCollectionTagFormat(tag.Id)}.png")))
|
||||
{
|
||||
tag.CoverImage = $"{ImageService.GetCollectionTagFormat(tag.Id)}.png";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("Cover Image Migration completed");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
1018
API/Data/Migrations/20210901150310_ReadingLists.Designer.cs
generated
Normal file
1018
API/Data/Migrations/20210901150310_ReadingLists.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
84
API/Data/Migrations/20210901150310_ReadingLists.cs
Normal file
84
API/Data/Migrations/20210901150310_ReadingLists.cs
Normal file
@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class ReadingLists : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ReadingList",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Title = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Summary = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Promoted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
LastModified = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ReadingList", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ReadingList_AspNetUsers_AppUserId",
|
||||
column: x => x.AppUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ReadingListItem",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
LibraryId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SeriesId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
VolumeId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ChapterId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Order = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ReadingListId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ReadingListItem", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ReadingListItem_ReadingList_ReadingListId",
|
||||
column: x => x.ReadingListId,
|
||||
principalTable: "ReadingList",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ReadingList_AppUserId",
|
||||
table: "ReadingList",
|
||||
column: "AppUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ReadingListItem_ReadingListId",
|
||||
table: "ReadingListItem",
|
||||
column: "ReadingListId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ReadingListItem_SeriesId_VolumeId_ChapterId_LibraryId",
|
||||
table: "ReadingListItem",
|
||||
columns: new[] { "SeriesId", "VolumeId", "ChapterId", "LibraryId" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ReadingListItem");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ReadingList");
|
||||
}
|
||||
}
|
||||
}
|
1022
API/Data/Migrations/20210901200442_ReadingListsAdditions.Designer.cs
generated
Normal file
1022
API/Data/Migrations/20210901200442_ReadingListsAdditions.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
API/Data/Migrations/20210901200442_ReadingListsAdditions.cs
Normal file
55
API/Data/Migrations/20210901200442_ReadingListsAdditions.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class ReadingListsAdditions : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ReadingListItem_ReadingList_ReadingListId",
|
||||
table: "ReadingListItem");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "ReadingListId",
|
||||
table: "ReadingListItem",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ReadingListItem_ReadingList_ReadingListId",
|
||||
table: "ReadingListItem",
|
||||
column: "ReadingListId",
|
||||
principalTable: "ReadingList",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ReadingListItem_ReadingList_ReadingListId",
|
||||
table: "ReadingListItem");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "ReadingListId",
|
||||
table: "ReadingListItem",
|
||||
type: "INTEGER",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ReadingListItem_ReadingList_ReadingListId",
|
||||
table: "ReadingListItem",
|
||||
column: "ReadingListId",
|
||||
principalTable: "ReadingList",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
1050
API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs
generated
Normal file
1050
API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,67 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class ReadingListsExtraRealationships : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ReadingListItem_ChapterId",
|
||||
table: "ReadingListItem",
|
||||
column: "ChapterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ReadingListItem_VolumeId",
|
||||
table: "ReadingListItem",
|
||||
column: "VolumeId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ReadingListItem_Chapter_ChapterId",
|
||||
table: "ReadingListItem",
|
||||
column: "ChapterId",
|
||||
principalTable: "Chapter",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ReadingListItem_Series_SeriesId",
|
||||
table: "ReadingListItem",
|
||||
column: "SeriesId",
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ReadingListItem_Volume_VolumeId",
|
||||
table: "ReadingListItem",
|
||||
column: "VolumeId",
|
||||
principalTable: "Volume",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ReadingListItem_Chapter_ChapterId",
|
||||
table: "ReadingListItem");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ReadingListItem_Series_SeriesId",
|
||||
table: "ReadingListItem");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ReadingListItem_Volume_VolumeId",
|
||||
table: "ReadingListItem");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ReadingListItem_ChapterId",
|
||||
table: "ReadingListItem");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ReadingListItem_VolumeId",
|
||||
table: "ReadingListItem");
|
||||
}
|
||||
}
|
||||
}
|
1046
API/Data/Migrations/20210906140845_ReadingListsChanges.Designer.cs
generated
Normal file
1046
API/Data/Migrations/20210906140845_ReadingListsChanges.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
API/Data/Migrations/20210906140845_ReadingListsChanges.cs
Normal file
43
API/Data/Migrations/20210906140845_ReadingListsChanges.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class ReadingListsChanges : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ReadingListItem_SeriesId_VolumeId_ChapterId_LibraryId",
|
||||
table: "ReadingListItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LibraryId",
|
||||
table: "ReadingListItem");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ReadingListItem_SeriesId",
|
||||
table: "ReadingListItem",
|
||||
column: "SeriesId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ReadingListItem_SeriesId",
|
||||
table: "ReadingListItem");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "LibraryId",
|
||||
table: "ReadingListItem",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ReadingListItem_SeriesId_VolumeId_ChapterId_LibraryId",
|
||||
table: "ReadingListItem",
|
||||
columns: new[] { "SeriesId", "VolumeId", "ChapterId", "LibraryId" },
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
1042
API/Data/Migrations/20210916142418_EntityImageRefactor.Designer.cs
generated
Normal file
1042
API/Data/Migrations/20210916142418_EntityImageRefactor.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
97
API/Data/Migrations/20210916142418_EntityImageRefactor.cs
Normal file
97
API/Data/Migrations/20210916142418_EntityImageRefactor.cs
Normal file
@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class EntityImageRefactor : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RowVersion",
|
||||
table: "AppUserProgresses");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "CoverImage",
|
||||
table: "Volume",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(byte[]),
|
||||
oldType: "BLOB",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "CoverImage",
|
||||
table: "Series",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(byte[]),
|
||||
oldType: "BLOB",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "CoverImage",
|
||||
table: "CollectionTag",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(byte[]),
|
||||
oldType: "BLOB",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "CoverImage",
|
||||
table: "Chapter",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(byte[]),
|
||||
oldType: "BLOB",
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<byte[]>(
|
||||
name: "CoverImage",
|
||||
table: "Volume",
|
||||
type: "BLOB",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<byte[]>(
|
||||
name: "CoverImage",
|
||||
table: "Series",
|
||||
type: "BLOB",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<byte[]>(
|
||||
name: "CoverImage",
|
||||
table: "CollectionTag",
|
||||
type: "BLOB",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<byte[]>(
|
||||
name: "CoverImage",
|
||||
table: "Chapter",
|
||||
type: "BLOB",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<uint>(
|
||||
name: "RowVersion",
|
||||
table: "AppUserProgresses",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0u);
|
||||
}
|
||||
}
|
||||
}
|
@ -229,10 +229,6 @@ namespace API.Data.Migrations
|
||||
b.Property<int>("PagesRead")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@ -292,8 +288,8 @@ namespace API.Data.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("CoverImage")
|
||||
.HasColumnType("BLOB");
|
||||
b.Property<string>("CoverImage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("CoverImageLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
@ -335,8 +331,8 @@ namespace API.Data.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("CoverImage")
|
||||
.HasColumnType("BLOB");
|
||||
b.Property<string>("CoverImage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("CoverImageLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
@ -440,14 +436,79 @@ namespace API.Data.Migrations
|
||||
b.ToTable("MangaFile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingList", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Promoted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.ToTable("ReadingList");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingListItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChapterId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ReadingListId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VolumeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChapterId");
|
||||
|
||||
b.HasIndex("ReadingListId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.HasIndex("VolumeId");
|
||||
|
||||
b.ToTable("ReadingListItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("CoverImage")
|
||||
.HasColumnType("BLOB");
|
||||
b.Property<string>("CoverImage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("CoverImageLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
@ -542,8 +603,8 @@ namespace API.Data.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("CoverImage")
|
||||
.HasColumnType("BLOB");
|
||||
b.Property<string>("CoverImage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
@ -780,6 +841,52 @@ namespace API.Data.Migrations
|
||||
b.Navigation("Chapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingList", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
.WithMany("ReadingLists")
|
||||
.HasForeignKey("AppUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AppUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingListItem", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Chapter", "Chapter")
|
||||
.WithMany()
|
||||
.HasForeignKey("ChapterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.ReadingList", "ReadingList")
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("ReadingListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
.WithMany()
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Volume", "Volume")
|
||||
.WithMany()
|
||||
.HasForeignKey("VolumeId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Chapter");
|
||||
|
||||
b.Navigation("ReadingList");
|
||||
|
||||
b.Navigation("Series");
|
||||
|
||||
b.Navigation("Volume");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Library", "Library")
|
||||
@ -892,6 +999,8 @@ namespace API.Data.Migrations
|
||||
|
||||
b.Navigation("Ratings");
|
||||
|
||||
b.Navigation("ReadingLists");
|
||||
|
||||
b.Navigation("UserPreferences");
|
||||
|
||||
b.Navigation("UserRoles");
|
||||
@ -909,6 +1018,11 @@ namespace API.Data.Migrations
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingList", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
{
|
||||
b.Navigation("Metadata");
|
||||
|
@ -1,10 +1,11 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data
|
||||
namespace API.Data.Repositories
|
||||
{
|
||||
public class AppUserProgressRepository : IAppUserProgressRepository
|
||||
{
|
||||
@ -15,6 +16,11 @@ namespace API.Data
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public void Update(AppUserProgress userProgress)
|
||||
{
|
||||
_context.Entry(userProgress).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will remove any entries that have chapterIds that no longer exists. This will execute the save as well.
|
||||
/// </summary>
|
||||
@ -25,8 +31,18 @@ namespace API.Data
|
||||
var rowsToRemove = await _context.AppUserProgresses
|
||||
.Where(progress => !chapterIds.Contains(progress.ChapterId))
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
var rowsToRemoveBookmarks = await _context.AppUserBookmark
|
||||
.Where(progress => !chapterIds.Contains(progress.ChapterId))
|
||||
.ToListAsync();
|
||||
|
||||
var rowsToRemoveReadingLists = await _context.ReadingListItem
|
||||
.Where(item => !chapterIds.Contains(item.ChapterId))
|
||||
.ToListAsync();
|
||||
|
||||
_context.RemoveRange(rowsToRemove);
|
||||
_context.RemoveRange(rowsToRemoveBookmarks);
|
||||
_context.RemoveRange(rowsToRemoveReadingLists);
|
||||
return await _context.SaveChangesAsync() > 0 ? rowsToRemove.Count : 0;
|
||||
}
|
||||
|
||||
@ -45,12 +61,19 @@ namespace API.Data
|
||||
.ToListAsync();
|
||||
|
||||
if (seriesIds.Count == 0) return false;
|
||||
|
||||
|
||||
return await _context.Series
|
||||
.Include(s => s.Library)
|
||||
.Where(s => seriesIds.Contains(s.Id) && s.Library.Type == libraryType)
|
||||
.AsNoTracking()
|
||||
.AnyAsync();
|
||||
}
|
||||
|
||||
public async Task<AppUserProgress> GetUserProgressAsync(int chapterId, int userId)
|
||||
{
|
||||
return await _context.AppUserProgresses
|
||||
.Where(p => p.ChapterId == chapterId && p.AppUserId == userId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
191
API/Data/Repositories/ChapterRepository.cs
Normal file
191
API/Data/Repositories/ChapterRepository.cs
Normal file
@ -0,0 +1,191 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Interfaces.Repositories;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories
|
||||
{
|
||||
public class ChapterRepository : IChapterRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public ChapterRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public void Update(Chapter chapter)
|
||||
{
|
||||
_context.Entry(chapter).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => chapterIds.Contains(c.Id))
|
||||
.Include(c => c.Volume)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates a partial IChapterInfoDto
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<IChapterInfoDto> GetChapterInfoDtoAsync(int chapterId)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => c.Id == chapterId)
|
||||
.Join(_context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new
|
||||
{
|
||||
ChapterNumber = chapter.Range,
|
||||
VolumeNumber = volume.Number,
|
||||
VolumeId = volume.Id,
|
||||
chapter.IsSpecial,
|
||||
volume.SeriesId,
|
||||
chapter.Pages
|
||||
})
|
||||
.Join(_context.Series, data => data.SeriesId, series => series.Id, (data, series) => new
|
||||
{
|
||||
data.ChapterNumber,
|
||||
data.VolumeNumber,
|
||||
data.VolumeId,
|
||||
data.IsSpecial,
|
||||
data.SeriesId,
|
||||
data.Pages,
|
||||
SeriesFormat = series.Format,
|
||||
SeriesName = series.Name,
|
||||
series.LibraryId
|
||||
})
|
||||
.Select(data => new BookInfoDto()
|
||||
{
|
||||
ChapterNumber = data.ChapterNumber,
|
||||
VolumeNumber = data.VolumeNumber + string.Empty,
|
||||
VolumeId = data.VolumeId,
|
||||
IsSpecial = data.IsSpecial,
|
||||
SeriesId =data.SeriesId,
|
||||
SeriesFormat = data.SeriesFormat,
|
||||
SeriesName = data.SeriesName,
|
||||
LibraryId = data.LibraryId,
|
||||
Pages = data.Pages
|
||||
})
|
||||
.AsNoTracking()
|
||||
.SingleAsync();
|
||||
}
|
||||
|
||||
public Task<int> GetChapterTotalPagesAsync(int chapterId)
|
||||
{
|
||||
return _context.Chapter
|
||||
.Where(c => c.Id == chapterId)
|
||||
.Select(c => c.Pages)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
public async Task<ChapterDto> GetChapterDtoAsync(int chapterId)
|
||||
{
|
||||
var chapter = await _context.Chapter
|
||||
.Include(c => c.Files)
|
||||
.ProjectTo<ChapterDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync(c => c.Id == chapterId);
|
||||
|
||||
return chapter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns non-tracked files for a given chapterId
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<MangaFile>> GetFilesForChapterAsync(int chapterId)
|
||||
{
|
||||
return await _context.MangaFile
|
||||
.Where(c => chapterId == c.ChapterId)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a Chapter for an Id. Includes linked <see cref="MangaFile"/>s.
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Chapter> GetChapterAsync(int chapterId)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Include(c => c.Files)
|
||||
.SingleOrDefaultAsync(c => c.Id == chapterId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Chapters for a volume id.
|
||||
/// </summary>
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<Chapter>> GetChaptersAsync(int volumeId)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => c.VolumeId == volumeId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cover image for a chapter id.
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> GetChapterCoverImageAsync(int chapterId)
|
||||
{
|
||||
|
||||
return await _context.Chapter
|
||||
.Where(c => c.Id == chapterId)
|
||||
.Select(c => c.CoverImage)
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<string>> GetAllCoverImagesAsync()
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Select(c => c.CoverImage)
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns cover images for locked chapters
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<string>> GetCoverImagesForLockedChaptersAsync()
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => c.CoverImageLocked)
|
||||
.Select(c => c.CoverImage)
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns non-tracked files for a set of <paramref name="chapterIds"/>
|
||||
/// </summary>
|
||||
/// <param name="chapterIds">List of chapter Ids</param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds)
|
||||
{
|
||||
return await _context.MangaFile
|
||||
.Where(c => chapterIds.Contains(c.ChapterId))
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Repositories;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data
|
||||
namespace API.Data.Repositories
|
||||
{
|
||||
public class CollectionTagRepository : ICollectionTagRepository
|
||||
{
|
||||
@ -48,11 +49,19 @@ namespace API.Data
|
||||
public async Task<IEnumerable<CollectionTag>> GetAllTagsAsync()
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
.Select(c => c)
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<string>> GetAllCoverImagesAsync()
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
.Select(t => t.CoverImage)
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
@ -100,9 +109,9 @@ namespace API.Data
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public Task<byte[]> GetCoverImageAsync(int collectionTagId)
|
||||
public async Task<string> GetCoverImageAsync(int collectionTagId)
|
||||
{
|
||||
return _context.CollectionTag
|
||||
return await _context.CollectionTag
|
||||
.Where(c => c.Id == collectionTagId)
|
||||
.Select(c => c.CoverImage)
|
||||
.AsNoTracking()
|
@ -2,10 +2,10 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data
|
||||
namespace API.Data.Repositories
|
||||
{
|
||||
public class FileRepository : IFileRepository
|
||||
{
|
@ -4,12 +4,12 @@ using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Repositories;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data
|
||||
namespace API.Data.Repositories
|
||||
{
|
||||
public class LibraryRepository : ILibraryRepository
|
||||
{
|
178
API/Data/Repositories/ReadingListRepository.cs
Normal file
178
API/Data/Repositories/ReadingListRepository.cs
Normal file
@ -0,0 +1,178 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.Entities;
|
||||
using API.Helpers;
|
||||
using API.Interfaces.Repositories;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories
|
||||
{
|
||||
public class ReadingListRepository : IReadingListRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public ReadingListRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public void Update(ReadingList list)
|
||||
{
|
||||
_context.Entry(list).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public void Remove(ReadingListItem item)
|
||||
{
|
||||
_context.ReadingListItem.Remove(item);
|
||||
}
|
||||
|
||||
public void BulkRemove(IEnumerable<ReadingListItem> items)
|
||||
{
|
||||
_context.ReadingListItem.RemoveRange(items);
|
||||
}
|
||||
|
||||
|
||||
public async Task<PagedList<ReadingListDto>> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams)
|
||||
{
|
||||
var query = _context.ReadingList
|
||||
.Where(l => l.AppUserId == userId || (includePromoted && l.Promoted ))
|
||||
.OrderBy(l => l.LastModified)
|
||||
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking();
|
||||
|
||||
return await PagedList<ReadingListDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
public async Task<ReadingList> GetReadingListByIdAsync(int readingListId)
|
||||
{
|
||||
return await _context.ReadingList
|
||||
.Where(r => r.Id == readingListId)
|
||||
.Include(r => r.Items)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId)
|
||||
{
|
||||
var userLibraries = _context.Library
|
||||
.Include(l => l.AppUsers)
|
||||
.Where(library => library.AppUsers.Any(user => user.Id == userId))
|
||||
.AsNoTracking()
|
||||
.Select(library => library.Id)
|
||||
.ToList();
|
||||
|
||||
var items = await _context.ReadingListItem
|
||||
.Where(s => s.ReadingListId == readingListId)
|
||||
.Join(_context.Chapter, s => s.ChapterId, chapter => chapter.Id, (data, chapter) => new
|
||||
{
|
||||
TotalPages = chapter.Pages,
|
||||
ChapterNumber = chapter.Range,
|
||||
readingListItem = data
|
||||
})
|
||||
.Join(_context.Volume, s => s.readingListItem.VolumeId, volume => volume.Id, (data, volume) => new
|
||||
{
|
||||
data.readingListItem,
|
||||
data.TotalPages,
|
||||
data.ChapterNumber,
|
||||
VolumeId = volume.Id,
|
||||
VolumeNumber = volume.Name,
|
||||
})
|
||||
.Join(_context.Series, s => s.readingListItem.SeriesId, series => series.Id,
|
||||
(data, s) => new
|
||||
{
|
||||
SeriesName = s.Name,
|
||||
SeriesFormat = s.Format,
|
||||
s.LibraryId,
|
||||
data.readingListItem,
|
||||
data.TotalPages,
|
||||
data.ChapterNumber,
|
||||
data.VolumeNumber,
|
||||
data.VolumeId
|
||||
})
|
||||
.Select(data => new ReadingListItemDto()
|
||||
{
|
||||
Id = data.readingListItem.Id,
|
||||
ChapterId = data.readingListItem.ChapterId,
|
||||
Order = data.readingListItem.Order,
|
||||
SeriesId = data.readingListItem.SeriesId,
|
||||
SeriesName = data.SeriesName,
|
||||
SeriesFormat = data.SeriesFormat,
|
||||
PagesTotal = data.TotalPages,
|
||||
ChapterNumber = data.ChapterNumber,
|
||||
VolumeNumber = data.VolumeNumber,
|
||||
LibraryId = data.LibraryId,
|
||||
VolumeId = data.VolumeId,
|
||||
ReadingListId = data.readingListItem.ReadingListId
|
||||
})
|
||||
.Where(o => userLibraries.Contains(o.LibraryId))
|
||||
.OrderBy(rli => rli.Order)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
// Attach progress information
|
||||
var fetchedChapterIds = items.Select(i => i.ChapterId);
|
||||
var progresses = await _context.AppUserProgresses
|
||||
.Where(p => fetchedChapterIds.Contains(p.ChapterId))
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var progress in progresses)
|
||||
{
|
||||
var progressItem = items.SingleOrDefault(i => i.ChapterId == progress.ChapterId && i.ReadingListId == readingListId);
|
||||
if (progressItem == null) continue;
|
||||
|
||||
progressItem.PagesRead = progress.PagesRead;
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public async Task<ReadingListDto> GetReadingListDtoByIdAsync(int readingListId, int userId)
|
||||
{
|
||||
return await _context.ReadingList
|
||||
.Where(r => r.Id == readingListId && (r.AppUserId == userId || r.Promoted))
|
||||
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReadingListItemDto>> AddReadingProgressModifiers(int userId, IList<ReadingListItemDto> items)
|
||||
{
|
||||
var chapterIds = items.Select(i => i.ChapterId).Distinct().ToList();
|
||||
var userProgress = await _context.AppUserProgresses
|
||||
.Where(p => p.AppUserId == userId && chapterIds.Contains(p.ChapterId))
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var progress = userProgress.Where(p => p.ChapterId == item.ChapterId);
|
||||
item.PagesRead = progress.Sum(p => p.PagesRead);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public async Task<ReadingListDto> GetReadingListDtoByTitleAsync(string title)
|
||||
{
|
||||
return await _context.ReadingList
|
||||
.Where(r => r.Title.Equals(title))
|
||||
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReadingListItem>> GetReadingListItemsByIdAsync(int readingListId)
|
||||
{
|
||||
return await _context.ReadingListItem
|
||||
.Where(r => r.ReadingListId == readingListId)
|
||||
.OrderBy(r => r.Order)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
@ -7,18 +9,17 @@ using API.DTOs.Filtering;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Repositories;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data
|
||||
namespace API.Data.Repositories
|
||||
{
|
||||
public class SeriesRepository : ISeriesRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly NaturalSortComparer _naturalSortComparer = new ();
|
||||
public SeriesRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
@ -115,16 +116,15 @@ namespace API.Data
|
||||
await AddVolumeModifiers(userId, volumes);
|
||||
SortSpecialChapters(volumes);
|
||||
|
||||
|
||||
|
||||
return volumes;
|
||||
}
|
||||
|
||||
private 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))
|
||||
{
|
||||
v.Chapters = v.Chapters.OrderBy(x => x.Range, _naturalSortComparer).ToList();
|
||||
v.Chapters = v.Chapters.OrderBy(x => x.Range, sorter).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,11 +189,16 @@ namespace API.Data
|
||||
/// </summary>
|
||||
/// <param name="seriesIds"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(int[] seriesIds)
|
||||
public async Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false)
|
||||
{
|
||||
return await _context.Volume
|
||||
.Where(v => seriesIds.Contains(v.SeriesId))
|
||||
.ToListAsync();
|
||||
var query = _context.Volume
|
||||
.Where(v => seriesIds.Contains(v.SeriesId));
|
||||
|
||||
if (includeChapters)
|
||||
{
|
||||
query = query.Include(v => v.Chapters);
|
||||
}
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteSeriesAsync(int seriesId)
|
||||
@ -221,27 +226,52 @@ namespace API.Data
|
||||
|
||||
public async Task<int[]> GetChapterIdsForSeriesAsync(int[] seriesIds)
|
||||
{
|
||||
var series = await _context.Series
|
||||
.Where(s => seriesIds.Contains(s.Id))
|
||||
.Include(s => s.Volumes)
|
||||
.ThenInclude(v => v.Chapters)
|
||||
var volumes = await _context.Volume
|
||||
.Where(v => seriesIds.Contains(v.SeriesId))
|
||||
.Include(v => v.Chapters)
|
||||
.ToListAsync();
|
||||
|
||||
IList<int> chapterIds = new List<int>();
|
||||
foreach (var s in series)
|
||||
foreach (var v in volumes)
|
||||
{
|
||||
foreach (var v in s.Volumes)
|
||||
foreach (var c in v.Chapters)
|
||||
{
|
||||
foreach (var c in v.Chapters)
|
||||
{
|
||||
chapterIds.Add(c.Id);
|
||||
}
|
||||
chapterIds.Add(c.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return chapterIds.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This returns a list of tuples<chapterId, seriesId> back for each series id passed
|
||||
/// </summary>
|
||||
/// <param name="seriesIds"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IDictionary<int, IList<int>>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds)
|
||||
{
|
||||
var volumes = await _context.Volume
|
||||
.Where(v => seriesIds.Contains(v.SeriesId))
|
||||
.Include(v => v.Chapters)
|
||||
.ToListAsync();
|
||||
|
||||
var seriesChapters = new Dictionary<int, IList<int>>();
|
||||
foreach (var v in volumes)
|
||||
{
|
||||
foreach (var c in v.Chapters)
|
||||
{
|
||||
if (!seriesChapters.ContainsKey(v.SeriesId))
|
||||
{
|
||||
var list = new List<int>();
|
||||
seriesChapters.Add(v.SeriesId, list);
|
||||
}
|
||||
seriesChapters[v.SeriesId].Add(c.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return seriesChapters;
|
||||
}
|
||||
|
||||
public async Task AddSeriesModifiers(int userId, List<SeriesDto> series)
|
||||
{
|
||||
var userProgress = await _context.AppUserProgresses
|
||||
@ -262,16 +292,7 @@ namespace API.Data
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<byte[]> GetVolumeCoverImageAsync(int volumeId)
|
||||
{
|
||||
return await _context.Volume
|
||||
.Where(v => v.Id == volumeId)
|
||||
.Select(v => v.CoverImage)
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<byte[]> GetSeriesCoverImageAsync(int seriesId)
|
||||
public async Task<string> GetSeriesCoverImageAsync(int seriesId)
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(s => s.Id == seriesId)
|
||||
@ -282,8 +303,9 @@ namespace API.Data
|
||||
|
||||
private async Task AddVolumeModifiers(int userId, IReadOnlyCollection<VolumeDto> volumes)
|
||||
{
|
||||
var volIds = volumes.Select(s => s.Id);
|
||||
var userProgress = await _context.AppUserProgresses
|
||||
.Where(p => p.AppUserId == userId && volumes.Select(s => s.Id).Contains(p.VolumeId))
|
||||
.Where(p => p.AppUserId == userId && volIds.Contains(p.VolumeId))
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
@ -457,5 +479,23 @@ namespace API.Data
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<string>> GetAllCoverImagesAsync()
|
||||
{
|
||||
return await _context.Series
|
||||
.Select(s => s.CoverImage)
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<string>> GetLockedCoverImagesAsync()
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(s => s.CoverImageLocked && !string.IsNullOrEmpty(s.CoverImage))
|
||||
.Select(s => s.CoverImage)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
@ -4,11 +4,11 @@ using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Repositories;
|
||||
using AutoMapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data
|
||||
namespace API.Data.Repositories
|
||||
{
|
||||
public class SettingsRepository : ISettingsRepository
|
||||
{
|
||||
@ -45,4 +45,4 @@ namespace API.Data
|
||||
return await _context.ServerSetting.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Repositories;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data
|
||||
namespace API.Data.Repositories
|
||||
{
|
||||
[Flags]
|
||||
public enum AppUserIncludes
|
||||
{
|
||||
None = 1,
|
||||
Progress = 2,
|
||||
Bookmarks = 4,
|
||||
ReadingLists = 8,
|
||||
Ratings = 16
|
||||
}
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
@ -35,35 +47,105 @@ namespace API.Data
|
||||
_context.Entry(preferences).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public void Update(AppUserBookmark bookmark)
|
||||
{
|
||||
_context.Entry(bookmark).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public void Delete(AppUser user)
|
||||
{
|
||||
_context.AppUser.Remove(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an AppUser by username. Returns back Progress information.
|
||||
/// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags.
|
||||
/// </summary>
|
||||
/// <param name="username"></param>
|
||||
/// <param name="includeFlags">Includes() you want. Pass multiple with flag1 | flag2 </param>
|
||||
/// <returns></returns>
|
||||
public async Task<AppUser> GetUserByUsernameAsync(string username)
|
||||
public async Task<AppUser> GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None)
|
||||
{
|
||||
return await _context.Users
|
||||
.Include(u => u.Progresses)
|
||||
.Include(u => u.Bookmarks)
|
||||
.SingleOrDefaultAsync(x => x.UserName == username);
|
||||
var query = _context.Users
|
||||
.Where(x => x.UserName == username);
|
||||
|
||||
query = AddIncludesToQuery(query, includeFlags);
|
||||
|
||||
return await query.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an AppUser by id. Returns back Progress information.
|
||||
/// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags.
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="includeFlags">Includes() you want. Pass multiple with flag1 | flag2 </param>
|
||||
/// <returns></returns>
|
||||
public async Task<AppUser> GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None)
|
||||
{
|
||||
var query = _context.Users
|
||||
.Where(x => x.Id == userId);
|
||||
|
||||
query = AddIncludesToQuery(query, includeFlags);
|
||||
|
||||
return await query.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<AppUserBookmark> GetBookmarkForPage(int page, int chapterId, int userId)
|
||||
{
|
||||
return await _context.AppUserBookmark
|
||||
.Where(b => b.Page == page && b.ChapterId == chapterId && b.AppUserId == userId)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
private static IQueryable<AppUser> AddIncludesToQuery(IQueryable<AppUser> query, AppUserIncludes includeFlags)
|
||||
{
|
||||
if (includeFlags.HasFlag(AppUserIncludes.Bookmarks))
|
||||
{
|
||||
query = query.Include(u => u.Bookmarks);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.Progress))
|
||||
{
|
||||
query = query.Include(u => u.Progresses);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.ReadingLists))
|
||||
{
|
||||
query = query.Include(u => u.ReadingLists);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.Ratings))
|
||||
{
|
||||
query = query.Include(u => u.Ratings);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This fetches the Id for a user. Use whenever you just need an ID.
|
||||
/// </summary>
|
||||
/// <param name="username"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<AppUser> GetUserByIdAsync(int id)
|
||||
public async Task<int> GetUserIdByUsernameAsync(string username)
|
||||
{
|
||||
return await _context.Users
|
||||
.Include(u => u.Progresses)
|
||||
.Include(u => u.Bookmarks)
|
||||
.SingleOrDefaultAsync(x => x.Id == id);
|
||||
.Where(x => x.UserName == username)
|
||||
.Select(u => u.Id)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an AppUser by username. Returns back Reading List and their Items.
|
||||
/// </summary>
|
||||
/// <param name="username"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<AppUser> GetUserWithReadingListsByUsernameAsync(string username)
|
||||
{
|
||||
return await _context.Users
|
||||
.Include(u => u.ReadingLists)
|
||||
.ThenInclude(l => l.Items)
|
||||
.SingleOrDefaultAsync(x => x.UserName == username);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||
@ -77,11 +159,6 @@ namespace API.Data
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public void AddRatingTracking(AppUserRating userRating)
|
||||
{
|
||||
_context.AppUserRating.Add(userRating);
|
||||
}
|
||||
|
||||
public async Task<AppUserPreferences> GetPreferencesAsync(string username)
|
||||
{
|
||||
return await _context.AppUserPreferences
|
||||
@ -129,10 +206,17 @@ namespace API.Data
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<AppUser> GetUserByApiKeyAsync(string apiKey)
|
||||
/// <summary>
|
||||
/// Fetches the UserId by API Key. This does not include any extra information
|
||||
/// </summary>
|
||||
/// <param name="apiKey"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<int> GetUserIdByApiKeyAsync(string apiKey)
|
||||
{
|
||||
return await _context.AppUser
|
||||
.SingleOrDefaultAsync(u => u.ApiKey.Equals(apiKey));
|
||||
.Where(u => u.ApiKey.Equals(apiKey))
|
||||
.Select(u => u.Id)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
|
56
API/Data/Repositories/VolumeRepository.cs
Normal file
56
API/Data/Repositories/VolumeRepository.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Interfaces.Repositories;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories
|
||||
{
|
||||
public class VolumeRepository : IVolumeRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
|
||||
public VolumeRepository(DataContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public void Update(Volume volume)
|
||||
{
|
||||
_context.Entry(volume).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public async Task<IList<MangaFile>> GetFilesForVolume(int volumeId)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => volumeId == c.VolumeId)
|
||||
.Include(c => c.Files)
|
||||
.SelectMany(c => c.Files)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GetVolumeCoverImageAsync(int volumeId)
|
||||
{
|
||||
return await _context.Volume
|
||||
.Where(v => v.Id == volumeId)
|
||||
.Select(v => v.CoverImage)
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => volumeIds.Contains(c.VolumeId))
|
||||
.Select(c => c.Id)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Repositories;
|
||||
using API.Entities;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Repositories;
|
||||
@ -24,14 +25,15 @@ namespace API.Data
|
||||
public IUserRepository UserRepository => new UserRepository(_context, _userManager, _mapper);
|
||||
public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper);
|
||||
|
||||
public IVolumeRepository VolumeRepository => new VolumeRepository(_context, _mapper);
|
||||
public IVolumeRepository VolumeRepository => new VolumeRepository(_context);
|
||||
|
||||
public ISettingsRepository SettingsRepository => new SettingsRepository(_context, _mapper);
|
||||
|
||||
public IAppUserProgressRepository AppUserProgressRepository => new AppUserProgressRepository(_context);
|
||||
public ICollectionTagRepository CollectionTagRepository => new CollectionTagRepository(_context, _mapper);
|
||||
public IFileRepository FileRepository => new FileRepository(_context);
|
||||
public IChapterRepository ChapterRepository => new ChapterRepository(_context);
|
||||
public IChapterRepository ChapterRepository => new ChapterRepository(_context, _mapper);
|
||||
public IReadingListRepository ReadingListRepository => new ReadingListRepository(_context, _mapper);
|
||||
|
||||
/// <summary>
|
||||
/// Commits changes to the DB. Completes the open transaction.
|
||||
@ -39,7 +41,6 @@ namespace API.Data
|
||||
/// <returns></returns>
|
||||
public bool Commit()
|
||||
{
|
||||
|
||||
return _context.SaveChanges() > 0;
|
||||
}
|
||||
/// <summary>
|
||||
|
@ -1,116 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Interfaces;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data
|
||||
{
|
||||
public class VolumeRepository : IVolumeRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public VolumeRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public void Update(Volume volume)
|
||||
{
|
||||
_context.Entry(volume).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a Chapter for an Id. Includes linked <see cref="MangaFile"/>s.
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Chapter> GetChapterAsync(int chapterId)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Include(c => c.Files)
|
||||
.SingleOrDefaultAsync(c => c.Id == chapterId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Chapters for a volume id.
|
||||
/// </summary>
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<Chapter>> GetChaptersAsync(int volumeId)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => c.VolumeId == volumeId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cover image for a chapter id.
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<byte[]> GetChapterCoverImageAsync(int chapterId)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => c.Id == chapterId)
|
||||
.Select(c => c.CoverImage)
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public async Task<ChapterDto> GetChapterDtoAsync(int chapterId)
|
||||
{
|
||||
var chapter = await _context.Chapter
|
||||
.Include(c => c.Files)
|
||||
.ProjectTo<ChapterDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync(c => c.Id == chapterId);
|
||||
|
||||
return chapter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns non-tracked files for a given chapterId
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<MangaFile>> GetFilesForChapterAsync(int chapterId)
|
||||
{
|
||||
return await _context.MangaFile
|
||||
.Where(c => chapterId == c.ChapterId)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns non-tracked files for a set of chapterIds
|
||||
/// </summary>
|
||||
/// <param name="chapterIds"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds)
|
||||
{
|
||||
return await _context.MangaFile
|
||||
.Where(c => chapterIds.Contains(c.ChapterId))
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<MangaFile>> GetFilesForVolume(int volumeId)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => volumeId == c.VolumeId)
|
||||
.Include(c => c.Files)
|
||||
.SelectMany(c => c.Files)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,10 @@ namespace API.Entities
|
||||
public AppUserPreferences UserPreferences { get; set; }
|
||||
public ICollection<AppUserBookmark> Bookmarks { get; set; }
|
||||
/// <summary>
|
||||
/// Reading lists associated with this user
|
||||
/// </summary>
|
||||
public ICollection<ReadingList> ReadingLists { get; set; }
|
||||
/// <summary>
|
||||
/// An API Key to interact with external services, like OPDS
|
||||
/// </summary>
|
||||
public string ApiKey { get; set; }
|
||||
|
@ -9,7 +9,7 @@ namespace API.Entities
|
||||
/// Represents the progress a single user has on a given Chapter.
|
||||
/// </summary>
|
||||
//[Index(nameof(SeriesId), nameof(VolumeId), nameof(ChapterId), nameof(AppUserId), IsUnique = true)]
|
||||
public class AppUserProgress : IEntityDate, IHasConcurrencyToken
|
||||
public class AppUserProgress : IEntityDate
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of Entity
|
||||
@ -55,16 +55,5 @@ namespace API.Entities
|
||||
/// Last date this was updated
|
||||
/// </summary>
|
||||
public DateTime LastModified { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[ConcurrencyCheck]
|
||||
public uint RowVersion { get; private set; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnSavingChanges()
|
||||
{
|
||||
RowVersion++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,11 @@ namespace API.Entities
|
||||
public ICollection<MangaFile> Files { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public byte[] CoverImage { get; set; }
|
||||
/// <summary>
|
||||
/// Absolute path to the (managed) image file
|
||||
/// </summary>
|
||||
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
||||
public string CoverImage { get; set; }
|
||||
public bool CoverImageLocked { get; set; }
|
||||
/// <summary>
|
||||
/// Total number of pages in all MangaFiles
|
||||
|
@ -14,11 +14,11 @@ namespace API.Entities
|
||||
/// Visible title of the Tag
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cover Image for the collection tag
|
||||
/// Absolute path to the (managed) image file
|
||||
/// </summary>
|
||||
public byte[] CoverImage { get; set; }
|
||||
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
||||
public string CoverImage { get; set; }
|
||||
/// <summary>
|
||||
/// Denotes if the CoverImage has been overridden by the user. If so, it will not be updated during normal scan operations.
|
||||
/// </summary>
|
||||
|
@ -30,9 +30,13 @@ namespace API.Entities
|
||||
public int ChapterId { get; set; }
|
||||
|
||||
// Methods
|
||||
/// <summary>
|
||||
/// If the File on disk's last modified time is after what is stored in MangaFile
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool HasFileBeenModified()
|
||||
{
|
||||
return !File.GetLastWriteTime(FilePath).Equals(LastModified);
|
||||
return File.GetLastWriteTime(FilePath) > LastModified;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
29
API/Entities/ReadingList.cs
Normal file
29
API/Entities/ReadingList.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.Entities.Interfaces;
|
||||
|
||||
namespace API.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a collection of <see cref="ReadingListItem"/> which represent individual chapters and an order.
|
||||
/// </summary>
|
||||
public class ReadingList : IEntityDate
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string Title { get; set; }
|
||||
public string Summary { get; set; }
|
||||
/// <summary>
|
||||
/// Reading lists that are promoted are only done by admins
|
||||
/// </summary>
|
||||
public bool Promoted { get; set; }
|
||||
|
||||
public ICollection<ReadingListItem> Items { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
|
||||
// Relationships
|
||||
public int AppUserId { get; set; }
|
||||
public AppUser AppUser { get; set; }
|
||||
|
||||
}
|
||||
}
|
25
API/Entities/ReadingListItem.cs
Normal file
25
API/Entities/ReadingListItem.cs
Normal file
@ -0,0 +1,25 @@
|
||||
namespace API.Entities
|
||||
{
|
||||
//[Index(nameof(SeriesId), nameof(VolumeId), nameof(ChapterId), IsUnique = true)]
|
||||
public class ReadingListItem
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public int SeriesId { get; set; }
|
||||
public int VolumeId { get; set; }
|
||||
public int ChapterId { get; set; }
|
||||
/// <summary>
|
||||
/// Order of the chapter within a Reading List
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
// Relationship
|
||||
public ReadingList ReadingList { get; set; }
|
||||
public int ReadingListId { get; set; }
|
||||
|
||||
// Idea, keep these for easy join statements
|
||||
public Series Series { get; set; }
|
||||
public Volume Volume { get; set; }
|
||||
public Chapter Chapter { get; set; }
|
||||
|
||||
}
|
||||
}
|
@ -36,7 +36,11 @@ namespace API.Entities
|
||||
public string Summary { get; set; } // TODO: Migrate into SeriesMetdata (with Metadata update)
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public byte[] CoverImage { get; set; }
|
||||
/// <summary>
|
||||
/// Absolute path to the (managed) image file
|
||||
/// </summary>
|
||||
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
||||
public string CoverImage { get; set; }
|
||||
/// <summary>
|
||||
/// Denotes if the CoverImage has been overridden by the user. If so, it will not be updated during normal scan operations.
|
||||
/// </summary>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.Entities.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Entities
|
||||
{
|
||||
@ -12,7 +13,11 @@ namespace API.Entities
|
||||
public IList<Chapter> Chapters { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public byte[] CoverImage { get; set; }
|
||||
/// <summary>
|
||||
/// Absolute path to the (managed) image file
|
||||
/// </summary>
|
||||
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
||||
public string CoverImage { get; set; }
|
||||
public int Pages { get; set; }
|
||||
|
||||
|
||||
@ -21,4 +26,4 @@ namespace API.Entities
|
||||
public Series Series { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ namespace API.Extensions
|
||||
public static bool HasFileBeenModifiedSince(this FileInfo fileInfo, DateTime comparison)
|
||||
{
|
||||
return DateTime.Compare(fileInfo.LastWriteTime, comparison) > 0;
|
||||
//return fileInfo?.LastWriteTime > comparison;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using API.Helpers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@ -7,7 +8,7 @@ namespace API.Extensions
|
||||
{
|
||||
public static class HttpExtensions
|
||||
{
|
||||
public static void AddPaginationHeader(this HttpResponse response, int currentPage,
|
||||
public static void AddPaginationHeader(this HttpResponse response, int currentPage,
|
||||
int itemsPerPage, int totalItems, int totalPages)
|
||||
{
|
||||
var paginationHeader = new PaginationHeader(currentPage, itemsPerPage, totalItems, totalPages);
|
||||
@ -15,7 +16,7 @@ namespace API.Extensions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
|
||||
response.Headers.Add("Pagination", JsonSerializer.Serialize(paginationHeader, options));
|
||||
response.Headers.Add("Access-Control-Expose-Headers", "Pagination");
|
||||
}
|
||||
@ -31,6 +32,18 @@ namespace API.Extensions
|
||||
using var sha1 = new System.Security.Cryptography.SHA256CryptoServiceProvider();
|
||||
response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(content).Select(x => x.ToString("X2"))));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Calculates SHA256 hash for a cover image filename and sets as ETag. Ensures Cache-Control: private header is added.
|
||||
/// </summary>
|
||||
/// <param name="response"></param>
|
||||
/// <param name="filename"></param>
|
||||
public static void AddCacheHeader(this HttpResponse response, string filename)
|
||||
{
|
||||
if (filename == null || filename.Length <= 0) return;
|
||||
using var sha1 = new System.Security.Cryptography.SHA256CryptoServiceProvider();
|
||||
response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(Encoding.UTF8.GetBytes(filename)).Select(x => x.ToString("X2"))));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Reader;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.Entities;
|
||||
using API.Helpers.Converters;
|
||||
using AutoMapper;
|
||||
@ -31,6 +33,9 @@ namespace API.Helpers
|
||||
|
||||
CreateMap<AppUserBookmark, BookmarkDto>();
|
||||
|
||||
CreateMap<ReadingList, ReadingListDto>();
|
||||
CreateMap<ReadingListItem, ReadingListItemDto>();
|
||||
|
||||
CreateMap<Series, SearchResultDto>()
|
||||
.ForMember(dest => dest.SeriesId,
|
||||
opt => opt.MapFrom(src => src.Id))
|
||||
|
30
API/Helpers/SQLHelper.cs
Normal file
30
API/Helpers/SQLHelper.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Helpers
|
||||
{
|
||||
public static class SqlHelper
|
||||
{
|
||||
public static List<T> RawSqlQuery<T>(DbContext context, string query, Func<DbDataReader, T> map)
|
||||
{
|
||||
using var command = context.Database.GetDbConnection().CreateCommand();
|
||||
command.CommandText = query;
|
||||
command.CommandType = CommandType.Text;
|
||||
|
||||
context.Database.OpenConnection();
|
||||
|
||||
using var result = command.ExecuteReader();
|
||||
var entities = new List<T>();
|
||||
|
||||
while (result.Read())
|
||||
{
|
||||
entities.Add(map(result));
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ namespace API.Interfaces
|
||||
void CleanupChapters(int[] chapterIds);
|
||||
void RefreshMetadata(int libraryId, bool forceUpdate = true);
|
||||
void CleanupTemp();
|
||||
void RefreshSeriesMetadata(int libraryId, int seriesId);
|
||||
void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false);
|
||||
void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false);
|
||||
void CancelStatsTasks();
|
||||
void RunStatCollection();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user