v0.7.2 - Reading Lists, CBL Support, OPDS Flattening and More! (#1959)
* WebP Covers + Series Detail Enhancements (#1652) * Implemented save covers as webp. Reworked screen to provide more information up front about webp and what browsers can support it. * cleaned up pages to use compact numbering and made compact numbering expand into one decimal place (20.5K) * Fixed an issue with adding new device * If a book has an invalid language set, drop the language altogether rather than reading in a corrupted entry. * Ensure genres and tags render alphabetically. Improved support for partial volumes in Comic parser. * Ensure all people, tags, collections, and genres are in alphabetical order. * Moved some code to Extensions to clean up code. * More unit tests * Cleaned up release year filter css * Tweaked some code in all series to make bulk deletes cleaner on the UI. * Trying out want to read and unread count on series detail page * Added Want to Read button for series page to make it easy to see when something is in want to read list and toggle it. Added tooltips instead of title to buttons, but they don't style correctly. Added a continue point under cover image. * Code smells * Bump versions by dotnet-bump-version. * Fixed Series Relations Schema (#1654) * Bump loader-utils from 2.0.2 to 2.0.3 in /UI/Web Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.2 to 2.0.3. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.3/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v2.0.2...v2.0.3) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Fixed is want to read coming back as a string and not working correctly. * Changed from to Continue to be more explicit * Added the first migration which exports data as a csv in temp/. This is the backup in case data is lost in the migration. * Note for later * Fixed the migration for the series relation so when deleting any series on any edge of the relationship, the SeriesRelation row deletes. * Change buttons back to titles on series detail page * Wrote the code to import relations from the backup. * Added an additional version check to avoid file io on migration. * Code cleanup Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Fresh Nightly Installs Work (#1659) * Bump loader-utils from 2.0.3 to 2.0.4 in /UI/Web Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Removed a very old cgecj from Nov 2021 when data/appsettings was moved to config/ * Added some notes about migration * Removed a file that shouldn't have been there. Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Library Settings Modal + New Library Settings (#1660) * Bump loader-utils from 2.0.3 to 2.0.4 in /UI/Web Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Fixed want to read button on series detail not performing the correct action * Started the library settings. Added ability to update a cover image for a library. Updated backup db to also copy reading list (and now library) cover images. * Integrated Edit Library into new settings (not tested) and hooked up a wizard-like flow for new library. * Fixed a missing update event in backend when updating a library. * Disable Save when form invalid. Do inline validation on Library name when user types to ensure the name is valid. * Trim library names before you check anything * General code cleanup * Implemented advanced settings for library (include in dashboard, search, recommended) and ability to turn off folder watching for individual libraries. Refactored some code to streamline perf in some flows. * Removed old components replaced with new modal * Code smells Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * UX Alignment and bugfixes (#1663) * Refactored the design of reading list page to follow more in line with list view. Added release date on the reading list items, if it's set in underlying chapter. Fixed a bug where reordering the list items could sometimes not update correctly with drag and drop. * Removed a bug marker that I just fixed * When generating library covers, make them much smaller as they are only ever icons. * Fixed library settings not showing the correct image. * Fixed a bug where duplicate collection tags could be created. Fixed a bug where collection tag normalized title was being set to uppercase. Redesigned the edit collection tag modal to align with new library settings and provide inline name checks. * Updated edit reading list modal to align with new library settings modal pattern. Refactored the backend to ensure it flows correctly without allowing duplicate names. Don't show Continue point on series detail if the whole series is read. * Added some more unit tests around continue point * Fixed a bug on series detail when bulk selecting between volume and chapters, the code which determines which chapters are selected didn't take into account mixed layout for Storyline tab. * Refactored to generate an OpenAPI spec at root of Kavita. This will be loaded by a new API site for easy hosting. Deprecated EnableSwaggerUi preference as after validation new system works, this will be removed and instances can use our hosting to hit their server (or run a debug build). * Test GA * Reverted GA and instead do it in the build step. This will just force developers to commit it in. * GA please work * Removed redundant steps from test since build already does it. * Try another GA * Moved all test actions into initial build step, which should drastically cut down on time. Only run sonar if the secret is present (so not for forks). Updated build requirements for develop and stable docker pushes. * Fixed env variable * Okay not possible to do secrets in if statement * Fixed the build step to output the openapi.json where it's expected. * Fixed GA (#1664) * Bump versions by dotnet-bump-version. * Applied new _components layout structure to Kavita. All except manga as there is an open PR that drastically changes that module. (#1666) * [Experimental] Split Renderers - Double & Double (Manga) fixes (#1667) * Updated swiper and some packages for reported security issues * Fixed reading lists promotion not working * Refactor RenameFileForCopy to use iterative recursion, rather than functional. * Ensured that bookmarks are fetched and ordered by Created date. * Fixed a bug where bookmarks were coming back in the correct order, but due to filenames, would not sort correctly. * Default installs to Debug log level given errors users have and Debug not being too noisy * Added jumpbar to bookmarks page * Now added jumpbar to bookmarks * Refactored some code into pipes and added some debug messaging for prefetcher * Try loading next and prev chapter's first/last page to cache so it renders faster * Updated GetImage to do a bound check on max page. Fixed a critical bug in how manga reader updates image elements src to prefetch/load pages. I was not creating a new reference which broke Angular's ability to update DOM on changes. * Refactored the image setting code to use a single method which tries to use a cached image always. * Refactored code to use getPage which favors cache and simplifies image creation code * Started the work to split the canvas renderer into it's own component * Refactored a lot of common methods into a service for the reader to support the upcoming renderer split * Moved components to nested folder. Refactored more code to streamline image sending to child renderer. Added notes across the code to help streamline flow of data and who owns what. * Swapped out SQLite for Memory, but the one from hangfire. Added DisableConcurrentExecution on ProcessChange to avoid duplication when multiple threads execute at once. * Basic split right to left is working with canvas renderer * Left to right and right to left now work * Fixed a bug where pagesplitoption wasn't being updated when modifying menu * Canvas rendering still has a bug with switching between right to left -> left to right on the re-render, it will choose a bad state. All else works fine with it. * Updated canvas renderer to implement the ImageRenderer interface * Canvas renderer is done * Setup single renderer. Need to figure out how to share CSS between renderers and also share some global stuff, like image height. * Refactored code so that image-container is within the renderers themselves. Still broken in scaling, but working towards a solution. * Added double click to shortcut menu * Moved image containers within the renderers * Pushing up for Robbie * nothing new * Move common css to a single scss file * More css consolidation * Fixed a npe in isWideImage * Refactored page updates to renderers to include max pages. Rewrote most of renderer into observables. * Moved bookmark for second page to double renderer * Started hooking in double renderer renderPage() * Fixed height scaling, but now canvas renderer is broken again * Fixed a bug with canvas renderer not moving to next page. Streamlined the code for getting page amounts from the dfferent renderers * Added double click to bookmark for canvas * Stashing the code and taking a break * Nothing much, buffer is still broken * Got double renderer to render at least one page * Double renderer now has access to 5 images at any time, so it can make appropriate decisions on when to render double pages. * Fixed up double rendererer moving backward page calc * Forward logic seems to be working * Cleaned up dead code after testing * Moved a few loggers in folder watching to trace * Everything seems to work fine, time to do double manga renderer * Moved some css around and added the reverse double component * Only execute renderer's pipes when in the correct mode * Still working on double renderer * Fixed scaling issues on double * Updating double logic - Fixed: Fixed an issue where a second page would render when current page was wide. * Hooked up double renderer * Made changes but not sure if im making progress * double manga fixes * Claned some of robbies code * Fixing last page bug * Library Settings Modal + New Library Settings (#1660) * Bump loader-utils from 2.0.3 to 2.0.4 in /UI/Web Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Fixed want to read button on series detail not performing the correct action * Started the library settings. Added ability to update a cover image for a library. Updated backup db to also copy reading list (and now library) cover images. * Integrated Edit Library into new settings (not tested) and hooked up a wizard-like flow for new library. * Fixed a missing update event in backend when updating a library. * Disable Save when form invalid. Do inline validation on Library name when user types to ensure the name is valid. * Trim library names before you check anything * General code cleanup * Implemented advanced settings for library (include in dashboard, search, recommended) and ability to turn off folder watching for individual libraries. Refactored some code to streamline perf in some flows. * Removed old components replaced with new modal * Code smells Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * UX Alignment and bugfixes (#1663) * Refactored the design of reading list page to follow more in line with list view. Added release date on the reading list items, if it's set in underlying chapter. Fixed a bug where reordering the list items could sometimes not update correctly with drag and drop. * Removed a bug marker that I just fixed * When generating library covers, make them much smaller as they are only ever icons. * Fixed library settings not showing the correct image. * Fixed a bug where duplicate collection tags could be created. Fixed a bug where collection tag normalized title was being set to uppercase. Redesigned the edit collection tag modal to align with new library settings and provide inline name checks. * Updated edit reading list modal to align with new library settings modal pattern. Refactored the backend to ensure it flows correctly without allowing duplicate names. Don't show Continue point on series detail if the whole series is read. * Added some more unit tests around continue point * Fixed a bug on series detail when bulk selecting between volume and chapters, the code which determines which chapters are selected didn't take into account mixed layout for Storyline tab. * Refactored to generate an OpenAPI spec at root of Kavita. This will be loaded by a new API site for easy hosting. Deprecated EnableSwaggerUi preference as after validation new system works, this will be removed and instances can use our hosting to hit their server (or run a debug build). * Test GA * Reverted GA and instead do it in the build step. This will just force developers to commit it in. * GA please work * Removed redundant steps from test since build already does it. * Try another GA * Moved all test actions into initial build step, which should drastically cut down on time. Only run sonar if the secret is present (so not for forks). Updated build requirements for develop and stable docker pushes. * Fixed env variable * Okay not possible to do secrets in if statement * Fixed the build step to output the openapi.json where it's expected. * Fixed GA (#1664) * Bump versions by dotnet-bump-version. * Applied new _components layout structure to Kavita. All except manga as there is an open PR that drastically changes that module. (#1666) * Fixed typeahead and updated manga reader to new layout structure * Fixed book reader fonts lookups * Fixed up some build issues * Fixed a bad import of css image * Some cleanup and rewrote how we log out data. * Renderer can be null on first load when performing some work. * Library Settings Modal + New Library Settings (#1660) * Bump loader-utils from 2.0.3 to 2.0.4 in /UI/Web Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Fixed want to read button on series detail not performing the correct action * Started the library settings. Added ability to update a cover image for a library. Updated backup db to also copy reading list (and now library) cover images. * Integrated Edit Library into new settings (not tested) and hooked up a wizard-like flow for new library. * Fixed a missing update event in backend when updating a library. * Disable Save when form invalid. Do inline validation on Library name when user types to ensure the name is valid. * Trim library names before you check anything * General code cleanup * Implemented advanced settings for library (include in dashboard, search, recommended) and ability to turn off folder watching for individual libraries. Refactored some code to streamline perf in some flows. * Removed old components replaced with new modal * Code smells Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * UX Alignment and bugfixes (#1663) * Refactored the design of reading list page to follow more in line with list view. Added release date on the reading list items, if it's set in underlying chapter. Fixed a bug where reordering the list items could sometimes not update correctly with drag and drop. * Removed a bug marker that I just fixed * When generating library covers, make them much smaller as they are only ever icons. * Fixed library settings not showing the correct image. * Fixed a bug where duplicate collection tags could be created. Fixed a bug where collection tag normalized title was being set to uppercase. Redesigned the edit collection tag modal to align with new library settings and provide inline name checks. * Updated edit reading list modal to align with new library settings modal pattern. Refactored the backend to ensure it flows correctly without allowing duplicate names. Don't show Continue point on series detail if the whole series is read. * Added some more unit tests around continue point * Fixed a bug on series detail when bulk selecting between volume and chapters, the code which determines which chapters are selected didn't take into account mixed layout for Storyline tab. * Refactored to generate an OpenAPI spec at root of Kavita. This will be loaded by a new API site for easy hosting. Deprecated EnableSwaggerUi preference as after validation new system works, this will be removed and instances can use our hosting to hit their server (or run a debug build). * Test GA * Reverted GA and instead do it in the build step. This will just force developers to commit it in. * GA please work * Removed redundant steps from test since build already does it. * Try another GA * Moved all test actions into initial build step, which should drastically cut down on time. Only run sonar if the secret is present (so not for forks). Updated build requirements for develop and stable docker pushes. * Fixed env variable * Okay not possible to do secrets in if statement * Fixed the build step to output the openapi.json where it's expected. * Applied new _components layout structure to Kavita. All except manga as there is an open PR that drastically changes that module. (#1666) * Post merge cleanup * Again moving the file Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Basic Stats (#1673) * Refactored ResponseCache profiles into consts * Refactored code to use an extension method for getting user library ids. * Started server statistics, added a charting library, and added a table sort column (not finished) * Refactored code and have a fully working example of sortable headers. Still doesn't work with default sorting state, will work on that later. * Implemented file size, but it's too expensive, so commented out. * Added a migration to provide extension and length/size information in the DB to allow for faster stat apis. * Added the ability to force a library scan from library settings. * Refactored some apis to provide more of a file breakdown rather than just file size. * Working on visualization of file breakdown * Fixed the file breakdown visual * Fixed up 2 visualizations * Added back an api for member names, started work on top reads * Hooked up the other library types and username/days. * Preparing to remove top reads and refactor into Top users * Added LibraryId to AppUserProgress to help with complex lookups. * Added the new libraryId hook into some stats methods * Updated api methods to use libraryId for progress * More places where LibraryId is needed * Added some high level server stats * Got a ton done on server stats * Updated default theme (dark) to be the default root variables. This will allow user themes to override just what they want, rather than maintain their own css variables. * Implemented a monster query for top users by reading time. It's very slow and can be cleaned up likely. * Hooked up top reads. Code needs a big refactor. Handing off for Robbie treatment and I'll switch to User stats. * Implemented last 5 recently read series (broken) and added some basic css * Fixed recently read query * Cleanup the css a bit, Robbie we need you * More css love * Cleaned up DTOs that aren't needed anymore * Fixed top readers query * When calculating top readers, don't include read events where nothing is read (0 pages) * Hooked up the date into GetTopUsers * Hooked top readers up with days and refactored and cleaned up componets not used * Fixed up query * Started on a day by day breakdown, but going to take a break from stats. * Added a temp task to run some migration manually for stats to work * Ensure OPDS-PS uses new libraryId for progress reporting * Fixed a code smell * Adding some styling * adding more styles * Removed some debug stuff from user stats * Bump qs from 6.5.2 to 6.5.3 in /UI/Web Bumps [qs](https://github.com/ljharb/qs) from 6.5.2 to 6.5.3. - [Release notes](https://github.com/ljharb/qs/releases) - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.5.2...v6.5.3) --- updated-dependencies: - dependency-name: qs dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Tweaked some code for bad data cases * Refactored a chapter lookup to remove un-needed Volume join in 5 places across the code. * API push Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Hooked up the API layer to be able to extract images from PDF again for Tachiyomi explicitly (#1686) * Bump versions by dotnet-bump-version. * OPDS Enhancements (#1687) * Bump express from 4.17.2 to 4.18.2 in /UI/Web Bumps [express](https://github.com/expressjs/express) from 4.17.2 to 4.18.2. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.17.2...4.18.2) --- updated-dependencies: - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Bump decode-uri-component from 0.2.0 to 0.2.2 in /UI/Web Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2. - [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases) - [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2) --- updated-dependencies: - dependency-name: decode-uri-component dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Bump qs and express in /UI/Web Bumps [qs](https://github.com/ljharb/qs) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together. Updates `qs` from 6.5.3 to 6.11.0 - [Release notes](https://github.com/ljharb/qs/releases) - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.5.3...v6.11.0) Updates `express` from 4.17.2 to 4.18.2 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.17.2...4.18.2) --- updated-dependencies: - dependency-name: qs dependency-type: indirect - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Added genre and authors to Series level, added summary to volume and chapter level. Force order on reading list title as Chunky enforces their own sort order and doesn't respect the spec. * Moved all the reading list formatting logic to the backend. This allows us to re-use the UI logic for OPDS streams. * Fixed a broken unit test * Code smells Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Epub Table of Generation fixes for Sigil (#1689) * Fixed generating table of contents where key lookup could fail with how Sigil packs the epubs. * Tweaked Kavita's fallback ToC generation (when one doesn't exist in the epub) to also use CoalesceKey. * Code smells * Bump versions by dotnet-bump-version. * File Dimension API (#1690) * Implemented an api for getting file dimensions for a given chapter. This is for CDisplayEx integration. This might be usable in Double Renderer. * Added the cached filename for new API * Bump versions by dotnet-bump-version. * Send Non books to your Devices (#1691) * Only restrict non-epub/pdf for Kindle devices on Send To. * Removed restriction to email non-epub/pdfs to devices. * Bump versions by dotnet-bump-version. * Misc UI Tweaks (#1692) * Added a timeAgo pipe which shows live updates for a few areas. * Fixed some wording on stats page. Changed Total People count to just work on distinct names and not count multiple for different roles. * Tweaked the compact number so it only shows one decimal * Fixed a bug * Bump versions by dotnet-bump-version. * Reader Refactor Part 2 (#1694) * Updated swiper and some packages for reported security issues * Fixed reading lists promotion not working * Refactor RenameFileForCopy to use iterative recursion, rather than functional. * Ensured that bookmarks are fetched and ordered by Created date. * Fixed a bug where bookmarks were coming back in the correct order, but due to filenames, would not sort correctly. * Default installs to Debug log level given errors users have and Debug not being too noisy * Added jumpbar to bookmarks page * Now added jumpbar to bookmarks * Refactored some code into pipes and added some debug messaging for prefetcher * Try loading next and prev chapter's first/last page to cache so it renders faster * Updated GetImage to do a bound check on max page. Fixed a critical bug in how manga reader updates image elements src to prefetch/load pages. I was not creating a new reference which broke Angular's ability to update DOM on changes. * Refactored the image setting code to use a single method which tries to use a cached image always. * Refactored code to use getPage which favors cache and simplifies image creation code * Started the work to split the canvas renderer into it's own component * Refactored a lot of common methods into a service for the reader to support the upcoming renderer split * Moved components to nested folder. Refactored more code to streamline image sending to child renderer. Added notes across the code to help streamline flow of data and who owns what. * Swapped out SQLite for Memory, but the one from hangfire. Added DisableConcurrentExecution on ProcessChange to avoid duplication when multiple threads execute at once. * Basic split right to left is working with canvas renderer * Left to right and right to left now work * Fixed a bug where pagesplitoption wasn't being updated when modifying menu * Canvas rendering still has a bug with switching between right to left -> left to right on the re-render, it will choose a bad state. All else works fine with it. * Updated canvas renderer to implement the ImageRenderer interface * Canvas renderer is done * Setup single renderer. Need to figure out how to share CSS between renderers and also share some global stuff, like image height. * Refactored code so that image-container is within the renderers themselves. Still broken in scaling, but working towards a solution. * Added double click to shortcut menu * Moved image containers within the renderers * Pushing up for Robbie * nothing new * Move common css to a single scss file * More css consolidation * Fixed a npe in isWideImage * Refactored page updates to renderers to include max pages. Rewrote most of renderer into observables. * Moved bookmark for second page to double renderer * Started hooking in double renderer renderPage() * Fixed height scaling, but now canvas renderer is broken again * Fixed a bug with canvas renderer not moving to next page. Streamlined the code for getting page amounts from the dfferent renderers * Added double click to bookmark for canvas * Stashing the code and taking a break * Nothing much, buffer is still broken * Got double renderer to render at least one page * Double renderer now has access to 5 images at any time, so it can make appropriate decisions on when to render double pages. * Fixed up double rendererer moving backward page calc * Forward logic seems to be working * Cleaned up dead code after testing * Moved a few loggers in folder watching to trace * Everything seems to work fine, time to do double manga renderer * Moved some css around and added the reverse double component * Only execute renderer's pipes when in the correct mode * Still working on double renderer * Fixed scaling issues on double * Updating double logic - Fixed: Fixed an issue where a second page would render when current page was wide. * Hooked up double renderer * Made changes but not sure if im making progress * double manga fixes * Claned some of robbies code * Fixing last page bug * Library Settings Modal + New Library Settings (#1660) * Bump loader-utils from 2.0.3 to 2.0.4 in /UI/Web Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Fixed want to read button on series detail not performing the correct action * Started the library settings. Added ability to update a cover image for a library. Updated backup db to also copy reading list (and now library) cover images. * Integrated Edit Library into new settings (not tested) and hooked up a wizard-like flow for new library. * Fixed a missing update event in backend when updating a library. * Disable Save when form invalid. Do inline validation on Library name when user types to ensure the name is valid. * Trim library names before you check anything * General code cleanup * Implemented advanced settings for library (include in dashboard, search, recommended) and ability to turn off folder watching for individual libraries. Refactored some code to streamline perf in some flows. * Removed old components replaced with new modal * Code smells Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * UX Alignment and bugfixes (#1663) * Refactored the design of reading list page to follow more in line with list view. Added release date on the reading list items, if it's set in underlying chapter. Fixed a bug where reordering the list items could sometimes not update correctly with drag and drop. * Removed a bug marker that I just fixed * When generating library covers, make them much smaller as they are only ever icons. * Fixed library settings not showing the correct image. * Fixed a bug where duplicate collection tags could be created. Fixed a bug where collection tag normalized title was being set to uppercase. Redesigned the edit collection tag modal to align with new library settings and provide inline name checks. * Updated edit reading list modal to align with new library settings modal pattern. Refactored the backend to ensure it flows correctly without allowing duplicate names. Don't show Continue point on series detail if the whole series is read. * Added some more unit tests around continue point * Fixed a bug on series detail when bulk selecting between volume and chapters, the code which determines which chapters are selected didn't take into account mixed layout for Storyline tab. * Refactored to generate an OpenAPI spec at root of Kavita. This will be loaded by a new API site for easy hosting. Deprecated EnableSwaggerUi preference as after validation new system works, this will be removed and instances can use our hosting to hit their server (or run a debug build). * Test GA * Reverted GA and instead do it in the build step. This will just force developers to commit it in. * GA please work * Removed redundant steps from test since build already does it. * Try another GA * Moved all test actions into initial build step, which should drastically cut down on time. Only run sonar if the secret is present (so not for forks). Updated build requirements for develop and stable docker pushes. * Fixed env variable * Okay not possible to do secrets in if statement * Fixed the build step to output the openapi.json where it's expected. * Fixed GA (#1664) * Bump versions by dotnet-bump-version. * Applied new _components layout structure to Kavita. All except manga as there is an open PR that drastically changes that module. (#1666) * Fixed typeahead and updated manga reader to new layout structure * Fixed book reader fonts lookups * Fixed up some build issues * Fixed a bad import of css image * Some cleanup and rewrote how we log out data. * Renderer can be null on first load when performing some work. * Library Settings Modal + New Library Settings (#1660) * Bump loader-utils from 2.0.3 to 2.0.4 in /UI/Web Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Fixed want to read button on series detail not performing the correct action * Started the library settings. Added ability to update a cover image for a library. Updated backup db to also copy reading list (and now library) cover images. * Integrated Edit Library into new settings (not tested) and hooked up a wizard-like flow for new library. * Fixed a missing update event in backend when updating a library. * Disable Save when form invalid. Do inline validation on Library name when user types to ensure the name is valid. * Trim library names before you check anything * General code cleanup * Implemented advanced settings for library (include in dashboard, search, recommended) and ability to turn off folder watching for individual libraries. Refactored some code to streamline perf in some flows. * Removed old components replaced with new modal * Code smells Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * UX Alignment and bugfixes (#1663) * Refactored the design of reading list page to follow more in line with list view. Added release date on the reading list items, if it's set in underlying chapter. Fixed a bug where reordering the list items could sometimes not update correctly with drag and drop. * Removed a bug marker that I just fixed * When generating library covers, make them much smaller as they are only ever icons. * Fixed library settings not showing the correct image. * Fixed a bug where duplicate collection tags could be created. Fixed a bug where collection tag normalized title was being set to uppercase. Redesigned the edit collection tag modal to align with new library settings and provide inline name checks. * Updated edit reading list modal to align with new library settings modal pattern. Refactored the backend to ensure it flows correctly without allowing duplicate names. Don't show Continue point on series detail if the whole series is read. * Added some more unit tests around continue point * Fixed a bug on series detail when bulk selecting between volume and chapters, the code which determines which chapters are selected didn't take into account mixed layout for Storyline tab. * Refactored to generate an OpenAPI spec at root of Kavita. This will be loaded by a new API site for easy hosting. Deprecated EnableSwaggerUi preference as after validation new system works, this will be removed and instances can use our hosting to hit their server (or run a debug build). * Test GA * Reverted GA and instead do it in the build step. This will just force developers to commit it in. * GA please work * Removed redundant steps from test since build already does it. * Try another GA * Moved all test actions into initial build step, which should drastically cut down on time. Only run sonar if the secret is present (so not for forks). Updated build requirements for develop and stable docker pushes. * Fixed env variable * Okay not possible to do secrets in if statement * Fixed the build step to output the openapi.json where it's expected. * Applied new _components layout structure to Kavita. All except manga as there is an open PR that drastically changes that module. (#1666) * Post merge cleanup * Again moving the file * Fixed an issue with switching to double renderer and the image not loading for cover image. * Fixed double manga last page repeating twice * Added ability to quickly save a few settings to user preferences from manga reader * Fixed up some success messaging * Single image and canvas could stack, last page on double wouldn't render. * Stashing code, want to work on something else * Suppress a concurrency issue when opening a fresh chapter to read. * Refactored a function into a pipe * Took care of one TODO * Tightened up the logic around single renderer handling fit to screen images. * Added some code to see how long api takes on average. * First pass integration of page dimensions into single renderer and base code * Canvas renderer pass for new page dimensions * On time left, don't use the word left again * Moved the page dimension code into manga service to make it seemless * Hooked in a replacement for image based isWide * Canvas renderer is working again * Double renderer now follows how Komga does it to keep it simple. * Double renderer is working really well so far. * don't use nbsp * Added response caching to file-dimensions and chapter info api * Allow chapter info to send back file dimensions optionally * Fixed an issue with dimensions api locking files on Windows * Refactored all code to use isWidePage * More fixes and cleanup * More double reverse logic * Recently Read stats page will allow you to click the items. Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * More Reader Fixes (#1696) * Fixed resizing or layout changes causing page change on double reader * Implemented the debug log pattern on double renderers. Fixed a case when navigation backwards and showing only one page. Updated so go to page or slider update will handle selecting the right page number for pair display. * All Spread cases for double working * Cleanup dead code * Ensure we can jump to last page * Bump versions by dotnet-bump-version. * Reading History (#1699) * Added new stat graph for pages read over time for all users. * Switched to reading events rather than pages read to get a better scale * Changed query to use Created date as LastModified wont work since I just did a migration on all rows. * Small cleanup on graph * Read by day completed and ready for user stats page. * Changed the initial stat report to be in 1 day, to avoid people trying and ditching the software from muddying up the stats. * Cleaned up stats page such that stats around series show their image and tweaked some layout and wordings * Fixed recently read order * Put read history on user profile * Final cleanup, Robbie needs to do a CSS pass before release. * Bump versions by dotnet-bump-version. * Performance Improvements and Some Polish (#1702) * Auto scale reading timeline * Added benchmarks for SharpImage and NetVips. When an epub has a malformed page, catch the error and present it better to the user. * Added a hint for an upcoming feature * Slightly sped up word count for epubs * Added one more test to reflect actual code. * Some light cleanup * Use compact number for stat lists * Fixed brightness being broken on manga reader * Replaced CoverToWebP SharpImage version with NetVips which is MUCH lighter on memory and CPU. * Added last modified on the progress dto for CdDisplayEx. * Code cleanup * Forgot one cleanup * Bump versions by dotnet-bump-version. * Holiday Changes (#1706) * Fixed a bug on bookmark mode not finding correct image for prefetcher. * Fixed up the edit series relationship modal on tablet viewports. * On double page mode, only bookmark 1 page if only 1 pages is renderered on screen. * Added percentage read of a given library and average hours read per week to user stats. * Fixed a bug in the reader with paging in bookmark mode * Added a "This Week" option to top readers history * Added date ranges for reading time. Added dates that don't have anything, but might remove. * On phone, when applying a metadata filter, when clicking apply, collapse the filter automatically. * Disable jump bar and the resuming from last spot when a custom sort is applied. * Ensure all Regex.Replace or Matches have timeouts set * Bump versions by dotnet-bump-version. * First PR of the new year (#1717) * Fixed a bug on bookmark mode not finding correct image for prefetcher. * Fixed up the edit series relationship modal on tablet viewports. * On double page mode, only bookmark 1 page if only 1 pages is renderered on screen. * Added percentage read of a given library and average hours read per week to user stats. * Fixed a bug in the reader with paging in bookmark mode * Added a "This Week" option to top readers history * Added date ranges for reading time. Added dates that don't have anything, but might remove. * On phone, when applying a metadata filter, when clicking apply, collapse the filter automatically. * Disable jump bar and the resuming from last spot when a custom sort is applied. * Ensure all Regex.Replace or Matches have timeouts set * Fixed a long standing bug where fit to height on tablets wouldn't center the image * Streamlined url parsing to be more reliable * Reduced an additional db query in chapter info. * Added a missing task to convert covers to webP and added messaging to help the user understand to run it after modifying the setting. * Changed OPDS to be enabled by default for new installs. This should reduce issues with users being confused about it before it's enabled. * When there are multiple files for a chapter, show a count card on the series detail to help user understand duplicates exist. Made the unread badge smaller to avoid collision. * Added Word Count to user stats and wired up average reading per week. * Fixed word count failing on some epubs * Removed some debug code * Don't give more information than is necessary about file paths for page dimensions. * Fixed a bug where pagination area would be too small when the book's content was less that height on default mode. * Updated Default layout mode to Scroll for books. * Added bytes in the UI and at an API layer for CDisplayEx * Don't log health checks to logs at all. * Changed Word Count to Length to match the way pages work * Made reading time more clear when min hours is 0 * Apply more aggressive coalescing when remapping bad metadata keys for epubs. * Changed the amount of padding between icon and text for side nav item. * Fixed a NPE on book reader (harmless) * Fixed an ordering issue where Volume 1 was a single file but also tagged as Chapter 1 and Volume 2 was Chapter 0. Thus Volume 2 was being selected for continue point when Volume 1 should have been. * When clicking on an activity stream header from dashboard, show the title on the resulting page. * Removed a property that can't be animated * Fixed a typeahead typescript issue * Added Size into Series Info and Added some tooltip and spacing changes to better explain some fields. * Added size for volume drawers and cleaned up some date edge case handling * Fixed an annoying bug where when on mobile opening a view with a metadata filter, Kavita would open the filter automatically. * Bump versions by dotnet-bump-version. * Quick fix for Double Renderer (#1719) * Disable emulate comic book when on single page reader * Fixed a regression where double page renderer wouldn't layout the images correctly * Bump versions by dotnet-bump-version. * Feature/stats finishoff (#1720) * Added ability to click on genres, tags, and people to view all items in a modal. * Made it so we can click and open a filtered search from generic list * Fixed broken epub pagination area due to a typo in a query selector * Added day breakdown, wrapping up stats * Bump versions by dotnet-bump-version. * Docker nonroot (#1650) * Added PUID, PGID and KAVITAUSER variable support in entrypoint.sh * Update the setting of ownership to avoid changing library files * Default to run as root, using user kavita if alternate UID/GID are provided * Only chown config folder and only if needed * Revert chmod on Kavita Co-authored-by: Muggz <mug@passw0rd.org> * Bump versions by dotnet-bump-version. * Manga Reader Work (#1729) * Instead of augmenting prefetcher to move across chapter bounds, let's try to instead just load 5 images (which the browser will cache) from next/prev so when it loads, it's much faster. * Trialing loading next/prev chapters 5 pages to have better next page loading experience. * Tweaked GetChapterInfo API to actually apply conditional includeDimensions parameter. * added a basic language file for upcoming work * Moved the bottom menu up a bit for iOS devices with handlebars. * Fixed fit to width on phones still having a horizontal scrollbar * Fixed a bug where there is extra space under the image when fit to width and on a phone due to pagination going to far. * Changed which variable we use for right pagination calculation * Fixing fit to height - Fixing height calc to account for horizontal scroll bar height. * Added a comment for the height scrollbar fix * Adding screenfull package # Added: - Added screenfull package to handle cross-platform browser fullscreen code # Removed: - Removed custom fullscreen code * Fixed a bug where switching from webtoon reader to other layout modes wouldn't render anything. Webtoon continuous scroll down is now broken. * Fixed it back to how it was and all is good. Need to call detectChanges explicitly. * Removed an additional undeeded save progress call on loadPage * Laid out the test case to move the page snapping to the backend with full unit tests. Current code is broken just like UI layer. * Refactored the snap points into the backend and ensure that it works correctly. * Fixed a broken unit test * Filter out spammy hubs/messages calls in the logs * Swallow all noisy messages that are from RequestLoggingMiddleware when the log level is on Information or above. * Added a common loading component to the app. Have yet to refactor all screens to use this. * Bump json5 from 2.2.0 to 2.2.3 in /UI/Web Bumps [json5](https://github.com/json5/json5) from 2.2.0 to 2.2.3. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v2.2.0...v2.2.3) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> * Alrigned all the loading messages and styles throughout the app * Webtoon reader will use max width of all images to ensure images align well. * On Original scaling mode, users can use the keyboard to scroll around the images without pagination kicking off. * Removed console logs * Fixed a public vs private issue * Fixed an issue around some cached files getting locked due to NetVips holding them during file size calculations. Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * [Manga Reader] Swipe Support (#1735) * Fixed a loading indicator that is always on * Started to add swipe directive * Implemented the ability to swipe to navigate pages in manga reader. * Swipe to paginate seems to be working reliably * Removed a bunch of junk from csproj and added a debug menu for testing on phone to smooth out experience. * Fixed a bug where reading list detail wouldn't render the set image of the reading list. * Added some instructions and code to allow connecting to dev instance easier. * Fixed up paging with keyboard where to ensure that when we hit the end of the scroll, we don't go to the next page instantly, but rather make the user press the key once more. * Fixed reading list image not properly renderering on reading list detail page. * Solved the swiping bug, need to play with threshold again. * Swipe is now working. Need to decide if I'm going to support reversing the direction with reading direction. * Hooked up swipe with reading direction code * Cleaned up some direction code to align to a new enum * Feature complete * Bump versions by dotnet-bump-version. * Fix pagination in Manga reader from last PR (#1736) * Added a new Double (No Cover) rendering mode which always has first 2 pages together unless wide. * Removed layout mode for build * Bump versions by dotnet-bump-version. * Better Themes, Stats, and bugfixes (#1740) * Fixed a bug where when clicking on a series rating for first time, the rating wasn't populating in the modal. * Fixed a bug on Scroll mode with immersive mode, the bottom bar could clip with the book body. * Cleanup some uses of var * Refactored text as json into a type so I don't have to copy/paste everywhere * Theme styles now override the defaults and theme owners no longer need to maintain all the variables themselves. Themes can now override the color of the header on mobile devices via --theme-color and Kavita will now update both theme color as well as color scheme. * Fixed a bug where last active on user stats wasn't for the particular user. * Added a more accurate word count calculation and the ability to see the word counts year over year. * Added a new table for long term statistics, like number of files over the years. No views are present for this data, I will add them later. * Bump versions by dotnet-bump-version. * Swipe Issues (#1745) * Updated theme support to be able to customize the tile color dynamically from a theme via --tile-color. In addition, --theme-color will update apple-mobile-web-app-status-bar-style as well as the non-apple variants * Removed --manga-reader-bg-color as it wasn't used anywhere. Fixed double pagination on swipe. * Cleaned up some dead threshold code for swipe. * Started refactoring tests to use an abstract test class. Stopping because I should do on the .net 7 branch to avoid large merge conflicts. Tests need to be re-designed so they can run in parallel. * Fixed a bug in reading lists where when deleting an item, order could be miscalculated. * Started adding new information for stat service. Refactored time spent reading to be more accurate by taking average time against how much of the chapter the user has read. * Hooked up total time reading at server stat level. Don't show fancy graphs on mobile. * Added new stats for v0.7 * Added a test for Clearing want to read * Fixed a few tests that weren't resetting state between runs * Fixed some broken unit tests * Ensure all Series queries sort by a case invariant string. * Added more aggressive caching of images. This will result in a min delay on pages after a cover is changed. * Fixed a bug where if during new word count calculation, new word count is zero, restoring the old count wasn't working. * Cleaned up some of the code for getting time estimates * Fixed a bug where triggering swipe right wasn't working when there was no scroll * Delete the temp folder for creating a download after a full zip is created. * Bump versions by dotnet-bump-version. * Stat hotfix (#1748) * Fixed a bug where a divide by 0 could occur * Email change now requires a password * Bump versions by dotnet-bump-version. * Holiday Bugfixes (#1762) * Don't show "not much going on" when we are actively downloading * Swipe to paginate is now behind a flag in the user preferences. * Added a new server setting for host name, if the server sits behind a reverse proxy. If this is set, email link generation will use it and will not perform any checks on accessibility (thus email will always send) * Refactored the code that checks if the server is accessible to check if host name is set, and thus return rue if so. * Added back the system drawing library for markdown parsing. * Fixed a validation error * Fixed a bug where folder watching could get re-triggered when it was disabled at a server level. * Made the manga reader loader absolute positioned for better visibility * Indentation * Bump versions by dotnet-bump-version. * Angular 15 (#1764) * Updated ngx-virtual-scroller * Removed the karma test config as it's breaking migration * Reverted to pre angular 15 * Upgraded packages and reverted target to ES6 for older devices * It's broken. Need to also find the safari version for old Ipads * Fixes some code in default pipe and many updates to packages. Removed support for old iOS versions as it restricted Kavita from using newer features. Build still broken. * More progress in getting build working on Angular 15. Removed polyfills.ts for new angular config * Remove all.css for icons and use scss instead * Removed stuff that isn't needed * Migrated extended linting to eslint, ran on project and updated issues. Removed a duplicate component that did nothing. Fixed a few places where lifecycle hooks werent being called as interface wasn't implemented. * App builds correctly. Source maps are still needed. * Fixed source maps and removed more testing stuff. I will re-add later in another release when I figure out how to properly tackle dependencies on backend. * Reverted back to old source map definition * Bump versions by dotnet-bump-version. * Angular 15 (#1765) * Refactored some code in BookService to make the code easier to understand * More lint fixes * Use npm ci for installs in pipeline * Fixed build system again by deleting nodejs. New build system uses package-lcok going forward. * Bump versions by dotnet-bump-version. * [skip ci] Misc stuff (#1766) * Refactored some code in BookService to make the code easier to understand * More lint fixes * Use npm ci for installs in pipeline * Fixed build system again by deleting nodejs. New build system uses package-lcok going forward. * Added a test case for Reading Time Estimation calculations * Some cleanup * Added even more testing to try and get scare's issue captured. * Automatic Collection Creation (#1768) * Made the unread badges slightly smaller and rounded on top right. * A bit more tweaks on the not read badges. Looking really nice now. * In order to start the work on managing collections from ScanLoop, I needed to refactor collection apis into the service layer and add unit tests. Removed ToUpper Normalization for new tags. * Hooked up ability to auto generate collections from SeriesGroup metadata tag. * Bump versions by dotnet-bump-version. * Auto Collection Bugfixes (#1769) * SeriesGroup tag can now have comma separated value to allow a series to be a part of multiple collections. * Added a missing unit test * Refactored how collection tags are created to work in the scan loop reliably. * Added a unit test for RemoveTagsWithoutSeries * Fixed a bug in reading list title generation to avoid Volume 0 if the underlying file had a title set. Fixed a misconfigured unit test. * Bump versions by dotnet-bump-version. * Scanner Performance Improvements (#1774) * Refactored the Genre code to be faster and used a dictonary to avoid some lookups. May fix the rare foreign constraint issue. * Refactored tag to the same implementation as Genre. Ensure when grabbing tags from ComicInfo, we normalize and throw out duplicates. * Removed an internal "external" field that was planned for Genres and Tags, but now with new plugin architecture, not needed. * Bump versions by dotnet-bump-version. * Spelling, grammar, and related consistency improvements (#1756) * Spelling, grammar, and word structure improvements * Email service text reworded to account for the Host Name feature * Bump versions by dotnet-bump-version. * Stat Polish (#1775) * SeriesGroup tag can now have comma separated value to allow a series to be a part of multiple collections. * Added a missing unit test * Refactored how collection tags are created to work in the scan loop reliably. * Added a unit test for RemoveTagsWithoutSeries * Fixed a bug in reading list title generation to avoid Volume 0 if the underlying file had a title set. Fixed a misconfigured unit test. * On User stats page, don't show the user selector on reading history, despite if youre an admin. Cleaned up how we show days with 0 reading events to be more clear. * Refactored the name of a component to reflect what it does * Removed plugin not using * Fix an issue where coalescing a key in epub might have multiple html files ending with the key. In this case, let's take the first. * Added PikaPods to the Readme * Tried to fix layout shift for charts, but need Robbie's help * Chart styling # Added: - Added: Added styling to force charts into their respective containers. # Removed: - Removed: Removed code blocking charts from being visible on mobile. * Merge conflict --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * UTC Dates + CDisplayEx API Enhancements (#1781) * Introduced a new claim on the Token to get UserId as well as Username, thus allowing for many places of reduced DB calls. All users will need to reauthenticate. Introduced UTC Dates throughout the application, they are not exposed in all DTOs, that will come later when we fully switch over. For now, Utc dates will be updated along side timezone specific dates. Refactored get-progress/progress api to be 50% faster by reducing how much data is loaded from the query. * Speed up the following apis: collection/search, download/bookmarks, reader/bookmark-info, recommended/quick-reads, recommended/quick-catchup-reads, recommended/highly-rated, recommended/more-in, recommended/rediscover, want-to-read/ * Added a migration to sync all dates with their new UTC counterpart. * Added LastReadingProgressUtc onto ChapterDto for some browsing apis, but not all. Added LastReadingProgressUtc to reading list items. Refactored the migration to run raw SQL which is much faster. * Added LastReadingProgressUtc onto ChapterDto for some browsing apis, but not all. Added LastReadingProgressUtc to reading list items. Refactored the migration to run raw SQL which is much faster. * Fixed the unit tests * Fixed an issue with auto mapper which was causing progress page number to not get sent to UI * series/volume has chapter last reading progress * Added filesize and library name on reading list item dto for CDisplayEx. * Some minor code cleanup * Forgot to fill a field * Bump versions by dotnet-bump-version. * Reading List Fixes (#1784) * Add ability to save readinglist comicinfo fields in Chapter. * Added the appropriate fields and migration for Reading List generation. * Started the reading list code * Started building out the CBL import code with some initial unit tests. * Fixed first unit test * Started refactoring control code into services and writing unit tests for ReadingLists. Found a logic issue around reading list title between create/update. Will be corrected in this branch with unit tests. * Can't figure out how to mock UserManager, so had to uncomment a few tests. * Tooltip for total pages read shows the full number * Tweaked the math a bit for average reading per week. * Fixed up the reading list unit tests. Fixed an issue where when inserting chapters into a blank reading list, the initial reading list item would have an order of 1 instead of 0. * Cleaned up the code to allow the reading list code to be localized easily and fixed up a bug in last PR. * Fixed a sorting issue on reading activity * Tweaked the code around reading list actionables not showing due to some weird filter. * Fixed edit library settings not opening on library detail page * Fixed a bug where reading activity dates would be out of order due to a bug in how charts works. A temp hack has been added. * Disable promotion in edit reading list modal since non-admins can (and should have) been able to use it. * Fixed a bug where non-admins couldn't update their OWN reading lists. Made uploading a cover image for readinglists now check against the user's reading list access to allow non-admin's to set images. * Fixed an issue introduced earlier in PR where adding chapters to reading list could cause order to get skewed. * Fixed another regression from earlier commit * Hooked in Import CBL flow. No functionality yet. * Code is a mess. Shifting how the whole import process is going to be done. Commiting so I can pivot drastically. * Very rough code for first step is done. * Ui has started, I've run out of steam for this feature. * Cleaned up the UI code a bit to make the step tracker nature easier without a dedicated component. * Much flow implementation and tweaking to how validation checks and what is sent back. * Removed import via cbl code as it's not done. Pushing to next release. * Bump versions by dotnet-bump-version. * Allow changing listening ip addresses (#1713) * Allow changing listening ip address * Use Json serialize for appsettings.config saving * BOM * IP Address validation * ip address reset * ValidIpAddress regex * Bump versions by dotnet-bump-version. * Release Testing Time (#1785) * Fixed a bug with getting continue point where there was a single volume unread and a later volume with chapters inside it, the chapters were being picked. * Fixed a bug where resuming from jump key wasn't working (develop) * Cleaned up the spacing * Bump versions by dotnet-bump-version. * Release Testing Bugs (#1790) * Stop showing loading indicator when no next/prev chapter * Fixed a bug where manage collections wasn't named correctly in UI. * Slight tweaks on email flow * Bump versions by dotnet-bump-version. * Release Testing Part 2 (#1794) * Stop showing loading indicator when no next/prev chapter * Fixed a bug where manage collections wasn't named correctly in UI. * Slight tweaks on email flow * Fixed a bug where we were grabbing wrong property for book layout mode * Fixed an issue where pagination area wasn't properly spanning window on different scaling modes. * Fixed a bug where right pagination area wasn't sticking to the right hand side on original scaling * Added a note from reading an epub3 * Reworked some of the readme * Bump versions by dotnet-bump-version. * Small Build Fix (#1795) * Stop showing loading indicator when no next/prev chapter * Fixed a bug where manage collections wasn't named correctly in UI. * Slight tweaks on email flow * Fixed a bug where we were grabbing wrong property for book layout mode * Fixed an issue where pagination area wasn't properly spanning window on different scaling modes. * Fixed a bug where right pagination area wasn't sticking to the right hand side on original scaling * Added a note from reading an epub3 * Reworked some of the readme * Changed the build to ci * Bump versions by dotnet-bump-version. * Final Release Testing (#1796) * Fix some wording * Fixed up stats to have total info on hover * Fixed up a stat card not having clickable hint * Bump versions by dotnet-bump-version. * v0.7 Release (#1797) * Bump versions by dotnet-bump-version. * Aligned build with main (#1801) * Bump versions by dotnet-bump-version. * Revert "Aligned build with main (#1801)" This reverts commit 3f6eb5fe17934cc5aa3b227b165e184bf47ade14. * v0.7 Issues for Hotfix (#1812) * Fix signalr admin messages sending (#1809) * Changed messsage hub to use userIds * SignalR events are fixed * Fixed broken advanced tab on library settings * Fixed regex timeout security issues. * Added a migration for GMT+1 users where their UTC dates were getting broken somehow. * Removed a console.log * Fixed a migration name --------- Co-authored-by: Snd-R <76580768+Snd-R@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Fixed manage collections not being toggleable (#1813) * Bump versions by dotnet-bump-version. * Version bump (#1814) * Bump versions by dotnet-bump-version. * Random Changes and Enhancements (#1819) * When skipping over folders in a scan, inform the ui * Try out new backout condition for library watcher. * Tweaked the code for folder watching to be more intense on killing if stuck in inotify loop. * Streamlined my implementation of enhanced LibraryWatcher * Added new extension method to make complex where statements cleaner. * Added an implementation to flatten series and not show them if they have relationships defined. Only the parent would show. Currently disabled until i figure out how to apply it. * Added the ability to collapse series that are not the primary entry point to reading. Configurable in library settings, only applies when all libraries in a filter have the property to true. * Exclude from parsing .@_thumb directories, a QNAP system folder. Show number of items a JumpKey has * Refactored some time reading to display in days, months, years or minutes. * Bump versions by dotnet-bump-version. * Epub 3.2 Grouping issue (#1823) * Added font swapping and removed some unneeded css * Fixed a bug where epub 3 tags weren't being applied for grouping * Bump versions by dotnet-bump-version. * Revert ability to run docker as non-root due to issues with Synology. (#1832) * Bump versions by dotnet-bump-version. * CBL Import (#1834) * Wrote my own step tracker and added a prev button. Works up to first conflict flow. * Everything but final import is hooked up in the UI. Polish still needed, but getting there. * Making more progress in the CBL import flow. * Ready for the last step * Cleaned up some logic to prepare for the last step and reset * Users like order to be starting at 1 * Fixed a few bugs around cbl import * CBL import is ready for some basic testing * Added a reading list hook on side nav * Fixed up unit tests * Added icons and color to the import flow * Tweaked some phrasing * Hooked up a loading variable but disabled the component as it didn't look good. * Styling it up * changed an icon to better fit --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * .NET 7 + Spring Cleaning (#1677) * Updated to net7.0 * Updated GA to .net 7 * Updated System.IO.Abstractions to use New factory. * Converted Regex into SourceGenerator in Parser. * Updated more regex to source generators. * Enabled Nullability and more regex changes throughout codebase. * Parser is 100% GeneratedRegexified * Lots of nullability code * Enabled nullability for all repositories. * Fixed another unit test * Refactored some code around and took care of some todos. * Updating code for nullability and cleaning up methods that aren't used anymore. Refctored all uses of Parser.Normalize() to use new extension * More nullability exercises. 500 warnings to go. * Fixed a bug where custom file uploads for entities wouldn't save in webP. * Nullability is done for all DTOs * Fixed all unit tests and nullability for the project. Only OPDS is left which will be done with an upcoming OPDS enhancement. * Use localization in book service after validating * Code smells * Switched to preview build of swashbuckle for .net7 support * Fixed up merge issues * Disable emulate comic book when on single page reader * Fixed a regression where double page renderer wouldn't layout the images correctly * Updated to swashbuckle which support .net 7 * Fixed a bad GA action * Some code cleanup * More code smells * Took care of most of nullable issues * Fixed a broken test due to having more than one test run in parallel * I'm really not sure why the unit tests are failing or are so extremely slow on .net 7 * Updated all dependencies * Fixed up build and removed hardcoded framework from build scripts. (this merge removes Regex Source generators). Unit tests are completely busted. * Unit tests and code cleanup. Needs shakeout now. * Adjusted Series model since a few fields are not-nullable. Removed dead imports on the project. * Refactored to use Builder pattern for all unit tests. * Switched nullability down to warnings. It wasn't possible to switch due to constraint issues in DB Migration. * Bump versions by dotnet-bump-version. * Added noindex to prevent SEO from indexing user's sites. Reverted http3 support as docker users were having port issue (#1850) * Bump versions by dotnet-bump-version. * Fixed docker instances not being able to start due to misconfigured default for IpAddress binding (#1851) * Bump versions by dotnet-bump-version. * Fixing docker bind issue (#1852) # Fixed: - Fixed an issue for docker users conflicting with a previous feature inclusion for multiple IP addresses * Docker bind fix (#1853) * Fixing docker bind issue # Fixed: - Fixed an issue for docker users conflicting with a previous feature inclusion for multiple IP addresses * build error fix * Docker bind fix (#1854) * Fixing docker bind issue # Fixed: - Fixed an issue for docker users conflicting with a previous feature inclusion for multiple IP addresses * build error fix * re-raise * fix (#1855) * Bump versions by dotnet-bump-version. * Added vertical reading mode to the book reader (#1787) * Add vertical reading mode support and update API for reading mode preference * Removed dead code, added a fix for scroll end margins in chrome when in vertical mode(book reader). Added back some comments * Added Description()] annotation for the ReadingMode enum, like other enums, and added summary documentation * Added the ability to scroll in vertical writing style without holding down shift. Also renamed the book reader's readingMode to writing style. * Renamed the BookReadingMode to BookWritingStyle. And changed the migrations accordingly. * Fixed some minor bugs, regarding scrolling and vertical writing style when the book settings is open. * Fixed a minor bug where the graphics regarding the current page would require the mouse to be moved before it got updated when switching between writing styles. * Fixed some bugs regarding furigana getting a bit cropped same for images * Add vertical reading mode support and update API for reading mode preference * Removed dead code, added a fix for scroll end margins in chrome when in vertical mode(book reader). Added back some comments * Added Description()] annotation for the ReadingMode enum, like other enums, and added summary documentation * Added the ability to scroll in vertical writing style without holding down shift. Also renamed the book reader's readingMode to writing style. * Renamed the BookReadingMode to BookWritingStyle. And changed the migrations accordingly. * Fixed some minor bugs, regarding scrolling and vertical writing style when the book settings is open. * Fixed a minor bug where the graphics regarding the current page would require the mouse to be moved before it got updated when switching between writing styles. * Fixed some bugs regarding furigana getting a bit cropped same for images * Added reset support for writing style, after rebase. * Changes pagination for vertical scrolling such as the user will need to scroll to end before being able to paginate. Previously it felt unnatural and the user could accidentally paginate while scrolling on mobile. * Pagination would not stick to the left if the content was smaller than the reader in vertical writing style. * Fixed summary text * Added missing line, fixes build error * Addresses the comments given in code-review. * Moved columnGap outside the class, and changed it to a const * Bump versions by dotnet-bump-version. * Bumped css budget (#1856) * Bump versions by dotnet-bump-version. * Fix for docker bind (#1857) #Added: - Added: null IpAddresses to appsettings * Bump versions by dotnet-bump-version. * Default to empty string for IpAddress to allow to fallback into existing experience (#1859) Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com> * Bump versions by dotnet-bump-version. * CBL Import Rework (#1862) * Fixed a typo in a log * Invalid XML files now "validate" correctly by sending back a failure. * Cleaned up messaging on backend and frontend to provide some linking on series name when collision, handle corrupt xml files, etc. * When reading list conflict occurs, show the reading list name that's conflicting. Started refactoring the code to allow multiple files to be imported at once. * Started adding new CBL elements for some enhancements I have planned with maintainers. * Default to empty string for IpAddress to allow to fallback into existing experience * Tweaked the layout of reading list page (not complete), moved some not used much controls to page extras and reordered the buttons for reading list * Edit Reading Lists now allows selection of cover image from existing items * Fixed a bug where cover chooser base64 to image would fail to write webp files. * Refactored the validate step to now handle multiple files in one go. * Clean up code * Don't show CBL name if there were xml errors that prevented showing it * Don't allow user to go prev step after they perform the import. * Cleaned up the heading code for accordions * Fixed a bug with import keeping failed items * Sort the failures to the bottom of result windows * CBL import is pretty solid. Need one pass from Robbie on Reading List Page * Bump versions by dotnet-bump-version. * Fix for themes not applying (#1873) # Fixed: - Fixed: Fixed an issue where themes would not apply properly. * Bump versions by dotnet-bump-version. * OPDS-PS v1.2 Support + a few bugfixes (#1869) * Fixed up a localization lookup test case * Refactored some webp to a unified method * Cleaned up some code * Expanded webp conversion for covers to all entities * Code cleanup * Prompt the user when they are about to delete multiple series via bulk actions * Aligned Kavita to OPDS-PS 1.2. * Fixed a bug where clearing metadata filter of series name didn't clear the actual field. * Added some documentation * Refactored how covert covers to webp works. Now we will handle all custom covers for all entities. Volumes and Series will not be touched but instead be updated via a RefreshCovers call. This will fix up the references much faster. * Fixed up the OPDS-PS 1.2 attributes to only show on PS links * Bump versions by dotnet-bump-version. * Fixes bus in vertical writing mode. (#1871) * Bump versions by dotnet-bump-version. * Misc Fixes + Enhancements (#1875) * Moved Collapse Series with relationships into a user preference rather than library setting. * Fixed bookmarks not converting to webp after initial save * Fixed a bug where when merging we'd print out a duplicate series error when we shouldn't have * Fixed a bug where clicking on a genre or tag from server stats wouldn't load all-series page in a filtered state. * Implemented the ability to have Login role and thus disable accounts. * Ensure first time flow gets the Login role * Refactored user management screen so that pending users can be edited or deleted before the end user accepts the invite. A side effect is old legacy users that were here before email was required can now be deleted. * Show a progress bar under the main series image on larger viewports to show whole series progress. * Removed code no longer needed * Cleanup tags, people, collections without connections after editing series metadata. * Moved the Entity Builders to the main project * Bump versions by dotnet-bump-version. * Reworks how the image sizes in the book reader gets handled (#1872) * Fixes bus in vertical writing mode. * Reworks how the images gets resized. They should be more adaptive and pages with single images will get centered. * Adds back an unintended change * Revert "Fixes bus in vertical writing mode." This reverts commit a1cf9675def45fd626a149d90f573104b5d280cd. * Added debounce and timeout to the updateImageSize to prevent it from getting run before the page loads * Fixes semicolons missing semicolons * Improves the detection in checkSingleImagePage. * Bump versions by dotnet-bump-version. * Base Url implementation (#1824) * Base Url implementation * PR requested changes * Bump versions by dotnet-bump-version. * Reading List Polish (#1879) * Use Reading Order to count epub pages rather than raw HTML files. * Send email on background thread for initial invite flow. * Reorder default writing style for new users so Horizontal is default * Changed reading activity to use average hours read rather than events to bring more meaningful data. * added ability to start reading incognito from the top of series detail, needs a bit of styling help though. * Refactored extensions out into their own package, added new fields for reading list to cover total run, cbl import now takes those dates and overrides on import. Replaced many instances of numbers to be comma separated. * Added ability to edit reading list run start and end year/month. Refactored some code for valid month/year into a helper method. * Added a way to see the reading list's release years. * Added some merged image code, but had to remove due to cover dimensions not fixed. * tweaked style for accessibility mode on reading list items * Tweaked css for non virtualized and virtualized containers * Fixed release updates failing * Commented out the merge code. * Typo on words read per year * Fixed unit tests * Fixed virtualized scroll * Cleanup CSS * Bump versions by dotnet-bump-version. * Fixed an issue where docker installs were trying to update wwwroot, when docker doesn't do reverse proxy stuff. (#1880) Fixed a bug in reading list date calculations. * Bump versions by dotnet-bump-version. * Security Event Logging & Bugfixes (#1882) * Fixed bookmarking failing to convert to webp * Brought the ag-swipe/ng-swipe code into Kavita due to being abandoned by developer and angular requirements. * Fixed average reading time per week finally * Cleaned up some extra decimals on time duration pipe * Don't try to update index.html for base url on local. Fixed ag-swipe on prod mode. * Updated a link on theme manager to point to the new github * Range knobs should be primary color on firefox too * Implemented the ability to get thumbnails of pages inside an archive or pdf. * Updated packages and fixed opds-ps 1.2 issue * Fixed lock file * Allow Kavita's Swagger to hit instances with CORS * Added IP/Request logging for Security Audits * Linked up Summary tag from CBL into Kavita. * Redid the migration so SecurityEvent now has UTC date as well. * Split security logging to a separate file * Update to new versions of checkout and setup * Added a PR check on PR body to ensure that it doesn't contain any characters that break our discord hook. * Updating action * optimize regex in action * Fixed an issue where fit to width would cause the actual height of the image to be shown for pagination bars, instead of rendered. * Added some new code in GetPageFromFiles to ensure pages that exceed array map down to last file. * Added comment about robots * Fixed up unit tests for new ReaderService signature * Kavita now cleans up empty reading lists at night * Don't allow nightly cleanup to run if we are running media conversion tasks * Fixed some bugs in typeahead, it should behave much more reliably. * Fix an issue where emulate comic book wasn't extending to the bottom properly * Added support for Series Chapter 001 Volume 001 * Refactor XFrameOptions="SameOrigins" out to allow users to override in appsettings.json. * Added a rate limiter for some endpoints, but it doesn't seem to be triggering --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Security Event Logging & Bugfixes (rerelease) (#1883) * Fixed bookmarking failing to convert to webp * Brought the ag-swipe/ng-swipe code into Kavita due to being abandoned by developer and angular requirements. * Fixed average reading time per week finally * Cleaned up some extra decimals on time duration pipe * Don't try to update index.html for base url on local. Fixed ag-swipe on prod mode. * Updated a link on theme manager to point to the new github * Range knobs should be primary color on firefox too * Implemented the ability to get thumbnails of pages inside an archive or pdf. * Updated packages and fixed opds-ps 1.2 issue * Fixed lock file * Allow Kavita's Swagger to hit instances with CORS * Added IP/Request logging for Security Audits * Linked up Summary tag from CBL into Kavita. * Redid the migration so SecurityEvent now has UTC date as well. * Split security logging to a separate file * Update to new versions of checkout and setup * Added a PR check on PR body to ensure that it doesn't contain any characters that break our discord hook. * Updating action * optimize regex in action * Fixed an issue where fit to width would cause the actual height of the image to be shown for pagination bars, instead of rendered. * Added some new code in GetPageFromFiles to ensure pages that exceed array map down to last file. * Added comment about robots * Fixed up unit tests for new ReaderService signature * Kavita now cleans up empty reading lists at night * Don't allow nightly cleanup to run if we are running media conversion tasks * Fixed some bugs in typeahead, it should behave much more reliably. * Fix an issue where emulate comic book wasn't extending to the bottom properly * Added support for Series Chapter 001 Volume 001 * Refactor XFrameOptions="SameOrigins" out to allow users to override in appsettings.json. * Added a rate limiter for some endpoints, but it doesn't seem to be triggering * Fixed bad yaml --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Some fixes from last release (#1884) * Removed SecurityEvent middleware solution. It was out of scope originally. * Fixed manage users still calling pending when the api is no more * Added back the online indicator on manage users * Bump versions by dotnet-bump-version. * Added push to ghcr.io (#1885) * Added container push to ghcr.io * Updated login method * Bump versions by dotnet-bump-version. * Donate styling (#1886) * Donate styling # Changed - Changed the styling and positioning of the donate button to still be visible but less intrusive and more cohesive to the application. * Updated pr action * Revert "Updated pr action" This reverts commit d3f7c7b2edfdcdbb79acb9e7816247ef5ebc30ea. * Bump versions by dotnet-bump-version. * Fix workflow # Fixed: - Fixed ghcr.io build - Updated PR action * Bump versions by dotnet-bump-version. * Tweaks (#1890) * Updated number inputs with a more mobile friendly control * Started writing lots of unit tests on PersonHelper to try and hammer out foreign constraint * Fixes side-nav actionable alignment * Added some unit tests * Buffed out the unit tests * Applied input modes throughout the app * Fixed a small bug in refresh token validation to make it work correctly * Try out a new way to block multithreading from interacting with people during series metadata update. * Fixed the lock code to properly lock, which should help with any constraint issues. * Locking notes * Tweaked locking on people to prevent a constraint issue. This slows down the scanner a bit, but not much. Will tweak after validating on a user's server. * Replaced all DBFactory.Series with SeriesBuilder. * Replaced all DBFactory.Volume() with VolumeBuilder * Replaced SeriesMetadata with Builder * Replaced DBFactory.CollectionTag * Lots of refactoring to streamline entity creation * Fixed one of the unit tests * Refactored all of new Library() * Removed tag and genre * Removed new SeriesMetadata * Refactored new Volume() * MangaFile() * ReadingList() * Refactored all of Chapter and ReadingList * Add title to all event widget flows * Updated Base Url to inform user it doesn't work for docker users with non-root user. * Added unit test coverage to FormatChapterTitle and FormatChapterName. * Started on Unit test for scanner, but need to finish it later. --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * OPDS Flattening (#1904) * Flattening OPDS Structure # Changed - Flattened OPDS structure to reduce user taps. * Fixing format * Fixing book series titles * Optimized file size to use pre-calculated data to avoid an I/O touch. * Fixes #1898 by aligning all content headers to the correct MIME types * Remove dead code * Fixed a bug with continue point where it fails on chapters or volumes tagged with a range --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * OPDS formatting fix (#1905) # Fixed - Fixed a formatting issue where there was a unnecessary - at the beginning of titles. (develop) # Changed - Updated github action version. (develop) * Bump versions by dotnet-bump-version. * Password Validation and Google phishing error pass (#1913) * Updating password validation # Changed: - Changed: Updated password validation to use pattern so user can not submit or create passwords <6 or >32 characters long. * Removed the svg for image in case it's causing google security issues. --------- Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com> * Bump versions by dotnet-bump-version. * More Google Security Changes (#1923) * Removed packages not used anymore * Reverted removing lazysizes * Removed the svg for image in case it's causing google security issues. * Removed unneeded service * Bump versions by dotnet-bump-version. * Manga Reader Fixes (#1925) * Fixed up an issue where image might be cut off in fit to height * Removed some calls to backend for translating age rating to a string * Fixed an issue with sizing on page splitting right to left. * Ensure all image access requires apikey * Removed a TODO * Bump versions by dotnet-bump-version. * Bugfix/base url in docker (#1901) * fixed: BaseUrl option is not supported in Docker - discussion in #1824, the checks should not have been added in the first place * changed: contributors require .NET 7.0+ in order to build * Bump versions by dotnet-bump-version. * Random Cleanup + OPDS Base Url Support (#1926) * Updated a ton of dependencies. PDFs reader got a big update from PDF.js 2.6 -> 3.x * Rolled back fontawesome update * Updated to latest angular patch. Fixed search being too long instead of just to the end of the browser screen. * Fixed alignment on download icon for download indicator in cards * Include progress information on Want To Read API and when marking something as Read, perform cleanup service on want to read. * Removed mark-read updating want to read. As there are series restrictions and it could be misleading. * Tweaked login page spacing when form is dirty * Replaced an object instantiation * Commented out a few tests that always break when updating NetVips (but always work) * Updated ngx-toastr * Added styles for alerts to Kavita. They were somehow missing. Fixed an issue where when OPDS was disabled, user preferences wouldn't tell them. * Wired up a reset base url button to match Ip Addresses * Disable ipAddress and port for docker users * Removed cache dir since it's kinda pointless currently * Started the update for OPDS BaseUrl support * Fixed OPDS url not reflecting base url on localhost * Added extra plumbing to allow sending a real email when testing a custom service. * Implemented OPDS support under Base Url. Added pagination to all APIs where applicable. * Added a swallowing of permission denied on Updating baseurl in index.html for inapplicable users. * Fixed a bad test * Bump versions by dotnet-bump-version. * Bugfixes from last PR (#1928) * Fixed a regression where confirm-email was validating for a real email when it's not required. * Fixed fit to screen breaking as canvas was still showing on the screen when it should have been hidden. * Fixed a missing age rating in the pipe * Fixed fit to height not properly aligning the image * Tweaked Startup check for base url * Bump versions by dotnet-bump-version. * ComicInfo Derived Reading Lists (#1929) * Implemented the ability to generate reading lists from StoryArc and StoryArcNumber ComicInfo fields. * Refactored to add AlternativeSeries support. * Fixed up the handling when we need to update reading list where order is already present. * Refactored how skipping empty reading list pairs works * Library Settings for Reading List Creation (#1930) * Added the code for Library Settings to allow turning off Reading List creation. * Fixed a typo * Bump versions by dotnet-bump-version. * Pre-Release Shakeout (#1932) * Allow users to setup an account with a fake email and the same username without hitting the validate username code. * Fixed a bug where opds url could have //. Fixed a bug where baseurl wasn't being used for invite link generation * Fixed enum mismatch causing age rating to display incorrectly on UI * Bump versions by dotnet-bump-version. * Release Testing Day 1 (#1933) * Enhance plugin/authenticate to allow RefreshToken to be returned as well. * When typing a series name, min, or max filter, press enter to apply metadata filter. * Cleaned up the documentation around MaxCount and TotalCount * Fixed a bug where PublicationStatus wasn't being correctly set due to some strange logic I coded. * Fixed bookmark mode not having access to critical page dimensions. Fetching bookmark info api now returns dimensions by default. * Fixed pagination scaling code for different fitting options * Fixed missing code to persist page split in manga reader * Removed unneeded prefetch of blank images in bookmark mode * Bump versions by dotnet-bump-version. * Release Testing Day 2 (#1937) * Removed unneded ngModel on password field * Fixed some bad validation messages on Edit Reading List modal and disabled save button * Added a lot of trace code to help debug a foreign constraint issue. * Fixed a bug where after a series is scanned, generate covers for series didn't respect webp cover generation. * Fixed library last scan being stored in Utc, but expected to be server time. * Fixed up some of that trace logging being way too verbose. Fixed a case where when a missing storyarc number, the whole pair was dropped. Now, it will default that item to the end of the reading list. Fixed a bug where Start and End dates weren't being calculated after generating a reading list. * Fixed a bug where the default admin user for reading list creation from files was selecting the wrong user. Changed so that when there is a bad pair (aka number missing) and only 1 pair, then we wont constantly reorder the item. * Fixed unit test * Bump versions by dotnet-bump-version. * Logging Change (#1941) * Tweaked trace logging for foreign constraint issue. Removed some locks where we werent touching important code that needed locking on. * Reverted back some code * Bump versions by dotnet-bump-version. * Reading List Bug (#1943) * Refactored how we calculate start and end dates for a reading list generated from Scan. * Cleaned up the logging messages a bit * By default, allow DB statements to include params, since there's no PPI involved. * Bump versions by dotnet-bump-version. * Tidying up (#1944) * Fixing nav logo * Adding emulate book for single renderer * enabling control * forcing hardware acceleration on infinite scroller * Fixing review comments - Refactoring bookshadow to use shared css - Adding issue comment for hardware acceleration hack * Fixing bookshadow happening on first and last page * Bump versions by dotnet-bump-version. * Release Testing Day 3 (#1946) * Removed extra trace messages as the people issue might have been resolved. * When registering, disable button until form is valid. Allow non-email formatted emails, but not blank. * Fixed opds not having http(s):// * Added a new API to allow scanning all libraries from end point * Moved Bookmarks directory to Media tab * Fixed an edge case for finding next chapter when we had volume 1,2 etc but they had the same chapter number. * Code cleanup * Bump versions by dotnet-bump-version. * Release Testing Day 3 (#1951) * Code cleanup. Fixed OPDS images missing api key. Fixed theme color on site manifest not being black. * Removed a console.log from timeago pipe * Reading list page is now alphabetical and the modal for adding to a reading list is ordered by most recent. * Fixed a bug where remove read from reading list failed due to Calculating Start and End date assuming chapter would always be there. * Fixed a bug where reading list cover would get reset when editing the reading list. * Fixed a bug where reading list item didn't have not read badge. It's on old style. * Fixed a bug where user-preferences was hitting an admin only api when there was a better alternative * Slight memory improvement on a common db call * Fixed a bug where resetting to default theme when a theme was deleted was throwing an exception and failing. * All Login dtos now have the active KavitaVersion to make external apps able to handle what version of the API they are connecting with. * Fixed up a case where getVolume repo method always assumed there was a volume by that Id. * Bump versions by dotnet-bump-version. * Release Testing Final Stretch (#1952) * Fixed a bug breaking ability to save server settings * Fixed a case where if a user updated appsetting.json, the DB wouldn't update to stay in alignment. * Bump versions by dotnet-bump-version. * v0.7.2 - Reading Lists, CBL Support, OPDS Flattening and More! (#1953) * Version bump * Fixed scan all libraries not doing it on background thread from request. * Bump versions by dotnet-bump-version. * Version correction * Fixed the merge --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: Mike <github@emailisgood.com> Co-authored-by: Muggz <mug@passw0rd.org> Co-authored-by: Domenic Fiore <DomenicF@users.noreply.github.com> Co-authored-by: Kupferhirn <kupferhirn@brokensoft.net> Co-authored-by: Snd-R <76580768+Snd-R@users.noreply.github.com> Co-authored-by: CKolle <115696142+CKolle@users.noreply.github.com> Co-authored-by: Gazy Mahomar <gmahomarf@users.noreply.github.com> Co-authored-by: Chris Plaatjes <kizaing@gmail.com> Co-authored-by: Andy <mrclock9090@yahoo.com>
76
.github/workflows/sonar-scan.yml
vendored
@ -2,30 +2,37 @@ name: .NET Build Test and Sonar Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: '*'
|
||||
branches: '**'
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
types: [synchronize]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR Body
|
||||
uses: JJ/github-pr-contains-action@releases/v10
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
bodyDoesNotContain: "[\"|`]"
|
||||
build:
|
||||
name: Build .Net
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v2
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Install Swashbuckle CLI
|
||||
shell: powershell
|
||||
run: dotnet tool install -g --version 6.4.0 Swashbuckle.AspNetCore.Cli
|
||||
run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
@ -75,24 +82,23 @@ jobs:
|
||||
- name: Test
|
||||
run: dotnet test --no-restore --verbosity normal
|
||||
|
||||
|
||||
version:
|
||||
name: Bump version on Develop push
|
||||
needs: [ build ]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v2
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Install Swashbuckle CLI
|
||||
run: dotnet tool install -g --version 6.4.0 Swashbuckle.AspNetCore.Cli
|
||||
run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
@ -111,6 +117,9 @@ jobs:
|
||||
name: Build Nightly Docker if Develop push
|
||||
needs: [ build, version ]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
steps:
|
||||
- name: Find Current Pull Request
|
||||
@ -140,7 +149,7 @@ jobs:
|
||||
echo "::set-output name=BODY::$body"
|
||||
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: develop
|
||||
|
||||
@ -151,7 +160,7 @@ jobs:
|
||||
- run: |
|
||||
cd UI/Web || exit
|
||||
echo 'Installing web dependencies'
|
||||
npm ci --legacy-peer-deps
|
||||
npm ci
|
||||
|
||||
echo 'Building UI'
|
||||
npm run prod
|
||||
@ -177,21 +186,28 @@ jobs:
|
||||
run: echo "${{steps.get-version.outputs.assembly-version}}"
|
||||
|
||||
- name: Compile dotnet app
|
||||
uses: actions/setup-dotnet@v2
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Install Swashbuckle CLI
|
||||
run: dotnet tool install -g --version 6.4.0 Swashbuckle.AspNetCore.Cli
|
||||
run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
- run: ./monorepo-build.sh
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
@ -206,7 +222,7 @@ jobs:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
push: true
|
||||
tags: kizaing/kavita:nightly, kizaing/kavita:nightly-${{ steps.parse-version.outputs.VERSION }}
|
||||
tags: kizaing/kavita:nightly, kizaing/kavita:nightly-${{ steps.parse-version.outputs.VERSION }}, ghcr.io/kareadita/kavita:nightly, ghcr.io/kareadita/kavita:nightly-${{ steps.parse-version.outputs.VERSION }}
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
@ -224,6 +240,9 @@ jobs:
|
||||
name: Build Stable Docker if Main push
|
||||
needs: [ build ]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
|
||||
@ -254,7 +273,7 @@ jobs:
|
||||
echo "::set-output name=BODY::$body"
|
||||
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: main
|
||||
|
||||
@ -266,7 +285,7 @@ jobs:
|
||||
|
||||
cd UI/Web || exit
|
||||
echo 'Installing web dependencies'
|
||||
npm ci --legacy-peer-deps
|
||||
npm install
|
||||
|
||||
echo 'Building UI'
|
||||
npm run prod
|
||||
@ -294,20 +313,27 @@ jobs:
|
||||
id: parse-version
|
||||
|
||||
- name: Compile dotnet app
|
||||
uses: actions/setup-dotnet@v2
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 7.0.x
|
||||
- name: Install Swashbuckle CLI
|
||||
run: dotnet tool install -g --version 6.4.0 Swashbuckle.AspNetCore.Cli
|
||||
run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
- run: ./monorepo-build.sh
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
@ -322,7 +348,7 @@ jobs:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
push: true
|
||||
tags: kizaing/kavita:latest, kizaing/kavita:${{ steps.parse-version.outputs.VERSION }}
|
||||
tags: kizaing/kavita:latest, kizaing/kavita:${{ steps.parse-version.outputs.VERSION }}, ghcr.io/kareadita/kavita:latest, ghcr.io/kareadita/kavita:${{ steps.parse-version.outputs.VERSION }}
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -10,9 +10,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
|
||||
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.2" />
|
||||
<PackageReference Include="NSubstitute" Version="4.4.0" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.5" />
|
||||
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.5" />
|
||||
<PackageReference Include="NSubstitute" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -16,7 +16,7 @@ namespace API.Benchmark;
|
||||
[MemoryDiagnoser]
|
||||
[RankColumn]
|
||||
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||
[SimpleJob(launchCount: 1, warmupCount: 5, targetCount: 20)]
|
||||
[SimpleJob(launchCount: 1, warmupCount: 5, invocationCount: 20)]
|
||||
public class ArchiveServiceBenchmark
|
||||
{
|
||||
private readonly ArchiveService _archiveService;
|
||||
|
@ -1,9 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Order;
|
||||
|
||||
namespace API.Benchmark;
|
||||
|
||||
|
@ -14,22 +14,11 @@ namespace API.Benchmark;
|
||||
[MemoryDiagnoser]
|
||||
[RankColumn]
|
||||
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||
[SimpleJob(launchCount: 1, warmupCount: 5, targetCount: 20)]
|
||||
[SimpleJob(launchCount: 1, warmupCount: 5, invocationCount: 20)]
|
||||
public class EpubBenchmark
|
||||
{
|
||||
private const string FilePath = @"E:\Books\Invaders of the Rokujouma\Invaders of the Rokujouma - Volume 01.epub";
|
||||
private readonly Regex WordRegex = new Regex(@"\b\w+\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
// [Benchmark]
|
||||
// public async Task GetWordCount_PassByString()
|
||||
// {
|
||||
// using var book = await EpubReader.OpenBookAsync(FilePath, BookService.BookReaderOptions);
|
||||
// foreach (var bookFile in book.Content.Html.Values)
|
||||
// {
|
||||
// GetBookWordCount_PassByString(await bookFile.ReadContentAsTextAsync());
|
||||
// ;
|
||||
// }
|
||||
// }
|
||||
private readonly Regex _wordRegex = new Regex(@"\b\w+\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
[Benchmark]
|
||||
public async Task GetWordCount_PassByRef()
|
||||
@ -111,6 +100,6 @@ public class EpubBenchmark
|
||||
|
||||
|
||||
return doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]")
|
||||
.Sum(node => WordRegex.Matches(node.InnerText).Count);
|
||||
.Sum(node => _wordRegex.Matches(node.InnerText).Count);
|
||||
}
|
||||
}
|
||||
|
@ -74,5 +74,24 @@ public class ParserBenchmarks
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Test_CharacterReplace()
|
||||
{
|
||||
foreach (var name in _names)
|
||||
{
|
||||
var d = name.Contains('a');
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Test_StringReplace()
|
||||
{
|
||||
foreach (var name in _names)
|
||||
{
|
||||
|
||||
var d = name.Contains("a");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,23 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.10" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="NSubstitute" Version="4.4.0" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="17.2.3" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.11" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.11" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
@ -7,6 +7,7 @@ using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
@ -29,6 +30,7 @@ public abstract class AbstractDbTest
|
||||
protected const string BackupDirectory = "C:/kavita/config/backups/";
|
||||
protected const string LogDirectory = "C:/kavita/config/logs/";
|
||||
protected const string BookmarkDirectory = "C:/kavita/config/bookmarks/";
|
||||
protected const string SiteThemeDirectory = "C:/kavita/config/themes/";
|
||||
protected const string TempDirectory = "C:/kavita/config/temp/";
|
||||
protected const string DataDirectory = "C:/data/";
|
||||
|
||||
@ -77,18 +79,9 @@ public abstract class AbstractDbTest
|
||||
|
||||
_context.ServerSetting.Update(setting);
|
||||
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Manga",
|
||||
Folders = new List<FolderPath>()
|
||||
{
|
||||
new FolderPath()
|
||||
{
|
||||
Path = "C:/data/"
|
||||
}
|
||||
},
|
||||
Series = new List<Series>()
|
||||
});
|
||||
_context.Library.Add(new LibraryBuilder("Manga")
|
||||
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
|
||||
.Build());
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
@ -103,6 +96,7 @@ public abstract class AbstractDbTest
|
||||
fileSystem.AddDirectory(CoverImageDirectory);
|
||||
fileSystem.AddDirectory(BackupDirectory);
|
||||
fileSystem.AddDirectory(BookmarkDirectory);
|
||||
fileSystem.AddDirectory(SiteThemeDirectory);
|
||||
fileSystem.AddDirectory(LogDirectory);
|
||||
fileSystem.AddDirectory(TempDirectory);
|
||||
fileSystem.AddDirectory(DataDirectory);
|
||||
|
@ -36,7 +36,6 @@ public class ComicInfoTests
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
#region CalculatedCount
|
||||
|
||||
[Fact]
|
||||
|
@ -1,26 +0,0 @@
|
||||
using API.Data;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="API.Entities.Series"/>
|
||||
/// </summary>
|
||||
public class SeriesTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Darker than Black")]
|
||||
public void CreateSeries(string name)
|
||||
{
|
||||
var key = API.Services.Tasks.Scanner.Parser.Parser.Normalize(name);
|
||||
var series = DbFactory.Series(name);
|
||||
Assert.Equal(0, series.Id);
|
||||
Assert.Equal(0, series.Pages);
|
||||
Assert.Equal(name, series.Name);
|
||||
Assert.Null(series.CoverImage);
|
||||
Assert.Equal(name, series.LocalizedName);
|
||||
Assert.Equal(name, series.SortName);
|
||||
Assert.Equal(name, series.OriginalName);
|
||||
Assert.Equal(key, series.NormalizedName);
|
||||
}
|
||||
}
|
@ -4,7 +4,8 @@ using System.Linq;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Parser;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Extensions;
|
||||
@ -13,22 +14,15 @@ public class ChapterListExtensionsTests
|
||||
{
|
||||
private static Chapter CreateChapter(string range, string number, MangaFile file, bool isSpecial)
|
||||
{
|
||||
return new Chapter()
|
||||
{
|
||||
Range = range,
|
||||
Number = number,
|
||||
Files = new List<MangaFile>() {file},
|
||||
IsSpecial = isSpecial
|
||||
};
|
||||
return new ChapterBuilder(number, range)
|
||||
.WithIsSpecial(isSpecial)
|
||||
.WithFile(file)
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static MangaFile CreateFile(string file, MangaFormat format)
|
||||
{
|
||||
return new MangaFile()
|
||||
{
|
||||
FilePath = file,
|
||||
Format = format
|
||||
};
|
||||
return new MangaFileBuilder(file, format).Build();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -3,7 +3,7 @@ using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Parser;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using API.Tests.Helpers;
|
||||
@ -27,7 +27,7 @@ public class ParserInfoListExtensions
|
||||
[InlineData(new[] {"1", "1", "3-5", "5", "8", "0", "0"}, new[] {"1", "3-5", "5", "8", "0"})]
|
||||
public void DistinctVolumesTest(string[] volumeNumbers, string[] expectedNumbers)
|
||||
{
|
||||
var infos = volumeNumbers.Select(n => new ParserInfo() {Volumes = n}).ToList();
|
||||
var infos = volumeNumbers.Select(n => new ParserInfo() {Series = "", Volumes = n}).ToList();
|
||||
Assert.Equal(expectedNumbers, infos.DistinctVolumes());
|
||||
}
|
||||
|
||||
@ -45,8 +45,10 @@ public class ParserInfoListExtensions
|
||||
string.Empty));
|
||||
}
|
||||
|
||||
var files = inputChapters.Select(s => EntityFactory.CreateMangaFile(s, MangaFormat.Archive, 199)).ToList();
|
||||
var chapter = EntityFactory.CreateChapter("0-6", false, files);
|
||||
var files = inputChapters.Select(s => new MangaFileBuilder(s, MangaFormat.Archive, 199).Build()).ToList();
|
||||
var chapter = new ChapterBuilder("0-6")
|
||||
.WithFiles(files)
|
||||
.Build();
|
||||
|
||||
Assert.Equal(expectedHasInfo, infos.HasInfo(chapter));
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Data;
|
||||
using API.Data.Misc;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Helpers.Builders;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Extensions;
|
||||
@ -18,27 +21,15 @@ public class QueryableExtensionsTests
|
||||
{
|
||||
var items = new List<Series>()
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
Metadata = new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
},
|
||||
new Series()
|
||||
{
|
||||
Metadata = new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Unknown,
|
||||
}
|
||||
},
|
||||
new Series()
|
||||
{
|
||||
Metadata = new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.X18Plus,
|
||||
}
|
||||
},
|
||||
new SeriesBuilder("Test 1")
|
||||
.WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build())
|
||||
.Build(),
|
||||
new SeriesBuilder("Test 2")
|
||||
.WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Unknown).Build())
|
||||
.Build(),
|
||||
new SeriesBuilder("Test 3")
|
||||
.WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.X18Plus).Build())
|
||||
.Build()
|
||||
};
|
||||
|
||||
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||
@ -56,40 +47,16 @@ public class QueryableExtensionsTests
|
||||
{
|
||||
var items = new List<CollectionTag>()
|
||||
{
|
||||
new CollectionTag()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
}
|
||||
},
|
||||
new CollectionTag()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Unknown,
|
||||
},
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
}
|
||||
},
|
||||
new CollectionTag()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.X18Plus,
|
||||
}
|
||||
}
|
||||
},
|
||||
new CollectionTagBuilder("Test")
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build())
|
||||
.Build(),
|
||||
new CollectionTagBuilder("Test 2")
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Unknown).Build())
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build())
|
||||
.Build(),
|
||||
new CollectionTagBuilder("Test 3")
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.X18Plus).Build())
|
||||
.Build(),
|
||||
};
|
||||
|
||||
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||
@ -107,40 +74,16 @@ public class QueryableExtensionsTests
|
||||
{
|
||||
var items = new List<Genre>()
|
||||
{
|
||||
new Genre()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
}
|
||||
},
|
||||
new Genre()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Unknown,
|
||||
},
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
}
|
||||
},
|
||||
new Genre()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.X18Plus,
|
||||
}
|
||||
}
|
||||
},
|
||||
new GenreBuilder("A")
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build())
|
||||
.Build(),
|
||||
new GenreBuilder("B")
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Unknown).Build())
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build())
|
||||
.Build(),
|
||||
new GenreBuilder("C")
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.X18Plus).Build())
|
||||
.Build(),
|
||||
};
|
||||
|
||||
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||
@ -158,40 +101,16 @@ public class QueryableExtensionsTests
|
||||
{
|
||||
var items = new List<Tag>()
|
||||
{
|
||||
new Tag()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
}
|
||||
},
|
||||
new Tag()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Unknown,
|
||||
},
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
}
|
||||
},
|
||||
new Tag()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.X18Plus,
|
||||
}
|
||||
}
|
||||
},
|
||||
new TagBuilder("Test 1")
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build())
|
||||
.Build(),
|
||||
new TagBuilder("Test 2")
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Unknown).Build())
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build())
|
||||
.Build(),
|
||||
new TagBuilder("Test 3")
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.X18Plus).Build())
|
||||
.Build(),
|
||||
};
|
||||
|
||||
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||
@ -209,40 +128,16 @@ public class QueryableExtensionsTests
|
||||
{
|
||||
var items = new List<Person>()
|
||||
{
|
||||
new Person()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
}
|
||||
},
|
||||
new Person()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Unknown,
|
||||
},
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
}
|
||||
}
|
||||
},
|
||||
new Person()
|
||||
{
|
||||
SeriesMetadatas = new List<SeriesMetadata>()
|
||||
{
|
||||
new SeriesMetadata()
|
||||
{
|
||||
AgeRating = AgeRating.X18Plus,
|
||||
}
|
||||
}
|
||||
},
|
||||
new PersonBuilder("Test", PersonRole.Character)
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build())
|
||||
.Build(),
|
||||
new PersonBuilder("Test", PersonRole.Character)
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Unknown).Build())
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Teen).Build())
|
||||
.Build(),
|
||||
new PersonBuilder("Test", PersonRole.Character)
|
||||
.WithSeriesMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.X18Plus).Build())
|
||||
.Build(),
|
||||
};
|
||||
|
||||
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||
@ -258,20 +153,12 @@ public class QueryableExtensionsTests
|
||||
[InlineData(false, 1)]
|
||||
public void RestrictAgainstAgeRestriction_ReadingList_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount)
|
||||
{
|
||||
|
||||
var items = new List<ReadingList>()
|
||||
{
|
||||
new ReadingList()
|
||||
{
|
||||
AgeRating = AgeRating.Teen,
|
||||
},
|
||||
new ReadingList()
|
||||
{
|
||||
AgeRating = AgeRating.Unknown,
|
||||
},
|
||||
new ReadingList()
|
||||
{
|
||||
AgeRating = AgeRating.X18Plus
|
||||
},
|
||||
new ReadingListBuilder("Test List").WithRating(AgeRating.Teen).Build(),
|
||||
new ReadingListBuilder("Test List").WithRating(AgeRating.Unknown).Build(),
|
||||
new ReadingListBuilder("Test List").WithRating(AgeRating.X18Plus).Build(),
|
||||
};
|
||||
|
||||
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||
|
@ -3,124 +3,31 @@ using System.Linq;
|
||||
using API.Comparators;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Parser;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using API.Helpers.Builders;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Extensions;
|
||||
|
||||
public class SeriesExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker than Black"}, true)]
|
||||
[InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker_than_Black"}, true)]
|
||||
[InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker then Black!"}, false)]
|
||||
[InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"Salem's Lot"}, true)]
|
||||
[InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"salems lot"}, true)]
|
||||
[InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"salem's lot"}, true)]
|
||||
// Different normalizations pass as we check normalization against an on-the-fly calculation so we don't delete series just because we change how normalization works
|
||||
[InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot", "salems lot"}, new [] {"salem's lot"}, true)]
|
||||
[InlineData(new [] {"Rent-a-Girlfriend", "Rent-a-Girlfriend", "Kanojo, Okarishimasu", "rentagirlfriend"}, new [] {"Kanojo, Okarishimasu"}, true)]
|
||||
public void NameInListTest(string[] seriesInput, string[] list, bool expected)
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Name = seriesInput[0],
|
||||
LocalizedName = seriesInput[1],
|
||||
OriginalName = seriesInput[2],
|
||||
NormalizedName = seriesInput.Length == 4 ? seriesInput[3] : API.Services.Tasks.Scanner.Parser.Parser.Normalize(seriesInput[0]),
|
||||
Metadata = new SeriesMetadata()
|
||||
};
|
||||
|
||||
Assert.Equal(expected, series.NameInList(list));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker than Black"}, MangaFormat.Archive, true)]
|
||||
[InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker_than_Black"}, MangaFormat.Archive, true)]
|
||||
[InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker then Black!"}, MangaFormat.Archive, false)]
|
||||
[InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"Salem's Lot"}, MangaFormat.Archive, true)]
|
||||
[InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"salems lot"}, MangaFormat.Archive, true)]
|
||||
[InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"salem's lot"}, MangaFormat.Archive, true)]
|
||||
// Different normalizations pass as we check normalization against an on-the-fly calculation so we don't delete series just because we change how normalization works
|
||||
[InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot", "salems lot"}, new [] {"salem's lot"}, MangaFormat.Archive, true)]
|
||||
[InlineData(new [] {"Rent-a-Girlfriend", "Rent-a-Girlfriend", "Kanojo, Okarishimasu", "rentagirlfriend"}, new [] {"Kanojo, Okarishimasu"}, MangaFormat.Archive, true)]
|
||||
public void NameInListParserInfoTest(string[] seriesInput, string[] list, MangaFormat format, bool expected)
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Name = seriesInput[0],
|
||||
LocalizedName = seriesInput[1],
|
||||
OriginalName = seriesInput[2],
|
||||
NormalizedName = seriesInput.Length == 4 ? seriesInput[3] : API.Services.Tasks.Scanner.Parser.Parser.Normalize(seriesInput[0]),
|
||||
Metadata = new SeriesMetadata(),
|
||||
};
|
||||
|
||||
var parserInfos = list.Select(s => new ParsedSeries()
|
||||
{
|
||||
Name = s,
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize(s),
|
||||
}).ToList();
|
||||
|
||||
// This doesn't do any checks against format
|
||||
Assert.Equal(expected, series.NameInList(parserInfos));
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, "Darker than Black", true)]
|
||||
[InlineData(new [] {"Rent-a-Girlfriend", "Rent-a-Girlfriend", "Kanojo, Okarishimasu", "rentagirlfriend"}, "Kanojo, Okarishimasu", true)]
|
||||
[InlineData(new [] {"Rent-a-Girlfriend", "Rent-a-Girlfriend", "Kanojo, Okarishimasu", "rentagirlfriend"}, "Rent", false)]
|
||||
public void NameInParserInfoTest(string[] seriesInput, string parserSeries, bool expected)
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Name = seriesInput[0],
|
||||
LocalizedName = seriesInput[1],
|
||||
OriginalName = seriesInput[2],
|
||||
NormalizedName = seriesInput.Length == 4 ? seriesInput[3] : API.Services.Tasks.Scanner.Parser.Parser.Normalize(seriesInput[0]),
|
||||
Metadata = new SeriesMetadata()
|
||||
};
|
||||
var info = new ParserInfo
|
||||
{
|
||||
Series = parserSeries
|
||||
};
|
||||
|
||||
Assert.Equal(expected, series.NameInParserInfo(info));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCoverImage_MultipleSpecials_Comics()
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 0,
|
||||
Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume,
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 1",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 2",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithCoverImage("Special 1")
|
||||
.WithIsSpecial(true)
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithCoverImage("Special 2")
|
||||
.WithIsSpecial(true)
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
Assert.Equal("Special 1", series.GetCoverImage());
|
||||
|
||||
@ -129,33 +36,20 @@ public class SeriesExtensionsTests
|
||||
[Fact]
|
||||
public void GetCoverImage_MultipleSpecials_Books()
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Format = MangaFormat.Epub,
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 0,
|
||||
Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume,
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 1",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 2",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithCoverImage("Special 1")
|
||||
.WithIsSpecial(true)
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithCoverImage("Special 2")
|
||||
.WithIsSpecial(true)
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
Assert.Equal("Special 1", series.GetCoverImage());
|
||||
}
|
||||
@ -163,33 +57,20 @@ public class SeriesExtensionsTests
|
||||
[Fact]
|
||||
public void GetCoverImage_JustChapters_Comics()
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 0,
|
||||
Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume,
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2.5",
|
||||
CoverImage = "Special 1",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2",
|
||||
CoverImage = "Special 2",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder("2.5")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 1")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("2")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 2")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
foreach (var vol in series.Volumes)
|
||||
{
|
||||
@ -202,39 +83,24 @@ public class SeriesExtensionsTests
|
||||
[Fact]
|
||||
public void GetCoverImage_JustChaptersAndSpecials_Comics()
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 0,
|
||||
Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume,
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2.5",
|
||||
CoverImage = "Special 1",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2",
|
||||
CoverImage = "Special 2",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 3",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder("2.5")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 1")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("2")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 2")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(true)
|
||||
.WithCoverImage("Special 3")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
foreach (var vol in series.Volumes)
|
||||
{
|
||||
@ -247,54 +113,31 @@ public class SeriesExtensionsTests
|
||||
[Fact]
|
||||
public void GetCoverImage_VolumesChapters_Comics()
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 0,
|
||||
Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume,
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2.5",
|
||||
CoverImage = "Special 1",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2",
|
||||
CoverImage = "Special 2",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 3",
|
||||
}
|
||||
},
|
||||
},
|
||||
new Volume()
|
||||
{
|
||||
Number = 1,
|
||||
Name = "1",
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "0",
|
||||
CoverImage = "Volume 1",
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder("2.5")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 1")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("2")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 2")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(true)
|
||||
.WithCoverImage("Special 3")
|
||||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Volume 1")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
foreach (var vol in series.Volumes)
|
||||
{
|
||||
@ -307,54 +150,31 @@ public class SeriesExtensionsTests
|
||||
[Fact]
|
||||
public void GetCoverImage_VolumesChaptersAndSpecials_Comics()
|
||||
{
|
||||
var series = new Series()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 0,
|
||||
Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume,
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2.5",
|
||||
CoverImage = "Special 1",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "2",
|
||||
CoverImage = "Special 2",
|
||||
},
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = true,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
||||
CoverImage = "Special 3",
|
||||
}
|
||||
},
|
||||
},
|
||||
new Volume()
|
||||
{
|
||||
Number = 1,
|
||||
Name = "1",
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
IsSpecial = false,
|
||||
Number = "0",
|
||||
CoverImage = "Volume 1",
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder("2.5")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 1")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("2")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Special 2")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(true)
|
||||
.WithCoverImage("Special 3")
|
||||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Volume 1")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
foreach (var vol in series.Volumes)
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Tests.Helpers;
|
||||
using Xunit;
|
||||
|
||||
@ -16,16 +17,14 @@ public class VolumeListExtensionsTests
|
||||
{
|
||||
var volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("3", false),
|
||||
EntityFactory.CreateChapter("4", false),
|
||||
}),
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false),
|
||||
EntityFactory.CreateChapter("0", true),
|
||||
}),
|
||||
new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
.WithChapter(new ChapterBuilder("4").Build())
|
||||
.Build(),
|
||||
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build())
|
||||
.Build(),
|
||||
};
|
||||
|
||||
Assert.Equal(volumes[0].Number, volumes.GetCoverImage(MangaFormat.Archive).Number);
|
||||
@ -36,16 +35,14 @@ public class VolumeListExtensionsTests
|
||||
{
|
||||
var volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("3", false),
|
||||
EntityFactory.CreateChapter("4", false),
|
||||
}),
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false),
|
||||
EntityFactory.CreateChapter("0", true),
|
||||
}),
|
||||
new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
.WithChapter(new ChapterBuilder("4").Build())
|
||||
.Build(),
|
||||
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build())
|
||||
.Build(),
|
||||
};
|
||||
|
||||
Assert.Equal(volumes[1].Name, volumes.GetCoverImage(MangaFormat.Epub).Name);
|
||||
@ -56,16 +53,14 @@ public class VolumeListExtensionsTests
|
||||
{
|
||||
var volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("3", false),
|
||||
EntityFactory.CreateChapter("4", false),
|
||||
}),
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false),
|
||||
EntityFactory.CreateChapter("0", true),
|
||||
}),
|
||||
new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
.WithChapter(new ChapterBuilder("4").Build())
|
||||
.Build(),
|
||||
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build())
|
||||
.Build(),
|
||||
};
|
||||
|
||||
Assert.Equal(volumes[1].Name, volumes.GetCoverImage(MangaFormat.Pdf).Name);
|
||||
@ -76,16 +71,14 @@ public class VolumeListExtensionsTests
|
||||
{
|
||||
var volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("3", false),
|
||||
EntityFactory.CreateChapter("4", false),
|
||||
}),
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false),
|
||||
EntityFactory.CreateChapter("0", true),
|
||||
}),
|
||||
new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
.WithChapter(new ChapterBuilder("4").Build())
|
||||
.Build(),
|
||||
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build())
|
||||
.Build(),
|
||||
};
|
||||
|
||||
Assert.Equal(volumes[0].Name, volumes.GetCoverImage(MangaFormat.Image).Name);
|
||||
@ -96,16 +89,14 @@ public class VolumeListExtensionsTests
|
||||
{
|
||||
var volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("3", false),
|
||||
EntityFactory.CreateChapter("4", false),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false),
|
||||
EntityFactory.CreateChapter("0", true),
|
||||
}),
|
||||
new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
.WithChapter(new ChapterBuilder("4").Build())
|
||||
.Build(),
|
||||
new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build())
|
||||
.Build(),
|
||||
};
|
||||
|
||||
Assert.Equal(volumes[1].Name, volumes.GetCoverImage(MangaFormat.Image).Name);
|
||||
|
@ -3,7 +3,9 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using Xunit;
|
||||
|
||||
@ -51,11 +53,10 @@ public class CacheHelperTests
|
||||
[Fact]
|
||||
public void ShouldUpdateCoverImage_OnFirstRun()
|
||||
{
|
||||
var file = new MangaFile()
|
||||
{
|
||||
FilePath = TestCoverArchive,
|
||||
LastModified = DateTime.Now
|
||||
};
|
||||
|
||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
||||
.WithLastModified(DateTime.Now)
|
||||
.Build();
|
||||
Assert.True(_cacheHelper.ShouldUpdateCoverImage(null, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)),
|
||||
false, false));
|
||||
}
|
||||
@ -64,11 +65,9 @@ public class CacheHelperTests
|
||||
public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetNotLocked()
|
||||
{
|
||||
// Represents first run
|
||||
var file = new MangaFile()
|
||||
{
|
||||
FilePath = TestCoverArchive,
|
||||
LastModified = DateTime.Now
|
||||
};
|
||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
||||
.WithLastModified(DateTime.Now)
|
||||
.Build();
|
||||
Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)),
|
||||
false, false));
|
||||
}
|
||||
@ -77,11 +76,9 @@ public class CacheHelperTests
|
||||
public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetNotLocked_2()
|
||||
{
|
||||
// Represents first run
|
||||
var file = new MangaFile()
|
||||
{
|
||||
FilePath = TestCoverArchive,
|
||||
LastModified = DateTime.Now
|
||||
};
|
||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
||||
.WithLastModified(DateTime.Now)
|
||||
.Build();
|
||||
Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now,
|
||||
false, false));
|
||||
}
|
||||
@ -90,11 +87,9 @@ public class CacheHelperTests
|
||||
public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetLocked()
|
||||
{
|
||||
// Represents first run
|
||||
var file = new MangaFile()
|
||||
{
|
||||
FilePath = TestCoverArchive,
|
||||
LastModified = DateTime.Now
|
||||
};
|
||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
||||
.WithLastModified(DateTime.Now)
|
||||
.Build();
|
||||
Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)),
|
||||
false, true));
|
||||
}
|
||||
@ -103,11 +98,9 @@ public class CacheHelperTests
|
||||
public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetLocked_Modified()
|
||||
{
|
||||
// Represents first run
|
||||
var file = new MangaFile()
|
||||
{
|
||||
FilePath = TestCoverArchive,
|
||||
LastModified = DateTime.Now
|
||||
};
|
||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
||||
.WithLastModified(DateTime.Now)
|
||||
.Build();
|
||||
Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)),
|
||||
false, true));
|
||||
}
|
||||
@ -129,11 +122,10 @@ public class CacheHelperTests
|
||||
var cacheHelper = new CacheHelper(fileService);
|
||||
|
||||
var created = DateTime.Now.Subtract(TimeSpan.FromHours(1));
|
||||
var file = new MangaFile()
|
||||
{
|
||||
FilePath = TestCoverArchive,
|
||||
LastModified = DateTime.Now.Subtract(TimeSpan.FromMinutes(1))
|
||||
};
|
||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
||||
.WithLastModified(DateTime.Now.Subtract(TimeSpan.FromMinutes(1)))
|
||||
.Build();
|
||||
|
||||
Assert.True(cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, created,
|
||||
false, false));
|
||||
}
|
||||
@ -154,17 +146,14 @@ public class CacheHelperTests
|
||||
var fileService = new FileService(fileSystem);
|
||||
var cacheHelper = new CacheHelper(fileService);
|
||||
|
||||
var chapter = new Chapter()
|
||||
{
|
||||
Created = filesystemFile.LastWriteTime.DateTime,
|
||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
||||
};
|
||||
var chapter = new ChapterBuilder("1")
|
||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
||||
.WithCreated(filesystemFile.LastWriteTime.DateTime)
|
||||
.Build();
|
||||
|
||||
var file = new MangaFile()
|
||||
{
|
||||
FilePath = TestCoverArchive,
|
||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
||||
};
|
||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
||||
.Build();
|
||||
Assert.True(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
||||
}
|
||||
|
||||
@ -184,17 +173,15 @@ public class CacheHelperTests
|
||||
var fileService = new FileService(fileSystem);
|
||||
var cacheHelper = new CacheHelper(fileService);
|
||||
|
||||
var chapter = new Chapter()
|
||||
{
|
||||
Created = filesystemFile.LastWriteTime.DateTime,
|
||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
||||
};
|
||||
var chapter = new ChapterBuilder("1")
|
||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
||||
.WithCreated(filesystemFile.LastWriteTime.DateTime)
|
||||
.Build();
|
||||
|
||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
||||
.Build();
|
||||
|
||||
var file = new MangaFile()
|
||||
{
|
||||
FilePath = TestCoverArchive,
|
||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
||||
};
|
||||
Assert.True(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
||||
}
|
||||
|
||||
@ -214,17 +201,14 @@ public class CacheHelperTests
|
||||
var fileService = new FileService(fileSystem);
|
||||
var cacheHelper = new CacheHelper(fileService);
|
||||
|
||||
var chapter = new Chapter()
|
||||
{
|
||||
Created = filesystemFile.LastWriteTime.DateTime,
|
||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
||||
};
|
||||
var chapter = new ChapterBuilder("1")
|
||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
||||
.WithCreated(filesystemFile.LastWriteTime.DateTime)
|
||||
.Build();
|
||||
|
||||
var file = new MangaFile()
|
||||
{
|
||||
FilePath = TestCoverArchive,
|
||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
||||
};
|
||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
||||
.Build();
|
||||
Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, true, file));
|
||||
}
|
||||
|
||||
@ -245,17 +229,14 @@ public class CacheHelperTests
|
||||
var fileService = new FileService(fileSystem);
|
||||
var cacheHelper = new CacheHelper(fileService);
|
||||
|
||||
var chapter = new Chapter()
|
||||
{
|
||||
Created = DateTime.Now.Subtract(TimeSpan.FromMinutes(10)),
|
||||
LastModified = DateTime.Now.Subtract(TimeSpan.FromMinutes(10))
|
||||
};
|
||||
var chapter = new ChapterBuilder("1")
|
||||
.WithLastModified(DateTime.Now.Subtract(TimeSpan.FromMinutes(10)))
|
||||
.WithCreated(DateTime.Now.Subtract(TimeSpan.FromMinutes(10)))
|
||||
.Build();
|
||||
|
||||
var file = new MangaFile()
|
||||
{
|
||||
FilePath = Path.Join(TestCoverImageDirectory, TestCoverArchive),
|
||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
||||
};
|
||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
||||
.Build();
|
||||
Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
||||
}
|
||||
|
||||
@ -275,17 +256,15 @@ public class CacheHelperTests
|
||||
var fileService = new FileService(fileSystem);
|
||||
var cacheHelper = new CacheHelper(fileService);
|
||||
|
||||
var chapter = new Chapter()
|
||||
{
|
||||
Created = DateTime.Now.Subtract(TimeSpan.FromMinutes(10)),
|
||||
LastModified = DateTime.Now
|
||||
};
|
||||
var chapter = new ChapterBuilder("1")
|
||||
.WithLastModified(DateTime.Now)
|
||||
.WithCreated(DateTime.Now.Subtract(TimeSpan.FromMinutes(10)))
|
||||
.Build();
|
||||
|
||||
var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive)
|
||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
||||
.Build();
|
||||
|
||||
var file = new MangaFile()
|
||||
{
|
||||
FilePath = Path.Join(TestCoverImageDirectory, TestCoverArchive),
|
||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
||||
};
|
||||
Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
||||
}
|
||||
|
||||
|
@ -1,68 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
|
||||
namespace API.Tests.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Used to help quickly create DB entities for Unit Testing
|
||||
/// </summary>
|
||||
public static class EntityFactory
|
||||
{
|
||||
public static Series CreateSeries(string name)
|
||||
{
|
||||
return new Series()
|
||||
{
|
||||
Name = name,
|
||||
SortName = name,
|
||||
LocalizedName = name,
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize(name),
|
||||
Volumes = new List<Volume>(),
|
||||
Metadata = new SeriesMetadata()
|
||||
};
|
||||
}
|
||||
|
||||
public static Volume CreateVolume(string volumeNumber, List<Chapter> chapters = null)
|
||||
{
|
||||
var chaps = chapters ?? new List<Chapter>();
|
||||
var pages = chaps.Count > 0 ? chaps.Max(c => c.Pages) : 0;
|
||||
return new Volume()
|
||||
{
|
||||
Name = volumeNumber,
|
||||
Number = (int) API.Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber),
|
||||
Pages = pages,
|
||||
Chapters = chaps
|
||||
};
|
||||
}
|
||||
|
||||
public static Chapter CreateChapter(string range, bool isSpecial, List<MangaFile> files = null, int pageCount = 0)
|
||||
{
|
||||
return new Chapter()
|
||||
{
|
||||
IsSpecial = isSpecial,
|
||||
Range = range,
|
||||
Number = API.Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(range) + string.Empty,
|
||||
Files = files ?? new List<MangaFile>(),
|
||||
Pages = pageCount,
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
public static MangaFile CreateMangaFile(string filename, MangaFormat format, int pages)
|
||||
{
|
||||
return new MangaFile()
|
||||
{
|
||||
FilePath = filename,
|
||||
Format = format,
|
||||
Pages = pages
|
||||
};
|
||||
}
|
||||
|
||||
public static CollectionTag CreateCollectionTag(int id, string title, string summary, bool promoted)
|
||||
{
|
||||
return DbFactory.CollectionTag(id, title, summary, promoted);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Helpers;
|
||||
@ -13,9 +14,9 @@ public class GenreHelperTests
|
||||
{
|
||||
var allGenres = new List<Genre>
|
||||
{
|
||||
DbFactory.Genre("Action"),
|
||||
DbFactory.Genre("action"),
|
||||
DbFactory.Genre("Sci-fi"),
|
||||
new GenreBuilder("Action").Build(),
|
||||
new GenreBuilder("action").Build(),
|
||||
new GenreBuilder("Sci-fi").Build(),
|
||||
};
|
||||
var genreAdded = new List<Genre>();
|
||||
|
||||
@ -33,9 +34,9 @@ public class GenreHelperTests
|
||||
{
|
||||
var allGenres = new List<Genre>
|
||||
{
|
||||
DbFactory.Genre("Action"),
|
||||
DbFactory.Genre("action"),
|
||||
DbFactory.Genre("Sci-fi"),
|
||||
new GenreBuilder("Action").Build(),
|
||||
new GenreBuilder("action").Build(),
|
||||
new GenreBuilder("Sci-fi").Build(),
|
||||
|
||||
};
|
||||
var genreAdded = new List<Genre>();
|
||||
@ -54,19 +55,19 @@ public class GenreHelperTests
|
||||
{
|
||||
var existingGenres = new List<Genre>
|
||||
{
|
||||
DbFactory.Genre("Action"),
|
||||
DbFactory.Genre("action"),
|
||||
DbFactory.Genre("Sci-fi"),
|
||||
new GenreBuilder("Action").Build(),
|
||||
new GenreBuilder("action").Build(),
|
||||
new GenreBuilder("Sci-fi").Build(),
|
||||
};
|
||||
|
||||
|
||||
GenreHelper.AddGenreIfNotExists(existingGenres, DbFactory.Genre("Action"));
|
||||
GenreHelper.AddGenreIfNotExists(existingGenres, new GenreBuilder("Action").Build());
|
||||
Assert.Equal(3, existingGenres.Count);
|
||||
|
||||
GenreHelper.AddGenreIfNotExists(existingGenres, DbFactory.Genre("action"));
|
||||
GenreHelper.AddGenreIfNotExists(existingGenres, new GenreBuilder("action").Build());
|
||||
Assert.Equal(3, existingGenres.Count);
|
||||
|
||||
GenreHelper.AddGenreIfNotExists(existingGenres, DbFactory.Genre("Shonen"));
|
||||
GenreHelper.AddGenreIfNotExists(existingGenres, new GenreBuilder("Shonen").Build());
|
||||
Assert.Equal(4, existingGenres.Count);
|
||||
}
|
||||
|
||||
@ -75,13 +76,13 @@ public class GenreHelperTests
|
||||
{
|
||||
var existingGenres = new List<Genre>
|
||||
{
|
||||
DbFactory.Genre("Action"),
|
||||
DbFactory.Genre("Sci-fi"),
|
||||
new GenreBuilder("Action").Build(),
|
||||
new GenreBuilder("Sci-fi").Build(),
|
||||
};
|
||||
|
||||
var peopleFromChapters = new List<Genre>
|
||||
{
|
||||
DbFactory.Genre("Action"),
|
||||
new GenreBuilder("Action").Build(),
|
||||
};
|
||||
|
||||
var genreRemoved = new List<Genre>();
|
||||
@ -99,8 +100,8 @@ public class GenreHelperTests
|
||||
{
|
||||
var existingGenres = new List<Genre>
|
||||
{
|
||||
DbFactory.Genre("Action"),
|
||||
DbFactory.Genre("Sci-fi"),
|
||||
new GenreBuilder("Action").Build(),
|
||||
new GenreBuilder("Sci-fi").Build(),
|
||||
};
|
||||
|
||||
var peopleFromChapters = new List<Genre>();
|
||||
|
@ -3,8 +3,9 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using API.Entities.Enums;
|
||||
using API.Parser;
|
||||
using API.Extensions;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
|
||||
namespace API.Tests.Helpers;
|
||||
|
||||
@ -29,12 +30,12 @@ public static class ParserInfoFactory
|
||||
public static void AddToParsedInfo(IDictionary<ParsedSeries, IList<ParserInfo>> collectedSeries, ParserInfo info)
|
||||
{
|
||||
var existingKey = collectedSeries.Keys.FirstOrDefault(ps =>
|
||||
ps.Format == info.Format && ps.NormalizedName == API.Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series));
|
||||
ps.Format == info.Format && ps.NormalizedName == info.Series.ToNormalized());
|
||||
existingKey ??= new ParsedSeries()
|
||||
{
|
||||
Format = info.Format,
|
||||
Name = info.Series,
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series)
|
||||
NormalizedName = info.Series.ToNormalized()
|
||||
};
|
||||
if (collectedSeries.GetType() == typeof(ConcurrentDictionary<,>))
|
||||
{
|
||||
|
@ -2,9 +2,11 @@
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Parser;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Helpers;
|
||||
@ -21,23 +23,13 @@ public class ParserInfoHelperTests
|
||||
ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Archive});
|
||||
//AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Epub});
|
||||
|
||||
var series = new Series()
|
||||
{
|
||||
Name = "Darker Than Black",
|
||||
LocalizedName = "Darker Than Black",
|
||||
OriginalName = "Darker Than Black",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 1,
|
||||
Name = "1"
|
||||
}
|
||||
},
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker Than Black"),
|
||||
Metadata = new SeriesMetadata(),
|
||||
Format = MangaFormat.Epub
|
||||
};
|
||||
var series = new SeriesBuilder("Darker Than Black")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithName("1")
|
||||
.Build())
|
||||
.WithLocalizedName("Darker Than Black")
|
||||
.Build();
|
||||
|
||||
Assert.False(ParserInfoHelpers.SeriesHasMatchingParserInfoFormat(series, infos));
|
||||
}
|
||||
@ -50,23 +42,14 @@ public class ParserInfoHelperTests
|
||||
ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Archive});
|
||||
ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Epub});
|
||||
|
||||
var series = new Series()
|
||||
{
|
||||
Name = "Darker Than Black",
|
||||
LocalizedName = "Darker Than Black",
|
||||
OriginalName = "Darker Than Black",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 1,
|
||||
Name = "1"
|
||||
}
|
||||
},
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker Than Black"),
|
||||
Metadata = new SeriesMetadata(),
|
||||
Format = MangaFormat.Epub
|
||||
};
|
||||
|
||||
var series = new SeriesBuilder("Darker Than Black")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithName("1")
|
||||
.Build())
|
||||
.WithLocalizedName("Darker Than Black")
|
||||
.Build();
|
||||
|
||||
Assert.True(ParserInfoHelpers.SeriesHasMatchingParserInfoFormat(series, infos));
|
||||
}
|
||||
|
@ -1,22 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Helpers;
|
||||
|
||||
public class PersonHelperTests
|
||||
{
|
||||
#region UpdatePeople
|
||||
[Fact]
|
||||
public void UpdatePeople_ShouldAddNewPeople()
|
||||
{
|
||||
var allPeople = new List<Person>
|
||||
{
|
||||
DbFactory.Person("Joe Shmo", PersonRole.CoverArtist),
|
||||
DbFactory.Person("Joe Shmo", PersonRole.Writer)
|
||||
new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
|
||||
new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
|
||||
};
|
||||
var peopleAdded = new List<Person>();
|
||||
|
||||
@ -34,9 +38,9 @@ public class PersonHelperTests
|
||||
{
|
||||
var allPeople = new List<Person>
|
||||
{
|
||||
DbFactory.Person("Joe Shmo", PersonRole.CoverArtist),
|
||||
DbFactory.Person("Joe Shmo", PersonRole.Writer),
|
||||
DbFactory.Person("Sally Ann", PersonRole.CoverArtist),
|
||||
new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
|
||||
new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
|
||||
new PersonBuilder("Sally Ann", PersonRole.CoverArtist).Build(),
|
||||
|
||||
};
|
||||
var peopleAdded = new List<Person>();
|
||||
@ -48,14 +52,166 @@ public class PersonHelperTests
|
||||
|
||||
Assert.Equal(3, allPeople.Count);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UpdatePeopleList
|
||||
|
||||
[Fact]
|
||||
public void UpdatePeopleList_NullTags_NoChanges()
|
||||
{
|
||||
// Arrange
|
||||
ICollection<PersonDto> tags = null;
|
||||
var series = new SeriesBuilder("Test Series").Build();
|
||||
var allTags = new List<Person>();
|
||||
var handleAddCalled = false;
|
||||
var onModifiedCalled = false;
|
||||
|
||||
// Act
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Writer, tags, series, allTags, p => handleAddCalled = true, () => onModifiedCalled = true);
|
||||
|
||||
// Assert
|
||||
Assert.False(handleAddCalled);
|
||||
Assert.False(onModifiedCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdatePeopleList_AddNewTag_TagAddedAndOnModifiedCalled()
|
||||
{
|
||||
// Arrange
|
||||
const PersonRole role = PersonRole.Writer;
|
||||
var tags = new List<PersonDto>
|
||||
{
|
||||
new PersonDto { Id = 1, Name = "John Doe", Role = role }
|
||||
};
|
||||
var series = new SeriesBuilder("Test Series").Build();
|
||||
var allTags = new List<Person>();
|
||||
var handleAddCalled = false;
|
||||
var onModifiedCalled = false;
|
||||
|
||||
// Act
|
||||
PersonHelper.UpdatePeopleList(role, tags, series, allTags, p =>
|
||||
{
|
||||
handleAddCalled = true;
|
||||
series.Metadata.People.Add(p);
|
||||
}, () => onModifiedCalled = true);
|
||||
|
||||
// Assert
|
||||
Assert.True(handleAddCalled);
|
||||
Assert.True(onModifiedCalled);
|
||||
Assert.Single(series.Metadata.People);
|
||||
Assert.Equal("John Doe", series.Metadata.People.First().Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdatePeopleList_RemoveExistingTag_TagRemovedAndOnModifiedCalled()
|
||||
{
|
||||
// Arrange
|
||||
const PersonRole role = PersonRole.Writer;
|
||||
var tags = new List<PersonDto>();
|
||||
var series = new SeriesBuilder("Test Series").Build();
|
||||
var person = new PersonBuilder("John Doe", role).Build();
|
||||
person.Id = 1;
|
||||
series.Metadata.People.Add(person);
|
||||
var allTags = new List<Person>
|
||||
{
|
||||
person
|
||||
};
|
||||
var handleAddCalled = false;
|
||||
var onModifiedCalled = false;
|
||||
|
||||
// Act
|
||||
PersonHelper.UpdatePeopleList(role, tags, series, allTags, p =>
|
||||
{
|
||||
handleAddCalled = true;
|
||||
series.Metadata.People.Add(p);
|
||||
}, () => onModifiedCalled = true);
|
||||
|
||||
// Assert
|
||||
Assert.False(handleAddCalled);
|
||||
Assert.True(onModifiedCalled);
|
||||
Assert.Empty(series.Metadata.People);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdatePeopleList_UpdateExistingTag_OnModifiedCalled()
|
||||
{
|
||||
// Arrange
|
||||
const PersonRole role = PersonRole.Writer;
|
||||
var tags = new List<PersonDto>
|
||||
{
|
||||
new PersonDto { Id = 1, Name = "John Doe", Role = role }
|
||||
};
|
||||
var series = new SeriesBuilder("Test Series").Build();
|
||||
var person = new PersonBuilder("John Doe", role).Build();
|
||||
person.Id = 1;
|
||||
series.Metadata.People.Add(person);
|
||||
var allTags = new List<Person>
|
||||
{
|
||||
person
|
||||
};
|
||||
var handleAddCalled = false;
|
||||
var onModifiedCalled = false;
|
||||
|
||||
// Act
|
||||
PersonHelper.UpdatePeopleList(role, tags, series, allTags, p =>
|
||||
{
|
||||
handleAddCalled = true;
|
||||
series.Metadata.People.Add(p);
|
||||
}, () => onModifiedCalled = true);
|
||||
|
||||
// Assert
|
||||
Assert.False(handleAddCalled);
|
||||
Assert.False(onModifiedCalled);
|
||||
Assert.Single(series.Metadata.People);
|
||||
Assert.Equal("John Doe", series.Metadata.People.First().Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdatePeopleList_NoChanges_HandleAddAndOnModifiedNotCalled()
|
||||
{
|
||||
// Arrange
|
||||
const PersonRole role = PersonRole.Writer;
|
||||
var tags = new List<PersonDto>
|
||||
{
|
||||
new PersonDto { Id = 1, Name = "John Doe", Role = role }
|
||||
};
|
||||
var series = new SeriesBuilder("Test Series").Build();
|
||||
var person = new PersonBuilder("John Doe", role).Build();
|
||||
person.Id = 1;
|
||||
series.Metadata.People.Add(person);
|
||||
var allTags = new List<Person>
|
||||
{
|
||||
new PersonBuilder("John Doe", role).Build()
|
||||
};
|
||||
var handleAddCalled = false;
|
||||
var onModifiedCalled = false;
|
||||
|
||||
// Act
|
||||
PersonHelper.UpdatePeopleList(role, tags, series, allTags, p =>
|
||||
{
|
||||
handleAddCalled = true;
|
||||
series.Metadata.People.Add(p);
|
||||
}, () => onModifiedCalled = true);
|
||||
|
||||
// Assert
|
||||
Assert.False(handleAddCalled);
|
||||
Assert.False(onModifiedCalled);
|
||||
Assert.Single(series.Metadata.People);
|
||||
Assert.Equal("John Doe", series.Metadata.People.First().Name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region RemovePeople
|
||||
[Fact]
|
||||
public void RemovePeople_ShouldRemovePeopleOfSameRole()
|
||||
{
|
||||
var existingPeople = new List<Person>
|
||||
{
|
||||
DbFactory.Person("Joe Shmo", PersonRole.CoverArtist),
|
||||
DbFactory.Person("Joe Shmo", PersonRole.Writer)
|
||||
new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
|
||||
new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
|
||||
};
|
||||
var peopleRemoved = new List<Person>();
|
||||
PersonHelper.RemovePeople(existingPeople, new[] {"Joe Shmo", "Sally Ann"}, PersonRole.Writer, person =>
|
||||
@ -72,8 +228,8 @@ public class PersonHelperTests
|
||||
{
|
||||
var existingPeople = new List<Person>
|
||||
{
|
||||
DbFactory.Person("Joe Shmo", PersonRole.CoverArtist),
|
||||
DbFactory.Person("Joe Shmo", PersonRole.Writer)
|
||||
new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
|
||||
new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
|
||||
};
|
||||
var peopleRemoved = new List<Person>();
|
||||
PersonHelper.RemovePeople(existingPeople, new[] {"Joe Shmo", "Sally Ann"}, PersonRole.Writer, person =>
|
||||
@ -98,9 +254,9 @@ public class PersonHelperTests
|
||||
{
|
||||
var existingPeople = new List<Person>
|
||||
{
|
||||
DbFactory.Person("Joe Shmo", PersonRole.Writer),
|
||||
DbFactory.Person("Joe Shmo", PersonRole.Writer),
|
||||
DbFactory.Person("Joe Shmo", PersonRole.CoverArtist)
|
||||
new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
|
||||
new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
|
||||
new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
|
||||
};
|
||||
var peopleRemoved = new List<Person>();
|
||||
PersonHelper.RemovePeople(existingPeople, new List<string>(), PersonRole.Writer, person =>
|
||||
@ -112,19 +268,23 @@ public class PersonHelperTests
|
||||
Assert.Equal(2, peopleRemoved.Count);
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region KeepOnlySamePeopleBetweenLists
|
||||
[Fact]
|
||||
public void KeepOnlySamePeopleBetweenLists()
|
||||
{
|
||||
var existingPeople = new List<Person>
|
||||
{
|
||||
DbFactory.Person("Joe Shmo", PersonRole.CoverArtist),
|
||||
DbFactory.Person("Joe Shmo", PersonRole.Writer),
|
||||
DbFactory.Person("Sally", PersonRole.Writer),
|
||||
new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
|
||||
new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
|
||||
new PersonBuilder("Sally", PersonRole.Writer).Build(),
|
||||
};
|
||||
|
||||
var peopleFromChapters = new List<Person>
|
||||
{
|
||||
DbFactory.Person("Joe Shmo", PersonRole.CoverArtist),
|
||||
new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
|
||||
};
|
||||
|
||||
var peopleRemoved = new List<Person>();
|
||||
@ -136,26 +296,120 @@ public class PersonHelperTests
|
||||
|
||||
Assert.Equal(2, peopleRemoved.Count);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region AddPeople
|
||||
|
||||
[Fact]
|
||||
public void AddPersonIfNotExists_ShouldAddPerson_WhenPersonDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var metadataPeople = new List<Person>();
|
||||
var person = new PersonBuilder("John Smith", PersonRole.Character).Build();
|
||||
|
||||
// Act
|
||||
PersonHelper.AddPersonIfNotExists(metadataPeople, person);
|
||||
|
||||
// Assert
|
||||
Assert.Single(metadataPeople);
|
||||
Assert.Contains(person, metadataPeople);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPersonIfNotExists_ShouldNotAddPerson_WhenPersonAlreadyExists()
|
||||
{
|
||||
// Arrange
|
||||
var metadataPeople = new List<Person>
|
||||
{
|
||||
new PersonBuilder("John Smith", PersonRole.Character)
|
||||
.WithId(1)
|
||||
.Build()
|
||||
};
|
||||
var person = new PersonBuilder("John Smith", PersonRole.Character).Build();
|
||||
// Act
|
||||
PersonHelper.AddPersonIfNotExists(metadataPeople, person);
|
||||
|
||||
// Assert
|
||||
Assert.Single(metadataPeople);
|
||||
Assert.NotNull(metadataPeople.SingleOrDefault(p =>
|
||||
p.Name.Equals(person.Name) && p.Role == person.Role && p.NormalizedName == person.NormalizedName));
|
||||
Assert.Equal(1, metadataPeople.First().Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPersonIfNotExists_ShouldNotAddPerson_WhenPersonNameIsNullOrEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var metadataPeople = new List<Person>();
|
||||
var person2 = new PersonBuilder(string.Empty, PersonRole.Character).Build();
|
||||
|
||||
// Act
|
||||
PersonHelper.AddPersonIfNotExists(metadataPeople, person2);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(metadataPeople);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPersonIfNotExists_ShouldAddPerson_WhenPersonNameIsDifferentButRoleIsSame()
|
||||
{
|
||||
// Arrange
|
||||
var metadataPeople = new List<Person>
|
||||
{
|
||||
new PersonBuilder("John Smith", PersonRole.Character).Build()
|
||||
};
|
||||
var person = new PersonBuilder("John Doe", PersonRole.Character).Build();
|
||||
|
||||
// Act
|
||||
PersonHelper.AddPersonIfNotExists(metadataPeople, person);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, metadataPeople.Count);
|
||||
Assert.Contains(person, metadataPeople);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPersonIfNotExists_ShouldAddPerson_WhenPersonNameIsSameButRoleIsDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var metadataPeople = new List<Person>
|
||||
{
|
||||
new PersonBuilder("John Doe", PersonRole.Writer).Build()
|
||||
};
|
||||
var person = new PersonBuilder("John Smith", PersonRole.Character).Build();
|
||||
|
||||
// Act
|
||||
PersonHelper.AddPersonIfNotExists(metadataPeople, person);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, metadataPeople.Count);
|
||||
Assert.Contains(person, metadataPeople);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
public void AddPeople_ShouldAddOnlyNonExistingPeople()
|
||||
{
|
||||
var existingPeople = new List<Person>
|
||||
{
|
||||
DbFactory.Person("Joe Shmo", PersonRole.CoverArtist),
|
||||
DbFactory.Person("Joe Shmo", PersonRole.Writer),
|
||||
DbFactory.Person("Sally", PersonRole.Writer),
|
||||
new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build(),
|
||||
new PersonBuilder("Joe Shmo", PersonRole.Writer).Build(),
|
||||
new PersonBuilder("Sally", PersonRole.Writer).Build(),
|
||||
};
|
||||
|
||||
|
||||
PersonHelper.AddPersonIfNotExists(existingPeople, DbFactory.Person("Joe Shmo", PersonRole.CoverArtist));
|
||||
PersonHelper.AddPersonIfNotExists(existingPeople, new PersonBuilder("Joe Shmo", PersonRole.CoverArtist).Build());
|
||||
Assert.Equal(3, existingPeople.Count);
|
||||
|
||||
PersonHelper.AddPersonIfNotExists(existingPeople, DbFactory.Person("Joe Shmo", PersonRole.Writer));
|
||||
PersonHelper.AddPersonIfNotExists(existingPeople, new PersonBuilder("Joe Shmo", PersonRole.Writer).Build());
|
||||
Assert.Equal(3, existingPeople.Count);
|
||||
|
||||
PersonHelper.AddPersonIfNotExists(existingPeople, DbFactory.Person("Joe Shmo Two", PersonRole.CoverArtist));
|
||||
PersonHelper.AddPersonIfNotExists(existingPeople, new PersonBuilder("Joe Shmo Two", PersonRole.CoverArtist).Build());
|
||||
Assert.Equal(4, existingPeople.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,9 @@ using System.Linq;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using Xunit;
|
||||
|
||||
@ -15,147 +17,161 @@ public class SeriesHelperTests
|
||||
[Fact]
|
||||
public void FindSeries_ShouldFind_SameFormat()
|
||||
{
|
||||
var series = DbFactory.Series("Darker than Black");
|
||||
var series = new SeriesBuilder("Darker than Black").Build();
|
||||
series.OriginalName = "Something Random";
|
||||
series.Format = MangaFormat.Archive;
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Name = "Darker than Black",
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker than Black")
|
||||
NormalizedName = "Darker than Black".ToNormalized()
|
||||
}));
|
||||
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Name = "Darker than Black".ToLower(),
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker than Black")
|
||||
NormalizedName = "Darker than Black".ToNormalized()
|
||||
}));
|
||||
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Name = "Darker than Black".ToUpper(),
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker than Black")
|
||||
NormalizedName = "Darker than Black".ToNormalized()
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindSeries_ShouldFind_NullName()
|
||||
{
|
||||
var series = new SeriesBuilder("Darker than Black").Build();
|
||||
series.OriginalName = null;
|
||||
series.Format = MangaFormat.Archive;
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Name = "Darker than Black",
|
||||
NormalizedName = "Darker than Black".ToNormalized()
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindSeries_ShouldNotFind_WrongFormat()
|
||||
{
|
||||
var series = DbFactory.Series("Darker than Black");
|
||||
var series = new SeriesBuilder("Darker than Black").Build();
|
||||
series.OriginalName = "Something Random";
|
||||
series.Format = MangaFormat.Archive;
|
||||
Assert.False(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Image,
|
||||
Name = "Darker than Black",
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker than Black")
|
||||
NormalizedName = "Darker than Black".ToNormalized()
|
||||
}));
|
||||
|
||||
Assert.False(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Image,
|
||||
Name = "Darker than Black".ToLower(),
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker than Black")
|
||||
NormalizedName = "Darker than Black".ToNormalized()
|
||||
}));
|
||||
|
||||
Assert.False(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Image,
|
||||
Name = "Darker than Black".ToUpper(),
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker than Black")
|
||||
NormalizedName = "Darker than Black".ToNormalized()
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindSeries_ShouldFind_UsingOriginalName()
|
||||
{
|
||||
var series = DbFactory.Series("Darker than Black");
|
||||
var series = new SeriesBuilder("Darker than Black").Build();
|
||||
series.OriginalName = "Something Random";
|
||||
series.Format = MangaFormat.Image;
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Image,
|
||||
Name = "Something Random",
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something Random")
|
||||
NormalizedName = "Something Random".ToNormalized()
|
||||
}));
|
||||
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Image,
|
||||
Name = "Something Random".ToLower(),
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something Random")
|
||||
NormalizedName = "Something Random".ToNormalized()
|
||||
}));
|
||||
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Image,
|
||||
Name = "Something Random".ToUpper(),
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something Random")
|
||||
NormalizedName = "Something Random".ToNormalized()
|
||||
}));
|
||||
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Image,
|
||||
Name = "SomethingRandom".ToUpper(),
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("SomethingRandom")
|
||||
NormalizedName = "SomethingRandom".ToNormalized()
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindSeries_ShouldFind_UsingLocalizedName()
|
||||
{
|
||||
var series = DbFactory.Series("Darker than Black");
|
||||
var series = new SeriesBuilder("Darker than Black").Build();
|
||||
series.LocalizedName = "Something Random";
|
||||
series.Format = MangaFormat.Image;
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Image,
|
||||
Name = "Something Random",
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something Random")
|
||||
NormalizedName = "Something Random".ToNormalized()
|
||||
}));
|
||||
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Image,
|
||||
Name = "Something Random".ToLower(),
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something Random")
|
||||
NormalizedName = "Something Random".ToNormalized()
|
||||
}));
|
||||
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Image,
|
||||
Name = "Something Random".ToUpper(),
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something Random")
|
||||
NormalizedName = "Something Random".ToNormalized()
|
||||
}));
|
||||
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Image,
|
||||
Name = "SomethingRandom".ToUpper(),
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("SomethingRandom")
|
||||
NormalizedName = "SomethingRandom".ToNormalized()
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindSeries_ShouldFind_UsingLocalizedName_2()
|
||||
{
|
||||
var series = DbFactory.Series("My Dress-Up Darling");
|
||||
var series = new SeriesBuilder("My Dress-Up Darling").Build();
|
||||
series.LocalizedName = "Sono Bisque Doll wa Koi wo Suru";
|
||||
series.Format = MangaFormat.Archive;
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Name = "My Dress-Up Darling",
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("My Dress-Up Darling")
|
||||
NormalizedName = "My Dress-Up Darling".ToNormalized()
|
||||
}));
|
||||
|
||||
Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
Name = "Sono Bisque Doll wa Koi wo Suru".ToLower(),
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Sono Bisque Doll wa Koi wo Suru")
|
||||
NormalizedName = "Sono Bisque Doll wa Koi wo Suru".ToNormalized()
|
||||
}));
|
||||
}
|
||||
#endregion
|
||||
@ -165,13 +181,13 @@ public class SeriesHelperTests
|
||||
{
|
||||
var existingSeries = new List<Series>()
|
||||
{
|
||||
EntityFactory.CreateSeries("Darker than Black Vol 1"),
|
||||
EntityFactory.CreateSeries("Darker than Black"),
|
||||
EntityFactory.CreateSeries("Beastars"),
|
||||
new SeriesBuilder("Darker than Black Vol 1").Build(),
|
||||
new SeriesBuilder("Darker than Black").Build(),
|
||||
new SeriesBuilder("Beastars").Build(),
|
||||
};
|
||||
var missingSeries = new List<Series>()
|
||||
{
|
||||
EntityFactory.CreateSeries("Darker than Black Vol 1"),
|
||||
new SeriesBuilder("Darker than Black Vol 1").Build(),
|
||||
};
|
||||
existingSeries = SeriesHelper.RemoveMissingSeries(existingSeries, missingSeries, out var removeCount).ToList();
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Helpers;
|
||||
@ -13,9 +14,9 @@ public class TagHelperTests
|
||||
{
|
||||
var allTags = new List<Tag>
|
||||
{
|
||||
DbFactory.Tag("Action"),
|
||||
DbFactory.Tag("action"),
|
||||
DbFactory.Tag("Sci-fi"),
|
||||
new TagBuilder("Action").Build(),
|
||||
new TagBuilder("action").Build(),
|
||||
new TagBuilder("Sci-fi").Build(),
|
||||
};
|
||||
var tagAdded = new List<Tag>();
|
||||
|
||||
@ -37,9 +38,9 @@ public class TagHelperTests
|
||||
{
|
||||
var allTags = new List<Tag>
|
||||
{
|
||||
DbFactory.Tag("Action"),
|
||||
DbFactory.Tag("action"),
|
||||
DbFactory.Tag("Sci-fi"),
|
||||
new TagBuilder("Action").Build(),
|
||||
new TagBuilder("action").Build(),
|
||||
new TagBuilder("Sci-fi").Build(),
|
||||
|
||||
};
|
||||
var tagAdded = new List<Tag>();
|
||||
@ -62,19 +63,19 @@ public class TagHelperTests
|
||||
{
|
||||
var existingTags = new List<Tag>
|
||||
{
|
||||
DbFactory.Tag("Action"),
|
||||
DbFactory.Tag("action"),
|
||||
DbFactory.Tag("Sci-fi"),
|
||||
new TagBuilder("Action").Build(),
|
||||
new TagBuilder("action").Build(),
|
||||
new TagBuilder("Sci-fi").Build(),
|
||||
};
|
||||
|
||||
|
||||
TagHelper.AddTagIfNotExists(existingTags, DbFactory.Tag("Action"));
|
||||
TagHelper.AddTagIfNotExists(existingTags, new TagBuilder("Action").Build());
|
||||
Assert.Equal(3, existingTags.Count);
|
||||
|
||||
TagHelper.AddTagIfNotExists(existingTags, DbFactory.Tag("action"));
|
||||
TagHelper.AddTagIfNotExists(existingTags, new TagBuilder("action").Build());
|
||||
Assert.Equal(3, existingTags.Count);
|
||||
|
||||
TagHelper.AddTagIfNotExists(existingTags, DbFactory.Tag("Shonen"));
|
||||
TagHelper.AddTagIfNotExists(existingTags, new TagBuilder("Shonen").Build());
|
||||
Assert.Equal(4, existingTags.Count);
|
||||
}
|
||||
|
||||
@ -83,13 +84,13 @@ public class TagHelperTests
|
||||
{
|
||||
var existingTags = new List<Tag>
|
||||
{
|
||||
DbFactory.Tag("Action"),
|
||||
DbFactory.Tag("Sci-fi"),
|
||||
new TagBuilder("Action").Build(),
|
||||
new TagBuilder("Sci-fi").Build(),
|
||||
};
|
||||
|
||||
var peopleFromChapters = new List<Tag>
|
||||
{
|
||||
DbFactory.Tag("Action"),
|
||||
new TagBuilder("Action").Build(),
|
||||
};
|
||||
|
||||
var tagRemoved = new List<Tag>();
|
||||
@ -107,8 +108,8 @@ public class TagHelperTests
|
||||
{
|
||||
var existingTags = new List<Tag>
|
||||
{
|
||||
DbFactory.Tag("Action"),
|
||||
DbFactory.Tag("Sci-fi"),
|
||||
new TagBuilder("Action").Build(),
|
||||
new TagBuilder("Sci-fi").Build(),
|
||||
};
|
||||
|
||||
var peopleFromChapters = new List<Tag>();
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using API.Parser;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -1,7 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using API.Entities.Enums;
|
||||
using API.Parser;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -51,7 +52,7 @@ public class DefaultParserTests
|
||||
{
|
||||
const string rootDirectory = "/manga/";
|
||||
var tokens = expectedParseInfo.Split("~");
|
||||
var actual = new ParserInfo {Chapters = "0", Volumes = "0"};
|
||||
var actual = new ParserInfo {Series = "", Chapters = "0", Volumes = "0"};
|
||||
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
|
||||
Assert.Equal(tokens[0], actual.Series);
|
||||
Assert.Equal(tokens[1], actual.Volumes);
|
||||
@ -100,6 +101,14 @@ public class DefaultParserTests
|
||||
|
||||
#region Parse
|
||||
|
||||
[Fact]
|
||||
public void Parse_MangaLibrary_JustCover_ShouldReturnNull()
|
||||
{
|
||||
const string rootPath = @"E:/Manga/";
|
||||
var actual = _defaultParser.Parse(@"E:/Manga/Accel World/cover.png", rootPath);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ParseInfo_Manga()
|
||||
{
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using API.Entities.Enums;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@ -82,6 +81,7 @@ public class MangaParserTests
|
||||
[InlineData("몰?루 아카이브 7.5권", "7.5")]
|
||||
[InlineData("63권#200", "63")]
|
||||
[InlineData("시즌34삽화2", "34")]
|
||||
[InlineData("Accel World Chapter 001 Volume 002", "2")]
|
||||
public void ParseVolumeTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename));
|
||||
@ -196,6 +196,7 @@ public class MangaParserTests
|
||||
[InlineData("Манга Том 1 3-4 Глава", "Манга")]
|
||||
[InlineData("Esquire 6권 2021년 10월호", "Esquire")]
|
||||
[InlineData("Accel World: Vol 1", "Accel World")]
|
||||
[InlineData("Accel World Chapter 001 Volume 002", "Accel World")]
|
||||
public void ParseSeriesTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));
|
||||
@ -279,6 +280,7 @@ public class MangaParserTests
|
||||
[InlineData("Манга Глава 2", "2")]
|
||||
[InlineData("Манга 2 Глава", "2")]
|
||||
[InlineData("Манга Том 1 2 Глава", "2")]
|
||||
[InlineData("Accel World Chapter 001 Volume 002", "1")]
|
||||
public void ParseChaptersTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
||||
|
@ -1,5 +1,5 @@
|
||||
using API.Entities.Enums;
|
||||
using API.Parser;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Parser;
|
||||
|
@ -249,7 +249,7 @@ public class ParserTests
|
||||
[InlineData("The ()quick brown fox jumps over the lazy dog")]
|
||||
[InlineData("The (quick (brown)) fox jumps over the lazy dog")]
|
||||
[InlineData("The (quick (brown) fox jumps over the lazy dog)")]
|
||||
public void BalancedParenTestMatches(string input)
|
||||
public void BalancedParenTest_Matches(string input)
|
||||
{
|
||||
Assert.Matches($@"^{BalancedParen}$", input);
|
||||
}
|
||||
@ -261,7 +261,7 @@ public class ParserTests
|
||||
[InlineData("The quick (brown)) fox jumps over the lazy dog")]
|
||||
[InlineData("The quick (brown) fox jumps over the lazy dog)")]
|
||||
[InlineData("(The ))(quick (brown) fox jumps over the lazy dog")]
|
||||
public void BalancedParenTestDoesNotMatch(string input)
|
||||
public void BalancedParenTest_DoesNotMatch(string input)
|
||||
{
|
||||
Assert.DoesNotMatch($@"^{BalancedParen}$", input);
|
||||
}
|
||||
@ -273,9 +273,9 @@ public class ParserTests
|
||||
[InlineData("The []quick brown fox jumps over the lazy dog")]
|
||||
[InlineData("The [quick [brown]] fox jumps over the lazy dog")]
|
||||
[InlineData("The [quick [brown] fox jumps over the lazy dog]")]
|
||||
public void BalancedBrackTestMatches(string input)
|
||||
public void BalancedBracketTest_Matches(string input)
|
||||
{
|
||||
Assert.Matches($@"^{BalancedBrack}$", input);
|
||||
Assert.Matches($@"^{BalancedBracket}$", input);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -285,8 +285,8 @@ public class ParserTests
|
||||
[InlineData("The quick [brown]] fox jumps over the lazy dog")]
|
||||
[InlineData("The quick [brown] fox jumps over the lazy dog]")]
|
||||
[InlineData("[The ]][quick [brown] fox jumps over the lazy dog")]
|
||||
public void BalancedBrackTestDoesNotMatch(string input)
|
||||
public void BalancedBracketTest_DoesNotMatch(string input)
|
||||
{
|
||||
Assert.DoesNotMatch($@"^{BalancedBrack}$", input);
|
||||
Assert.DoesNotMatch($@"^{BalancedBracket}$", input);
|
||||
}
|
||||
}
|
||||
|
178
API.Tests/Repository/CollectionTagRepositoryTests.cs
Normal file
@ -0,0 +1,178 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Repository;
|
||||
|
||||
public class CollectionTagRepositoryTests
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
private readonly DbConnection _connection;
|
||||
private readonly DataContext _context;
|
||||
|
||||
private const string CacheDirectory = "C:/kavita/config/cache/";
|
||||
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
||||
private const string BackupDirectory = "C:/kavita/config/backups/";
|
||||
private const string DataDirectory = "C:/data/";
|
||||
|
||||
public CollectionTagRepositoryTests()
|
||||
{
|
||||
var contextOptions = new DbContextOptionsBuilder().UseSqlite(CreateInMemoryDatabase()).Options;
|
||||
_connection = RelationalOptionsExtension.Extract(contextOptions).Connection;
|
||||
|
||||
_context = new DataContext(contextOptions);
|
||||
Task.Run(SeedDb).GetAwaiter().GetResult();
|
||||
|
||||
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
|
||||
var mapper = config.CreateMapper();
|
||||
_unitOfWork = new UnitOfWork(_context, mapper, null);
|
||||
}
|
||||
|
||||
#region Setup
|
||||
|
||||
private static DbConnection CreateInMemoryDatabase()
|
||||
{
|
||||
var connection = new SqliteConnection("Filename=:memory:");
|
||||
|
||||
connection.Open();
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private async Task<bool> SeedDb()
|
||||
{
|
||||
await _context.Database.MigrateAsync();
|
||||
var filesystem = CreateFileSystem();
|
||||
|
||||
await Seed.SeedSettings(_context,
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem));
|
||||
|
||||
var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync();
|
||||
setting.Value = CacheDirectory;
|
||||
|
||||
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync();
|
||||
setting.Value = BackupDirectory;
|
||||
|
||||
_context.ServerSetting.Update(setting);
|
||||
|
||||
|
||||
var lib = new LibraryBuilder("Manga")
|
||||
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
|
||||
.Build();
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
UserName = "majora2007",
|
||||
Libraries = new List<Library>()
|
||||
{
|
||||
lib
|
||||
}
|
||||
});
|
||||
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
private async Task ResetDb()
|
||||
{
|
||||
_context.Series.RemoveRange(_context.Series.ToList());
|
||||
_context.AppUserRating.RemoveRange(_context.AppUserRating.ToList());
|
||||
_context.Genre.RemoveRange(_context.Genre.ToList());
|
||||
_context.CollectionTag.RemoveRange(_context.CollectionTag.ToList());
|
||||
_context.Person.RemoveRange(_context.Person.ToList());
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static MockFileSystem CreateFileSystem()
|
||||
{
|
||||
var fileSystem = new MockFileSystem();
|
||||
fileSystem.Directory.SetCurrentDirectory("C:/kavita/");
|
||||
fileSystem.AddDirectory("C:/kavita/config/");
|
||||
fileSystem.AddDirectory(CacheDirectory);
|
||||
fileSystem.AddDirectory(CoverImageDirectory);
|
||||
fileSystem.AddDirectory(BackupDirectory);
|
||||
fileSystem.AddDirectory(DataDirectory);
|
||||
|
||||
return fileSystem;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RemoveTagsWithoutSeries
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveTagsWithoutSeries_ShouldRemoveTags()
|
||||
{
|
||||
var library = new LibraryBuilder("Test", LibraryType.Manga).Build();
|
||||
var series = new SeriesBuilder("Test 1").Build();
|
||||
var commonTag = new CollectionTagBuilder("Tag 1").Build();
|
||||
series.Metadata.CollectionTags.Add(commonTag);
|
||||
series.Metadata.CollectionTags.Add(new CollectionTagBuilder("Tag 2").Build());
|
||||
|
||||
var series2 = new SeriesBuilder("Test 1").Build();
|
||||
series2.Metadata.CollectionTags.Add(commonTag);
|
||||
library.Series.Add(series);
|
||||
library.Series.Add(series2);
|
||||
_unitOfWork.LibraryRepository.Add(library);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
Assert.Equal(2, series.Metadata.CollectionTags.Count);
|
||||
Assert.Single(series2.Metadata.CollectionTags);
|
||||
|
||||
// Delete both series
|
||||
_unitOfWork.SeriesRepository.Remove(series);
|
||||
_unitOfWork.SeriesRepository.Remove(series2);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
// Validate that both tags exist
|
||||
Assert.Equal(2, (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count());
|
||||
|
||||
await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
|
||||
|
||||
Assert.Empty(await _unitOfWork.CollectionTagRepository.GetAllTagsAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveTagsWithoutSeries_ShouldNotRemoveTags()
|
||||
{
|
||||
var library = new LibraryBuilder("Test", LibraryType.Manga).Build();
|
||||
var series = new SeriesBuilder("Test 1").Build();
|
||||
var commonTag = new CollectionTagBuilder("Tag 1").Build();
|
||||
series.Metadata.CollectionTags.Add(commonTag);
|
||||
series.Metadata.CollectionTags.Add(new CollectionTagBuilder("Tag 2").Build());
|
||||
|
||||
var series2 = new SeriesBuilder("Test 1").Build();
|
||||
series2.Metadata.CollectionTags.Add(commonTag);
|
||||
library.Series.Add(series);
|
||||
library.Series.Add(series2);
|
||||
_unitOfWork.LibraryRepository.Add(library);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
Assert.Equal(2, series.Metadata.CollectionTags.Count);
|
||||
Assert.Single(series2.Metadata.CollectionTags);
|
||||
|
||||
await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
|
||||
|
||||
// Validate that both tags exist
|
||||
Assert.Equal(2, (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count());
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
@ -6,7 +6,9 @@ using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
@ -40,7 +42,7 @@ public class SeriesRepositoryTests
|
||||
|
||||
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
|
||||
var mapper = config.CreateMapper();
|
||||
_unitOfWork = new UnitOfWork(_context, mapper, null);
|
||||
_unitOfWork = new UnitOfWork(_context, mapper, null!);
|
||||
}
|
||||
|
||||
#region Setup
|
||||
@ -70,10 +72,9 @@ public class SeriesRepositoryTests
|
||||
|
||||
_context.ServerSetting.Update(setting);
|
||||
|
||||
var lib = new Library()
|
||||
{
|
||||
Name = "Manga", Folders = new List<FolderPath>() {new FolderPath() {Path = "C:/data/"}}
|
||||
};
|
||||
var lib = new LibraryBuilder("Manga")
|
||||
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
|
||||
.Build();
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -115,37 +116,31 @@ public class SeriesRepositoryTests
|
||||
|
||||
private async Task SetupSeriesData()
|
||||
{
|
||||
var library = new Library()
|
||||
{
|
||||
Name = "Manga",
|
||||
Type = LibraryType.Manga,
|
||||
Folders = new List<FolderPath>()
|
||||
{
|
||||
new FolderPath() {Path = "C:/data/manga/"}
|
||||
}
|
||||
};
|
||||
|
||||
var s = DbFactory.Series("The Idaten Deities Know Only Peace", "Heion Sedai no Idaten-tachi");
|
||||
s.Format = MangaFormat.Archive;
|
||||
|
||||
library.Series = new List<Series>()
|
||||
{
|
||||
s,
|
||||
};
|
||||
var library = new LibraryBuilder("GetFullSeriesByAnyName Manga", LibraryType.Manga)
|
||||
.WithFolderPath(new FolderPathBuilder("C:/data/manga/").Build())
|
||||
.WithSeries(new SeriesBuilder("The Idaten Deities Know Only Peace")
|
||||
.WithLocalizedName("Heion Sedai no Idaten-tachi")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
_unitOfWork.LibraryRepository.Add(library);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
|
||||
[InlineData("Heion Sedai no Idaten-tachi", "", MangaFormat.Archive, "The Idaten Deities Know Only Peace")] // Matching on localized name in DB
|
||||
[InlineData("Heion Sedai no Idaten-tachi", "", MangaFormat.Pdf, null)]
|
||||
[Theory]
|
||||
[InlineData("The Idaten Deities Know Only Peace", MangaFormat.Archive, "", "The Idaten Deities Know Only Peace")] // Matching on series name in DB
|
||||
[InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Archive, "The Idaten Deities Know Only Peace", "The Idaten Deities Know Only Peace")] // Matching on localized name in DB
|
||||
[InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Pdf, "", null)]
|
||||
public async Task GetFullSeriesByAnyName_Should(string seriesName, MangaFormat format, string localizedName, string? expected)
|
||||
{
|
||||
var firstSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||
await ResetDb();
|
||||
await SetupSeriesData();
|
||||
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(seriesName, localizedName,
|
||||
1, format);
|
||||
2, format, false);
|
||||
if (expected == null)
|
||||
{
|
||||
Assert.Null(series);
|
||||
@ -157,6 +152,4 @@ public class SeriesRepositoryTests
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//public async Task
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ using System.IO.Abstractions.TestingHelpers;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using API.Archive;
|
||||
using API.Data.Metadata;
|
||||
using API.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
@ -154,11 +153,11 @@ public class ArchiveServiceTests
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData("v10.cbz", "v10.expected.png")]
|
||||
[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.png")]
|
||||
[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.png")]
|
||||
//[InlineData("v10.cbz", "v10.expected.png")] // Commented out as these break usually when NetVips is updated
|
||||
//[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.png")]
|
||||
//[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.png")]
|
||||
[InlineData("macos_native.zip", "macos_native.png")]
|
||||
[InlineData("v10 - duplicate covers.cbz", "v10 - duplicate covers.expected.png")]
|
||||
//[InlineData("v10 - duplicate covers.cbz", "v10 - duplicate covers.expected.png")]
|
||||
[InlineData("sorting.zip", "sorting.expected.png")]
|
||||
[InlineData("test.zip", "test.expected.jpg")]
|
||||
public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFile)
|
||||
@ -187,11 +186,11 @@ public class ArchiveServiceTests
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData("v10.cbz", "v10.expected.png")]
|
||||
[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.png")]
|
||||
[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.png")]
|
||||
//[InlineData("v10.cbz", "v10.expected.png")] // Commented out as these break usually when NetVips is updated
|
||||
//[InlineData("v10 - with folder.cbz", "v10 - with folder.expected.png")]
|
||||
//[InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.png")]
|
||||
[InlineData("macos_native.zip", "macos_native.png")]
|
||||
[InlineData("v10 - duplicate covers.cbz", "v10 - duplicate covers.expected.png")]
|
||||
//[InlineData("v10 - duplicate covers.cbz", "v10 - duplicate covers.expected.png")]
|
||||
[InlineData("sorting.zip", "sorting.expected.png")]
|
||||
public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOutputFile)
|
||||
{
|
||||
@ -245,6 +244,17 @@ public class ArchiveServiceTests
|
||||
Assert.Equal(summaryInfo, comicInfo.Summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldHaveComicInfo_CanParseUmlaut()
|
||||
{
|
||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
||||
var archive = Path.Join(testDirectory, "Umlaut.zip");
|
||||
|
||||
var comicInfo = _archiveService.GetComicInfo(archive);
|
||||
Assert.NotNull(comicInfo);
|
||||
Assert.Equal("Belladonna", comicInfo.Series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldHaveComicInfo_WithAuthors()
|
||||
{
|
||||
|
@ -1,18 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@ -83,18 +81,9 @@ public class BackupServiceTests
|
||||
setting.Value = BackupDirectory;
|
||||
|
||||
_context.ServerSetting.Update(setting);
|
||||
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Manga",
|
||||
Folders = new List<FolderPath>()
|
||||
{
|
||||
new FolderPath()
|
||||
{
|
||||
Path = "C:/data/"
|
||||
}
|
||||
}
|
||||
});
|
||||
_context.Library.Add(new LibraryBuilder("Manga")
|
||||
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
|
||||
.Build());
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
@ -10,7 +9,10 @@ using API.Data.Repositories;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
@ -85,17 +87,9 @@ public class BookmarkServiceTests
|
||||
|
||||
_context.ServerSetting.Update(setting);
|
||||
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Manga",
|
||||
Folders = new List<FolderPath>()
|
||||
{
|
||||
new FolderPath()
|
||||
{
|
||||
Path = "C:/data/"
|
||||
}
|
||||
}
|
||||
});
|
||||
_context.Library.Add(new LibraryBuilder("Manga")
|
||||
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
|
||||
.Build());
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
@ -136,27 +130,16 @@ public class BookmarkServiceTests
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("1")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb").Build();
|
||||
_context.Series.Add(series);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -194,27 +177,17 @@ public class BookmarkServiceTests
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithChapter(new ChapterBuilder("0")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb").Build();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
_context.Series.Add(series);
|
||||
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
@ -270,28 +243,17 @@ public class BookmarkServiceTests
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb").Build();
|
||||
|
||||
_context.Series.Add(series);
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -342,7 +304,7 @@ public class BookmarkServiceTests
|
||||
|
||||
|
||||
Assert.Equal(2, ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories).Count());
|
||||
Assert.False(ds.FileSystem.FileInfo.FromFileName(Path.Join(BookmarkDirectory, "1/1/1/0001.jpg")).Exists);
|
||||
Assert.False(ds.FileSystem.FileInfo.New(Path.Join(BookmarkDirectory, "1/1/1/0001.jpg")).Exists);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@ -357,27 +319,18 @@ public class BookmarkServiceTests
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb").Build();
|
||||
|
||||
_context.Series.Add(series);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -419,28 +372,17 @@ public class BookmarkServiceTests
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb").Build();
|
||||
|
||||
_context.Series.Add(series);
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -483,28 +425,15 @@ public class BookmarkServiceTests
|
||||
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
var series = new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Chapters = new List<Chapter>()
|
||||
{
|
||||
new Chapter()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1")
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb").Build();
|
||||
|
||||
_context.Series.Add(series);
|
||||
|
||||
@ -528,7 +457,7 @@ public class BookmarkServiceTests
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks);
|
||||
Assert.NotEmpty(user.Bookmarks);
|
||||
Assert.NotEmpty(user!.Bookmarks);
|
||||
|
||||
series.Volumes = new List<Volume>();
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
|
@ -8,8 +8,9 @@ using API.Data;
|
||||
using API.Data.Metadata;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Parser;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
@ -116,17 +117,9 @@ public class CacheServiceTests
|
||||
|
||||
_context.ServerSetting.Update(setting);
|
||||
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Manga",
|
||||
Folders = new List<FolderPath>()
|
||||
{
|
||||
new FolderPath()
|
||||
{
|
||||
Path = "C:/data/"
|
||||
}
|
||||
}
|
||||
});
|
||||
_context.Library.Add(new LibraryBuilder("Manga")
|
||||
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
|
||||
.Build());
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
@ -166,20 +159,11 @@ public class CacheServiceTests
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
|
||||
await ResetDB();
|
||||
var s = DbFactory.Series("Test");
|
||||
var v = DbFactory.Volume("1");
|
||||
var c = new Chapter()
|
||||
{
|
||||
Number = "1",
|
||||
Files = new List<MangaFile>()
|
||||
{
|
||||
new MangaFile()
|
||||
{
|
||||
Format = MangaFormat.Archive,
|
||||
FilePath = $"{DataDirectory}Test v1.zip",
|
||||
}
|
||||
}
|
||||
};
|
||||
var s = new SeriesBuilder("Test").Build();
|
||||
var v = new VolumeBuilder("1").Build();
|
||||
var c = new ChapterBuilder("1")
|
||||
.WithFile(new MangaFileBuilder($"{DataDirectory}Test v1.zip", MangaFormat.Archive).Build())
|
||||
.Build();
|
||||
v.Chapters.Add(c);
|
||||
s.Volumes.Add(v);
|
||||
s.LibraryId = 1;
|
||||
@ -206,8 +190,8 @@ public class CacheServiceTests
|
||||
// new ReadingItemService(archiveService, Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds));
|
||||
//
|
||||
// await ResetDB();
|
||||
// var s = DbFactory.Series("Test");
|
||||
// var v = DbFactory.Volume("1");
|
||||
// var s = new SeriesBuilder("Test").Build();
|
||||
// var v = new VolumeBuilder("1").Build();
|
||||
// var c = new Chapter()
|
||||
// {
|
||||
// Number = "1",
|
||||
@ -270,20 +254,10 @@ public class CacheServiceTests
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
|
||||
var c = new Chapter()
|
||||
{
|
||||
Files = new List<MangaFile>()
|
||||
{
|
||||
new MangaFile()
|
||||
{
|
||||
FilePath = $"{DataDirectory}1.epub"
|
||||
},
|
||||
new MangaFile()
|
||||
{
|
||||
FilePath = $"{DataDirectory}2.epub"
|
||||
}
|
||||
}
|
||||
};
|
||||
var c = new ChapterBuilder("1")
|
||||
.WithFile(new MangaFileBuilder($"{DataDirectory}1.epub", MangaFormat.Epub).Build())
|
||||
.WithFile(new MangaFileBuilder($"{DataDirectory}2.epub", MangaFormat.Epub).Build())
|
||||
.Build();
|
||||
cs.GetCachedFile(c);
|
||||
Assert.Same($"{DataDirectory}1.epub", cs.GetCachedFile(c));
|
||||
}
|
||||
@ -300,11 +274,9 @@ public class CacheServiceTests
|
||||
filesystem.AddFile($"{DataDirectory}1.zip", new MockFileData(""));
|
||||
filesystem.AddFile($"{DataDirectory}2.zip", new MockFileData(""));
|
||||
|
||||
var c = new Chapter()
|
||||
{
|
||||
Id = 1,
|
||||
Files = new List<MangaFile>()
|
||||
};
|
||||
var c = new ChapterBuilder("1")
|
||||
.WithId(1)
|
||||
.Build();
|
||||
|
||||
var fileIndex = 0;
|
||||
foreach (var file in c.Files)
|
||||
@ -337,26 +309,17 @@ public class CacheServiceTests
|
||||
filesystem.AddFile($"{DataDirectory}1.zip", new MockFileData(""));
|
||||
filesystem.AddFile($"{DataDirectory}2.zip", new MockFileData(""));
|
||||
|
||||
var c = new Chapter()
|
||||
{
|
||||
Id = 1,
|
||||
Files = new List<MangaFile>()
|
||||
{
|
||||
new MangaFile()
|
||||
{
|
||||
Id = 1,
|
||||
FilePath = $"{DataDirectory}1.zip",
|
||||
Pages = 10
|
||||
|
||||
},
|
||||
new MangaFile()
|
||||
{
|
||||
Id = 2,
|
||||
FilePath = $"{DataDirectory}2.zip",
|
||||
Pages = 5
|
||||
}
|
||||
}
|
||||
};
|
||||
var c = new ChapterBuilder("1")
|
||||
.WithId(1)
|
||||
.WithFile(new MangaFileBuilder($"{DataDirectory}1.zip", MangaFormat.Archive)
|
||||
.WithPages(10)
|
||||
.WithId(1)
|
||||
.Build())
|
||||
.WithFile(new MangaFileBuilder($"{DataDirectory}2.zip", MangaFormat.Archive)
|
||||
.WithPages(5)
|
||||
.WithId(2)
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
var fileIndex = 0;
|
||||
foreach (var file in c.Files)
|
||||
@ -389,20 +352,13 @@ public class CacheServiceTests
|
||||
filesystem.AddDirectory($"{CacheDirectory}1/");
|
||||
filesystem.AddFile($"{DataDirectory}1.zip", new MockFileData(""));
|
||||
|
||||
var c = new Chapter()
|
||||
{
|
||||
Id = 1,
|
||||
Files = new List<MangaFile>()
|
||||
{
|
||||
new MangaFile()
|
||||
{
|
||||
Id = 1,
|
||||
FilePath = $"{DataDirectory}1.zip",
|
||||
Pages = 10
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
var c = new ChapterBuilder("1")
|
||||
.WithId(1)
|
||||
.WithFile(new MangaFileBuilder($"{DataDirectory}1.zip", MangaFormat.Archive)
|
||||
.WithPages(10)
|
||||
.WithId(1)
|
||||
.Build())
|
||||
.Build();
|
||||
c.Pages = c.Files.Sum(f => f.Pages);
|
||||
|
||||
var fileIndex = 0;
|
||||
@ -437,26 +393,17 @@ public class CacheServiceTests
|
||||
filesystem.AddFile($"{DataDirectory}1.zip", new MockFileData(""));
|
||||
filesystem.AddFile($"{DataDirectory}2.zip", new MockFileData(""));
|
||||
|
||||
var c = new Chapter()
|
||||
{
|
||||
Id = 1,
|
||||
Files = new List<MangaFile>()
|
||||
{
|
||||
new MangaFile()
|
||||
{
|
||||
Id = 1,
|
||||
FilePath = $"{DataDirectory}1.zip",
|
||||
Pages = 10
|
||||
|
||||
},
|
||||
new MangaFile()
|
||||
{
|
||||
Id = 2,
|
||||
FilePath = $"{DataDirectory}2.zip",
|
||||
Pages = 5
|
||||
}
|
||||
}
|
||||
};
|
||||
var c = new ChapterBuilder("1")
|
||||
.WithId(1)
|
||||
.WithFile(new MangaFileBuilder($"{DataDirectory}1.zip", MangaFormat.Archive)
|
||||
.WithPages(10)
|
||||
.WithId(1)
|
||||
.Build())
|
||||
.WithFile(new MangaFileBuilder($"{DataDirectory}2.zip", MangaFormat.Archive)
|
||||
.WithPages(5)
|
||||
.WithId(2)
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
var fileIndex = 0;
|
||||
foreach (var file in c.Files)
|
||||
|
@ -1,28 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Settings;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Converters;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
@ -33,21 +27,18 @@ public class CleanupServiceTests : AbstractDbTest
|
||||
{
|
||||
private readonly ILogger<CleanupService> _logger = Substitute.For<ILogger<CleanupService>>();
|
||||
private readonly IEventHub _messageHub = Substitute.For<IEventHub>();
|
||||
private readonly IReaderService _readerService;
|
||||
|
||||
|
||||
public CleanupServiceTests() : base()
|
||||
{
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Manga",
|
||||
Folders = new List<FolderPath>()
|
||||
{
|
||||
new FolderPath()
|
||||
{
|
||||
Path = "C:/data/"
|
||||
}
|
||||
}
|
||||
});
|
||||
_context.Library.Add(new LibraryBuilder("Manga")
|
||||
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
|
||||
.Build());
|
||||
|
||||
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>(),
|
||||
Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
}
|
||||
|
||||
#region Setup
|
||||
@ -77,15 +68,15 @@ public class CleanupServiceTests : AbstractDbTest
|
||||
// Delete all Series to reset state
|
||||
await ResetDb();
|
||||
|
||||
var s = DbFactory.Series("Test 1");
|
||||
var s = new SeriesBuilder("Test 1").Build();
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
s = DbFactory.Series("Test 2");
|
||||
s = new SeriesBuilder("Test 2").Build();
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
s = DbFactory.Series("Test 3");
|
||||
s = new SeriesBuilder("Test 3").Build();
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(1000)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
@ -111,11 +102,11 @@ public class CleanupServiceTests : AbstractDbTest
|
||||
await ResetDb();
|
||||
|
||||
// Add 2 series with cover images
|
||||
var s = DbFactory.Series("Test 1");
|
||||
var s = new SeriesBuilder("Test 1").Build();
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
s = DbFactory.Series("Test 2");
|
||||
s = new SeriesBuilder("Test 2").Build();
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
@ -145,29 +136,23 @@ public class CleanupServiceTests : AbstractDbTest
|
||||
await ResetDb();
|
||||
|
||||
// Add 2 series with cover images
|
||||
var s = DbFactory.Series("Test 1");
|
||||
var v = DbFactory.Volume("1");
|
||||
v.Chapters.Add(new Chapter()
|
||||
{
|
||||
CoverImage = "v01_c01.jpg"
|
||||
});
|
||||
v.CoverImage = "v01_c01.jpg";
|
||||
s.Volumes.Add(v);
|
||||
s.CoverImage = "series_01.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
_context.Series.Add(new SeriesBuilder("Test 1")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("0").WithCoverImage("v01_c01.jpg").Build())
|
||||
.WithCoverImage("v01_c01.jpg")
|
||||
.Build())
|
||||
.WithCoverImage("series_01.jpg")
|
||||
.WithLibraryId(1)
|
||||
.Build());
|
||||
|
||||
s = DbFactory.Series("Test 2");
|
||||
v = DbFactory.Volume("1");
|
||||
v.Chapters.Add(new Chapter()
|
||||
{
|
||||
CoverImage = "v01_c03.jpg"
|
||||
});
|
||||
v.CoverImage = "v01_c03jpg";
|
||||
s.Volumes.Add(v);
|
||||
s.CoverImage = "series_03.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
_context.Series.Add(new SeriesBuilder("Test 2")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("0").WithCoverImage("v01_c03.jpg").Build())
|
||||
.WithCoverImage("v01_c03.jpg")
|
||||
.Build())
|
||||
.WithCoverImage("series_03.jpg")
|
||||
.WithLibraryId(1)
|
||||
.Build());
|
||||
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
@ -195,27 +180,26 @@ public class CleanupServiceTests : AbstractDbTest
|
||||
await ResetDb();
|
||||
|
||||
// Add 2 series with cover images
|
||||
var s = DbFactory.Series("Test 1");
|
||||
s.Metadata.CollectionTags = new List<CollectionTag>();
|
||||
s.Metadata.CollectionTags.Add(new CollectionTag()
|
||||
{
|
||||
Title = "Something",
|
||||
CoverImage = $"{ImageService.GetCollectionTagFormat(1)}.jpg"
|
||||
});
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
|
||||
s = DbFactory.Series("Test 2");
|
||||
s.Metadata.CollectionTags = new List<CollectionTag>();
|
||||
s.Metadata.CollectionTags.Add(new CollectionTag()
|
||||
{
|
||||
Title = "Something 2",
|
||||
CoverImage = $"{ImageService.GetCollectionTagFormat(2)}.jpg"
|
||||
});
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
_context.Series.Add(new SeriesBuilder("Test 1")
|
||||
.WithMetadata(new SeriesMetadataBuilder()
|
||||
.WithCollectionTag(new CollectionTagBuilder("Something")
|
||||
.WithCoverImage($"{ImageService.GetCollectionTagFormat(1)}.jpg")
|
||||
.Build())
|
||||
.Build())
|
||||
.WithCoverImage($"{ImageService.GetSeriesFormat(1)}.jpg")
|
||||
.WithLibraryId(1)
|
||||
.Build());
|
||||
|
||||
_context.Series.Add(new SeriesBuilder("Test 2")
|
||||
.WithMetadata(new SeriesMetadataBuilder()
|
||||
.WithCollectionTag(new CollectionTagBuilder("Something")
|
||||
.WithCoverImage($"{ImageService.GetCollectionTagFormat(2)}.jpg")
|
||||
.Build())
|
||||
.Build())
|
||||
.WithCoverImage($"{ImageService.GetSeriesFormat(3)}.jpg")
|
||||
.WithLibraryId(1)
|
||||
.Build());
|
||||
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
@ -247,18 +231,14 @@ public class CleanupServiceTests : AbstractDbTest
|
||||
UserName = "Joe",
|
||||
ReadingLists = new List<ReadingList>()
|
||||
{
|
||||
new ReadingList()
|
||||
{
|
||||
Title = "Something",
|
||||
NormalizedTitle = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something"),
|
||||
CoverImage = $"{ImageService.GetReadingListFormat(1)}.jpg"
|
||||
},
|
||||
new ReadingList()
|
||||
{
|
||||
Title = "Something 2",
|
||||
NormalizedTitle = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something 2"),
|
||||
CoverImage = $"{ImageService.GetReadingListFormat(2)}.jpg"
|
||||
}
|
||||
new ReadingListBuilder("Something")
|
||||
.WithRating(AgeRating.Unknown)
|
||||
.WithCoverImage($"{ImageService.GetReadingListFormat(1)}.jpg")
|
||||
.Build(),
|
||||
new ReadingListBuilder("Something 2")
|
||||
.WithRating(AgeRating.Unknown)
|
||||
.WithCoverImage($"{ImageService.GetReadingListFormat(2)}.jpg")
|
||||
.Build(),
|
||||
}
|
||||
});
|
||||
|
||||
@ -408,22 +388,20 @@ public class CleanupServiceTests : AbstractDbTest
|
||||
[Fact]
|
||||
public async Task CleanupDbEntries_CleanupAbandonedChapters()
|
||||
{
|
||||
var c = EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1);
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
c,
|
||||
}),
|
||||
}
|
||||
});
|
||||
var c = new ChapterBuilder("0")
|
||||
.WithPages(1)
|
||||
.Build();
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithNumber(1)
|
||||
.WithChapter(c)
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb").Build();
|
||||
|
||||
_context.Series.Add(series);
|
||||
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -432,10 +410,8 @@ public class CleanupServiceTests : AbstractDbTest
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkChaptersUntilAsRead(user, 1, 5);
|
||||
await _readerService.MarkChaptersUntilAsRead(user, 1, 5);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Validate correct chapters have read status
|
||||
@ -461,25 +437,15 @@ public class CleanupServiceTests : AbstractDbTest
|
||||
{
|
||||
var c = new CollectionTag()
|
||||
{
|
||||
Title = "Test Tag"
|
||||
};
|
||||
var s = new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>(),
|
||||
Metadata = new SeriesMetadata()
|
||||
{
|
||||
CollectionTags = new List<CollectionTag>()
|
||||
{
|
||||
c
|
||||
}
|
||||
}
|
||||
Title = "Test Tag",
|
||||
NormalizedTitle = "Test Tag".ToNormalized(),
|
||||
};
|
||||
var s = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithMetadata(new SeriesMetadataBuilder().WithCollectionTag(c).Build())
|
||||
.Build();
|
||||
s.Library = new LibraryBuilder("Test LIb").Build();
|
||||
|
||||
_context.Series.Add(s);
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
@ -511,20 +477,11 @@ public class CleanupServiceTests : AbstractDbTest
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
var s = new Series()
|
||||
{
|
||||
Name = "Test CleanupWantToRead_ShouldRemoveFullyReadSeries",
|
||||
Library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>(),
|
||||
Metadata = new SeriesMetadata()
|
||||
{
|
||||
PublicationStatus = PublicationStatus.Completed
|
||||
}
|
||||
};
|
||||
var s = new SeriesBuilder("Test CleanupWantToRead_ShouldRemoveFullyReadSeries")
|
||||
.WithMetadata(new SeriesMetadataBuilder().WithPublicationStatus(PublicationStatus.Completed).Build())
|
||||
.Build();
|
||||
|
||||
s.Library = new LibraryBuilder("Test LIb").Build();
|
||||
_context.Series.Add(s);
|
||||
|
||||
var user = new AppUser()
|
||||
@ -539,10 +496,7 @@ public class CleanupServiceTests : AbstractDbTest
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||
Substitute.For<IEventHub>());
|
||||
|
||||
await readerService.MarkSeriesAsRead(user, s.Id);
|
||||
await _readerService.MarkSeriesAsRead(user, s.Id);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
var cleanupService = new CleanupService(Substitute.For<ILogger<CleanupService>>(), _unitOfWork,
|
||||
|
@ -2,14 +2,14 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Metadata;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
@ -34,19 +34,14 @@ public class CollectionTagServiceTests : AbstractDbTest
|
||||
private async Task SeedSeries()
|
||||
{
|
||||
if (_context.CollectionTag.Any()) return;
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Library 2",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>()
|
||||
{
|
||||
EntityFactory.CreateSeries("Series 1"),
|
||||
EntityFactory.CreateSeries("Series 2"),
|
||||
}
|
||||
});
|
||||
|
||||
_context.CollectionTag.Add(DbFactory.CollectionTag(0, "Tag 1", string.Empty, false));
|
||||
_context.CollectionTag.Add(DbFactory.CollectionTag(0, "Tag 2", string.Empty, true));
|
||||
_context.Library.Add(new LibraryBuilder("Library 2", LibraryType.Manga)
|
||||
.WithSeries(new SeriesBuilder("Series 1").Build())
|
||||
.WithSeries(new SeriesBuilder("Series 2").Build())
|
||||
.Build());
|
||||
|
||||
_context.CollectionTag.Add(new CollectionTagBuilder("Tag 1").Build());
|
||||
_context.CollectionTag.Add(new CollectionTagBuilder("Tag 2").WithIsPromoted(true).Build());
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
@ -64,8 +59,8 @@ public class CollectionTagServiceTests : AbstractDbTest
|
||||
public async Task UpdateTag_ShouldUpdateFields()
|
||||
{
|
||||
await SeedSeries();
|
||||
_context.CollectionTag.Add(EntityFactory.CreateCollectionTag(3, "UpdateTag_ShouldUpdateFields",
|
||||
string.Empty, true));
|
||||
|
||||
_context.CollectionTag.Add(new CollectionTagBuilder("UpdateTag_ShouldUpdateFields").WithId(3).WithIsPromoted(true).Build());
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
await _service.UpdateTag(new CollectionTagDto()
|
||||
@ -87,7 +82,7 @@ public class CollectionTagServiceTests : AbstractDbTest
|
||||
{
|
||||
await SeedSeries();
|
||||
var ids = new[] {1, 2};
|
||||
await _service.AddTagToSeries(await _unitOfWork.CollectionTagRepository.GetFullTagAsync(1), ids);
|
||||
await _service.AddTagToSeries(await _unitOfWork.CollectionTagRepository.GetTagAsync(1, CollectionTagIncludes.SeriesMetadata), ids);
|
||||
|
||||
var metadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(ids);
|
||||
Assert.True(metadatas.ElementAt(0).CollectionTags.Any(t => t.Title.Equals("Tag 1")));
|
||||
@ -99,7 +94,7 @@ public class CollectionTagServiceTests : AbstractDbTest
|
||||
{
|
||||
await SeedSeries();
|
||||
var ids = new[] {1, 2};
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(2);
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(2, CollectionTagIncludes.SeriesMetadata);
|
||||
await _service.AddTagToSeries(tag, ids);
|
||||
|
||||
await _service.RemoveTagFromSeries(tag, new[] {1});
|
||||
|
@ -5,7 +5,6 @@ using API.DTOs.Device;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums.Device;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
@ -75,7 +75,7 @@ public class DirectoryServiceTests
|
||||
[Fact]
|
||||
public void TraverseTreeParallelForEach_DontCountExcludedDirectories_ShouldBe28()
|
||||
{
|
||||
var testDirectory = "/manga/";
|
||||
const string testDirectory = "/manga/";
|
||||
var fileSystem = new MockFileSystem();
|
||||
for (var i = 0; i < 28; i++)
|
||||
{
|
||||
@ -85,6 +85,7 @@ public class DirectoryServiceTests
|
||||
fileSystem.AddFile($"{Path.Join(testDirectory, "@eaDir")}file_{29}.jpg", new MockFileData(""));
|
||||
fileSystem.AddFile($"{Path.Join(testDirectory, ".DS_Store")}file_{30}.jpg", new MockFileData(""));
|
||||
fileSystem.AddFile($"{Path.Join(testDirectory, ".qpkg")}file_{30}.jpg", new MockFileData(""));
|
||||
fileSystem.AddFile($"{Path.Join(testDirectory, ".@_thumb")}file_{30}.jpg", new MockFileData(""));
|
||||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
var files = new List<string>();
|
||||
@ -151,7 +152,7 @@ public class DirectoryServiceTests
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
var files = ds.GetFiles(testDirectory, API.Services.Tasks.Scanner.Parser.Parser.ArchiveFileExtensions).ToList();
|
||||
|
||||
Assert.Equal(10, files.Count());
|
||||
Assert.Equal(10, files.Count);
|
||||
Assert.All(files, s => fileSystem.Path.GetExtension(s).Equals(".zip"));
|
||||
}
|
||||
|
||||
@ -170,7 +171,7 @@ public class DirectoryServiceTests
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
var files = ds.GetFiles(testDirectory).ToList();
|
||||
|
||||
Assert.Equal(11, files.Count());
|
||||
Assert.Equal(11, files.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -188,7 +189,7 @@ public class DirectoryServiceTests
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
var files = ds.GetFiles(testDirectory).ToList();
|
||||
|
||||
Assert.Equal(11, files.Count());
|
||||
Assert.Equal(11, files.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -206,7 +207,7 @@ public class DirectoryServiceTests
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
var files = ds.GetFiles(testDirectory).ToList();
|
||||
|
||||
Assert.Equal(10, files.Count());
|
||||
Assert.Equal(10, files.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -224,7 +225,7 @@ public class DirectoryServiceTests
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
var files = ds.GetFiles(testDirectory).ToList();
|
||||
|
||||
Assert.Equal(10, files.Count());
|
||||
Assert.Equal(10, files.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -242,7 +243,7 @@ public class DirectoryServiceTests
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
var files = ds.GetFiles(testDirectory).ToList();
|
||||
|
||||
Assert.Equal(10, files.Count());
|
||||
Assert.Equal(10, files.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -324,7 +325,7 @@ public class DirectoryServiceTests
|
||||
ds.CopyFileToDirectory($"{testDirectory}file/data-0.txt", "/manga/output/");
|
||||
Assert.True(fileSystem.FileExists("/manga/output/data-0.txt"));
|
||||
Assert.True(fileSystem.FileExists("/manga/file/data-0.txt"));
|
||||
Assert.True(fileSystem.FileInfo.FromFileName("/manga/file/data-0.txt").Length == fileSystem.FileInfo.FromFileName("/manga/output/data-0.txt").Length);
|
||||
Assert.True(fileSystem.FileInfo.New("/manga/file/data-0.txt").Length == fileSystem.FileInfo.New("/manga/output/data-0.txt").Length);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@ -339,7 +340,7 @@ public class DirectoryServiceTests
|
||||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
var ex = Assert.Throws<DirectoryNotFoundException>(() => ds.CopyDirectoryToDirectory("/comics/", "/manga/output/"));
|
||||
Assert.Equal(ex.Message, "Source directory does not exist or could not be found: " + "/comics/");
|
||||
Assert.Equal("Source directory does not exist or could not be found: " + "/comics/", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -352,7 +353,7 @@ public class DirectoryServiceTests
|
||||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
ds.CopyDirectoryToDirectory($"{testDirectory}empty/", "/manga/output/");
|
||||
Assert.Empty(fileSystem.DirectoryInfo.FromDirectoryName("/manga/output/").GetFiles());
|
||||
Assert.Empty(fileSystem.DirectoryInfo.New("/manga/output/").GetFiles());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -426,7 +427,7 @@ public class DirectoryServiceTests
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
ds.ExistOrCreate("c:/manga/output/");
|
||||
|
||||
Assert.True(ds.FileSystem.DirectoryInfo.FromDirectoryName("c:/manga/output/").Exists);
|
||||
Assert.True(ds.FileSystem.DirectoryInfo.New("c:/manga/output/").Exists);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@ -447,9 +448,9 @@ public class DirectoryServiceTests
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
ds.ClearAndDeleteDirectory($"{testDirectory}");
|
||||
Assert.Empty(ds.GetFiles("/manga/", searchOption: SearchOption.AllDirectories));
|
||||
Assert.Empty(ds.FileSystem.DirectoryInfo.FromDirectoryName("/manga/").GetDirectories());
|
||||
Assert.True(ds.FileSystem.DirectoryInfo.FromDirectoryName("/manga/").Exists);
|
||||
Assert.False(ds.FileSystem.DirectoryInfo.FromDirectoryName("/manga/base").Exists);
|
||||
Assert.Empty(ds.FileSystem.DirectoryInfo.New("/manga/").GetDirectories());
|
||||
Assert.True(ds.FileSystem.DirectoryInfo.New("/manga/").Exists);
|
||||
Assert.False(ds.FileSystem.DirectoryInfo.New("/manga/base").Exists);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@ -469,9 +470,9 @@ public class DirectoryServiceTests
|
||||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
ds.ClearDirectory($"{testDirectory}file/");
|
||||
Assert.Empty(ds.FileSystem.DirectoryInfo.FromDirectoryName($"{testDirectory}file/").GetDirectories());
|
||||
Assert.True(ds.FileSystem.DirectoryInfo.FromDirectoryName("/manga/").Exists);
|
||||
Assert.True(ds.FileSystem.DirectoryInfo.FromDirectoryName($"{testDirectory}file/").Exists);
|
||||
Assert.Empty(ds.FileSystem.DirectoryInfo.New($"{testDirectory}file/").GetDirectories());
|
||||
Assert.True(ds.FileSystem.DirectoryInfo.New("/manga/").Exists);
|
||||
Assert.True(ds.FileSystem.DirectoryInfo.New($"{testDirectory}file/").Exists);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -486,9 +487,9 @@ public class DirectoryServiceTests
|
||||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
ds.ClearDirectory($"{testDirectory}");
|
||||
Assert.Empty(ds.FileSystem.DirectoryInfo.FromDirectoryName($"{testDirectory}").GetDirectories());
|
||||
Assert.True(ds.FileSystem.DirectoryInfo.FromDirectoryName(testDirectory).Exists);
|
||||
Assert.False(ds.FileSystem.DirectoryInfo.FromDirectoryName($"{testDirectory}file/").Exists);
|
||||
Assert.Empty(ds.FileSystem.DirectoryInfo.New($"{testDirectory}").GetDirectories());
|
||||
Assert.True(ds.FileSystem.DirectoryInfo.New(testDirectory).Exists);
|
||||
Assert.False(ds.FileSystem.DirectoryInfo.New($"{testDirectory}file/").Exists);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@ -586,7 +587,7 @@ public class DirectoryServiceTests
|
||||
ds.CopyFilesToDirectory(new []{MockUnixSupport.Path($"{testDirectory}file.zip")}, "/manga/output/");
|
||||
ds.CopyFilesToDirectory(new []{MockUnixSupport.Path($"{testDirectory}file.zip")}, "/manga/output/");
|
||||
var outputFiles = ds.GetFiles("/manga/output/").Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList();
|
||||
Assert.Equal(4, outputFiles.Count()); // we have 2 already there and 2 copies
|
||||
Assert.Equal(4, outputFiles.Count); // we have 2 already there and 2 copies
|
||||
// For some reason, this has C:/ on directory even though everything is emulated (System.IO.Abstractions issue, not changing)
|
||||
// https://github.com/TestableIO/System.IO.Abstractions/issues/831
|
||||
Assert.True(outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("/manga/output/file (3).zip"))
|
||||
@ -644,10 +645,10 @@ public class DirectoryServiceTests
|
||||
const string testDirectory = "/manga/";
|
||||
var fileSystem = new MockFileSystem();
|
||||
fileSystem.AddDirectory($"{testDirectory}dir1");
|
||||
var di = fileSystem.DirectoryInfo.FromDirectoryName($"{testDirectory}dir1");
|
||||
var di = fileSystem.DirectoryInfo.New($"{testDirectory}dir1");
|
||||
di.Attributes |= FileAttributes.System;
|
||||
fileSystem.AddDirectory($"{testDirectory}dir2");
|
||||
di = fileSystem.DirectoryInfo.FromDirectoryName($"{testDirectory}dir2");
|
||||
di = fileSystem.DirectoryInfo.New($"{testDirectory}dir2");
|
||||
di.Attributes |= FileAttributes.Hidden;
|
||||
fileSystem.AddDirectory($"{testDirectory}dir3");
|
||||
fileSystem.AddFile($"{testDirectory}file_0.zip", new MockFileData(""));
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
@ -9,15 +8,13 @@ using API.Data;
|
||||
using API.Data.Metadata;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Parser;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using AutoMapper;
|
||||
using DotNet.Globbing;
|
||||
using Flurl.Util;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@ -121,17 +118,9 @@ public class ParseScannedFilesTests
|
||||
|
||||
_context.ServerSetting.Update(setting);
|
||||
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Manga",
|
||||
Folders = new List<FolderPath>()
|
||||
{
|
||||
new FolderPath()
|
||||
{
|
||||
Path = DataDirectory
|
||||
}
|
||||
}
|
||||
});
|
||||
_context.Library.Add(new LibraryBuilder("Manga")
|
||||
.WithFolderPath(new FolderPathBuilder(DataDirectory).Build())
|
||||
.Build());
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
@ -254,7 +243,7 @@ public class ParseScannedFilesTests
|
||||
var foundParsedSeries = new ParsedSeries()
|
||||
{
|
||||
Name = parsedFiles.First().Series,
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize(parsedFiles.First().Series),
|
||||
NormalizedName = parsedFiles.First().Series.ToNormalized(),
|
||||
Format = parsedFiles.First().Format
|
||||
};
|
||||
|
||||
|
72
API.Tests/Services/ProcessSeriesTests.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System.IO;
|
||||
using API.Data;
|
||||
using API.Data.Metadata;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Metadata;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using API.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Services;
|
||||
|
||||
public class ProcessSeriesTests
|
||||
{
|
||||
|
||||
|
||||
#region UpdateSeriesMetadata
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateVolumes
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateChapters
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region AddOrUpdateFileForChapter
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateChapterFromComicInfo
|
||||
|
||||
// public void UpdateChapterFromComicInfo_()
|
||||
// {
|
||||
// // TODO: Do this
|
||||
// var file = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 1.cbz");
|
||||
// // Chapter and ComicInfo
|
||||
// var chapter = new ChapterBuilder("1")
|
||||
// .WithId(0)
|
||||
// .WithFile(new MangaFileBuilder(file, MangaFormat.Archive).Build())
|
||||
// .Build();
|
||||
//
|
||||
// var ps = new ProcessSeries(Substitute.For<IUnitOfWork>(), Substitute.For<ILogger<ProcessSeries>>(),
|
||||
// Substitute.For<IEventHub>(), Substitute.For<IDirectoryService>()
|
||||
// , Substitute.For<ICacheHelper>(), Substitute.For<IReadingItemService>(), Substitute.For<IFileService>(),
|
||||
// Substitute.For<IMetadataService>(),
|
||||
// Substitute.For<IWordCountAnalyzerService>(),
|
||||
// Substitute.For<ICollectionTagService>(), Substitute.For<IReadingListService>());
|
||||
//
|
||||
// ps.UpdateChapterFromComicInfo(chapter, new ComicInfo()
|
||||
// {
|
||||
//
|
||||
// });
|
||||
// }
|
||||
|
||||
#endregion
|
||||
}
|
@ -3,9 +3,11 @@ using System.Linq;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Parser;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services.Tasks;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using API.Tests.Helpers;
|
||||
using Xunit;
|
||||
|
||||
@ -23,23 +25,14 @@ public class ScannerServiceTests
|
||||
|
||||
var existingSeries = new List<Series>
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
Name = "Darker Than Black",
|
||||
LocalizedName = "Darker Than Black",
|
||||
OriginalName = "Darker Than Black",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Volume()
|
||||
{
|
||||
Number = 1,
|
||||
Name = "1"
|
||||
}
|
||||
},
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker Than Black"),
|
||||
Metadata = new SeriesMetadata(),
|
||||
Format = MangaFormat.Epub
|
||||
}
|
||||
new SeriesBuilder("Darker Than Black")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithName("1")
|
||||
.Build())
|
||||
.WithLocalizedName("Darker Than Black")
|
||||
.Build()
|
||||
};
|
||||
|
||||
Assert.Equal(1, ScannerService.FindSeriesNotOnDisk(existingSeries, infos).Count());
|
||||
@ -56,76 +49,23 @@ public class ScannerServiceTests
|
||||
|
||||
var existingSeries = new List<Series>
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
Name = "Cage of Eden",
|
||||
LocalizedName = "Cage of Eden",
|
||||
OriginalName = "Cage of Eden",
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Cage of Eden"),
|
||||
Metadata = new SeriesMetadata(),
|
||||
Format = MangaFormat.Archive
|
||||
},
|
||||
new Series()
|
||||
{
|
||||
Name = "Darker Than Black",
|
||||
LocalizedName = "Darker Than Black",
|
||||
OriginalName = "Darker Than Black",
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker Than Black"),
|
||||
Metadata = new SeriesMetadata(),
|
||||
Format = MangaFormat.Archive
|
||||
}
|
||||
new SeriesBuilder("Cage of Eden")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithName("1")
|
||||
.Build())
|
||||
.WithLocalizedName("Darker Than Black")
|
||||
.Build(),
|
||||
new SeriesBuilder("Darker Than Black")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithName("1")
|
||||
.Build())
|
||||
.WithLocalizedName("Darker Than Black")
|
||||
.Build(),
|
||||
};
|
||||
|
||||
|
||||
|
||||
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")]
|
||||
// [InlineData(new [] {"Darker than Black"}, "Darker Than Black", "Darker than Black")]
|
||||
// [InlineData(new [] {"Darker than Black"}, "Darker Than Black!", "Darker than Black")]
|
||||
// [InlineData(new [] {""}, "Runaway Jack", "Runaway Jack")]
|
||||
// public void MergeNameTest(string[] existingSeriesNames, string parsedInfoName, string expected)
|
||||
// {
|
||||
// var collectedSeries = new ConcurrentDictionary<ParsedSeries, List<ParserInfo>>();
|
||||
// foreach (var seriesName in existingSeriesNames)
|
||||
// {
|
||||
// AddToParsedInfo(collectedSeries, new ParserInfo() {Series = seriesName, Format = MangaFormat.Archive});
|
||||
// }
|
||||
//
|
||||
// var actualName = new ParseScannedFiles(_bookService, _logger).MergeName(collectedSeries, new ParserInfo()
|
||||
// {
|
||||
// Series = parsedInfoName,
|
||||
// Format = MangaFormat.Archive
|
||||
// });
|
||||
//
|
||||
// Assert.Equal(expected, actualName);
|
||||
// }
|
||||
|
||||
// [Fact]
|
||||
// public void RemoveMissingSeries_Should_RemoveSeries()
|
||||
// {
|
||||
// var existingSeries = new List<Series>()
|
||||
// {
|
||||
// EntityFactory.CreateSeries("Darker than Black Vol 1"),
|
||||
// EntityFactory.CreateSeries("Darker than Black"),
|
||||
// EntityFactory.CreateSeries("Beastars"),
|
||||
// };
|
||||
// var missingSeries = new List<Series>()
|
||||
// {
|
||||
// EntityFactory.CreateSeries("Darker than Black Vol 1"),
|
||||
// };
|
||||
// existingSeries = ScannerService.RemoveMissingSeries(existingSeries, missingSeries, out var removeCount).ToList();
|
||||
//
|
||||
// Assert.DoesNotContain(missingSeries[0].Name, existingSeries.Select(s => s.Name));
|
||||
// Assert.Equal(missingSeries.Count, removeCount);
|
||||
// }
|
||||
|
||||
|
||||
// TODO: I want a test for UpdateSeries where if I have chapter 10 and now it's mapping into Vol 2 Chapter 10,
|
||||
// if I can do it without deleting the underlying chapter (aka id change)
|
||||
|
||||
}
|
||||
|
@ -1,138 +1,71 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Enums.Theme;
|
||||
using API.Entities.Enums.UserPreferences;
|
||||
using API.Helpers;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace API.Tests.Services;
|
||||
|
||||
public class SiteThemeServiceTests
|
||||
|
||||
public abstract class SiteThemeServiceTest : AbstractDbTest
|
||||
{
|
||||
private readonly ILogger<ThemeService> _logger = Substitute.For<ILogger<ThemeService>>();
|
||||
private readonly ITestOutputHelper _testOutputHelper;
|
||||
private readonly IEventHub _messageHub = Substitute.For<IEventHub>();
|
||||
|
||||
private readonly DbConnection _connection;
|
||||
private readonly DataContext _context;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
private const string CacheDirectory = "C:/kavita/config/cache/";
|
||||
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
||||
private const string BackupDirectory = "C:/kavita/config/backups/";
|
||||
private const string BookmarkDirectory = "C:/kavita/config/bookmarks/";
|
||||
private const string SiteThemeDirectory = "C:/kavita/config/themes/";
|
||||
|
||||
public SiteThemeServiceTests()
|
||||
protected SiteThemeServiceTest(ITestOutputHelper testOutputHelper) : base()
|
||||
{
|
||||
var contextOptions = new DbContextOptionsBuilder()
|
||||
.UseSqlite(CreateInMemoryDatabase())
|
||||
.Options;
|
||||
_connection = RelationalOptionsExtension.Extract(contextOptions).Connection;
|
||||
|
||||
_context = new DataContext(contextOptions);
|
||||
Task.Run(SeedDb).GetAwaiter().GetResult();
|
||||
|
||||
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
|
||||
var mapper = config.CreateMapper();
|
||||
_unitOfWork = new UnitOfWork(_context, mapper, null);
|
||||
_testOutputHelper = testOutputHelper;
|
||||
}
|
||||
|
||||
#region Setup
|
||||
|
||||
private static DbConnection CreateInMemoryDatabase()
|
||||
{
|
||||
var connection = new SqliteConnection("Filename=:memory:");
|
||||
|
||||
connection.Open();
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private async Task<bool> SeedDb()
|
||||
{
|
||||
await _context.Database.MigrateAsync();
|
||||
var filesystem = CreateFileSystem();
|
||||
|
||||
await Seed.SeedSettings(_context, new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem));
|
||||
|
||||
var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync();
|
||||
setting.Value = CacheDirectory;
|
||||
|
||||
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync();
|
||||
setting.Value = BackupDirectory;
|
||||
|
||||
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync();
|
||||
setting.Value = BookmarkDirectory;
|
||||
|
||||
_context.ServerSetting.Update(setting);
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
UserName = "Joe",
|
||||
UserPreferences = new AppUserPreferences
|
||||
{
|
||||
Theme = Seed.DefaultThemes[0]
|
||||
}
|
||||
});
|
||||
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Manga",
|
||||
Folders = new List<FolderPath>()
|
||||
{
|
||||
new FolderPath()
|
||||
{
|
||||
Path = "C:/data/"
|
||||
}
|
||||
}
|
||||
});
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
private static MockFileSystem CreateFileSystem()
|
||||
{
|
||||
var fileSystem = new MockFileSystem();
|
||||
fileSystem.Directory.SetCurrentDirectory("C:/kavita/");
|
||||
fileSystem.AddDirectory("C:/kavita/config/");
|
||||
fileSystem.AddDirectory(CacheDirectory);
|
||||
fileSystem.AddDirectory(CoverImageDirectory);
|
||||
fileSystem.AddDirectory(BackupDirectory);
|
||||
fileSystem.AddDirectory(BookmarkDirectory);
|
||||
fileSystem.AddDirectory(SiteThemeDirectory);
|
||||
fileSystem.AddDirectory("C:/data/");
|
||||
|
||||
return fileSystem;
|
||||
}
|
||||
|
||||
private async Task ResetDb()
|
||||
protected override async Task ResetDb()
|
||||
{
|
||||
_context.SiteTheme.RemoveRange(_context.SiteTheme);
|
||||
await _context.SaveChangesAsync();
|
||||
// Recreate defaults
|
||||
await Seed.SeedThemes(_context);
|
||||
}
|
||||
|
||||
#endregion
|
||||
[Fact]
|
||||
public async Task UpdateDefault_ShouldThrowOnInvalidId()
|
||||
{
|
||||
await ResetDb();
|
||||
_testOutputHelper.WriteLine($"[UpdateDefault_ShouldThrowOnInvalidId] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}");
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||
|
||||
_context.SiteTheme.Add(new SiteTheme()
|
||||
{
|
||||
Name = "Custom",
|
||||
NormalizedName = "Custom".ToNormalized(),
|
||||
Provider = ThemeProvider.User,
|
||||
FileName = "custom.css",
|
||||
IsDefault = false
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<KavitaException>(async () => await siteThemeService.UpdateDefault(10));
|
||||
Assert.Equal("Theme file missing or invalid", ex.Message);
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Scan_ShouldFindCustomFile()
|
||||
{
|
||||
await ResetDb();
|
||||
_testOutputHelper.WriteLine($"[Scan_ShouldOnlyInsertOnceOnSecondScan] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}");
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
@ -146,6 +79,8 @@ public class SiteThemeServiceTests
|
||||
public async Task Scan_ShouldOnlyInsertOnceOnSecondScan()
|
||||
{
|
||||
await ResetDb();
|
||||
_testOutputHelper.WriteLine(
|
||||
$"[Scan_ShouldOnlyInsertOnceOnSecondScan] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}");
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
@ -157,7 +92,8 @@ public class SiteThemeServiceTests
|
||||
await siteThemeService.Scan();
|
||||
|
||||
var customThemes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos()).Where(t =>
|
||||
API.Services.Tasks.Scanner.Parser.Parser.Normalize(t.Name).Equals(API.Services.Tasks.Scanner.Parser.Parser.Normalize("custom")));
|
||||
t.Name.ToNormalized().Equals("custom".ToNormalized()));
|
||||
|
||||
Assert.Single(customThemes);
|
||||
}
|
||||
|
||||
@ -165,6 +101,7 @@ public class SiteThemeServiceTests
|
||||
public async Task Scan_ShouldDeleteWhenFileDoesntExistOnSecondScan()
|
||||
{
|
||||
await ResetDb();
|
||||
_testOutputHelper.WriteLine($"[Scan_ShouldDeleteWhenFileDoesntExistOnSecondScan] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}");
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
@ -176,16 +113,17 @@ public class SiteThemeServiceTests
|
||||
filesystem.RemoveFile($"{SiteThemeDirectory}custom.css");
|
||||
await siteThemeService.Scan();
|
||||
|
||||
var customThemes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos()).Where(t =>
|
||||
API.Services.Tasks.Scanner.Parser.Parser.Normalize(t.Name).Equals(API.Services.Tasks.Scanner.Parser.Parser.Normalize("custom")));
|
||||
var themes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos());
|
||||
|
||||
Assert.Empty(customThemes);
|
||||
Assert.Equal(0, themes.Count(t =>
|
||||
t.Name.ToNormalized().Equals("custom".ToNormalized())));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetContent_ShouldReturnContent()
|
||||
{
|
||||
await ResetDb();
|
||||
_testOutputHelper.WriteLine($"[GetContent_ShouldReturnContent] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}");
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
@ -194,7 +132,7 @@ public class SiteThemeServiceTests
|
||||
_context.SiteTheme.Add(new SiteTheme()
|
||||
{
|
||||
Name = "Custom",
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Custom"),
|
||||
NormalizedName = "Custom".ToNormalized(),
|
||||
Provider = ThemeProvider.User,
|
||||
FileName = "custom.css",
|
||||
IsDefault = false
|
||||
@ -211,6 +149,7 @@ public class SiteThemeServiceTests
|
||||
public async Task UpdateDefault_ShouldHaveOneDefault()
|
||||
{
|
||||
await ResetDb();
|
||||
_testOutputHelper.WriteLine($"[UpdateDefault_ShouldHaveOneDefault] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}");
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
@ -219,7 +158,7 @@ public class SiteThemeServiceTests
|
||||
_context.SiteTheme.Add(new SiteTheme()
|
||||
{
|
||||
Name = "Custom",
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Custom"),
|
||||
NormalizedName = "Custom".ToNormalized(),
|
||||
Provider = ThemeProvider.User,
|
||||
FileName = "custom.css",
|
||||
IsDefault = false
|
||||
@ -228,6 +167,7 @@ public class SiteThemeServiceTests
|
||||
|
||||
var customTheme = (await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("Custom"));
|
||||
|
||||
Assert.NotNull(customTheme);
|
||||
await siteThemeService.UpdateDefault(customTheme.Id);
|
||||
|
||||
|
||||
@ -235,31 +175,5 @@ public class SiteThemeServiceTests
|
||||
Assert.Equal(customTheme.Id, (await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateDefault_ShouldThrowOnInvalidId()
|
||||
{
|
||||
await ResetDb();
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||
|
||||
_context.SiteTheme.Add(new SiteTheme()
|
||||
{
|
||||
Name = "Custom",
|
||||
NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Custom"),
|
||||
Provider = ThemeProvider.User,
|
||||
FileName = "custom.css",
|
||||
IsDefault = false
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
|
||||
var ex = await Assert.ThrowsAsync<KavitaException>(async () => await siteThemeService.UpdateDefault(10));
|
||||
Assert.Equal("Theme file missing or invalid", ex.Message);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
namespace API.Tests.Services;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services.Tasks;
|
||||
|
||||
namespace API.Tests.Services;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
@ -24,6 +28,8 @@ public class TachiyomiServiceTests
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly DataContext _context;
|
||||
private readonly ReaderService _readerService;
|
||||
private readonly TachiyomiService _tachiyomiService;
|
||||
private const string CacheDirectory = "C:/kavita/config/cache/";
|
||||
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
||||
private const string BackupDirectory = "C:/kavita/config/backups/";
|
||||
@ -41,6 +47,10 @@ public class TachiyomiServiceTests
|
||||
_mapper = config.CreateMapper();
|
||||
_unitOfWork = new UnitOfWork(_context, _mapper, null);
|
||||
|
||||
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
_tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), _readerService);
|
||||
|
||||
}
|
||||
|
||||
@ -72,10 +82,11 @@ public class TachiyomiServiceTests
|
||||
|
||||
_context.ServerSetting.Update(setting);
|
||||
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Manga", Folders = new List<FolderPath>() {new FolderPath() {Path = "C:/data/"}}
|
||||
});
|
||||
_context.Library.Add(
|
||||
new LibraryBuilder("Manga")
|
||||
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
|
||||
.Build()
|
||||
);
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
@ -111,39 +122,29 @@ public class TachiyomiServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
var series = new Series
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("95", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("96", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", true, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("3", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("4", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
},
|
||||
Pages = 7
|
||||
};
|
||||
var library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>() { series }
|
||||
};
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("95").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("3").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("4").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithChapter(new ChapterBuilder("31").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("32").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithPages(7)
|
||||
.Build();
|
||||
|
||||
var library = new LibraryBuilder("Test LIb", LibraryType.Manga)
|
||||
.WithSeries(series)
|
||||
.Build();
|
||||
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -156,10 +157,7 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
|
||||
Assert.Null(latestChapter);
|
||||
}
|
||||
@ -169,39 +167,28 @@ public class TachiyomiServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
var series = new Series
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("95", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("96", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", true, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("3", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("4", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
},
|
||||
Pages = 7
|
||||
};
|
||||
var library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>() { series }
|
||||
};
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("95").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("3").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("4").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithChapter(new ChapterBuilder("31").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("32").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithPages(7)
|
||||
.Build();
|
||||
|
||||
var library = new LibraryBuilder("Test LIb", LibraryType.Manga)
|
||||
.WithSeries(series)
|
||||
.Build();
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -214,16 +201,14 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkSeriesAsRead(user,1);
|
||||
await _readerService.MarkSeriesAsRead(user,1);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
|
||||
Assert.Equal("96", latestChapter.Number);
|
||||
}
|
||||
@ -233,39 +218,28 @@ public class TachiyomiServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
var series = new Series
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("95", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("96", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("21", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("23", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
},
|
||||
Pages = 7
|
||||
};
|
||||
var library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>() { series }
|
||||
};
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("95").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("21").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("22").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithChapter(new ChapterBuilder("31").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("32").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithPages(7)
|
||||
.Build();
|
||||
|
||||
var library = new LibraryBuilder("Test LIb", LibraryType.Manga)
|
||||
.WithSeries(series)
|
||||
.Build();
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -278,16 +252,14 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await tachiyomiService.MarkChaptersUntilAsRead(user,1,21);
|
||||
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,21);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
|
||||
Assert.Equal("21", latestChapter.Number);
|
||||
}
|
||||
@ -296,39 +268,28 @@ public class TachiyomiServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
var series = new Series
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("95", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("96", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", true, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("21", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("23", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
},
|
||||
Pages = 7
|
||||
};
|
||||
var library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>() { series }
|
||||
};
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("95").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("21").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("22").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithChapter(new ChapterBuilder("31").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("32").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithPages(7)
|
||||
.Build();
|
||||
|
||||
var library = new LibraryBuilder("Test LIb", LibraryType.Manga)
|
||||
.WithSeries(series)
|
||||
.Build();
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -341,17 +302,15 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
|
||||
await tachiyomiService.MarkChaptersUntilAsRead(user,1,1/10_000F);
|
||||
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,1/10_000F);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
Assert.Equal("0.0001", latestChapter.Number);
|
||||
}
|
||||
|
||||
@ -360,32 +319,22 @@ public class TachiyomiServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
var series = new Series
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>(), 199),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>(), 192),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>(), 255),
|
||||
}),
|
||||
},
|
||||
Pages = 646
|
||||
};
|
||||
var library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>() { series }
|
||||
};
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("0").WithPages(199).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("0").WithPages(192).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithChapter(new ChapterBuilder("0").WithPages(255).Build())
|
||||
.Build())
|
||||
.WithPages(646)
|
||||
.Build();
|
||||
|
||||
var library = new LibraryBuilder("Test LIb", LibraryType.Manga)
|
||||
.WithSeries(series)
|
||||
.Build();
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -398,17 +347,15 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
|
||||
await readerService.MarkSeriesAsRead(user, 1);
|
||||
await _readerService.MarkSeriesAsRead(user, 1);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
Assert.Equal("0.0003", latestChapter.Number);
|
||||
}
|
||||
|
||||
@ -418,37 +365,26 @@ public class TachiyomiServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
var series = new Series
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("95", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("96", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1997", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2002", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("2", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2005", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("3", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
},
|
||||
Pages = 7
|
||||
};
|
||||
var library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Comic,
|
||||
Series = new List<Series>() { series }
|
||||
};
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("95").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1997")
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2002")
|
||||
.WithChapter(new ChapterBuilder("2").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2005")
|
||||
.WithChapter(new ChapterBuilder("3").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithPages(7)
|
||||
.Build();
|
||||
|
||||
var library = new LibraryBuilder("Test LIb", LibraryType.Manga)
|
||||
.WithSeries(series)
|
||||
.Build();
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -461,17 +397,14 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
|
||||
await tachiyomiService.MarkChaptersUntilAsRead(user,1,2002/10_000F);
|
||||
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,2002/10_000F);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
Assert.Equal("0.2002", latestChapter.Number);
|
||||
}
|
||||
|
||||
@ -485,39 +418,28 @@ public class TachiyomiServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
var series = new Series
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("95", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("96", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", true, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("3", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("4", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
},
|
||||
Pages = 7
|
||||
};
|
||||
var library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>() { series }
|
||||
};
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("95").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("3").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("4").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithChapter(new ChapterBuilder("31").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("32").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithPages(7)
|
||||
.Build();
|
||||
|
||||
var library = new LibraryBuilder("Test LIb", LibraryType.Manga)
|
||||
.WithSeries(series)
|
||||
.Build();
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -530,10 +452,7 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
|
||||
Assert.Null(latestChapter);
|
||||
}
|
||||
@ -542,39 +461,28 @@ public class TachiyomiServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
var series = new Series
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("95", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("96", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", true, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("3", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("4", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
},
|
||||
Pages = 7
|
||||
};
|
||||
var library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>() { series }
|
||||
};
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("95").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("3").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("4").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithChapter(new ChapterBuilder("31").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("32").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithPages(7)
|
||||
.Build();
|
||||
|
||||
var library = new LibraryBuilder("Test LIb", LibraryType.Manga)
|
||||
.WithSeries(series)
|
||||
.Build();
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -587,16 +495,13 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkSeriesAsRead(user,1);
|
||||
await _readerService.MarkSeriesAsRead(user,1);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
|
||||
Assert.Equal("96", latestChapter.Number);
|
||||
}
|
||||
@ -606,39 +511,28 @@ public class TachiyomiServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
var series = new Series
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("95", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("96", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("21", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("23", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
},
|
||||
Pages = 7
|
||||
};
|
||||
var library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>() { series }
|
||||
};
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("95").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("21").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("23").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithChapter(new ChapterBuilder("31").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("32").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithPages(7)
|
||||
.Build();
|
||||
|
||||
var library = new LibraryBuilder("Test LIb", LibraryType.Manga)
|
||||
.WithSeries(series)
|
||||
.Build();
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -651,16 +545,13 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await tachiyomiService.MarkChaptersUntilAsRead(user,1,21);
|
||||
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,21);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
|
||||
Assert.Equal("21", latestChapter.Number);
|
||||
}
|
||||
@ -668,40 +559,28 @@ public class TachiyomiServiceTests
|
||||
public async Task MarkChaptersUntilAsRead_ShouldReturnEncodedVolume_Progress()
|
||||
{
|
||||
await ResetDb();
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(new ChapterBuilder("95").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("21").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("23").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithChapter(new ChapterBuilder("31").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("32").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithPages(7)
|
||||
.Build();
|
||||
|
||||
var series = new Series
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("95", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("96", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", true, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("21", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("23", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
},
|
||||
Pages = 7
|
||||
};
|
||||
var library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>() { series }
|
||||
};
|
||||
var library = new LibraryBuilder("Test LIb", LibraryType.Manga)
|
||||
.WithSeries(series)
|
||||
.Build();
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
@ -714,17 +593,14 @@ public class TachiyomiServiceTests
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var tachiyomiService = new TachiyomiService(_unitOfWork, _mapper, Substitute.For<ILogger<ReaderService>>(), readerService);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
|
||||
await tachiyomiService.MarkChaptersUntilAsRead(user,1,1/10_000F);
|
||||
await _tachiyomiService.MarkChaptersUntilAsRead(user,1,1/10_000F);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||
var latestChapter = await _tachiyomiService.GetLatestChapter(1, 1);
|
||||
Assert.Equal("0.0001", latestChapter.Number);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 143 KiB |
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 155 KiB |
After Width: | Height: | Size: 151 KiB |
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 155 KiB |
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 124 KiB |
@ -0,0 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<ComicInfo>
|
||||
<Series>Accel World</Series>
|
||||
<Number>2</Number>
|
||||
</ComicInfo>
|
@ -0,0 +1,6 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<ComicInfo>
|
||||
<Series>Hajime no Ippo</Series>
|
||||
<Number>3</Number>
|
||||
<AgeRating>M</AgeRating>
|
||||
</ComicInfo>
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1 @@
|
||||
This is an example of a layout. All files in here have non-copyrighted data but emulate real series to ensure the Process series Works as expected.
|
@ -1,13 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using API.Services.Tasks.Metadata;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
@ -28,7 +30,8 @@ public class WordCountAnalysisTests : AbstractDbTest
|
||||
public WordCountAnalysisTests() : base()
|
||||
{
|
||||
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||
Substitute.For<IEventHub>());
|
||||
Substitute.For<IEventHub>(), Substitute.For<IImageService>(),
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem()));
|
||||
}
|
||||
|
||||
protected override async Task ResetDb()
|
||||
@ -42,25 +45,26 @@ public class WordCountAnalysisTests : AbstractDbTest
|
||||
public async Task ReadingTimeShouldBeNonZero()
|
||||
{
|
||||
await ResetDb();
|
||||
var series = EntityFactory.CreateSeries("Test Series");
|
||||
series.Format = MangaFormat.Epub;
|
||||
var chapter = EntityFactory.CreateChapter("", false, new List<MangaFile>()
|
||||
{
|
||||
EntityFactory.CreateMangaFile(
|
||||
Path.Join(_testDirectory, "The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub"),
|
||||
MangaFormat.Epub, 0)
|
||||
});
|
||||
var series = new SeriesBuilder("Test Series")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.Build();
|
||||
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Test",
|
||||
Type = LibraryType.Book,
|
||||
Series = new List<Series>() {series}
|
||||
});
|
||||
var chapter = new ChapterBuilder("")
|
||||
.WithFile(new MangaFileBuilder(
|
||||
Path.Join(_testDirectory,
|
||||
"The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub"),
|
||||
MangaFormat.Epub).Build())
|
||||
.Build();
|
||||
|
||||
_context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book)
|
||||
.WithSeries(series)
|
||||
.Build());
|
||||
|
||||
series.Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>() {chapter})
|
||||
new VolumeBuilder("0")
|
||||
.WithChapter(chapter)
|
||||
.Build(),
|
||||
};
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
@ -97,26 +101,23 @@ public class WordCountAnalysisTests : AbstractDbTest
|
||||
public async Task ReadingTimeShouldIncreaseWhenNewBookAdded()
|
||||
{
|
||||
await ResetDb();
|
||||
var series = EntityFactory.CreateSeries("Test Series");
|
||||
series.Format = MangaFormat.Epub;
|
||||
var chapter = EntityFactory.CreateChapter("", false, new List<MangaFile>()
|
||||
{
|
||||
EntityFactory.CreateMangaFile(
|
||||
Path.Join(_testDirectory, "The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub"),
|
||||
MangaFormat.Epub, 0)
|
||||
});
|
||||
var chapter = new ChapterBuilder("")
|
||||
.WithFile(new MangaFileBuilder(
|
||||
Path.Join(_testDirectory,
|
||||
"The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub"),
|
||||
MangaFormat.Epub).Build())
|
||||
.Build();
|
||||
var series = new SeriesBuilder("Test Series")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder("0")
|
||||
.WithChapter(chapter)
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Test",
|
||||
Type = LibraryType.Book,
|
||||
Series = new List<Series>() {series}
|
||||
});
|
||||
_context.Library.Add(new LibraryBuilder("Test", LibraryType.Book)
|
||||
.WithSeries(series)
|
||||
.Build());
|
||||
|
||||
series.Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>() {chapter})
|
||||
};
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
@ -124,18 +125,19 @@ public class WordCountAnalysisTests : AbstractDbTest
|
||||
var cacheService = new CacheHelper(new FileService());
|
||||
var service = new WordCountAnalyzerService(Substitute.For<ILogger<WordCountAnalyzerService>>(), _unitOfWork,
|
||||
Substitute.For<IEventHub>(), cacheService, _readerService);
|
||||
|
||||
|
||||
await service.ScanSeries(1, 1);
|
||||
|
||||
var chapter2 = EntityFactory.CreateChapter("2", false, new List<MangaFile>()
|
||||
{
|
||||
EntityFactory.CreateMangaFile(
|
||||
Path.Join(_testDirectory, "The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub"),
|
||||
MangaFormat.Epub, 0)
|
||||
});
|
||||
var chapter2 = new ChapterBuilder("2")
|
||||
.WithFile(new MangaFileBuilder(
|
||||
Path.Join(_testDirectory,
|
||||
"The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub"),
|
||||
MangaFormat.Epub).Build())
|
||||
.Build();
|
||||
|
||||
series.Volumes.Add(EntityFactory.CreateVolume("1", new List<Chapter>() {chapter2}));
|
||||
|
||||
series.Volumes.Add(new VolumeBuilder("1")
|
||||
.WithChapter(chapter2)
|
||||
.Build());
|
||||
|
||||
series.Volumes.First().Chapters.Add(chapter2);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
@ -2,12 +2,14 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<AnalysisMode>Default</AnalysisMode>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<TieredPGO>true</TieredPGO>
|
||||
<TieredCompilation>true</TieredCompilation>
|
||||
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
||||
<Nullable>warnings</Nullable>
|
||||
<LangVersion>latestmajor</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">
|
||||
@ -51,36 +53,37 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||
<PackageReference Include="Docnet.Core" Version="2.4.0-alpha.4" />
|
||||
<PackageReference Include="ExCSS" Version="4.1.0" />
|
||||
<PackageReference Include="Flurl" Version="3.0.6" />
|
||||
<PackageReference Include="Flurl" Version="3.0.7" />
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||
<PackageReference Include="Hangfire" Version="1.7.31" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.31" />
|
||||
<PackageReference Include="Hangfire.InMemory" Version="0.3.4" />
|
||||
<PackageReference Include="Hangfire" Version="1.7.34" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.34" />
|
||||
<PackageReference Include="Hangfire.InMemory" Version="0.3.7" />
|
||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.2" />
|
||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.3" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
|
||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.10">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.1" />
|
||||
<PackageReference Include="NetVips" Version="2.2.0" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.13.1" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.1.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||
<PackageReference Include="NetVips" Version="2.3.0" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.14.2" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.1.6" />
|
||||
<PackageReference Include="Serilog" Version="2.12.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.2.0-dev-00752" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
|
||||
@ -88,17 +91,17 @@
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.47.0.55603">
|
||||
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.55.0.65544">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.24.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="17.2.3" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.29.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="19.2.11" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.0-alpha1" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -119,6 +122,12 @@
|
||||
<None Remove="kavita.db" />
|
||||
<None Remove="covers\**" />
|
||||
<None Remove="wwwroot\**" />
|
||||
<None Remove="config\cache\**" />
|
||||
<None Remove="config\logs\**" />
|
||||
<None Remove="config\covers\**" />
|
||||
<None Remove="config\bookmarks\**" />
|
||||
<None Remove="config\backups\**" />
|
||||
<None Remove="config\temp\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -129,6 +138,12 @@
|
||||
<Compile Remove="temp\**" />
|
||||
<Compile Remove="covers\**" />
|
||||
<Compile Remove="wwwroot\**" />
|
||||
<Compile Remove="config\cache\**" />
|
||||
<Compile Remove="config\logs\**" />
|
||||
<Compile Remove="config\covers\**" />
|
||||
<Compile Remove="config\bookmarks\**" />
|
||||
<Compile Remove="config\backups\**" />
|
||||
<Compile Remove="config\temp\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -144,6 +159,8 @@
|
||||
<EmbeddedResource Remove="config\temp\**" />
|
||||
<EmbeddedResource Remove="config\stats\**" />
|
||||
<EmbeddedResource Remove="wwwroot\**" />
|
||||
<EmbeddedResource Remove="config\cache\**" />
|
||||
<EmbeddedResource Remove="config\bookmarks\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -167,6 +184,7 @@
|
||||
<Content Update="bin\$(Configuration)\$(AssemblyName).xml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Remove="config\bookmarks\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -5,7 +5,7 @@ namespace API.Comparators;
|
||||
public class NumericComparer : IComparer
|
||||
{
|
||||
|
||||
public int Compare(object x, object y)
|
||||
public int Compare(object? x, object? y)
|
||||
{
|
||||
if((x is string xs) && (y is string ys))
|
||||
{
|
||||
|
@ -31,7 +31,11 @@ public static class PolicyConstants
|
||||
/// Used to give a user ability to Change Restrictions on their account
|
||||
/// </summary>
|
||||
public const string ChangeRestrictionRole = "Change Restriction";
|
||||
/// <summary>
|
||||
/// Used to give a user ability to Login to their account
|
||||
/// </summary>
|
||||
public const string LoginRole = "Login";
|
||||
|
||||
public static readonly ImmutableArray<string> ValidRoles =
|
||||
ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole);
|
||||
ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole, LoginRole);
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
@ -14,16 +13,19 @@ using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Errors;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Middleware.RateLimit;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using Hangfire;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers;
|
||||
@ -125,16 +127,9 @@ public class AccountController : BaseApiController
|
||||
return BadRequest(usernameValidation);
|
||||
}
|
||||
|
||||
var user = new AppUser()
|
||||
{
|
||||
UserName = registerDto.Username,
|
||||
Email = registerDto.Email,
|
||||
UserPreferences = new AppUserPreferences
|
||||
{
|
||||
Theme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme()
|
||||
},
|
||||
ApiKey = HashUtil.ApiKey()
|
||||
};
|
||||
var user = new AppUserBuilder(registerDto.Username, registerDto.Email,
|
||||
await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build();
|
||||
|
||||
|
||||
var result = await _userManager.CreateAsync(user, registerDto.Password);
|
||||
if (!result.Succeeded) return BadRequest(result.Errors);
|
||||
@ -146,6 +141,7 @@ public class AccountController : BaseApiController
|
||||
|
||||
var roleResult = await _userManager.AddToRoleAsync(user, PolicyConstants.AdminRole);
|
||||
if (!roleResult.Succeeded) return BadRequest(result.Errors);
|
||||
await _userManager.AddToRoleAsync(user, PolicyConstants.LoginRole);
|
||||
|
||||
return new UserDto
|
||||
{
|
||||
@ -154,7 +150,8 @@ public class AccountController : BaseApiController
|
||||
Token = await _tokenService.CreateToken(user),
|
||||
RefreshToken = await _tokenService.CreateRefreshToken(user),
|
||||
ApiKey = user.ApiKey,
|
||||
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
|
||||
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences),
|
||||
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -184,6 +181,8 @@ public class AccountController : BaseApiController
|
||||
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper());
|
||||
|
||||
if (user == null) return Unauthorized("Your credentials are not correct");
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized("Your account is disabled. Contact the server admin.");
|
||||
|
||||
var result = await _signInManager
|
||||
.CheckPasswordSignInAsync(user, loginDto.Password, true);
|
||||
@ -200,6 +199,8 @@ public class AccountController : BaseApiController
|
||||
|
||||
// Update LastActive on account
|
||||
user.UpdateLastActive();
|
||||
|
||||
// NOTE: This can likely be removed
|
||||
user.UserPreferences ??= new AppUserPreferences
|
||||
{
|
||||
Theme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme()
|
||||
@ -213,10 +214,15 @@ public class AccountController : BaseApiController
|
||||
var dto = _mapper.Map<UserDto>(user);
|
||||
dto.Token = await _tokenService.CreateToken(user);
|
||||
dto.RefreshToken = await _tokenService.CreateRefreshToken(user);
|
||||
var pref = await _unitOfWork.UserRepository.GetPreferencesAsync(user.UserName);
|
||||
dto.KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion))
|
||||
.Value;
|
||||
var pref = await _unitOfWork.UserRepository.GetPreferencesAsync(user.UserName!);
|
||||
if (pref == null) return Ok(dto);
|
||||
|
||||
pref.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
||||
dto.Preferences = _mapper.Map<UserPreferencesDto>(pref);
|
||||
return dto;
|
||||
|
||||
return Ok(dto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -248,7 +254,7 @@ public class AccountController : BaseApiController
|
||||
.GetFields(BindingFlags.Public | BindingFlags.Static)
|
||||
.Where(f => f.FieldType == typeof(string))
|
||||
.ToDictionary(f => f.Name,
|
||||
f => (string) f.GetValue(null)).Values.ToList();
|
||||
f => (string) f.GetValue(null)!).Values.ToList();
|
||||
}
|
||||
|
||||
|
||||
@ -260,6 +266,7 @@ public class AccountController : BaseApiController
|
||||
public async Task<ActionResult<string>> ResetApiKey()
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
user.ApiKey = HashUtil.ApiKey();
|
||||
|
||||
@ -281,7 +288,7 @@ public class AccountController : BaseApiController
|
||||
/// <param name="dto"></param>
|
||||
/// <returns>Returns just if the email was sent or server isn't reachable</returns>
|
||||
[HttpPost("update/email")]
|
||||
public async Task<ActionResult> UpdateEmail(UpdateEmailDto dto)
|
||||
public async Task<ActionResult> UpdateEmail(UpdateEmailDto? dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized("You do not have permission");
|
||||
@ -297,7 +304,7 @@ public class AccountController : BaseApiController
|
||||
}
|
||||
|
||||
// Validate no other users exist with this email
|
||||
if (user.Email.Equals(dto.Email)) return Ok("Nothing to do");
|
||||
if (user.Email!.Equals(dto.Email)) return Ok("Nothing to do");
|
||||
|
||||
// Check if email is used by another user
|
||||
var existingUserEmail = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
@ -335,7 +342,7 @@ public class AccountController : BaseApiController
|
||||
{
|
||||
EmailAddress = string.IsNullOrEmpty(user.Email) ? dto.Email : user.Email,
|
||||
InstallId = BuildInfo.Version.ToString(),
|
||||
InvitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName,
|
||||
InvitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!,
|
||||
ServerConfirmationLink = emailLink
|
||||
});
|
||||
}
|
||||
@ -357,7 +364,7 @@ public class AccountController : BaseApiController
|
||||
}
|
||||
|
||||
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
@ -367,9 +374,9 @@ public class AccountController : BaseApiController
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized("You do not have permission");
|
||||
if (dto == null) return BadRequest("Invalid payload");
|
||||
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
if (!await _accountService.HasChangeRestrictionRole(user)) return BadRequest("You do not have permission");
|
||||
|
||||
user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating;
|
||||
user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns;
|
||||
@ -387,7 +394,7 @@ public class AccountController : BaseApiController
|
||||
return BadRequest("There was an error updating the age restriction");
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
@ -402,13 +409,14 @@ public class AccountController : BaseApiController
|
||||
public async Task<ActionResult> UpdateAccount(UpdateUserDto dto)
|
||||
{
|
||||
var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (adminUser == null) return Unauthorized();
|
||||
if (!await _unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized("You do not have permission");
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(dto.UserId);
|
||||
if (user == null) return BadRequest("User does not exist");
|
||||
|
||||
// Check if username is changing
|
||||
if (!user.UserName.Equals(dto.Username))
|
||||
if (!user.UserName!.Equals(dto.Username))
|
||||
{
|
||||
// Validate username change
|
||||
var errors = await _accountService.ValidateUsername(dto.Username);
|
||||
@ -488,12 +496,13 @@ public class AccountController : BaseApiController
|
||||
public async Task<ActionResult<string>> GetInviteUrl(int userId, bool withBaseUrl)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.EmailConfirmed)
|
||||
return BadRequest("User is already confirmed");
|
||||
if (string.IsNullOrEmpty(user.ConfirmationToken))
|
||||
return BadRequest("Manual setup is unable to be completed. Please cancel and recreate the invite.");
|
||||
|
||||
return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email, withBaseUrl);
|
||||
return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl);
|
||||
}
|
||||
|
||||
|
||||
@ -520,23 +529,14 @@ public class AccountController : BaseApiController
|
||||
if (emailValidationErrors.Any())
|
||||
{
|
||||
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
if (await _userManager.IsEmailConfirmedAsync(invitedUser))
|
||||
return BadRequest($"User is already registered as {invitedUser.UserName}");
|
||||
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
|
||||
return BadRequest($"User is already registered as {invitedUser!.UserName}");
|
||||
return BadRequest("User is already invited under this email and has yet to accepted invite.");
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new user
|
||||
var user = new AppUser()
|
||||
{
|
||||
UserName = dto.Email,
|
||||
Email = dto.Email,
|
||||
ApiKey = HashUtil.ApiKey(),
|
||||
UserPreferences = new AppUserPreferences
|
||||
{
|
||||
Theme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme()
|
||||
}
|
||||
};
|
||||
var user = new AppUserBuilder(dto.Email, dto.Email, await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build();
|
||||
|
||||
try
|
||||
{
|
||||
@ -607,19 +607,14 @@ public class AccountController : BaseApiController
|
||||
var accessible = await _accountService.CheckIfAccessible(Request);
|
||||
if (accessible)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _emailService.SendConfirmationEmail(new ConfirmationEmailDto()
|
||||
// Do the email send on a background thread to ensure UI can move forward without having to wait for a timeout when users use fake emails
|
||||
BackgroundJob.Enqueue(() => _emailService.SendConfirmationEmail(new ConfirmationEmailDto()
|
||||
{
|
||||
EmailAddress = dto.Email,
|
||||
InvitingUser = adminUser.UserName,
|
||||
InvitingUser = adminUser.UserName!,
|
||||
ServerConfirmationLink = emailLink
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
/* Swallow exception */
|
||||
}
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
return Ok(new InviteUserResponse
|
||||
@ -655,7 +650,11 @@ public class AccountController : BaseApiController
|
||||
|
||||
// Validate Password and Username
|
||||
var validationErrors = new List<ApiException>();
|
||||
// This allows users that use a fake email with the same username to continue setting up the account
|
||||
if (!dto.Username.Equals(dto.Email) && !user.UserName!.Equals(dto.Username))
|
||||
{
|
||||
validationErrors.AddRange(await _accountService.ValidateUsername(dto.Username));
|
||||
}
|
||||
validationErrors.AddRange(await _accountService.ValidatePassword(user, dto.Password));
|
||||
|
||||
if (validationErrors.Any())
|
||||
@ -680,18 +679,19 @@ public class AccountController : BaseApiController
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
|
||||
user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName,
|
||||
AppUserIncludes.UserPreferences);
|
||||
user = (await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName,
|
||||
AppUserIncludes.UserPreferences))!;
|
||||
|
||||
// Perform Login code
|
||||
return new UserDto
|
||||
{
|
||||
Username = user.UserName,
|
||||
Email = user.Email,
|
||||
Username = user.UserName!,
|
||||
Email = user.Email!,
|
||||
Token = await _tokenService.CreateToken(user),
|
||||
RefreshToken = await _tokenService.CreateRefreshToken(user),
|
||||
ApiKey = user.ApiKey,
|
||||
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
|
||||
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences),
|
||||
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value
|
||||
};
|
||||
}
|
||||
|
||||
@ -731,7 +731,7 @@ public class AccountController : BaseApiController
|
||||
|
||||
// For the user's connected devices to pull the new information in
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate,
|
||||
MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
|
||||
MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
|
||||
|
||||
// Perform Login code
|
||||
return Ok();
|
||||
@ -775,6 +775,7 @@ public class AccountController : BaseApiController
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("forgot-password")]
|
||||
[EnableRateLimiting("Authentication")]
|
||||
public async Task<ActionResult<string>> ForgotPassword([FromQuery] string email)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email);
|
||||
@ -832,18 +833,19 @@ public class AccountController : BaseApiController
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName,
|
||||
user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName!,
|
||||
AppUserIncludes.UserPreferences);
|
||||
|
||||
// Perform Login code
|
||||
return new UserDto
|
||||
{
|
||||
Username = user.UserName,
|
||||
Email = user.Email,
|
||||
Username = user!.UserName!,
|
||||
Email = user.Email!,
|
||||
Token = await _tokenService.CreateToken(user),
|
||||
RefreshToken = await _tokenService.CreateRefreshToken(user),
|
||||
ApiKey = user.ApiKey,
|
||||
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
|
||||
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences),
|
||||
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value
|
||||
};
|
||||
}
|
||||
|
||||
@ -853,6 +855,7 @@ public class AccountController : BaseApiController
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("resend-confirmation-email")]
|
||||
[EnableRateLimiting("Authentication")]
|
||||
public async Task<ActionResult<string>> ResendConfirmationSendEmail([FromQuery] int userId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
@ -873,8 +876,8 @@ public class AccountController : BaseApiController
|
||||
{
|
||||
await _emailService.SendMigrationEmail(new EmailMigrationDto()
|
||||
{
|
||||
EmailAddress = user.Email,
|
||||
Username = user.UserName,
|
||||
EmailAddress = user.Email!,
|
||||
Username = user.UserName!,
|
||||
ServerConfirmationLink = emailLink,
|
||||
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
|
||||
});
|
||||
@ -908,8 +911,8 @@ public class AccountController : BaseApiController
|
||||
if (emailValidationErrors.Any())
|
||||
{
|
||||
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
if (await _userManager.IsEmailConfirmedAsync(invitedUser))
|
||||
return BadRequest($"User is already registered as {invitedUser.UserName}");
|
||||
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
|
||||
return BadRequest($"User is already registered as {invitedUser!.UserName}");
|
||||
|
||||
_logger.LogInformation("A user is attempting to login, but hasn't accepted email invite");
|
||||
return BadRequest("User is already invited under this email and has yet to accepted invite.");
|
||||
|
@ -37,6 +37,7 @@ public class BookController : BaseApiController
|
||||
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
|
||||
{
|
||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||
if (dto == null) return BadRequest("Chapter does not exist");
|
||||
var bookTitle = string.Empty;
|
||||
switch (dto.SeriesFormat)
|
||||
{
|
||||
@ -93,6 +94,7 @@ public class BookController : BaseApiController
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter is not valid");
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Chapter is not valid");
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
|
||||
|
||||
var key = BookService.CoalesceKeyForAnyFile(book, file);
|
||||
@ -116,8 +118,9 @@ public class BookController : BaseApiController
|
||||
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter is not valid");
|
||||
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Chapter is not valid");
|
||||
|
||||
try
|
||||
{
|
||||
return Ok(await _bookService.GenerateTableOfContents(chapter));
|
||||
@ -140,6 +143,7 @@ public class BookController : BaseApiController
|
||||
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
|
||||
{
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
||||
var path = _cacheService.GetCachedFile(chapter);
|
||||
|
||||
var baseUrl = "//" + Request.Host + Request.PathBase + "/api/";
|
||||
|
135
API/Controllers/CBLController.cs
Normal file
@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.ReadingLists.CBL;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for the CBL import flow
|
||||
/// </summary>
|
||||
public class CblController : BaseApiController
|
||||
{
|
||||
private readonly IReadingListService _readingListService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
public CblController(IReadingListService readingListService, IDirectoryService directoryService)
|
||||
{
|
||||
_readingListService = readingListService;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The first step in a cbl import. This validates the cbl file that if an import occured, would it be successful.
|
||||
/// If this returns errors, the cbl will always be rejected by Kavita.
|
||||
/// </summary>
|
||||
/// <param name="file">FormBody with parameter name of cbl</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("validate")]
|
||||
public async Task<ActionResult<CblImportSummaryDto>> ValidateCbl([FromForm(Name = "cbl")] IFormFile file)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
try
|
||||
{
|
||||
var cbl = await SaveAndLoadCblFile(file);
|
||||
var importSummary = await _readingListService.ValidateCblFile(userId, cbl);
|
||||
importSummary.FileName = file.FileName;
|
||||
return Ok(importSummary);
|
||||
}
|
||||
catch (ArgumentNullException)
|
||||
{
|
||||
return Ok(new CblImportSummaryDto()
|
||||
{
|
||||
FileName = file.FileName,
|
||||
Success = CblImportResult.Fail,
|
||||
Results = new List<CblBookResult>()
|
||||
{
|
||||
new CblBookResult()
|
||||
{
|
||||
Reason = CblImportReason.InvalidFile
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return Ok(new CblImportSummaryDto()
|
||||
{
|
||||
FileName = file.FileName,
|
||||
Success = CblImportResult.Fail,
|
||||
Results = new List<CblBookResult>()
|
||||
{
|
||||
new CblBookResult()
|
||||
{
|
||||
Reason = CblImportReason.InvalidFile
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Performs the actual import (assuming dryRun = false)
|
||||
/// </summary>
|
||||
/// <param name="file">FormBody with parameter name of cbl</param>
|
||||
/// <param name="dryRun">If true, will only emulate the import but not perform. This should be done to preview what will happen</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("import")]
|
||||
public async Task<ActionResult<CblImportSummaryDto>> ImportCbl([FromForm(Name = "cbl")] IFormFile file, [FromForm(Name = "dryRun")] bool dryRun = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var cbl = await SaveAndLoadCblFile(file);
|
||||
var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cbl, dryRun);
|
||||
importSummary.FileName = file.FileName;
|
||||
return Ok(importSummary);
|
||||
} catch (ArgumentNullException)
|
||||
{
|
||||
return Ok(new CblImportSummaryDto()
|
||||
{
|
||||
FileName = file.FileName,
|
||||
Success = CblImportResult.Fail,
|
||||
Results = new List<CblBookResult>()
|
||||
{
|
||||
new CblBookResult()
|
||||
{
|
||||
Reason = CblImportReason.InvalidFile
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return Ok(new CblImportSummaryDto()
|
||||
{
|
||||
FileName = file.FileName,
|
||||
Success = CblImportResult.Fail,
|
||||
Results = new List<CblBookResult>()
|
||||
{
|
||||
new CblBookResult()
|
||||
{
|
||||
Reason = CblImportReason.InvalidFile
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task<CblReadingList> SaveAndLoadCblFile(IFormFile file)
|
||||
{
|
||||
var filename = Path.GetRandomFileName();
|
||||
var outputFile = Path.Join(_directoryService.TempDirectory, filename);
|
||||
await using var stream = System.IO.File.Create(outputFile);
|
||||
await file.CopyToAsync(stream);
|
||||
stream.Close();
|
||||
return ReadingListService.LoadCblFromPath(outputFile);
|
||||
}
|
||||
}
|
@ -1,14 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Metadata;
|
||||
using API.SignalR;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -35,16 +33,17 @@ public class CollectionController : BaseApiController
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<IEnumerable<CollectionTagDto>> GetAllTags()
|
||||
public async Task<ActionResult<IEnumerable<CollectionTagDto>>> GetAllTags()
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized();
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
if (isAdmin)
|
||||
{
|
||||
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||
return Ok(await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync());
|
||||
}
|
||||
|
||||
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(user.Id);
|
||||
return Ok(await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(user.Id));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -55,13 +54,13 @@ public class CollectionController : BaseApiController
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("search")]
|
||||
public async Task<IEnumerable<CollectionTagDto>> SearchTags(string queryString)
|
||||
public async Task<ActionResult<IEnumerable<CollectionTagDto>>> SearchTags(string queryString)
|
||||
{
|
||||
queryString ??= string.Empty;
|
||||
queryString = queryString.Replace(@"%", string.Empty);
|
||||
if (queryString.Length == 0) return await GetAllTags();
|
||||
|
||||
return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString, User.GetUserId());
|
||||
return Ok(await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString, User.GetUserId()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -126,7 +125,7 @@ public class CollectionController : BaseApiController
|
||||
{
|
||||
try
|
||||
{
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(updateSeriesForTagDto.Tag.Id);
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updateSeriesForTagDto.Tag.Id, CollectionTagIncludes.SeriesMetadata);
|
||||
if (tag == null) return BadRequest("Not a valid Tag");
|
||||
tag.SeriesMetadatas ??= new List<SeriesMetadata>();
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
@ -9,9 +7,7 @@ using API.DTOs.Device;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using ExCSS;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
@ -39,6 +35,7 @@ public class DeviceController : BaseApiController
|
||||
public async Task<ActionResult> CreateOrUpdateDevice(CreateDeviceDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
|
||||
if (user == null) return Unauthorized();
|
||||
var device = await _deviceService.Create(dto, user);
|
||||
|
||||
if (device == null) return BadRequest("There was an error when creating the device");
|
||||
@ -50,6 +47,7 @@ public class DeviceController : BaseApiController
|
||||
public async Task<ActionResult> UpdateDevice(UpdateDeviceDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
|
||||
if (user == null) return Unauthorized();
|
||||
var device = await _deviceService.Update(dto, user);
|
||||
|
||||
if (device == null) return BadRequest("There was an error when updating the device");
|
||||
@ -67,6 +65,7 @@ public class DeviceController : BaseApiController
|
||||
{
|
||||
if (deviceId <= 0) return BadRequest("Not a valid deviceId");
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
|
||||
if (user == null) return Unauthorized();
|
||||
if (await _deviceService.Delete(user, deviceId)) return Ok();
|
||||
|
||||
return BadRequest("Could not delete device");
|
||||
|
@ -93,13 +93,13 @@ public class DownloadController : BaseApiController
|
||||
public async Task<ActionResult> DownloadVolume(int volumeId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId);
|
||||
if (volume == null) return BadRequest("Volume doesn't exist");
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
try
|
||||
{
|
||||
return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series.Name} - Volume {volume.Number}.zip");
|
||||
return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series!.Name} - Volume {volume.Number}.zip");
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
@ -110,6 +110,7 @@ public class DownloadController : BaseApiController
|
||||
private async Task<bool> HasDownloadPermission()
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return false;
|
||||
return await _accountService.HasDownloadPermission(user);
|
||||
}
|
||||
|
||||
@ -130,11 +131,12 @@ public class DownloadController : BaseApiController
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Invalid chapter");
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId);
|
||||
try
|
||||
{
|
||||
return await DownloadFiles(files, $"download_{User.GetUsername()}_c{chapterId}", $"{series.Name} - Chapter {chapter.Number}.zip");
|
||||
return await DownloadFiles(files, $"download_{User.GetUsername()}_c{chapterId}", $"{series!.Name} - Chapter {chapter.Number}.zip");
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
@ -177,8 +179,9 @@ public class DownloadController : BaseApiController
|
||||
public async Task<ActionResult> DownloadSeries(int seriesId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||
if (series == null) return BadRequest("Invalid Series");
|
||||
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
||||
try
|
||||
{
|
||||
return await DownloadFiles(files, $"download_{User.GetUsername()}_s{seriesId}", $"{series.Name}.zip");
|
||||
@ -201,13 +204,13 @@ public class DownloadController : BaseApiController
|
||||
if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest("Bookmarks cannot be empty");
|
||||
|
||||
// We know that all bookmarks will be for one single seriesId
|
||||
var userId = User.GetUserId();
|
||||
var username = User.GetUsername();
|
||||
var userId = User.GetUserId()!;
|
||||
var username = User.GetUsername()!;
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);
|
||||
|
||||
var files = await _bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id));
|
||||
|
||||
var filename = $"{series.Name} - Bookmarks.zip";
|
||||
var filename = $"{series!.Name} - Bookmarks.zip";
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), 0F));
|
||||
var seriesIds = string.Join("_", downloadBookmarkDto.Bookmarks.Select(b => b.SeriesId).Distinct());
|
||||
|
@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
@ -7,6 +8,7 @@ using API.Extensions;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MimeTypes;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
@ -32,14 +34,15 @@ public class ImageController : BaseApiController
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"chapterId"})]
|
||||
public async Task<ActionResult> GetChapterCoverImage(int chapterId)
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"chapterId", "apiKey"})]
|
||||
public async Task<ActionResult> GetChapterCoverImage(int chapterId, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty);
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -48,14 +51,15 @@ public class ImageController : BaseApiController
|
||||
/// <param name="libraryId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("library-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"libraryId"})]
|
||||
public async Task<ActionResult> GetLibraryCoverImage(int libraryId)
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"libraryId", "apiKey"})]
|
||||
public async Task<ActionResult> GetLibraryCoverImage(int libraryId, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty);
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -64,14 +68,15 @@ public class ImageController : BaseApiController
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("volume-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"volumeId"})]
|
||||
public async Task<ActionResult> GetVolumeCoverImage(int volumeId)
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"volumeId", "apiKey"})]
|
||||
public async Task<ActionResult> GetVolumeCoverImage(int volumeId, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty);
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -79,17 +84,18 @@ public class ImageController : BaseApiController
|
||||
/// </summary>
|
||||
/// <param name="seriesId">Id of Series</param>
|
||||
/// <returns></returns>
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"seriesId"})]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"seriesId", "apiKey"})]
|
||||
[HttpGet("series-cover")]
|
||||
public async Task<ActionResult> GetSeriesCoverImage(int seriesId)
|
||||
public async Task<ActionResult> GetSeriesCoverImage(int seriesId, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty);
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
Response.AddCacheHeader(path);
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -98,14 +104,15 @@ public class ImageController : BaseApiController
|
||||
/// <param name="collectionTagId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("collection-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"collectionTagId"})]
|
||||
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId)
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"collectionTagId", "apiKey"})]
|
||||
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty);
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -114,14 +121,15 @@ public class ImageController : BaseApiController
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("readinglist-cover")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"readingListId"})]
|
||||
public async Task<ActionResult> GetReadingListCoverImage(int readingListId)
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"readingListId", "apiKey"})]
|
||||
public async Task<ActionResult> GetReadingListCoverImage(int readingListId, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty);
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -137,15 +145,16 @@ public class ImageController : BaseApiController
|
||||
public async Task<ActionResult> GetBookmarkImage(int chapterId, int pageNum, string apiKey)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
var bookmark = await _unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, userId);
|
||||
if (bookmark == null) return BadRequest("Bookmark does not exist");
|
||||
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
var file = new FileInfo(Path.Join(bookmarkDirectory, bookmark.FileName));
|
||||
var format = Path.GetExtension(file.FullName).Replace(".", string.Empty);
|
||||
var format = Path.GetExtension(file.FullName);
|
||||
|
||||
return PhysicalFile(file.FullName, "image/" + format, Path.GetFileName(file.FullName));
|
||||
return PhysicalFile(file.FullName, MimeTypeMap.GetMimeType(format), Path.GetFileName(file.FullName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -155,15 +164,16 @@ public class ImageController : BaseApiController
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy="RequireAdminRole")]
|
||||
[HttpGet("cover-upload")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"filename"})]
|
||||
public ActionResult GetCoverUploadImage(string filename)
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"filename", "apiKey"})]
|
||||
public async Task<ActionResult> GetCoverUploadImage(string filename, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
if (filename.Contains("..")) return BadRequest("Invalid Filename");
|
||||
|
||||
var path = Path.Join(_directoryService.TempDirectory, filename);
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"File does not exist");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty);
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.JumpBar;
|
||||
using API.DTOs.Search;
|
||||
using API.DTOs.System;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
@ -17,7 +16,6 @@ using API.Services;
|
||||
using API.Services.Tasks.Scanner;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -113,13 +111,21 @@ public class LibraryController : BaseApiController
|
||||
return Ok(_directoryService.ListDirectory(path));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Return all libraries in the Server
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a given library, generate the jump bar information
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("jump-bar")]
|
||||
public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId)
|
||||
{
|
||||
@ -129,7 +135,11 @@ public class LibraryController : BaseApiController
|
||||
return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Grants a user account access to a Library
|
||||
/// </summary>
|
||||
/// <param name="updateLibraryForUserDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("grant-access")]
|
||||
public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto)
|
||||
@ -174,14 +184,34 @@ public class LibraryController : BaseApiController
|
||||
return BadRequest("There was a critical issue. Please try again.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans a given library for file changes.
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="force">If true, will ignore any optimizations to avoid file I/O and will treat similar to a first scan</param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("scan")]
|
||||
public ActionResult Scan(int libraryId, bool force = false)
|
||||
{
|
||||
if (libraryId <= 0) return BadRequest("Invalid libraryId");
|
||||
_taskScheduler.ScanLibrary(libraryId, force);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans a given library for file changes. If another scan task is in progress, will reschedule the invocation for 3 hours in future.
|
||||
/// </summary>
|
||||
/// <param name="force">If true, will ignore any optimizations to avoid file I/O and will treat similar to a first scan</param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("scan-all")]
|
||||
public ActionResult ScanAll(bool force = false)
|
||||
{
|
||||
_taskScheduler.ScanLibraries(force);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("refresh-metadata")]
|
||||
public ActionResult RefreshMetadata(int libraryId, bool force = true)
|
||||
@ -209,6 +239,7 @@ public class LibraryController : BaseApiController
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(dto.ApiKey);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
// Validate user has Admin privileges
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
@ -244,7 +275,6 @@ public class LibraryController : BaseApiController
|
||||
|
||||
try
|
||||
{
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
||||
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
|
||||
{
|
||||
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
|
||||
@ -252,6 +282,9 @@ public class LibraryController : BaseApiController
|
||||
"You cannot delete a library while a scan is in progress. Please wait for scan to complete or restart Kavita then try to delete");
|
||||
}
|
||||
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
|
||||
if (library == null) return BadRequest("Library no longer exists");
|
||||
|
||||
// Due to a bad schema that I can't figure out how to fix, we need to erase all RelatedSeries before we delete the library
|
||||
// Aka SeriesRelation has an invalid foreign key
|
||||
foreach (var s in await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(library.Id,
|
||||
@ -317,8 +350,10 @@ public class LibraryController : BaseApiController
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto dto)
|
||||
{
|
||||
var newName = dto.Name.Trim();
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders);
|
||||
if (library == null) return BadRequest("Library doesn't exist");
|
||||
|
||||
var newName = dto.Name.Trim();
|
||||
if (await _unitOfWork.LibraryRepository.LibraryExists(newName) && !library.Name.Equals(newName))
|
||||
return BadRequest("Library name already exists");
|
||||
|
||||
@ -335,6 +370,8 @@ public class LibraryController : BaseApiController
|
||||
library.IncludeInRecommended = dto.IncludeInRecommended;
|
||||
library.IncludeInSearch = dto.IncludeInSearch;
|
||||
library.ManageCollections = dto.ManageCollections;
|
||||
library.ManageReadingLists = dto.ManageReadingLists;
|
||||
|
||||
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
|
||||
|
@ -31,6 +31,7 @@ public class MetadataController : BaseApiController
|
||||
/// <param name="libraryIds">String separated libraryIds or null for all genres</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("genres")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
@ -51,6 +52,7 @@ public class MetadataController : BaseApiController
|
||||
/// <param name="libraryIds">String separated libraryIds or null for all people</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("people")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryIds)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
@ -68,6 +70,7 @@ public class MetadataController : BaseApiController
|
||||
/// <param name="libraryIds">String separated libraryIds or null for all tags</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("tags")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
@ -132,6 +135,7 @@ public class MetadataController : BaseApiController
|
||||
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("languages")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
public async Task<ActionResult<IList<LanguageDto>>> GetAllLanguages(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||
@ -145,6 +149,7 @@ public class MetadataController : BaseApiController
|
||||
}
|
||||
|
||||
[HttpGet("all-languages")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)]
|
||||
public IEnumerable<LanguageDto> GetAllValidLanguages()
|
||||
{
|
||||
return CultureInfo.GetCultures(CultureTypes.AllCultures).Select(c =>
|
||||
|
@ -20,6 +20,7 @@ using API.Services;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MimeTypes;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
@ -37,7 +38,6 @@ public class OpdsController : BaseApiController
|
||||
|
||||
private readonly XmlSerializer _xmlSerializer;
|
||||
private readonly XmlSerializer _xmlOpenSearchSerializer;
|
||||
private const string Prefix = "/api/opds/";
|
||||
private readonly FilterDto _filterDto = new FilterDto()
|
||||
{
|
||||
Formats = new List<MangaFormat>(),
|
||||
@ -62,7 +62,8 @@ public class OpdsController : BaseApiController
|
||||
SortOptions = null,
|
||||
PublicationStatus = new List<PublicationStatus>()
|
||||
};
|
||||
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||
private readonly ChapterSortComparer _chapterSortComparer = ChapterSortComparer.Default;
|
||||
private const int PageSize = 20;
|
||||
|
||||
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
|
||||
IDirectoryService directoryService, ICacheService cacheService,
|
||||
@ -79,7 +80,6 @@ public class OpdsController : BaseApiController
|
||||
|
||||
_xmlSerializer = new XmlSerializer(typeof(Feed));
|
||||
_xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription));
|
||||
|
||||
}
|
||||
|
||||
[HttpPost("{apiKey}")]
|
||||
@ -89,7 +89,10 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var feed = CreateFeed("Kavita", string.Empty, apiKey);
|
||||
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
|
||||
var feed = CreateFeed("Kavita", string.Empty, apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, "root");
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
@ -101,7 +104,7 @@ public class OpdsController : BaseApiController
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/on-deck"),
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/on-deck"),
|
||||
}
|
||||
});
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
@ -114,7 +117,7 @@ public class OpdsController : BaseApiController
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/recently-added"),
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/recently-added"),
|
||||
}
|
||||
});
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
@ -127,7 +130,7 @@ public class OpdsController : BaseApiController
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/reading-list"),
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/reading-list"),
|
||||
}
|
||||
});
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
@ -140,7 +143,7 @@ public class OpdsController : BaseApiController
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/libraries"),
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/libraries"),
|
||||
}
|
||||
});
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
@ -153,12 +156,25 @@ public class OpdsController : BaseApiController
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/collections"),
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/collections"),
|
||||
}
|
||||
});
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
||||
private async Task<Tuple<string, string>> GetPrefix()
|
||||
{
|
||||
var baseUrl = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl)).Value;
|
||||
var prefix = "/api/opds/";
|
||||
if (!Configuration.DefaultBaseUrl.Equals(baseUrl))
|
||||
{
|
||||
// We need to update the Prefix to account for baseUrl
|
||||
prefix = baseUrl + "api/opds/";
|
||||
}
|
||||
|
||||
return new Tuple<string, string>(baseUrl, prefix);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("{apiKey}/libraries")]
|
||||
[Produces("application/xml")]
|
||||
@ -166,9 +182,10 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
var libraries = await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId);
|
||||
var feed = CreateFeed("All Libraries", $"{apiKey}/libraries", apiKey);
|
||||
var feed = CreateFeed("All Libraries", $"{prefix}{apiKey}/libraries", apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, "libraries");
|
||||
foreach (var library in libraries)
|
||||
{
|
||||
@ -178,7 +195,7 @@ public class OpdsController : BaseApiController
|
||||
Title = library.Name,
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/libraries/{library.Id}"),
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/libraries/{library.Id}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -192,15 +209,17 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
IEnumerable<CollectionTagDto> tags = isAdmin ? (await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync())
|
||||
var tags = isAdmin ? (await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync())
|
||||
: (await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(userId));
|
||||
|
||||
|
||||
var feed = CreateFeed("All Collections", $"{apiKey}/collections", apiKey);
|
||||
var feed = CreateFeed("All Collections", $"{prefix}{apiKey}/collections", apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, "collections");
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
@ -211,9 +230,9 @@ public class OpdsController : BaseApiController
|
||||
Summary = tag.Summary,
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/collections/{tag.Id}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/collection-cover?collectionId={tag.Id}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/collection-cover?collectionId={tag.Id}")
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/collections/{tag.Id}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionId={tag.Id}&apiKey={apiKey}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/collection-cover?collectionId={tag.Id}&apiKey={apiKey}")
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -228,8 +247,10 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
IEnumerable <CollectionTagDto> tags;
|
||||
@ -248,20 +269,16 @@ public class OpdsController : BaseApiController
|
||||
return BadRequest("Collection does not exist or you don't have access");
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, new UserParams()
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
PageSize = 20
|
||||
});
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, GetUserParams(pageNumber));
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id));
|
||||
|
||||
var feed = CreateFeed(tag.Title + " Collection", $"{apiKey}/collections/{collectionId}", apiKey);
|
||||
var feed = CreateFeed(tag.Title + " Collection", $"{prefix}{apiKey}/collections/{collectionId}", apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, $"collections-{collectionId}");
|
||||
AddPagination(feed, series, $"{Prefix}{apiKey}/collections/{collectionId}");
|
||||
AddPagination(feed, series, $"{prefix}{apiKey}/collections/{collectionId}");
|
||||
|
||||
foreach (var seriesDto in series)
|
||||
{
|
||||
feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey));
|
||||
feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey, prefix, baseUrl));
|
||||
}
|
||||
|
||||
|
||||
@ -274,15 +291,14 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
|
||||
var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, true, new UserParams()
|
||||
{
|
||||
PageNumber = pageNumber
|
||||
});
|
||||
var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId,
|
||||
true, GetUserParams(pageNumber), false);
|
||||
|
||||
|
||||
var feed = CreateFeed("All Reading Lists", $"{apiKey}/reading-list", apiKey);
|
||||
var feed = CreateFeed("All Reading Lists", $"{prefix}{apiKey}/reading-list", apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, "reading-list");
|
||||
foreach (var readingListDto in readingLists)
|
||||
{
|
||||
@ -293,7 +309,7 @@ public class OpdsController : BaseApiController
|
||||
Summary = readingListDto.Summary,
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/reading-list/{readingListDto.Id}"),
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/reading-list/{readingListDto.Id}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -301,29 +317,42 @@ public class OpdsController : BaseApiController
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
|
||||
private static UserParams GetUserParams(int pageNumber)
|
||||
{
|
||||
return new UserParams()
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
PageSize = PageSize
|
||||
};
|
||||
}
|
||||
|
||||
[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 (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
|
||||
var userWithLists = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName, AppUserIncludes.ReadingListsWithItems);
|
||||
var userWithLists = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user!.UserName!, AppUserIncludes.ReadingListsWithItems);
|
||||
if (userWithLists == null) return Unauthorized();
|
||||
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 feed = CreateFeed(readingList.Title + " Reading List", $"{prefix}{apiKey}/reading-list/{readingListId}", apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, $"reading-list-{readingListId}");
|
||||
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId)).ToList();
|
||||
foreach (var item in items)
|
||||
{
|
||||
feed.Entries.Add(CreateChapter(apiKey, $"{item.Order} - {item.SeriesName}: {item.Title}", string.Empty, item.ChapterId, item.VolumeId, item.SeriesId));
|
||||
feed.Entries.Add(
|
||||
CreateChapter(apiKey, $"{item.Order} - {item.SeriesName}: {item.Title}",
|
||||
string.Empty, item.ChapterId, item.VolumeId, item.SeriesId, prefix, baseUrl));
|
||||
}
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
}
|
||||
@ -334,6 +363,7 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
var library =
|
||||
(await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).SingleOrDefault(l =>
|
||||
@ -343,20 +373,16 @@ public class OpdsController : BaseApiController
|
||||
return BadRequest("User does not have access to this library");
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, new UserParams()
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
PageSize = 20
|
||||
}, _filterDto);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, GetUserParams(pageNumber), _filterDto);
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id));
|
||||
|
||||
var feed = CreateFeed(library.Name, $"{apiKey}/libraries/{libraryId}", apiKey);
|
||||
var feed = CreateFeed(library.Name, $"{apiKey}/libraries/{libraryId}", apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, $"library-{library.Name}");
|
||||
AddPagination(feed, series, $"{Prefix}{apiKey}/libraries/{libraryId}");
|
||||
AddPagination(feed, series, $"{prefix}{apiKey}/libraries/{libraryId}");
|
||||
|
||||
foreach (var seriesDto in series)
|
||||
{
|
||||
feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey));
|
||||
feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey, prefix, baseUrl));
|
||||
}
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
@ -368,21 +394,18 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
var recentlyAdded = await _unitOfWork.SeriesRepository.GetRecentlyAdded(0, userId, new UserParams()
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
PageSize = 20
|
||||
}, _filterDto);
|
||||
var recentlyAdded = await _unitOfWork.SeriesRepository.GetRecentlyAdded(0, userId, GetUserParams(pageNumber), _filterDto);
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(recentlyAdded.Select(s => s.Id));
|
||||
|
||||
var feed = CreateFeed("Recently Added", $"{apiKey}/recently-added", apiKey);
|
||||
var feed = CreateFeed("Recently Added", $"{prefix}{apiKey}/recently-added", apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, "recently-added");
|
||||
AddPagination(feed, recentlyAdded, $"{Prefix}{apiKey}/recently-added");
|
||||
AddPagination(feed, recentlyAdded, $"{prefix}{apiKey}/recently-added");
|
||||
|
||||
foreach (var seriesDto in recentlyAdded)
|
||||
{
|
||||
feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey));
|
||||
feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey, prefix, baseUrl));
|
||||
}
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
@ -394,23 +417,23 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
|
||||
var userId = await GetUser(apiKey);
|
||||
var userParams = new UserParams()
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
};
|
||||
var userParams = GetUserParams(pageNumber);
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, 0, userParams, _filterDto);
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(pagedList.Select(s => s.Id));
|
||||
|
||||
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
||||
|
||||
var feed = CreateFeed("On Deck", $"{apiKey}/on-deck", apiKey);
|
||||
var feed = CreateFeed("On Deck", $"{prefix}{apiKey}/on-deck", apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, "on-deck");
|
||||
AddPagination(feed, pagedList, $"{Prefix}{apiKey}/on-deck");
|
||||
AddPagination(feed, pagedList, $"{prefix}{apiKey}/on-deck");
|
||||
|
||||
foreach (var seriesDto in pagedList)
|
||||
{
|
||||
feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey));
|
||||
feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey, prefix, baseUrl));
|
||||
}
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
@ -422,6 +445,7 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
|
||||
@ -432,18 +456,17 @@ public class OpdsController : BaseApiController
|
||||
query = query.Replace(@"%", string.Empty);
|
||||
// Get libraries user has access to
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList();
|
||||
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.SearchSeries(userId, isAdmin, libraries.Select(l => l.Id).ToArray(), query);
|
||||
|
||||
var feed = CreateFeed(query, $"{apiKey}/series?query=" + query, apiKey);
|
||||
var feed = CreateFeed(query, $"{prefix}{apiKey}/series?query=" + query, apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, "search-series");
|
||||
foreach (var seriesDto in series.Series)
|
||||
{
|
||||
feed.Entries.Add(CreateSeries(seriesDto, apiKey));
|
||||
feed.Entries.Add(CreateSeries(seriesDto, apiKey, prefix, baseUrl));
|
||||
}
|
||||
|
||||
foreach (var collection in series.Collections)
|
||||
@ -456,11 +479,11 @@ public class OpdsController : BaseApiController
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation,
|
||||
Prefix + $"{apiKey}/collections/{collection.Id}"),
|
||||
$"{prefix}{apiKey}/collections/{collection.Id}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image,
|
||||
$"/api/image/collection-cover?collectionId={collection.Id}"),
|
||||
$"{baseUrl}api/image/collection-cover?collectionId={collection.Id}&apiKey={apiKey}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image,
|
||||
$"/api/image/collection-cover?collectionId={collection.Id}")
|
||||
$"{baseUrl}api/image/collection-cover?collectionId={collection.Id}&apiKey={apiKey}")
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -474,7 +497,7 @@ public class OpdsController : BaseApiController
|
||||
Summary = readingListDto.Summary,
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/reading-list/{readingListDto.Id}"),
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/reading-list/{readingListDto.Id}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -494,6 +517,7 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var feed = new OpenSearchDescription()
|
||||
{
|
||||
ShortName = "Search",
|
||||
@ -501,7 +525,7 @@ public class OpdsController : BaseApiController
|
||||
Url = new SearchLink()
|
||||
{
|
||||
Type = FeedLinkType.AtomAcquisition,
|
||||
Template = $"{Prefix}{apiKey}/series?query=" + "{searchTerms}"
|
||||
Template = $"{prefix}{apiKey}/series?query=" + "{searchTerms}"
|
||||
}
|
||||
};
|
||||
|
||||
@ -517,39 +541,50 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
|
||||
var feed = CreateFeed(series.Name + " - Storyline", $"{apiKey}/series/{series.Id}", apiKey);
|
||||
var feed = CreateFeed(series.Name + " - Storyline", $"{prefix}{apiKey}/series/{series.Id}", apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, $"series-{series.Id}");
|
||||
feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/series-cover?seriesId={seriesId}"));
|
||||
feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesId}&apiKey={apiKey}"));
|
||||
|
||||
var seriesDetail = await _seriesService.GetSeriesDetail(seriesId, userId);
|
||||
foreach (var volume in seriesDetail.Volumes)
|
||||
{
|
||||
// If there is only one chapter to the Volume, we will emulate a volume to flatten the amount of hops a user must go through
|
||||
if (volume.Chapters.Count == 1)
|
||||
var chapters = (await _unitOfWork.ChapterRepository.GetChaptersAsync(volume.Id)).OrderBy(x => double.Parse(x.Number),
|
||||
_chapterSortComparer);
|
||||
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
var firstChapter = volume.Chapters.First();
|
||||
var chapter = CreateChapter(apiKey, volume.Name, firstChapter.Summary, firstChapter.Id, volume.Id, seriesId);
|
||||
chapter.Id = firstChapter.Id.ToString();
|
||||
feed.Entries.Add(chapter);
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapter.Id);
|
||||
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, volume.Id, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
}
|
||||
else
|
||||
{
|
||||
feed.Entries.Add(CreateVolume(volume, seriesId, apiKey));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
foreach (var storylineChapter in seriesDetail.StorylineChapters.Where(c => !c.IsSpecial))
|
||||
{
|
||||
feed.Entries.Add(CreateChapter(apiKey, storylineChapter.Title, storylineChapter.Summary, storylineChapter.Id, storylineChapter.VolumeId, seriesId));
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(storylineChapter.Id);
|
||||
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(storylineChapter.Id);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, storylineChapter.VolumeId, storylineChapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var special in seriesDetail.Specials)
|
||||
{
|
||||
feed.Entries.Add(CreateChapter(apiKey, special.Title, special.Summary, special.Id, special.VolumeId, seriesId));
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(special.Id);
|
||||
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(special.Id);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, special.VolumeId, special.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
}
|
||||
}
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
@ -561,6 +596,7 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||
@ -568,21 +604,17 @@ public class OpdsController : BaseApiController
|
||||
var chapters =
|
||||
(await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number),
|
||||
_chapterSortComparer);
|
||||
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume.Name + $" - {SeriesService.FormatChapterName(libraryType)}s ", $"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey);
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s ",
|
||||
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{SeriesService.FormatChapterName(libraryType)}s");
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
feed.Entries.Add(new FeedEntry()
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapter.Id);
|
||||
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
Id = chapter.Id.ToString(),
|
||||
Title = SeriesService.FormatChapterTitle(chapter, libraryType),
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapter.Id}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapter.Id}")
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
@ -594,18 +626,21 @@ public class OpdsController : BaseApiController
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Chapter doesn't exist");
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume.Name + $" - {SeriesService.FormatChapterName(libraryType)}s", $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey);
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s",
|
||||
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey, prefix, baseUrl);
|
||||
SetFeedId(feed, $"series-{series.Id}-volume-{volumeId}-{SeriesService.FormatChapterName(libraryType)}-{chapterId}-files");
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapterId, mangaFile, series, chapter, apiKey));
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapterId, mangaFile, series, chapter, apiKey, prefix, baseUrl));
|
||||
}
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
@ -683,7 +718,7 @@ public class OpdsController : BaseApiController
|
||||
feed.StartIndex = (Math.Max(list.CurrentPage - 1, 0) * list.PageSize) + 1;
|
||||
}
|
||||
|
||||
private static FeedEntry CreateSeries(SeriesDto seriesDto, SeriesMetadataDto metadata, string apiKey)
|
||||
private static FeedEntry CreateSeries(SeriesDto seriesDto, SeriesMetadataDto metadata, string apiKey, string prefix, string baseUrl)
|
||||
{
|
||||
return new FeedEntry()
|
||||
{
|
||||
@ -693,7 +728,7 @@ public class OpdsController : BaseApiController
|
||||
Authors = metadata.Writers.Select(p => new FeedAuthor()
|
||||
{
|
||||
Name = p.Name,
|
||||
Uri = "http://opds-spec.org/author"
|
||||
Uri = "http://opds-spec.org/author/" + p.Id
|
||||
}).ToList(),
|
||||
Categories = metadata.Genres.Select(g => new FeedCategory()
|
||||
{
|
||||
@ -702,14 +737,14 @@ public class OpdsController : BaseApiController
|
||||
}).ToList(),
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{seriesDto.Id}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/series-cover?seriesId={seriesDto.Id}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/series-cover?seriesId={seriesDto.Id}")
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/series/{seriesDto.Id}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesDto.Id}&apiKey={apiKey}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesDto.Id}&apiKey={apiKey}")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static FeedEntry CreateSeries(SearchResultDto searchResultDto, string apiKey)
|
||||
private static FeedEntry CreateSeries(SearchResultDto searchResultDto, string apiKey, string prefix, string baseUrl)
|
||||
{
|
||||
return new FeedEntry()
|
||||
{
|
||||
@ -717,33 +752,14 @@ public class OpdsController : BaseApiController
|
||||
Title = $"{searchResultDto.Name} ({searchResultDto.Format})",
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{searchResultDto.SeriesId}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/series-cover?seriesId={searchResultDto.SeriesId}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/series-cover?seriesId={searchResultDto.SeriesId}")
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/series/{searchResultDto.SeriesId}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={searchResultDto.SeriesId}&apiKey={apiKey}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={searchResultDto.SeriesId}&apiKey={apiKey}")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static FeedEntry CreateVolume(VolumeDto volumeDto, int seriesId, string apiKey)
|
||||
{
|
||||
return new FeedEntry()
|
||||
{
|
||||
Id = volumeDto.Id.ToString(),
|
||||
Title = volumeDto.Name,
|
||||
Summary = volumeDto.Chapters.First().Summary ?? string.Empty,
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation,
|
||||
Prefix + $"{apiKey}/series/{seriesId}/volume/{volumeDto.Id}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image,
|
||||
$"/api/image/volume-cover?volumeId={volumeDto.Id}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image,
|
||||
$"/api/image/volume-cover?volumeId={volumeDto.Id}")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static FeedEntry CreateChapter(string apiKey, string title, string summary, int chapterId, int volumeId, int seriesId)
|
||||
private static FeedEntry CreateChapter(string apiKey, string title, string summary, int chapterId, int volumeId, int seriesId, string prefix, string baseUrl)
|
||||
{
|
||||
return new FeedEntry()
|
||||
{
|
||||
@ -753,30 +769,40 @@ public class OpdsController : BaseApiController
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation,
|
||||
Prefix + $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}"),
|
||||
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image,
|
||||
$"/api/image/chapter-cover?chapterId={chapterId}"),
|
||||
$"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image,
|
||||
$"/api/image/chapter-cover?chapterId={chapterId}")
|
||||
$"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<FeedEntry> CreateChapterWithFile(int seriesId, int volumeId, int chapterId, MangaFile mangaFile, SeriesDto series, ChapterDto chapter, string apiKey)
|
||||
private async Task<FeedEntry> CreateChapterWithFile(int seriesId, int volumeId, int chapterId, MangaFile mangaFile, SeriesDto series, ChapterDto chapter, string apiKey, string prefix, string baseUrl)
|
||||
{
|
||||
var fileSize =
|
||||
mangaFile.Bytes > 0 ? DirectoryService.GetHumanReadableBytes(mangaFile.Bytes) :
|
||||
DirectoryService.GetHumanReadableBytes(_directoryService.GetTotalSize(new List<string>()
|
||||
{mangaFile.FilePath}));
|
||||
var fileType = _downloadService.GetContentTypeFromFile(mangaFile.FilePath);
|
||||
var filename = Uri.EscapeDataString(Path.GetFileName(mangaFile.FilePath) ?? string.Empty);
|
||||
var filename = Uri.EscapeDataString(Path.GetFileName(mangaFile.FilePath));
|
||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, await GetUser(apiKey));
|
||||
|
||||
var title = $"{series.Name} - ";
|
||||
if (volume.Chapters.Count == 1)
|
||||
|
||||
var title = $"{series.Name}";
|
||||
|
||||
if (volume!.Chapters.Count == 1)
|
||||
{
|
||||
SeriesService.RenameVolumeName(volume.Chapters.First(), volume, libraryType);
|
||||
title += $"{volume.Name}";
|
||||
if (volume.Name != "0")
|
||||
{
|
||||
title += $" - {volume.Name}";
|
||||
}
|
||||
}
|
||||
else if (volume.Number != 0)
|
||||
{
|
||||
title = $"{series.Name} - Volume {volume.Name} - {SeriesService.FormatChapterTitle(chapter, libraryType)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -786,7 +812,7 @@ public class OpdsController : BaseApiController
|
||||
// Chunky requires a file at the end. Our API ignores this
|
||||
var accLink =
|
||||
CreateLink(FeedLinkRelation.Acquisition, fileType,
|
||||
$"{Prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}/download/{filename}",
|
||||
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}/download/{filename}",
|
||||
filename);
|
||||
accLink.TotalPages = chapter.Pages;
|
||||
|
||||
@ -799,11 +825,11 @@ public class OpdsController : BaseApiController
|
||||
Format = mangaFile.Format.ToString(),
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"),
|
||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}"),
|
||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"{baseUrl}api/image/chapter-cover?chapterId={chapterId}&apiKey={apiKey}"),
|
||||
// We can't not include acc link in the feed, panels doesn't work with just page streaming option. We have to block download directly
|
||||
accLink,
|
||||
CreatePageStreamLink(series.LibraryId,seriesId, volumeId, chapterId, mangaFile, apiKey)
|
||||
await CreatePageStreamLink(series.LibraryId, seriesId, volumeId, chapterId, mangaFile, apiKey, prefix)
|
||||
},
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
@ -815,6 +841,16 @@ public class OpdsController : BaseApiController
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This returns a streamed image following OPDS-PS v1.2
|
||||
/// </summary>
|
||||
/// <param name="apiKey"></param>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="volumeId"></param>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="pageNumber"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{apiKey}/image")]
|
||||
public async Task<ActionResult> GetPageStreamedImage(string apiKey, [FromQuery] int libraryId, [FromQuery] int seriesId, [FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber)
|
||||
{
|
||||
@ -828,7 +864,7 @@ public class OpdsController : BaseApiController
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {pageNumber}");
|
||||
|
||||
var content = await _directoryService.ReadFileAsync(path);
|
||||
var format = Path.GetExtension(path).Replace(".", string.Empty);
|
||||
var format = Path.GetExtension(path);
|
||||
|
||||
// Calculates SHA1 Hash for byte[]
|
||||
Response.AddCacheHeader(content);
|
||||
@ -843,7 +879,7 @@ public class OpdsController : BaseApiController
|
||||
LibraryId =libraryId
|
||||
}, await GetUser(apiKey));
|
||||
|
||||
return File(content, "image/" + format);
|
||||
return File(content, MimeTypeMap.GetMimeType(format));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@ -860,9 +896,9 @@ public class OpdsController : BaseApiController
|
||||
if (files.Length == 0) return BadRequest("Cannot find icon");
|
||||
var path = files[0];
|
||||
var content = await _directoryService.ReadFileAsync(path);
|
||||
var format = Path.GetExtension(path).Replace(".", string.Empty);
|
||||
var format = Path.GetExtension(path);
|
||||
|
||||
return File(content, "image/" + format);
|
||||
return File(content, MimeTypeMap.GetMimeType(format));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -883,14 +919,25 @@ public class OpdsController : BaseApiController
|
||||
throw new KavitaException("User does not exist");
|
||||
}
|
||||
|
||||
private static FeedLink CreatePageStreamLink(int libraryId, int seriesId, int volumeId, int chapterId, MangaFile mangaFile, string apiKey)
|
||||
private async Task<FeedLink> CreatePageStreamLink(int libraryId, int seriesId, int volumeId, int chapterId, MangaFile mangaFile, string apiKey, string prefix)
|
||||
{
|
||||
var link = CreateLink(FeedLinkRelation.Stream, "image/jpeg", $"{Prefix}{apiKey}/image?libraryId={libraryId}&seriesId={seriesId}&volumeId={volumeId}&chapterId={chapterId}&pageNumber=" + "{pageNumber}");
|
||||
var userId = await GetUser(apiKey);
|
||||
var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, userId);
|
||||
|
||||
// TODO: Type could be wrong
|
||||
var link = CreateLink(FeedLinkRelation.Stream, "image/jpeg",
|
||||
$"{prefix}{apiKey}/image?libraryId={libraryId}&seriesId={seriesId}&volumeId={volumeId}&chapterId={chapterId}&pageNumber=" + "{pageNumber}");
|
||||
link.TotalPages = mangaFile.Pages;
|
||||
if (progress != null)
|
||||
{
|
||||
link.LastRead = progress.PageNum;
|
||||
link.LastReadDate = progress.LastModifiedUtc;
|
||||
}
|
||||
link.IsPageStream = true;
|
||||
return link;
|
||||
}
|
||||
|
||||
private static FeedLink CreateLink(string rel, string type, string href, string title = null)
|
||||
private static FeedLink CreateLink(string rel, string type, string href, string? title = null)
|
||||
{
|
||||
return new FeedLink()
|
||||
{
|
||||
@ -901,21 +948,21 @@ public class OpdsController : BaseApiController
|
||||
};
|
||||
}
|
||||
|
||||
private static Feed CreateFeed(string title, string href, string apiKey)
|
||||
private static Feed CreateFeed(string title, string href, string apiKey, string prefix, string baseUrl)
|
||||
{
|
||||
var link = CreateLink(FeedLinkRelation.Self, string.IsNullOrEmpty(href) ?
|
||||
FeedLinkType.AtomNavigation :
|
||||
FeedLinkType.AtomAcquisition, Prefix + href);
|
||||
FeedLinkType.AtomAcquisition, prefix + href);
|
||||
|
||||
return new Feed()
|
||||
{
|
||||
Title = title,
|
||||
Icon = Prefix + $"{apiKey}/favicon",
|
||||
Icon = $"{prefix}{apiKey}/favicon",
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
link,
|
||||
CreateLink(FeedLinkRelation.Start, FeedLinkType.AtomNavigation, Prefix + apiKey),
|
||||
CreateLink(FeedLinkRelation.Search, FeedLinkType.AtomSearch, Prefix + $"{apiKey}/search")
|
||||
CreateLink(FeedLinkRelation.Start, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}"),
|
||||
CreateLink(FeedLinkRelation.Search, FeedLinkType.AtomSearch, $"{prefix}{apiKey}/search")
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -2,7 +2,9 @@
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -38,12 +40,14 @@ public class PluginController : BaseApiController
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId <= 0) return Unauthorized();
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
_logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName, user.UserName, userId);
|
||||
_logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName, user!.UserName, userId);
|
||||
return new UserDto
|
||||
{
|
||||
Username = user.UserName,
|
||||
Username = user.UserName!,
|
||||
Token = await _tokenService.CreateToken(user),
|
||||
RefreshToken = await _tokenService.CreateRefreshToken(user),
|
||||
ApiKey = user.ApiKey,
|
||||
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -13,13 +13,13 @@ using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using MimeTypes;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
@ -57,9 +57,10 @@ public class ReaderController : BaseApiController
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("pdf")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)]
|
||||
public async Task<ActionResult> GetPdf(int chapterId)
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "apiKey"})]
|
||||
public async Task<ActionResult> GetPdf(int chapterId, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
if (chapter == null) return BadRequest("There was an issue finding pdf file for reading");
|
||||
|
||||
@ -89,14 +90,16 @@ public class ReaderController : BaseApiController
|
||||
/// <remarks>This will cache the chapter images for reading</remarks>
|
||||
/// <param name="chapterId">Chapter Id</param>
|
||||
/// <param name="page">Page in question</param>
|
||||
/// <param name="apiKey">User's API Key for authentication</param>
|
||||
/// <param name="extractPdf">Should Kavita extract pdf into images. Defaults to false.</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("image")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId","page", "extractPdf", "apiKey"})]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetImage(int chapterId, int page, bool extractPdf = false)
|
||||
public async Task<ActionResult> GetImage(int chapterId, int page, string apiKey, bool extractPdf = false)
|
||||
{
|
||||
if (page < 0) page = 0;
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var chapter = await _cacheService.Ensure(chapterId, extractPdf);
|
||||
if (chapter == null) return BadRequest("There was an issue finding image file for reading");
|
||||
|
||||
@ -104,9 +107,9 @@ public class ReaderController : BaseApiController
|
||||
{
|
||||
var path = _cacheService.GetCachedPagePath(chapter.Id, page);
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}. Try refreshing to allow re-cache.");
|
||||
var format = Path.GetExtension(path).Replace(".", "");
|
||||
var format = Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, "image/" + format, Path.GetFileName(path), true);
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path), true);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@ -115,6 +118,21 @@ public class ReaderController : BaseApiController
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("thumbnail")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "pageNum", "apiKey"})]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetThumbnail(int chapterId, int pageNum, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var chapter = await _cacheService.Ensure(chapterId, true);
|
||||
if (chapter == null) return BadRequest("There was an issue extracting images from chapter");
|
||||
var images = _cacheService.GetCachedPages(chapterId);
|
||||
|
||||
var path = await _readerService.GetThumbnail(chapter, pageNum, images);
|
||||
var format = Path.GetExtension(path);
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an image for a given bookmark series. Side effect: This will cache the bookmark images for reading.
|
||||
/// </summary>
|
||||
@ -124,12 +142,13 @@ public class ReaderController : BaseApiController
|
||||
/// <remarks>We must use api key as bookmarks could be leaked to other users via the API</remarks>
|
||||
/// <returns></returns>
|
||||
[HttpGet("bookmark-image")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"seriesId", "page", "apiKey"})]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetBookmarkImage(int seriesId, string apiKey, int page)
|
||||
{
|
||||
if (page < 0) page = 0;
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
|
||||
var totalPages = await _cacheService.CacheBookmarkForSeries(userId, seriesId);
|
||||
if (page > totalPages)
|
||||
@ -141,9 +160,9 @@ public class ReaderController : BaseApiController
|
||||
{
|
||||
var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page);
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
|
||||
var format = Path.GetExtension(path).Replace(".", string.Empty);
|
||||
var format = Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@ -164,15 +183,16 @@ public class ReaderController : BaseApiController
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "extractPdf"})]
|
||||
public async Task<ActionResult<IEnumerable<FileDimensionDto>>> GetFileDimensions(int chapterId, bool extractPdf = false)
|
||||
{
|
||||
if (chapterId <= 0) return null;
|
||||
if (chapterId <= 0) return ArraySegment<FileDimensionDto>.Empty;
|
||||
var chapter = await _cacheService.Ensure(chapterId, extractPdf);
|
||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
||||
return Ok(_cacheService.GetCachedFileDimensions(chapterId));
|
||||
return Ok(_cacheService.GetCachedFileDimensions(_cacheService.GetCachePath(chapterId)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
|
||||
/// </summary>
|
||||
/// <remarks>This is generally the first call when attempting to read to allow pre-generation of assets needed for reading</remarks>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="extractPdf">Should Kavita extract pdf into images. Defaults to false.</param>
|
||||
/// <param name="includeDimensions">Include file dimensions. Only useful for image based reading</param>
|
||||
@ -181,7 +201,7 @@ public class ReaderController : BaseApiController
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "extractPdf", "includeDimensions"})]
|
||||
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false)
|
||||
{
|
||||
if (chapterId <= 0) return null; // This can happen occasionally from UI, we should just ignore
|
||||
if (chapterId <= 0) return Ok(null); // This can happen occasionally from UI, we should just ignore
|
||||
var chapter = await _cacheService.Ensure(chapterId, extractPdf);
|
||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
||||
|
||||
@ -208,7 +228,7 @@ public class ReaderController : BaseApiController
|
||||
|
||||
if (includeDimensions)
|
||||
{
|
||||
info.PageDimensions = _cacheService.GetCachedFileDimensions(chapterId);
|
||||
info.PageDimensions = _cacheService.GetCachedFileDimensions(_cacheService.GetCachePath(chapterId));
|
||||
info.DoublePairs = _readerService.GetPairs(info.PageDimensions);
|
||||
}
|
||||
|
||||
@ -240,21 +260,31 @@ public class ReaderController : BaseApiController
|
||||
/// Returns various information about all bookmark files for a Series. Side effect: This will cache the bookmark images for reading.
|
||||
/// </summary>
|
||||
/// <param name="seriesId">Series Id for all bookmarks</param>
|
||||
/// <param name="includeDimensions">Include file dimensions (extra I/O). Defaults to true.</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("bookmark-info")]
|
||||
public async Task<ActionResult<BookmarkInfoDto>> GetBookmarkInfo(int seriesId)
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"seriesId", "includeDimensions"})]
|
||||
public async Task<ActionResult<BookmarkInfoDto>> GetBookmarkInfo(int seriesId, bool includeDimensions = true)
|
||||
{
|
||||
var totalPages = await _cacheService.CacheBookmarkForSeries(User.GetUserId(), seriesId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.None);
|
||||
|
||||
return Ok(new BookmarkInfoDto()
|
||||
var info = new BookmarkInfoDto()
|
||||
{
|
||||
SeriesName = series.Name,
|
||||
SeriesName = series!.Name,
|
||||
SeriesFormat = series.Format,
|
||||
SeriesId = series.Id,
|
||||
LibraryId = series.LibraryId,
|
||||
Pages = totalPages,
|
||||
});
|
||||
};
|
||||
|
||||
if (includeDimensions)
|
||||
{
|
||||
info.PageDimensions = _cacheService.GetCachedFileDimensions(_cacheService.GetBookmarkCachePath(seriesId));
|
||||
info.DoublePairs = _readerService.GetPairs(info.PageDimensions);
|
||||
}
|
||||
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
|
||||
@ -267,6 +297,7 @@ public class ReaderController : BaseApiController
|
||||
public async Task<ActionResult> MarkRead(MarkReadDto markReadDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
if (user == null) return Unauthorized();
|
||||
await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
|
||||
@ -284,6 +315,7 @@ public class ReaderController : BaseApiController
|
||||
public async Task<ActionResult> MarkUnread(MarkReadDto markReadDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
if (user == null) return Unauthorized();
|
||||
await _readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
|
||||
@ -300,6 +332,7 @@ public class ReaderController : BaseApiController
|
||||
public async Task<ActionResult> MarkVolumeAsUnread(MarkVolumeReadDto markVolumeReadDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||
await _readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters);
|
||||
@ -323,9 +356,10 @@ public class ReaderController : BaseApiController
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
|
||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||
if (user == null) return Unauthorized();
|
||||
await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, markVolumeReadDto.SeriesId,
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, markVolumeReadDto.SeriesId,
|
||||
markVolumeReadDto.VolumeId, 0, chapters.Sum(c => c.Pages)));
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
@ -346,6 +380,7 @@ public class ReaderController : BaseApiController
|
||||
public async Task<ActionResult> MarkMultipleAsRead(MarkVolumesReadDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
if (user == null) return Unauthorized();
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
|
||||
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
|
||||
@ -374,6 +409,7 @@ public class ReaderController : BaseApiController
|
||||
public async Task<ActionResult> MarkMultipleAsUnread(MarkVolumesReadDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
if (user == null) return Unauthorized();
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
|
||||
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
|
||||
@ -401,6 +437,7 @@ public class ReaderController : BaseApiController
|
||||
public async Task<ActionResult> MarkMultipleSeriesAsRead(MarkMultipleSeriesAsReadDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
if (user == null) return Unauthorized();
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
|
||||
@ -426,6 +463,7 @@ public class ReaderController : BaseApiController
|
||||
public async Task<ActionResult> MarkMultipleSeriesAsUnread(MarkMultipleSeriesAsReadDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
if (user == null) return Unauthorized();
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true);
|
||||
@ -499,44 +537,6 @@ public class ReaderController : BaseApiController
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, userId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks every chapter that is sorted below the passed number as Read. This will not mark any specials as read.
|
||||
/// </summary>
|
||||
/// <remarks>This is built for Tachiyomi and is not expected to be called by any other place</remarks>
|
||||
/// <returns></returns>
|
||||
[Obsolete("Deprecated. Use 'Tachiyomi/mark-chapter-until-as-read'")]
|
||||
[HttpPost("mark-chapter-until-as-read")]
|
||||
public async Task<ActionResult<bool>> MarkChaptersUntilAsRead(int seriesId, float chapterNumber)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
user.Progresses ??= new List<AppUserProgress>();
|
||||
|
||||
// Tachiyomi sends chapter 0.0f when there's no chapters read.
|
||||
// Due to the encoding for volumes this marks all chapters in volume 0 (loose chapters) as read so we ignore it
|
||||
if (chapterNumber == 0.0f) return true;
|
||||
|
||||
if (chapterNumber < 1.0f)
|
||||
{
|
||||
// This is a hack to track volume number. We need to map it back by x100
|
||||
var volumeNumber = int.Parse($"{chapterNumber * 100f}");
|
||||
await _readerService.MarkVolumesUntilAsRead(user, seriesId, volumeNumber);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _readerService.MarkChaptersUntilAsRead(user, seriesId, chapterNumber);
|
||||
}
|
||||
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok(true);
|
||||
if (await _unitOfWork.CommitAsync()) return Ok(true);
|
||||
|
||||
await _unitOfWork.RollbackAsync();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of bookmarked pages for a given Chapter
|
||||
/// </summary>
|
||||
@ -546,6 +546,7 @@ public class ReaderController : BaseApiController
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(user.Id, chapterId));
|
||||
}
|
||||
@ -559,6 +560,7 @@ public class ReaderController : BaseApiController
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks(FilterDto filterDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
|
||||
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id, filterDto));
|
||||
@ -573,6 +575,7 @@ public class ReaderController : BaseApiController
|
||||
public async Task<ActionResult> RemoveBookmarks(RemoveBookmarkForSeriesDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok("Nothing to remove");
|
||||
|
||||
try
|
||||
@ -612,6 +615,7 @@ public class ReaderController : BaseApiController
|
||||
public async Task<ActionResult> BulkRemoveBookmarks(BulkRemoveBookmarkForSeriesDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok("Nothing to remove");
|
||||
|
||||
try
|
||||
@ -648,6 +652,7 @@ public class ReaderController : BaseApiController
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(user.Id, volumeId));
|
||||
}
|
||||
@ -661,6 +666,7 @@ public class ReaderController : BaseApiController
|
||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||
|
||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(user.Id, seriesId));
|
||||
@ -725,7 +731,7 @@ public class ReaderController : BaseApiController
|
||||
/// <param name="volumeId"></param>
|
||||
/// <param name="currentChapterId"></param>
|
||||
/// <returns>chapter id for next manga</returns>
|
||||
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new string[] { "seriesId", "volumeId", "currentChapterId"})]
|
||||
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] { "seriesId", "volumeId", "currentChapterId"})]
|
||||
[HttpGet("next-chapter")]
|
||||
public async Task<ActionResult<int>> GetNextChapter(int seriesId, int volumeId, int currentChapterId)
|
||||
{
|
||||
@ -744,7 +750,7 @@ public class ReaderController : BaseApiController
|
||||
/// <param name="volumeId"></param>
|
||||
/// <param name="currentChapterId"></param>
|
||||
/// <returns>chapter id for next manga</returns>
|
||||
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new string[] { "seriesId", "volumeId", "currentChapterId"})]
|
||||
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] { "seriesId", "volumeId", "currentChapterId"})]
|
||||
[HttpGet("prev-chapter")]
|
||||
public async Task<ActionResult<int>> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId)
|
||||
{
|
||||
@ -759,6 +765,7 @@ public class ReaderController : BaseApiController
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("time-left")]
|
||||
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] { "seriesId"})]
|
||||
public async Task<ActionResult<HourEstimateRangeDto>> GetEstimateToCompletion(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
|
@ -1,22 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.DTOs.ReadingLists.CBL;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
@ -27,14 +22,12 @@ public class ReadingListController : BaseApiController
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IReadingListService _readingListService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService, IDirectoryService directoryService)
|
||||
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
_readingListService = readingListService;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -54,13 +47,15 @@ public class ReadingListController : BaseApiController
|
||||
/// </summary>
|
||||
/// <param name="includePromoted">Include Promoted Reading Lists along with user's Reading Lists. Defaults to true</param>
|
||||
/// <param name="userParams">Pagination parameters</param>
|
||||
/// <param name="sortByLastModified">Sort by last modified (most recent first) or by title (alphabetical)</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("lists")]
|
||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams, bool includePromoted = true)
|
||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams,
|
||||
bool includePromoted = true, bool sortByLastModified = false)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, includePromoted,
|
||||
userParams);
|
||||
userParams, sortByLastModified);
|
||||
Response.AddPaginationHeader(items.CurrentPage, items.PageSize, items.TotalCount, items.TotalPages);
|
||||
|
||||
return Ok(items);
|
||||
@ -186,8 +181,8 @@ public class ReadingListController : BaseApiController
|
||||
[HttpPost("create")]
|
||||
public async Task<ActionResult<ReadingListDto>> CreateList(CreateReadingListDto dto)
|
||||
{
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.ReadingLists);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
@ -428,6 +423,18 @@ public class ReadingListController : BaseApiController
|
||||
return Ok("Nothing to do");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of characters associated with the reading list
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("characters")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute)]
|
||||
public ActionResult<IEnumerable<PersonDto>> GetCharactersForList(int readingListId)
|
||||
{
|
||||
return Ok(_unitOfWork.ReadingListRepository.GetReadingListCharactersAsync(readingListId));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
@ -484,22 +491,4 @@ public class ReadingListController : BaseApiController
|
||||
if (string.IsNullOrEmpty(name)) return true;
|
||||
return Ok(await _unitOfWork.ReadingListRepository.ReadingListExists(name));
|
||||
}
|
||||
|
||||
// [HttpPost("import-cbl")]
|
||||
// public async Task<ActionResult<CblImportSummaryDto>> ImportCbl([FromForm(Name = "cbl")] IFormFile file, [FromForm(Name = "dryRun")] bool dryRun = false)
|
||||
// {
|
||||
// var userId = User.GetUserId();
|
||||
// var filename = Path.GetRandomFileName();
|
||||
// var outputFile = Path.Join(_directoryService.TempDirectory, filename);
|
||||
//
|
||||
// await using var stream = System.IO.File.Create(outputFile);
|
||||
// await file.CopyToAsync(stream);
|
||||
// stream.Close();
|
||||
// var cbl = ReadingListService.LoadCblFromPath(outputFile);
|
||||
//
|
||||
// // We need to pass the temp file back
|
||||
//
|
||||
// var importSummary = await _readingListService.ValidateCblFile(userId, cbl);
|
||||
// return importSummary.Results.Any() ? Ok(importSummary) : Ok(await _readingListService.CreateReadingListFromCbl(userId, cbl, dryRun));
|
||||
// }
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ public class RecommendedController : BaseApiController
|
||||
[HttpGet("highly-rated")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var userId = User.GetUserId()!;
|
||||
userParams ??= new UserParams();
|
||||
var series = await _unitOfWork.SeriesRepository.GetHighlyRated(userId, libraryId, userParams);
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
@ -54,6 +53,7 @@ public class SearchController : BaseApiController
|
||||
queryString = Services.Tasks.Scanner.Parser.Parser.CleanQuery(queryString);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized();
|
||||
var libraries = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(user.Id, QueryContext.Search).ToList();
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
@ -11,7 +10,6 @@ using API.DTOs.Metadata;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
@ -113,7 +111,7 @@ public class SeriesController : BaseApiController
|
||||
}
|
||||
|
||||
[HttpGet("volume")]
|
||||
public async Task<ActionResult<VolumeDto>> GetVolume(int volumeId)
|
||||
public async Task<ActionResult<VolumeDto?>> GetVolume(int volumeId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId));
|
||||
@ -137,7 +135,7 @@ public class SeriesController : BaseApiController
|
||||
public async Task<ActionResult> UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Ratings);
|
||||
if (!await _seriesService.UpdateRating(user, updateSeriesRatingDto)) return BadRequest("There was a critical error.");
|
||||
if (!await _seriesService.UpdateRating(user!, updateSeriesRatingDto)) return BadRequest("There was a critical error.");
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -159,14 +157,14 @@ public class SeriesController : BaseApiController
|
||||
}
|
||||
|
||||
series.Name = updateSeries.Name.Trim();
|
||||
series.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name);
|
||||
if (!string.IsNullOrEmpty(updateSeries.SortName.Trim()))
|
||||
series.NormalizedName = series.Name.ToNormalized();
|
||||
if (!string.IsNullOrEmpty(updateSeries.SortName?.Trim()))
|
||||
{
|
||||
series.SortName = updateSeries.SortName.Trim();
|
||||
}
|
||||
|
||||
series.LocalizedName = updateSeries.LocalizedName.Trim();
|
||||
series.NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.LocalizedName);
|
||||
series.LocalizedName = updateSeries.LocalizedName?.Trim();
|
||||
series.NormalizedLocalizedName = series.LocalizedName?.ToNormalized();
|
||||
|
||||
series.NameLocked = updateSeries.NameLocked;
|
||||
series.SortNameLocked = updateSeries.SortNameLocked;
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -8,7 +7,6 @@ using API.DTOs.Jobs;
|
||||
using API.DTOs.Stats;
|
||||
using API.DTOs.Update;
|
||||
using API.Extensions;
|
||||
using API.Logging;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using Hangfire;
|
||||
@ -16,7 +14,6 @@ using Hangfire.Storage;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TaskScheduler = API.Services.TaskScheduler;
|
||||
@ -33,14 +30,15 @@ public class ServerController : BaseApiController
|
||||
private readonly IVersionUpdaterService _versionUpdaterService;
|
||||
private readonly IStatsService _statsService;
|
||||
private readonly ICleanupService _cleanupService;
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IScannerService _scannerService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
|
||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger,
|
||||
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
||||
ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService, IScannerService scannerService, IAccountService accountService)
|
||||
ICleanupService cleanupService, IBookmarkService bookmarkService, IScannerService scannerService, IAccountService accountService,
|
||||
ITaskScheduler taskScheduler)
|
||||
{
|
||||
_applicationLifetime = applicationLifetime;
|
||||
_logger = logger;
|
||||
@ -49,23 +47,10 @@ public class ServerController : BaseApiController
|
||||
_versionUpdaterService = versionUpdaterService;
|
||||
_statsService = statsService;
|
||||
_cleanupService = cleanupService;
|
||||
_emailService = emailService;
|
||||
_bookmarkService = bookmarkService;
|
||||
_scannerService = scannerService;
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to Restart the server. Does not work, will shutdown the instance.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("restart")]
|
||||
public ActionResult RestartServer()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is restarting server from admin dashboard", User.GetUsername());
|
||||
|
||||
_applicationLifetime.StopApplication();
|
||||
return Ok();
|
||||
_taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -154,10 +139,14 @@ public class ServerController : BaseApiController
|
||||
{
|
||||
if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToWebP", Array.Empty<object>(),
|
||||
TaskScheduler.DefaultQueue, true)) return Ok();
|
||||
BackgroundJob.Enqueue(() => _bookmarkService.ConvertAllCoverToWebP());
|
||||
BackgroundJob.Enqueue(() => _taskScheduler.CovertAllCoversToWebP());
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads all the log files via a zip
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("logs")]
|
||||
public ActionResult GetLogs()
|
||||
{
|
||||
@ -182,6 +171,10 @@ public class ServerController : BaseApiController
|
||||
return Ok(await _versionUpdaterService.CheckForUpdate());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pull the Changelog for Kavita from Github and display
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("changelog")]
|
||||
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog()
|
||||
{
|
||||
@ -200,6 +193,10 @@ public class ServerController : BaseApiController
|
||||
return Ok(await _accountService.CheckIfAccessible(Request));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of reoccurring jobs. Scheduled ad-hoc jobs will not be returned.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("jobs")]
|
||||
public ActionResult<IEnumerable<JobDto>> GetJobs()
|
||||
{
|
||||
@ -214,6 +211,7 @@ public class ServerController : BaseApiController
|
||||
});
|
||||
|
||||
return Ok(recurringJobs);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
@ -46,7 +45,6 @@ public class SettingsController : BaseApiController
|
||||
_libraryWatcher = libraryWatcher;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("base-url")]
|
||||
public async Task<ActionResult<string>> GetBaseUrl()
|
||||
{
|
||||
@ -77,11 +75,11 @@ public class SettingsController : BaseApiController
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("reset-ip-addresses")]
|
||||
public async Task<ActionResult<ServerSettingDto>> ResetIPAddressesSettings()
|
||||
public async Task<ActionResult<ServerSettingDto>> ResetIpAddressesSettings()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is resetting IP Addresses Setting", User.GetUsername());
|
||||
var ipAddresses = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.IpAddresses);
|
||||
ipAddresses.Value = Configuration.DefaultIPAddresses;
|
||||
ipAddresses.Value = Configuration.DefaultIpAddresses;
|
||||
_unitOfWork.SettingsRepository.Update(ipAddresses);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync())
|
||||
@ -92,6 +90,28 @@ public class SettingsController : BaseApiController
|
||||
return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the Base url
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("reset-base-url")]
|
||||
public async Task<ActionResult<ServerSettingDto>> ResetBaseUrlSettings()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is resetting Base Url Setting", User.GetUsername());
|
||||
var baseUrl = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl);
|
||||
baseUrl.Value = Configuration.DefaultBaseUrl;
|
||||
_unitOfWork.SettingsRepository.Update(baseUrl);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync())
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
Configuration.BaseUrl = baseUrl.Value;
|
||||
return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the email service url
|
||||
/// </summary>
|
||||
@ -117,7 +137,9 @@ public class SettingsController : BaseApiController
|
||||
[HttpPost("test-email-url")]
|
||||
public async Task<ActionResult<EmailTestResultDto>> TestEmailServiceUrl(TestEmailDto dto)
|
||||
{
|
||||
return Ok(await _emailService.TestConnectivity(dto.Url));
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId());
|
||||
var emailService = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value;
|
||||
return Ok(await _emailService.TestConnectivity(dto.Url, user!.Email, !emailService.Equals(EmailService.DefaultApiUrl)));
|
||||
}
|
||||
|
||||
|
||||
@ -178,7 +200,7 @@ public class SettingsController : BaseApiController
|
||||
}
|
||||
|
||||
setting.Value = updateSettingsDto.IpAddresses;
|
||||
// IpAddesses is managed in appSetting.json
|
||||
// IpAddresses is managed in appSetting.json
|
||||
Configuration.IpAddresses = updateSettingsDto.IpAddresses;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
@ -192,6 +214,7 @@ public class SettingsController : BaseApiController
|
||||
? $"{path}/"
|
||||
: path;
|
||||
setting.Value = path;
|
||||
Configuration.BaseUrl = updateSettingsDto.BaseUrl;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ public class StatsController : BaseApiController
|
||||
public async Task<ActionResult<UserReadStatistics>> GetUserReadStatistics(int userId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user.Id != userId && !await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole))
|
||||
if (user!.Id != userId && !await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole))
|
||||
return Unauthorized("You are not authorized to view another user's statistics");
|
||||
|
||||
return Ok(await _statService.GetUserReadStatistics(userId, new List<int>()));
|
||||
@ -116,7 +116,7 @@ public class StatsController : BaseApiController
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
|
||||
if (!isAdmin && userId != user.Id) return BadRequest();
|
||||
if (!isAdmin && userId != user!.Id) return BadRequest();
|
||||
|
||||
return Ok(await _statService.ReadCountByDay(userId, days));
|
||||
}
|
||||
@ -136,7 +136,7 @@ public class StatsController : BaseApiController
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
|
||||
if (!isAdmin && userId != user.Id) return BadRequest();
|
||||
if (!isAdmin && userId != user!.Id) return BadRequest();
|
||||
|
||||
return Ok(await _statService.GetReadingHistory(userId));
|
||||
}
|
||||
|
@ -43,7 +43,8 @@ public class TachiyomiController : BaseApiController
|
||||
[HttpPost("mark-chapter-until-as-read")]
|
||||
public async Task<ActionResult<bool>> MarkChaptersUntilAsRead(int seriesId, float chapterNumber)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
var user = (await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(),
|
||||
AppUserIncludes.Progress))!;
|
||||
return Ok(await _tachiyomiService.MarkChaptersUntilAsRead(user, seriesId, chapterNumber));
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Theme;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using Kavita.Common;
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Uploads;
|
||||
@ -10,7 +9,6 @@ using Flurl.Http;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
@ -93,8 +91,9 @@ public class UploadController : BaseApiController
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, ImageService.GetSeriesFormat(uploadFileDto.Id));
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
|
||||
if (series == null) return BadRequest("Invalid Series");
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
@ -140,8 +139,9 @@ public class UploadController : BaseApiController
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id);
|
||||
if (tag == null) return BadRequest("Invalid Tag id");
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
@ -190,8 +190,9 @@ public class UploadController : BaseApiController
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
|
||||
if (readingList == null) return BadRequest("Reading list is not valid");
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
@ -218,6 +219,19 @@ public class UploadController : BaseApiController
|
||||
return BadRequest("Unable to save cover image to Reading List");
|
||||
}
|
||||
|
||||
private async Task<string> CreateThumbnail(UploadFileDto uploadFileDto, string filename, int thumbnailSize = 0)
|
||||
{
|
||||
var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP;
|
||||
if (thumbnailSize > 0)
|
||||
{
|
||||
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
|
||||
filename, convertToWebP, thumbnailSize);
|
||||
}
|
||||
|
||||
return _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
|
||||
filename, convertToWebP);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
|
||||
/// </summary>
|
||||
@ -238,7 +252,8 @@ public class UploadController : BaseApiController
|
||||
try
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
|
||||
if (chapter == null) return BadRequest("Invalid Chapter");
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
@ -246,9 +261,12 @@ public class UploadController : BaseApiController
|
||||
chapter.CoverImageLocked = true;
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
|
||||
if (volume != null)
|
||||
{
|
||||
volume.CoverImage = chapter.CoverImage;
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
}
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
@ -301,8 +319,7 @@ public class UploadController : BaseApiController
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url,
|
||||
$"{ImageService.GetLibraryFormat(uploadFileDto.Id)}", ImageService.LibraryThumbnailWidth);
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetLibraryFormat(uploadFileDto.Id)}", ImageService.LibraryThumbnailWidth);
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
@ -340,19 +357,20 @@ public class UploadController : BaseApiController
|
||||
try
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
if (chapter == null) return BadRequest("Chapter no longer exists");
|
||||
var originalFile = chapter.CoverImage;
|
||||
chapter.CoverImage = string.Empty;
|
||||
chapter.CoverImageLocked = false;
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
|
||||
var volume = (await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId))!;
|
||||
volume.CoverImage = chapter.CoverImage;
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
var series = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId))!;
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
System.IO.File.Delete(originalFile);
|
||||
if (originalFile != null) System.IO.File.Delete(originalFile);
|
||||
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true);
|
||||
return Ok();
|
||||
}
|
||||
|
@ -4,10 +4,7 @@ using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -41,18 +38,16 @@ public class UsersController : BaseApiController
|
||||
return BadRequest("Could not delete the user.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all users of this server
|
||||
/// </summary>
|
||||
/// <param name="includePending">This will include pending members</param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<MemberDto>>> GetUsers()
|
||||
public async Task<ActionResult<IEnumerable<MemberDto>>> GetUsers(bool includePending = false)
|
||||
{
|
||||
return Ok(await _unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpGet("pending")]
|
||||
public async Task<ActionResult<IEnumerable<MemberDto>>> GetPendingUsers()
|
||||
{
|
||||
return Ok(await _unitOfWork.UserRepository.GetPendingMemberDtosAsync());
|
||||
return Ok(await _unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync(!includePending));
|
||||
}
|
||||
|
||||
[HttpGet("myself")]
|
||||
@ -68,6 +63,7 @@ public class UsersController : BaseApiController
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
|
||||
if (library == null) return BadRequest("Library does not exist");
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
|
||||
}
|
||||
|
||||
@ -83,9 +79,8 @@ public class UsersController : BaseApiController
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(),
|
||||
AppUserIncludes.UserPreferences);
|
||||
var existingPreferences = user.UserPreferences;
|
||||
|
||||
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
||||
if (user == null) return Unauthorized();
|
||||
var existingPreferences = user!.UserPreferences;
|
||||
|
||||
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
|
||||
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
|
||||
@ -102,22 +97,24 @@ public class UsersController : BaseApiController
|
||||
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
|
||||
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
|
||||
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
||||
existingPreferences.BookReaderWritingStyle = preferencesDto.BookReaderWritingStyle;
|
||||
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
|
||||
existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode;
|
||||
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
|
||||
existingPreferences.GlobalPageLayoutMode = preferencesDto.GlobalPageLayoutMode;
|
||||
existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries;
|
||||
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
|
||||
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
||||
existingPreferences.Theme = preferencesDto.Theme ?? await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
||||
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
|
||||
existingPreferences.NoTransitions = preferencesDto.NoTransitions;
|
||||
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
|
||||
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
|
||||
|
||||
_unitOfWork.UserRepository.Update(existingPreferences);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
|
||||
return Ok(preferencesDto);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
@ -37,6 +36,9 @@ public class WantToReadController : BaseApiController
|
||||
userParams ??= new UserParams();
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(User.GetUserId(), userParams, filterDto);
|
||||
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(User.GetUserId(), pagedList);
|
||||
|
||||
return Ok(pagedList);
|
||||
}
|
||||
|
||||
@ -56,6 +58,7 @@ public class WantToReadController : BaseApiController
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(),
|
||||
AppUserIncludes.WantToRead);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var existingIds = user.WantToRead.Select(s => s.Id).ToList();
|
||||
existingIds.AddRange(dto.SeriesIds);
|
||||
@ -84,6 +87,7 @@ public class WantToReadController : BaseApiController
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(),
|
||||
AppUserIncludes.WantToRead);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
user.WantToRead = user.WantToRead.Where(s => !dto.SeriesIds.Contains(s.Id)).ToList();
|
||||
|
||||
|
@ -7,10 +7,10 @@ public class AgeRestrictionDto
|
||||
/// <summary>
|
||||
/// The maximum age rating a user has access to. -1 if not applicable
|
||||
/// </summary>
|
||||
public AgeRating AgeRating { get; set; } = AgeRating.NotApplicable;
|
||||
public required AgeRating AgeRating { get; set; } = AgeRating.NotApplicable;
|
||||
/// <summary>
|
||||
/// Are Unknowns explicitly allowed against age rating
|
||||
/// </summary>
|
||||
/// <remarks>Unknown is always lowest and default age rating. Setting this to false will ensure Teen age rating applies and unknowns are still filtered</remarks>
|
||||
public bool IncludeUnknowns { get; set; } = false;
|
||||
public required bool IncludeUnknowns { get; set; } = false;
|
||||
}
|
||||
|
@ -5,12 +5,12 @@ namespace API.DTOs.Account;
|
||||
public class ConfirmEmailDto
|
||||
{
|
||||
[Required]
|
||||
public string Email { get; set; }
|
||||
public string Email { get; set; } = default!;
|
||||
[Required]
|
||||
public string Token { get; set; }
|
||||
public string Token { get; set; } = default!;
|
||||
[Required]
|
||||
[StringLength(32, MinimumLength = 6)]
|
||||
public string Password { get; set; }
|
||||
public string Password { get; set; } = default!;
|
||||
[Required]
|
||||
public string Username { get; set; }
|
||||
public string Username { get; set; } = default!;
|
||||
}
|
||||
|