mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-02 13:14:28 -04:00
v0.6.0 - Polish, Polish, Polish + Send To Support! (#1604)
* New Scan Loop (#1447) * Staging the code for the new scan loop. * Implemented a basic idea of changes on drives triggering scan loop. Issues: 1. Scan by folder does not work, 2. Queuing system is very hacky and needs a separate thread, 3. Performance degregation could be very real. * Started writing unit test for new loop code * Implemented a basic method to scan a folder path with ignore support (not implemented, code in place) * Added some code to the parser to build out the idea of processing series in batches based on some top level folder. * Scan Series now uses the new code (folder based parsing) and now handles the LocalizedSeries issue. * Got library scan working with the new folder-based scan loop. Updated code to set FolderPath (for improved scan times and partial scan support). * Wrote some notes on update library scan loop. * Removed migration for merge * Reapplied the SeriesFolder migration after merge * Refactored a check that used multiple db calls into one. * Made lots of progress on ignore support, but some confusion on underlying library. Ticket created. On hold till then. * Updated Scan Library and Scan Series to exit early if no changes are on the underlying folders that need to be scanned. * Implemented the ability to have .kavitaignore files within your directories and Kavita will parse them and ignore files and directories based on rules within them. * Fixed an issue where ignore files nested wouldn't stack with higher level ignores * Wrote out some basic code that showcases how we can scan series or library based on file events on the underlying system. Very buggy, needs lots of edge case testing and logging and dupplication checking. * Things are working kinda. I'm getting lost in my own code and complexity. I'm not sure it's worth it. * Refactored ScanFiles out to Directory Service. * Refactored more code out to keep the code clean. * More unit tests * Refactored the signature of ParsedSeries to use IList. Started writing unit tests and reworked the UpdateLibrary to work how it used to with new scan loop code (note: using async update library/series does not work). * Fixed the bug where processSeriesInfos was being invoked twice per series and made the code work very similar to old code (except loose leaf files dont work) but with folder based scanning. * Prep for unit tests (updating broken ones with new implementations) * Just some notes. Not sure I want to finish this work. * Refactored the LibraryWatcher with some comments and state variables. * Undid the migrations in case I don't move forward with this branch * Started to clean the code and prepare for finishing this work. * Fixed a bad merge * Updated signatures to cleanup the code and commit to the new strategy for scanning. * Swapped out the code with async processing of series on a small library * The new scan loop is working in both Sync and Async methods. The code is slow and not optimized. This represents a good point to start polling and applying optimizations. * Refactored UpdateSeries out of Scanner and into a dedicated file. * Refactored how ProcessTasks are awaited to allow more async * Fixed an issue where side nav item wouldn't show correct highlight and migrated to OnPush * Moved where we start to stopwatch to encapsulate the full scan * Cleaned up SignalR events to report correctly (still needs a redesign) * Remove the "remove" code until I figure it out * Put in extremely expensive series deletion code for library scan. * Have Genre and Tag update the DB immediately to avoid dup issues * Taking a break * Moving to a lock with People was successful. Need to apply to others. * Refactored code for series level and tag and genre with new locking strategy. * New scan loop works. Next up optimization * Swapped out the Kavita log with svg for faster load * Refactored metadata updates to occur when the series are being updated. * Code cleanup * Added a new type of generic message (Info) to inform the user. * Code cleanup * Implemented an optimization which prevents any I/O (other than an attribute lookup) for Library/Series Scan. This can bring a recently updated library on network storage (650 series) to fully process in 2 seconds. Fixed a bug where File Analysis was running everytime for each non-epub file. * Fixed ARM x64 builds not being able to view PDF cover images due to a bad update in DocNet. * Some code cleanup * Added experimental signalr update code to have a more natural refresh of library-detail page * Hooked in ability to send new series events to UI * Moved all scan (file scan only) tasks into Scan Queue. Made it so scheduled ScanLibraries will now check if any existing task is being run and reschedule for 3 hours, and 10 mins for scan series. * Implemented the info event in the events widget and added a clear all button to dismiss all infos and errors. Added --event-widget-info-bg-color * Remove --drawer-background-color since it's not used * When new series added, inject directly into the view. * Some debug code cleanup * Fixed up the unit tests * Ensure all config directories exist on startup * Disabled Library Watching (that will go in next build) * Ensure update for series is admin only * Lots of code changes, scan series kinda works, specials are splitting, optimizations are failing. Demotivated on this work again. * Removed SeriesFolder migration * Added the SeriesFolder migration * Added a new pipe for dates so we can provide some nicer defaults. Added folder path to the series detail. * The scan optimizations now work for NTFS systems. * Removed a TODO * Migrated all the times to use DateTime.Now and not Utc. * Refactored some repo calls to use the includes flag pattern * Implemented a check for the library scan optimization check to validate if the library was updated (type change, library rename, folder change, or series deleted) and let the optimization be bypassed. * Added another optimization which will use just folder attribute of last write time if the drive is not NTFS. * Fixed a unit test * Some code cleanup * Bump versions by dotnet-bump-version. * Misc UI Fixes (#1450) * Fixed collection cover images not rendering * added a try/catch on sending email, so we fail silently if it doesn't send. * Fixed Go Back not returning to last scroll position due to layoutmode change resetting, despite nothing changing. * Fixed a bug where when turning between pages on default mode, the height calculations could get skewed. * Fixed a missing case for card item where it wouldn't show tooltip title for series. * Bump versions by dotnet-bump-version. * New Scan Loop Fixes (#1452) * Refactored ScanSeries to avoid a lot of extra work and fixed a bug where Scan Series would invoke the processing twice. Refactored the series selection code during process such that we use Localized Name as well, for cases where the original name was changed. Undid an optimization around Last Write time, since Linux file systems match how NTFS works. * Fixed part of the query * Added a NormalizedLocalizedName for quick searching in which a series needs grouping. Reworked scan loop code a bit to ensure we don't do extra work. Tweaked the widget logic to help display better and not show "Nothing going on here". * Fixed a bug where archives with ._ files would be counted as valid files, while they are actually just metadata files on Mac's. * Fixed a broken unit test * Bump versions by dotnet-bump-version. * Simplify parent lookup with Directory.GetParent (#1455) * Simplify parent lookup with Directory.GetParent * Address comments * Bump versions by dotnet-bump-version. * Scan Loop Fixes (#1459) * Added Last Folder Scanned time to series info modal. Tweaked the info event detail modal to have a primary and thus be auto-dismissable * Added an error event when multiple series are found in processing a series. * Fixed a bug where a series could get stuck with other series due to a bad select query. Started adding the force flag hook for the UI and designing the confirm. Confirm service now also has ability to hide the close button. Updated error events and logging in the loop, to be more informative * Fixed a bug where confirm service wasn't showing the proper body content. * Hooked up force scan series * refresh metadata now has force update * Fixed up the messaging with the prompt on scan, hooked it up properly in the scan library to avoid the check if the whole library needs to even be scanned. Fixed a bug where NormalizedLocalizedName wasn't being calculated on new entities. Started adding unit tests for this problematic repo method. * Fixed a bug where we updated NormalizedLocalizedName before we set it. * Send an info to the UI when series are spread between multiple library level folders. * Added some logger output when there are no files found in a folder. Return early if there are no files found, so we can avoid some small loops of code. * Fixed an issue where multiple series in a folder with localized series would cause unintended grouping. This is not supported and hence we will warn them and allow the bad grouping. * Added a case where scan series fails due to the folder being removed. We will now log an error * Normalize paths when finding the highest directory till root. * Fixed an issue with Scan Series where changing a series' folder to a different path but the original series folder existed with another series in it, would cause the series to not be deleted. * Fixed some bugs around specials causing a series merge issue on scan series. * Removed a bug marker * Cleaned up some of the scan loop and removed a test I don't need. * Remove any prompts for force flow, it doesn't work well. Leave the API as is though. * Fixed up a check for duplicate ScanLibrary calls * Bump versions by dotnet-bump-version. * Scroll Resume (#1460) * When we navigate from a page then back, resume back on the last scroll key (if clicked) * Resume jump key position when navigating back to a page. Removed some extra blank space on collection detail when a collection doesn't have a summary or cover image. * Ignore progress events on series cards * Added a url to swagger for /, which could be reverse proxy url * Bump versions by dotnet-bump-version. * Misc UI fixes (#1461) * Misc fixes - Fixed modal being stretched when not needed. - Fixed Logo vertical align - Fixed drawer content scroll, and from it being squished due to overridden by bootstrap. * series detail cover image stretch fix - Fixes: Fixes series detail cover image being stretched on larger resolutions * fixing empty lists scrollbar * Fixing want to read error * fixing unnecessary scrollbar * Fixing recently updated tooltip * Bump versions by dotnet-bump-version. * Folder Watching (#1467) * Hooked in a server setting to enable/disable folder watching * Validated the file rename change event * Validated delete file works * Tweaked some logic to determine if a change occurs on a folder or a file. * Added a note for an upcoming branch * Some minor changes in the loop that just shift where code runs. * Implemented ScanFolder api * Ensure we restart watchers when we modify a library folder. * Fixed a unit test * Bump versions by dotnet-bump-version. * More Scan Loop Bugfixes (#1471) * Updated scan time for watcher to 30 seconds for non-dev. Moved ScanFolder off the Scan queue as it doesn't need to be there. Updated loggers * Fixed jumpbar missing * Tweaked the messaging for CoverGen * When we return early due to nothing being done on library and series scan, make sure we kick off other tasks that need to occur. * Fixed a foreign constraint issue on Volumes when we were adding to a new series. * Fixed a case where when picking normalized series, capitalization differences wouldn't stack when they should. * Reduced the logging output on dev and prod settings. * Fixed a bug in the code that finds the highest directory from a file, where we were not checking against a normalized path. * Cleaned up some code * Fixed broken unit tests * Bump versions by dotnet-bump-version. * More Scan Loop Fixes (#1473) * Added a ToList() to avoid a bug where a person could be removed from a list while iterating over the list. * When deleting a series, want to read page will now automatically remove that series from the view. * Fixed a series lookup which was ignoring format * Ignore XML comment warnings * Removed a note since it was already working that way * Fixed unit test * Bump versions by dotnet-bump-version. * Misc UI Fixes (#1477) * Tweaked a Migration to log correctly only if something is going to be done. * Refactored Reading List Controller code into a dedicated service and cleaned up some methods that aren't needed anymore. * Fixed a bug where adding a new item to a reading list wasn't adding it at the end. * Fixed an issue where collection page would re-render the same covers on multiple items. * Fixed a missing margin-top which made the page extras drawer not render correctly and hence unclosable on small screens. * Added some timeout on manage users screen to give data time to flush. Added a dedicated token log for account flows, in case url encoding plays a part (but from testing it doesn't). * Reverted back to building for ES6 instead of es2020 for old Safari 12.5.5 browsers (10MB difference in build size). * Cleaned up the logic in removing series not found during scan loop. * Tweaked the timings for Library Watcher to 1 min and reprocess queue every 30 seconds. * Bump versions by dotnet-bump-version. * Added fixes for libvips (#1479) * Bump versions by dotnet-bump-version. * Tachiyomi + Fixes (#1481) * Fixed a bootstrap bug * Fixed repeating images on collection detail * Fixed up some logic in library watcher which wasn't processing all of the queue. * When parsing non-epubs in Book library, use Manga parsing for Volume support to better support Light Novels * Fixed some bugs with the tachiyomi plugin api's for progress tracking * Bump versions by dotnet-bump-version. * Adding Health controller (#1480) * Adding Health controller - Added: Added API endpoint for a health check to streamline docker healthy status. * review comment fixes * Bump versions by dotnet-bump-version. * Simplify Folder Watcher (#1484) * Refactored Library Watcher to use Hangfire under the hood. * Support .kavitaignore at root level. * Refactored a lot of the library watching code to process faster and handle when FileSystemWatcher runs out of internal buffer space. It's still not perfect, but good enough for basic use. * Make folder watching as experimental and default it to off by default. * Revert #1479 * Tweaked the messaging for OPDS to remove a note about download role. Moved some code closer to where it's used. * Cleaned up how the events widget reports * Fixed a null issue when deleting series in the UI * Cleaned up some debug code * Added more information for when we skip a scan * Cleaned up some logging messages in CoverGen tasks * More log message tweaks * Added some debug to help identify a rare issue * Fixed a bug where save bookmarks as webp could get reset to false when saving other server settings * Updated some documentation on library watcher. * Make LibraryWatcher fire every 5 mins * Bump versions by dotnet-bump-version. * Sort series by chapter number only when some chapters have no volume (#1487) * Sort series by chapter number only when some chapters have no volume information * Implement a Default static instance of ChapterSortComparer * Further use Default static Comparers * Add missing ToLit() as per comments * SQLite Hangfire (#1488) * Update to use SQLIte for Hangfire to retain information on tasks * Updated all external links to have noopener noreferrer * When watching folders, ensure the folders exist before creating watchers. * Tweaked the messaging for Email Service and added link to the project. * Bump versions by dotnet-bump-version. * Bump versions by dotnet-bump-version. * Fixed typeahead not working correctly (#1490) * Bump versions by dotnet-bump-version. * Release Testing Day 1 (#1491) * Fixed a bug where typeahead wouldn't automatically show results on relationship screen without an additional click. * Tweaked the code which checks if a modification occured to check on seconds rather than minutes * Clear cache will now clear temp/ directory as well. * Fixed an issue where Chrome was caching api responses when it shouldn't had. * Added a cleanup temp code * Ensure genres get removed during series scan when removed from metadata. * Fixed a bug where all epubs with a volume would show as Volume 0 in reading list * When a scan is in progress, don't let the user delete the library. * Bump versions by dotnet-bump-version. * Scan Loop Last Write Time Change (#1492) * Refactored invite user flow to separate error handling on create user flow and email flow. This should help users that have unique situations. * Switch to using files to check LastWriteTime. Debug code in for Robbie to test on rclone * Updated Parser namespace. Changed the LastWriteTime to check all files and folders. * Bump versions by dotnet-bump-version. * Release Testing Day 2 (#1493) * Added a no data section to collection detail. * Remove an optimization for skipping the whole library scan as it wasn't reliable * When resetting password, ensure the input is colored correctly * Fixed setting new password after resetting, throwing an error despite it actually being successful. Fixed incorrect messaging for Password Reset page. * Fixed a bug where reset password would show the side nav button and skew the page. Updated a lot of references to use Typed version for formcontrols. * Removed a migration from 0.5.0, 6 releases ago. * Added a null check so we don't throw an exception when connecting with signalR on unauthenticated users. * Bump versions by dotnet-bump-version. * Fixed a bug where a series with a relationship couldn't be deleted. (#1495) * Bump versions by dotnet-bump-version. * Release Testing Day 3 (#1496) * Tweaked log messaging for library scan when no files were scanned. * When a theme that is set gets removed due to a scan, inform the user to refresh. * Fixed a typo and make Darkness -> Brightness * Make download theme files allowed to be invoked by non-authenticated users, to allow new users to get the default theme. * Hide all series side nav item if there are no libraries exposed to the user * Fixed an API for Tachiyomi when syncing progress * Fixed dashboard not responding to Series Removed and Added events. Ensure we send SeriesRemoved events when they are deleted. * Reverted Hangfire SQLite due to aborted jobs being resumed, when they shouldnt. Fixed some scan loop issues where cover gen wouldn't be invoked always on new libraries. * Bump versions by dotnet-bump-version. * Updating series detail cover style (#1498) # FIxed - Fixed: Fixed an issue with series detail cover when scaled down. * Bump versions by dotnet-bump-version. * v0.5.6 Release (#1499) * Bump versions by dotnet-bump-version. * Bookmark RBS + Dynamic PGO (#1503) * Allow .NET to optimize code as it's running. * Implemented the ability to restrict users Bookmark ability. By default, users will need to now opt-in to get bookmark roles. * Fixed a tachiyomi progress syncing logic bug * Bump versions by dotnet-bump-version. * Updating series detail cover (#1509) * Updating series detail cover # Fixed - Fixed: Fixed an issue where the series detail cover would resize too large on ultra wide displays. * Fixing typos * Bump versions by dotnet-bump-version. * Logging Enhancements (#1521) * Recreated Kavita Logging with Serilog instead of Default. This needs to be move out of the appsettings now, to allow auto updater to patch. * Refactored the code to be completely configured via Code rather than appsettings.json. This is a required step for Auto Updating. * Added in the ability to send logs directly to the UI only for users on the log route. Stopping implementation as Alerts page will handle the rest of the implementation. * Fixed up the backup service to not rely on Config from appsettings.json * Tweaked the Logging levels available * Moved everything over to File-scoped namespaces * Moved everything over to File-scoped namespaces * Code cleanup, removed an old migration and changed so debug logging doesn't print sensitive db data * Removed dead code * Bump versions by dotnet-bump-version. * Misc Enhancements (#1525) * Moved the data connection for the Database out of appsettings.json and hardcoded it. This will allow for more customization and cleaner update process. * Removed unneeded code * Updated pdf viewer to 15.0.0 (pdf 2.6), which now supports east-asian fonts * Fixed up some regex parsing for volumes that have a float number. * Fixed a bug where the tooltip for Publication Status wouldn't show * Fixed some weird parsing rules where v1.1 would parse as volume 1 chapter 1 * Fixed a bug where bookmarking button was hidden for admins without bookmark role (due to migration) * Unified the star rating component in series detail to match metadata filter. * Fixed a bug in the bulk selection code when using shift selection, where the inverse of what was selected would be toggled. * Fixed some old code where if on all series page, only English as a language would return. We now return all languages of all libraries. * Updated api/metadata/languages documentation * Refactored some bookmark api names: get-bookmarks -> chapter-bookmarks, get-all-bookmarks -> all-bookmarks, get-series-bookmarks -> series-bookmarks, etc. * Refactored all cases of createSeriesFilter to filterUtiltityService. Added ability to search for a series on Bookmarks page. Fixed a bug where people filters wouldn't respect the disable flag froms ettings. * Cleaned up a bit of the circular downloader code. * Implemented Russian Parsing * Fixed an issue where some users that had a missing theme entry wouldn't be able to update their user preferences. * Refactored normalization to exclude !, thus allowing series with ! to be different from each other. * Fixed a migration exit case * Fixed broken unit test * Bump versions by dotnet-bump-version. * Fixed a version issue with migration (#1526) * Bump versions by dotnet-bump-version. * Metadata Bugfixes (#1511) * Fix XML deserialization of empty elements to integers * Fix assumption that environment uses US time format * Use series name as SeriesSort in epub * Address some PR comments * Add partial Equals(0 implementation to ComicInfo * Update ComicInfo unittest. Revert previous version * Bump versions by dotnet-bump-version. * Configure Animation Module (#1504) * Configure Animation Module Configure the Animation Module of Angular to disable animation on older iOS devices (<14) where it causes animate not defined errors. * Simplified disableAnimations Removed the regex iOS version check as it seemed to return false on iOS 12.5.5 meaning that the `!('animate' in document.documentElement)` did the job already. This also allows users to enable the experimental feature Web Animations on iOS 12.5.5 to have them enabled again. as note; navigator.userAgent returned the following on an iPad iOS 12.5.5 `Mozilla/5.0 (iPad; CPU OS 12_5_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Mobile/15E148 Safari/604.1` * added console output on disabled Added console.error if Web Animations have been disabled due to the browser not supporting this. * Bump versions by dotnet-bump-version. * Reader Bugs + New Features (#1536) * Updated a typo in manage tasks of Reoccuring -> Recurring * Fixed a bug in MinimumNumberFromRange where a regex wasn't properly constructed which could skew results. * Fixed a bug where Volume numbers that were a float wouldn't render correctly in the manga reader menu. * Added the ability to double click on the image to bookmark it. Optimized the bookmark and unbookmark flows to remove 2 DB calls and reworked some flow of calls to speed it up. Fixed some logic where when using double (manga) flow, both of the images wouldn't show the bookmark effect, despite both of them being saved. Likewise, fixed a bug where both images weren't updating UI state, so switching from double (manga) to single, the second image wouldn't show as bookmarked without a refresh. * Double click works perfectly for bookmarking * Collection cover image chooser will now prompt with all series covers by default. Reset button is now moved up to the first slot if applicable. * When a Completed series is fully read by a user, a nightly task will now remove that series from their Want to Read list. * Added ability to trigger Want to Read cleanup from Tasks page. * Moved the brightness readout to the label line and fixed a bootstrap migration bug where small buttons weren't actually small. * Implemented ability to filter against release year (min or max or both). * Fixed a log message that wasn't properly formatted when scan finished an no files changes. * Cleaned up some code and merged some methods * Implemented sort by Release year metadata filter. * Fixed the code that finds ComicInfo.xml inside archives to only check the root and check explicitly for casing, so it must be ComicInfo.xml. * Dependency updates * Refactored some strings into consts and used TriggerJob rather than just enqueuing * Fixed the prefetcher which wasn't properly loading in the correct order as it was designed. * Cleaned up all traces of CircularArray from MangaReader * Removed a debug code * Fixed a bug with webtoon reader in fullscreen mode where continuous reader wouldn't trigger * When cleaning up series from users' want to read lists, include both completed and cancelled. * Fixed a bug where small images wouldn't have the pagination area extend to the bottom on manga reader * Added a new method for hashing during prod builds and ensure we always use aot * Fixed a bug where the save button wouldn't enable when color change occured. * Cleaned up some issues in one of contributor's PR. * Bump versions by dotnet-bump-version. * Misc Polish and Fixes (#1542) * Moved LibraryWatcher to utilize a queue for calculating the change event to ensure the Watcher doesn't get overwhelmed on large moves. * Fixed a security vulnerability (https://huntr.dev/bounties/8a3e652f-d6bf-436e-877e-0eaf5c69ef95/). This will be disclosed in Stable release changelog. * Tweaked the log message template * Removed some dead code from Configuration json patcher * Fixed a bug with the ComicInfo finding to properly handle root level. Fixed a bug where sometimes scanner wouldn't choose the first file with ComicInfo for filling out information. * Added new setting for managing how many logs files are allowed, just like how backups work. * Added unit tests for new CleanupLogs code * Fixed a bug where manga reader background color wasn't actually sending from the UI * Added new stats for tracking to help understand usage in the app and what features are used or not. * Fixed Stats url * Fixed a bug where volumes that had larger than 1 difference wouldn't properly return next/prev chapter (for continuous reader) * Remove a redundant test step in build pipeline, since it's already done at PR stage. * Updated dockerfile to use the new Heath check endpoint * Allow force to pass through to scan loop * Removed some old config stuff from a safety check on config in entrypoint.sh * Fixed broken unit tests due to new RBS check and how we setup mock data. * Bump versions by dotnet-bump-version. * Removed some debug code (#1543) * Bump versions by dotnet-bump-version. * Parser optimization part1 (#1531) * Optimize CleanTitle * Optimize MangaEditionRegex * Optimize special regexes * Refactor manga|comic special parsing into simple tests * Word bind the special regexps. Support additional "special" use cases. * Updates to address PR comments * CleanTitle benchmarking * Use a smaller Comics Data set for benchmarking * Bump versions by dotnet-bump-version. * Tachiyomi unit tests and fixes (#1549) * Moved logic from TachiyomiController.cs to TachiyomiService.cs * Added GetLatestChapter Unit Tests * Tachiyomi more tests. Implemented test for yearly volumes * MarkVolumesUntilAsRead unit test * Registered tachiyomi service. Added new test * Fixed test pages * Added missing check if its single-file volume * Removed dead code * Added method documentation and breaked thousands with `_` * Review details and renamed test method to be more descriptive * Review changes - Removed automapper - Added spaces after commas - Added class documentation (copied from controller) - Made Culture static - Added 'R' doc linking to docs.ms - Added trycatch to service when saving progress and logged - Removed redundant qualifiers * finishing touches Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com> * Bump versions by dotnet-bump-version. * Folder Watching Polish + Epub Fix (#1550) * Fixed entrypoint writing bad json (from develop) * Fixed a bug where log file could write out a crap ton of information (serializing Series object) when a db error occurs. * Fixed an issue with scan loop where concurrency issues could occur on new series being added. * Tweaked the logger to suppress some noisy logs when using Debug log level. * Fixed a regression with epub parsing from v3.2 of Vers-One's release * Fixed up folder watching to work more reliable. Validated in production. * Code cleanup * Bump versions by dotnet-bump-version. * Fallback to other locations when ComicInfo.xml not at root of archive (#1551) * Fallback to other locations when ComicInfo.xml not at root of archive * Better ComicInfo test coverage and benchmarks * Add a rar archive to the ComicInfo test cases * Bump versions by dotnet-bump-version. * Series title word wrapping (#1519) * Added text-break class to series title. Simply added the class text-break from bootstrap to break on words. https://getbootstrap.com/docs/5.0/utilities/text/#word-break This has no issue with languages that do not or rarely use spaces, such as japanese and chinese. Used the following two series names to test; - 今まで一度も女扱いされたことがない女騎士を女扱いする漫画 - Imamade Ichido mo Onnaatsukai sareta Koto ga Nai Onna Kishi wo Onnaatsukai suru * Added text-break class to localized title Also added the text-break bootstrap class to the localized title, removed the word-break rule from css as it is redundant. * Enclosed LibraryName with span Enclosed {{libraryName}} with a span to remove the added space before the title, aligning it again with the start of the subtitle. This mimics series-detail-component.html * Bump versions by dotnet-bump-version. * Nested Menus (#1554) * added initial submenu * added submenu - needs a bit of more work * removed admin and nonadmin action split * the whole menu is build under the resetactions function * removed download from seriesAction * changed submenu layout changed submenu toggle icon fix for the hovering of submenu toggle * moved the cdMarkForCheck in the subscribe block * Bump versions by dotnet-bump-version. * Send To Device Support (#1557) * Tweaked the logging output * Started implementing some basic idea for devices * Updated Email Service with new API routes * Implemented basic DB structure and some APIs to prep for the UI and flows. * Added an abstract class to make Unit testing easier. * Removed dependency we don't need * Updated the UI to be able to show devices and add new devices. Email field will update the platform if the user hasn't interacted with it already. * Added ability to delete a device as well * Basic ability to send files to devices works * Refactored Action code to pass ActionItem back and allow for dynamic children based on an Observable (api). Hooked in ability to send a chapter to a device. There is no logic in the FE to validate type. * Fixed a broken unit test * Implemented the ability to edit a device * Code cleanup * Fixed a bad success message * Fixed broken unit test from updating mock layer * Bump versions by dotnet-bump-version. * Fixed a bug where when no devices, the submenu item would still render. (#1558) * Bump versions by dotnet-bump-version. * Extended Korean Filename Parsing Support (#1556) * Added Some Korean Volume Matches * Fixed Typo And Added Test Cases * Restore Chapter Decimal Support * Added Decimal Volume Support to -권, -화, -회 and -장 Merged -권 Pattern to -화, -회, -장 Pattern Added Decimal Test to ParseVolumeTest * Grouped Korean Tests * Fixed Regexp Comment * Bump versions by dotnet-bump-version. * Disable Animations + Lots of bugfixes and Polish (#1561) * Fixed inputs not showing inline validation due to a missing class * Fixed some checks * Increased the button size on manga reader (develop) * Migrated a type cast to a pure pipe * Sped up the check for if SendTo should render on the menu * Don't allow user to bookmark in bookmark mode * Fixed a bug where Scan Series would skip over Specials due to how new scan loop works. * Fixed scroll to top button persisting when navigating between pages * Edit Series modal now doesn't have a lock field for Series, which can't be locked as it is inheritently locked. Added some validation to ensure Name and SortName are required. * Fixed up some spacing * Fixed actionable menu not opening submenu on mobile * Cleaned up the layout of cover image on series detail * Show all volume or chapters (if only one volume) for cover selection on series * Don't open submenu to right if there is no space * Fixed up cover image not allowing custom saves of existing series/chapter/volume images. Fixed up logging so console output matches log file. * Implemented the ability to turn off css transitions in the UI. * Updated a note internally * Code smells * Added InstallId when pinging the email service to allow throughput tracking * Bump versions by dotnet-bump-version. * Auth Email Rework (#1567) * Hooked up Send to for Series and volumes and fixed a bug where Email Service errors weren't propagating to the UI layer. When performing actions on series detail, don't disable the button anymore. * Added send to action to volumes * Fixed a bug where .kavitaignore wasn't being applied at library root level * Added a notification for when a device is being sent a file. * Added a check in forgot password for users that do not have an email set or aren't confirmed. * Added a new api for change email and moved change password directly into new Account tab (styling and logic needs testing) * Save approx scroll position like with jump key, but on normal click of card. * Implemented the ability to change your email address or set one. This requires a 2 step process using a confirmation token. This needs polishing and css. * Removed an unused directive from codebase * Fixed up some typos on publicly * Updated query for Pending Invites to also check if the user account has not logged in at least once. * Cleaned up the css for validate email change * Hooked in an indicator to tell user that a user has an unconfirmed email * Cleaned up code smells * Bump versions by dotnet-bump-version. * Misc Polish (#1569) * Introduced a lock for DB work during the scan to hopefully reduce the concurrency issues * Don't allow multiple theme scans to occur * Fixed bulk actions not having all actions due to nested actionable menu changes * Refactored the Scan loop to be synchronous to avoid any issues. After first loop, no real performance issues. * Updated the LibraryWatcher when under many internal buffer full issues, to suspend watching for a full hour, to allow whatever downloading to complete. * Removed Semaphore as it's not needed anymore * Updated the output for logger to explicitly say from Kavita (if you're pushing to Seq) * Fixed a broken test * Fixed ReleaseYear not populating due to a change from a contributor around how to populate ReleaseYear. * Ensure when scan folder runs, that we don't double enqueue the same tasks. * Fixed user settings not loading the correct tab * Changed the Release Year -> Release * Added more refresh hooks in reader to hopefully ensure faster refreshes * Reset images between chapter loads to help flush image faster. Don't show broken image icon when an image is still loading. * Fixed the prefetcher not properly loading the correct images and hence, allowing a bit of lag between chapter loads. * Code smells * Bump versions by dotnet-bump-version. * Scan Loop Fixes (#1572) * Cleanup some messaging in the scan loop to be more context bearing * Added Response Caching to Series Detail for 1 min, due to the heavy nature of the call. * Refactored code to make it so that processing of series runs sync correctly. Added a log to inform the user of corrupted volume from buggy code in v0.5.6. * Moved folder watching out of experimental * Fixed an issue where empty folders could break the scan loop * Another fix for when dates aren't valid, the scanner wouldn't get the proper min and would throw an exception (develop) * Implemented the ability to edit release year from the UI for a series. * Added a unit test for some new logic * Code smells * Bump versions by dotnet-bump-version. * Scan Loop Fortification (#1573) * Cleanup some messaging in the scan loop to be more context bearing * Added Response Caching to Series Detail for 1 min, due to the heavy nature of the call. * Refactored code to make it so that processing of series runs sync correctly. Added a log to inform the user of corrupted volume from buggy code in v0.5.6. * Moved folder watching out of experimental * Fixed an issue where empty folders could break the scan loop * Another fix for when dates aren't valid, the scanner wouldn't get the proper min and would throw an exception (develop) * Implemented the ability to edit release year from the UI for a series. * Added a unit test for some new logic * Code smells * Rewrote the handler for suspending watching to be more resilient and ensure no two threads have a race condition. * More error handling for when a ScanFolder is invoked but multiple series belong to that folder, log it to the user and default to a library scan. * ScanSeries now will check for kavitaignores higher than it's own folder and respect library level. * Fixed an issue where image series with a folder name containing the word "folder" could get ignored as it thought the image was a cover image. When a series folder is moved or deleted, skip parent ignore finding. * Removed some old files, added in scanFolder a check if the series found for a folder is in a book library and if so to always do a library scan (as books are often nested into one folder with multiple series). Added some unit tests * Refactored some scan loop logic into ComicInfo, wrote tests and updated some documentation to make the fields more clear. * Added a test for GetLastWriteTime based on recent bug * Cleaned up some redundant code * Fixed a bad merge * Code smells * Removed a package that's no longer used. * Ensure we check against ScanQueue on ScanFolder enqueuing * Documentation and more bullet proofing to ensure Hangfire checks work more as expected * Bump versions by dotnet-bump-version. * Restricted Profiles (#1581) * Added ReadingList age rating from all series and started on some unit tests for the new flows. * Wrote more unit tests for Reading Lists * Added ability to restrict user accounts to a given age rating via admin edit user modal and invite user. This commit contains all basic code, but no query modifications. * When updating a reading list's title via UI, explicitly check if there is an existing RL with the same title. * Refactored Reading List calculation to work properly in the flows it's invoked from. * Cleaned up an unused method * Promoted Collections no longer show tags where a Series exists within them that is above the user's age rating. * Collection search now respects age restrictions * Series Detail page now checks if the user has explicit access (as a user might bypass with direct url access) * Hooked up age restriction for dashboard activity streams. * Refactored some methods from Series Controller and Library Controller to a new Search Controller to keep things organized * Updated Search to respect age restrictions * Refactored all the Age Restriction queries to extensions * Related Series no longer show up if they are out of the age restriction * Fixed a bad mapping for the update age restriction api * Fixed a UI state change after updating age restriction * Fixed unit test * Added a migration for reading lists * Code cleanup * Bump versions by dotnet-bump-version. * Misc Bugfixes (#1582) * Fixed a bug with RBS on non-admin accounts * Fixed a bug where get next/prev chapter wouldn't respect floating point volume numbers * Fixed a bad migration version check * When building kavita ignore exclusions, ignore blank lines. * Hooked up the GetFullSeriesByAnyName to check against OriginalName exactly * Refactored some code for building ignore from library root, to keep the code cleaner * Tweaked some messaging * Fixed a bad directory join when a change event occurs in a nested series folder. * Fixed a bug where cover generation would prioritize a special if there were only chapters in the series. * Fixed a bug where you couldn't update a series modal if there wasn't a release year present * Fixed an issue where renaming the Series in Kavita wouldn't allow ScanSeries to see the files, and thus would delete the Series. * Added an additional check with Hangfire to make sure ScanFolder doesn't kick off a change when a bunch of changes come through for the same directory, but a job is already running. * Added more documentation * Migrated more response caching to profiles and merged 2 apis into one, since they do the same thing. * Fixed a bug where NotApplicable age ratings were breaking Recently Updated Series * Cleaned up some cache profiles * More caching * Provide response caching on Get Next/Prev Chapter * Code smells * Bump versions by dotnet-bump-version. * New Series Relation - Edition (#1583) * Moved UpdateRelatedSeries from controller to SeriesService.cs * Added 2 tests. - UpdateRelatedSeries_ShouldDeletePrequelRelation - UpdateRelatedSeries_ShouldNotAllowDuplicates * Some docs and codestyle nitpicks * Simplified tests and made easier to read * Added 'Editions' series relation * Missing code to properly show the relations in the UI * Create Service for GetRelatedServices * Added unit test. Assert Edition, Prequel and Sequel do not return parent while others do * fixed missing userRating * Add requested changes: - Rename one test - Split one test into two tests * Bump versions by dotnet-bump-version. * Release Polish (#1586) * Fixed a scaling issue in the epub reader, where images could scale when they shouldn't. * Removed some caching on library/ api and added more output for a foreign key constraint * Hooked in Restricted Profile stat collection * Added a new boolean on age restrictions to explicitly allow unknowns or not. Since unknown is the default state of metadata, if users are allowed access to Unknown, age restricted content could leak. * Fixed a bug where sometimes series cover generation could fail under conditions where only specials existed. * Fixed foreign constraint issue when cleaning up series not seen at end of scan loop * Removed an additional epub parse when scanning and handled merging differently * Code smell * Bump versions by dotnet-bump-version. * Release Shakeout Day 1 (#1591) * Fixed an issue where reading list were not able to update their summary due to a duplicate title check. * Misc code smell cleanup * Updated .net dependencies and removed unneeded ones * Fixed an issue where removing a series from want to read list page wouldn't update the page correctly * Fixed age restriction not applied to Recommended page * Ensure that Genres and Tags are age restricted gated * Persons are now age gated as well * When you choose a cover, the new cover will properly be selected and will focus on it, in the cases there are many other covers available. * Fixed caching profiles * Added in a special hook when deleting a library to clear all series Relations before we delete * Bump versions by dotnet-bump-version. * Release Shakeout Day 2 (#1594) * Fixed a bad color on the PWA titlebar * Added more unit tests, cleaned up some dead code, and made it so when age restriction is Not Applicable, the Unknowns field disables * Don't show an empty menu when user has no permissions * Fixed deleting a library with relation causing library deleting to fail * Consolidated some includes code into one method for Series Repo * Small fixes * Bump versions by dotnet-bump-version. * Release Shakeout 3 (#1597) * Fixed a bug where bulk selection on series detail wouldn't allow you to select the whole card, only the checkbox. * Refactored the implementation of MarkChaptersAsRead to streamline it. * Fixed a bug where volume cards weren't properly updating their read state based on events from backend. * Added [ScannerService] to more loggers * Fixed invite user flow * Fixed broken edit user flow * Fixed calling device service on unauthenticated screens causing redirection * Fixed reset password via email not working when success message was sent back * Fixed broken white theme on book reader * Small tweaks to white theme * More fixes * Adjusted AutomaticRetries * Bump versions by dotnet-bump-version. * UI Polish (#1599) * Make the positioning of "Library | Recommended" consistent * Fix reading lists not navigating back to their library after deletion * Bump versions by dotnet-bump-version. * Release Shakeout 4 (#1600) * Fixed a bug where bulk selection on series detail wouldn't allow you to select the whole card, only the checkbox. * Refactored the implementation of MarkChaptersAsRead to streamline it. * Fixed a bug where volume cards weren't properly updating their read state based on events from backend. * Added [ScannerService] to more loggers * Fixed invite user flow * Fixed broken edit user flow * Fixed calling device service on unauthenticated screens causing redirection * Fixed reset password via email not working when success message was sent back * Fixed broken white theme on book reader * Small tweaks to white theme * More fixes * Adjusted AutomaticRetries * When an auth change occures, reset the devices in service so devices don't leak between profiles * Fixed a bug where sendTo for series wasn't properly taking into account specials (on series detail page) * Drop down how long series detail caches for to prevent signalr updates from refreshing the UI * Close submenus when hovering over other items, not just other submenus * Fixed a bug where scanning for themes would always report theme didn't exist * Added Hangfire.db back in * Fixed a bad build * Bump versions by dotnet-bump-version. * Fixed column layout on multiple components for the user settings (#1602) #Fixed - Fixed: Fixed an issue where the controls would extend outside of the container on the user account preferences page. * Bump versions by dotnet-bump-version. * v0.6 Release (#1603) Co-authored-by: tjarls <tjarls@gmail.com> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: Chris Plaatjes <kizaing@gmail.com> Co-authored-by: Ocgineer <rvanbeek1@gmail.com> Co-authored-by: ThePromidius <thepromidiusyt@gmail.com> Co-authored-by: Korakot Santiudommongkol <47130579+KorakotSanti@users.noreply.github.com> Co-authored-by: DeltaLaboratory <delta@deltalab.dev> Co-authored-by: TheIceCreamTroll <33820904+TheIceCreamTroll@users.noreply.github.com>
This commit is contained in:
parent
150e67031a
commit
ac3fe0b1f4
4
.github/workflows/sonar-scan.yml
vendored
4
.github/workflows/sonar-scan.yml
vendored
@ -123,7 +123,7 @@ jobs:
|
|||||||
|
|
||||||
develop:
|
develop:
|
||||||
name: Build Nightly Docker if Develop push
|
name: Build Nightly Docker if Develop push
|
||||||
needs: [ build, test, version ]
|
needs: [ build, version ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||||
steps:
|
steps:
|
||||||
@ -232,7 +232,7 @@ jobs:
|
|||||||
|
|
||||||
stable:
|
stable:
|
||||||
name: Build Stable Docker if Main push
|
name: Build Stable Docker if Main push
|
||||||
needs: [ build, test ]
|
needs: [ build ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||||
steps:
|
steps:
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -530,3 +530,4 @@ API.Tests/TestResults/
|
|||||||
UI/Web/.vscode/settings.json
|
UI/Web/.vscode/settings.json
|
||||||
/API.Tests/Services/Test Data/ArchiveService/CoverImages/output/*
|
/API.Tests/Services/Test Data/ArchiveService/CoverImages/output/*
|
||||||
UI/Web/.angular/
|
UI/Web/.angular/
|
||||||
|
BenchmarkDotNet.Artifacts
|
@ -10,15 +10,21 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
|
||||||
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.1" />
|
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.2" />
|
||||||
<PackageReference Include="NSubstitute" Version="4.4.0" />
|
<PackageReference Include="NSubstitute" Version="4.4.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="Data\SeriesNamesForNormalization.txt">
|
<Content Include="Data/*.txt">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="../API.Tests/Services/Test Data/ArchiveService/ComicInfos/*.zip">
|
||||||
|
<LinkBase>Data</LinkBase>
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
namespace API.Benchmark
|
|
||||||
{
|
|
||||||
public class ArchiveSerivceBenchmark
|
|
||||||
{
|
|
||||||
// Benchmark to test default GetNumberOfPages from archive
|
|
||||||
// vs a new method where I try to open the archive and return said stream
|
|
||||||
}
|
|
||||||
}
|
|
54
API.Benchmark/ArchiveServiceBenchmark.cs
Normal file
54
API.Benchmark/ArchiveServiceBenchmark.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO.Abstractions;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using API.Services;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Order;
|
||||||
|
|
||||||
|
namespace API.Benchmark;
|
||||||
|
|
||||||
|
[StopOnFirstError]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
[RankColumn]
|
||||||
|
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||||
|
[SimpleJob(launchCount: 1, warmupCount: 5, targetCount: 20)]
|
||||||
|
public class ArchiveServiceBenchmark
|
||||||
|
{
|
||||||
|
private readonly ArchiveService _archiveService;
|
||||||
|
private readonly IDirectoryService _directoryService;
|
||||||
|
private readonly IImageService _imageService;
|
||||||
|
|
||||||
|
public ArchiveServiceBenchmark()
|
||||||
|
{
|
||||||
|
_directoryService = new DirectoryService(null, new FileSystem());
|
||||||
|
_imageService = new ImageService(null, _directoryService);
|
||||||
|
_archiveService = new ArchiveService(new NullLogger<ArchiveService>(), _directoryService, _imageService);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Baseline = true)]
|
||||||
|
public void TestGetComicInfo_baseline()
|
||||||
|
{
|
||||||
|
if (_archiveService.GetComicInfo("Data/ComicInfo.zip") == null) {
|
||||||
|
throw new Exception("ComicInfo not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void TestGetComicInfo_duplicate()
|
||||||
|
{
|
||||||
|
if (_archiveService.GetComicInfo("Data/ComicInfo_duplicateInfos.zip") == null) {
|
||||||
|
throw new Exception("ComicInfo not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void TestGetComicInfo_outside_root()
|
||||||
|
{
|
||||||
|
if (_archiveService.GetComicInfo("Data/ComicInfo_outside_root.zip") == null) {
|
||||||
|
throw new Exception("ComicInfo not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark to test default GetNumberOfPages from archive
|
||||||
|
// vs a new method where I try to open the archive and return said stream
|
||||||
|
}
|
26
API.Benchmark/CleanTitleBenchmark.cs
Normal file
26
API.Benchmark/CleanTitleBenchmark.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Order;
|
||||||
|
|
||||||
|
namespace API.Benchmark;
|
||||||
|
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
public static class CleanTitleBenchmarks
|
||||||
|
{
|
||||||
|
private static IList<string> _names;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public static void LoadData() => _names = File.ReadAllLines("Data/Comics.txt");
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public static void TestCleanTitle()
|
||||||
|
{
|
||||||
|
foreach (var name in _names)
|
||||||
|
{
|
||||||
|
Services.Tasks.Scanner.Parser.Parser.CleanTitle(name, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
API.Benchmark/Data/Comics.txt
Normal file
112
API.Benchmark/Data/Comics.txt
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
One-Star Squadron 02 (of 06) (2022) (digital) (Son of Ultron-Empire).cbz
|
||||||
|
Batman & the Monster Men 06 (2006) (Kryptonia-DCP).cbr
|
||||||
|
Hauteville House -07- Expedition Vanikoro.cbr
|
||||||
|
Fantastic Four v3 #020.cbz
|
||||||
|
Thunderbolts 053.cbr
|
||||||
|
Moon Knight 010 2007 Red Lion-DCP .cbr
|
||||||
|
New X-Men 037.cbr
|
||||||
|
X-Men - Deadly Genesis 02 (2006) (BigBlue-DCP).cbr
|
||||||
|
Incredible Hercules 128.cbr
|
||||||
|
JLA - Year One 03 of 12.cbr
|
||||||
|
Daredevil v2 082 (2006) (Reiu-DCP).cbr
|
||||||
|
069 - Iron Man v4 035 (2009) (Minutemen-ZonesDiva).cbr
|
||||||
|
2000AD prog 2285 (2022) (digital) (Minutemen-juvecube).cbz
|
||||||
|
Tanguy et Laverdure - Intégrale - T07.cbz
|
||||||
|
Excalibur 026 (2022) (Digital) (Zone-Empire).cbz
|
||||||
|
DC vs. Vampires - Killers 001 (2022) (Webrip) (The Last Kryptonian-DCP).cbz
|
||||||
|
By the Horns 003 (2021) (Digital) (Mephisto-Empire).cbz
|
||||||
|
Incredible Hulks 630 (2011) (Minutemen-Fiji).cbz
|
||||||
|
Red Robin 010 (2010) (Minutemen-OTT).cbr
|
||||||
|
Les Droits de lHomme - OneShot - Collectif.cbz
|
||||||
|
Tout Gaston - Intégrale.cbr
|
||||||
|
Good Night, Hem (2021) (Digital) (Dipole-Empire).cbz
|
||||||
|
Bunny Mask - The Hollow Inside 001 (2022) (Digital) (Mephisto-Empire).cbz
|
||||||
|
Les MYTHICS - T14 - Avarice.cbr
|
||||||
|
Fantastic Four Special 01 (2006) (Nascent-DCP).cbr
|
||||||
|
Sonjaversal 006 (2021) (5 covers) (digital) (The Seeker-Empire).cbz
|
||||||
|
The Flash 779 (2022) (Digital) (Zone-Empire).cbz
|
||||||
|
Supergirl and the Legion of Super-Heroes 020 (2006) (CamelotScans-DCP).cbr
|
||||||
|
Time Before Time 015 (2022) (Digital) (Zone-Empire).cbz
|
||||||
|
Union Jack 02 (2006) (Red Lion-DCP).cbr
|
||||||
|
Le Corps est un Vêtement que l'on quitte.pdf
|
||||||
|
Helmet of Fate - Black Alice 01 (2007) (Racerx-DCP).cbz
|
||||||
|
Villains United 003 [2005] (Team-DCP).cbr
|
||||||
|
Punisher 002.cbr
|
||||||
|
Grendel - Devil's Odyssey 008 (2021) (digital) (NeverAngel-Empire).cbz
|
||||||
|
Uncanny X-Force 05.1 (2011) (Minutemen-Megatonic).cbz
|
||||||
|
Orcs & Gobelins - T14 - Shaaka.cbr
|
||||||
|
Les grands personnages de l'histoire en bandes dessinées - T67 - Suffren - La Bataille de Gondelou.cbz
|
||||||
|
Batman Adventures 013 (Jorl - Dcp).cbr
|
||||||
|
Norse Mythology II 003 (2021) (digital) (Son of Ultron-Empire).cbz
|
||||||
|
Ghost Rider 012 (2007) (Team-DCP).cbr
|
||||||
|
Once & Future 021 (2021) (digital) (Son of Ultron-Empire).cbz
|
||||||
|
The Seven Deadly Sins #1_ Seven Deadly Her - Nakaba Suzuki.epub
|
||||||
|
Kimagure Orange Road Omnibus #5_ Vol. 5 - Izumi Matsumoto.cbz
|
||||||
|
Booster Gold 36 2010 Minutemen-Oracle Saxon .cbr
|
||||||
|
New X-Men 023 (2006) (Reiu-DCP).cbr
|
||||||
|
World of Betty and Veronica Comics Digest 016 (2022) (Forsythe-DCP).cbz
|
||||||
|
Deadpool Team-Up 889 (2010) (noads) (LegionNever-CPS).cbr
|
||||||
|
Les bêtes de black city - T03 - le feu de la vengeance.cbr
|
||||||
|
The Brother of All Men 002 (2022) (digital) (Son of Ultron-Empire).cbz
|
||||||
|
DC Fifty-Two (52) Week One (2006) (Kryptonia-DCP).cbr
|
||||||
|
Heroes For Hire v2 09 (2007) (DarthScanner-DCP).cbr
|
||||||
|
Doom Patrol v4 012 [2005] (Bchry-DCP).cbr
|
||||||
|
Black Panther's Prey #1(Aieiebrazoff-DCP)-Repack.cbz
|
||||||
|
Hello Neighbor 02 - The Raven Brooks Disaster (2021) (Digital Rip) (Hourman-DCP).cbz
|
||||||
|
Grimm Spotlight - Cinderella vs. Zombies (2021) (digital) (The Seeker-Empire).cbz
|
||||||
|
Black's Myth 001 (2021) (digital) (Son of Ultron-Empire).cbz
|
||||||
|
Donjon Antipodes T02 +10001 Le Coffre aux Âmes.pdf
|
||||||
|
Ghost Rider 016 (2007) (Noads) (Team-DCP).cbr
|
||||||
|
JLA Classified 38 (2007) (Wolfrider-DCP).cbr
|
||||||
|
Olive 003 - On the Trail of the Nerpa (2021) (digital) (Mr Norrell-Empire).cbz
|
||||||
|
Avengers v3 #054.cbz
|
||||||
|
Doctor Strange - The Oath 01 (2006) (Kryptonia-DCP).cbr
|
||||||
|
Red Robin 006 2010 Minutemen-DTermined.cbr
|
||||||
|
056 - She-Hulk v2 032 (2008) (2 covers) (Minutemen-ReZone).cbr
|
||||||
|
DC Fifty-Two (52) Week 030 (2007) (Kryptonia-DCP).cbr
|
||||||
|
Detective Comics 1055 (2022) (Webrip) (The Last Kryptonian-DCP).cbz
|
||||||
|
Spider-Man vs. Vampires 01 2010 Minutemen-DTs .cbz
|
||||||
|
Grim 003 (2022) (digital) (Son of Ultron-Empire).cbz
|
||||||
|
Wastelanders - Star-Lord 001 (2022) (Digital) (Zone-Empire).cbz
|
||||||
|
Superman [2003-38] Adventures of Superman 621.cbr
|
||||||
|
Elektra - Black, White & Blood 001 (2022) (Digital) (Zone-Empire).cbz
|
||||||
|
Félix #15 - Heroic Album -1950- Le Tueur Fantome.cbz
|
||||||
|
Ms. Marvel v2 09 (2006) (Team-DCP).cbr
|
||||||
|
Stray Dogs - Dog Days 002 (2022) (digital) (Son of Ultron-Empire).cbz
|
||||||
|
My Date With Monsters 002 (2021) (Digital) (Mephisto-Empire).cbz
|
||||||
|
Friendly Neighborhood Spider-Man 02 (2006) (Variant Cvr) (Wildcarde1-DCP).cbr
|
||||||
|
Acriboréa -T03- Des millions de soleils.cbr
|
||||||
|
X-Men: Phoenix - Endsong 05 (of 5) [2005] (Team-DCP).cbr
|
||||||
|
Usagi Yojimbo - Lone Goat and Kid 006 (2022) (digital) (Son of Ultron-Empire).cbz
|
||||||
|
Robyn Hood Annual - The Swarm (2021) (digital) (The Seeker-Empire).cbz
|
||||||
|
Azrael #025.cbr
|
||||||
|
Nita Hawes' Nightmare Blog 002 (2021) (Digital) (Zone-Empire).cbz
|
||||||
|
Dark Avengers-Uncanny X-Men - Utopia 001.cbr
|
||||||
|
Naughty List 004 (2022) (digital) (Son of Ultron-Empire).cbz
|
||||||
|
Atalante - La Légende-04-L'Envol Des Boréades.cbz
|
||||||
|
Warlord of Mars 02 (6 covers).cbr
|
||||||
|
Action Comics 857 (2007) (CamelotScans-DCP).cbr
|
||||||
|
War For Earth - 3 002 (2022) (Webrip) (The Last Kryptonian-DCP).cbz
|
||||||
|
Oracle - T04 - Le Malformé.cbz
|
||||||
|
Battle Angel Alita #9_ Vol. 9 - Yukito Kishiro.epub
|
||||||
|
Les aventuriers de l'intermonde - T01 - Mission Athènes.cbz
|
||||||
|
Captain_America_and_The_Secret_Avengers_(2011)_(Minutemen-DTermined).cbr
|
||||||
|
She-Hulk 002 (2022) (Digital) (Zone-Empire).cbz
|
||||||
|
infinity inc 01 (2007) (racerx-dcp).cbz
|
||||||
|
Wonder Girl 004 (2021) (digital) (Son of Ultron-Empire).cbz
|
||||||
|
SEULS - T07 - Les Terres Basses.cbr
|
||||||
|
Out of Body 003 (2021) (digital) (Son of Ultron-Empire).cbz
|
||||||
|
Power Girl 09.cbr
|
||||||
|
Thor 614 (2 covers) (2010) (noads) (Archangel & FP-CPS).cbr
|
||||||
|
Iron Man 011 (2021) (Digital) (Zone-Empire).cbz
|
||||||
|
Ms. Marvel - Beyond the Limit 002 (2022) (Digital) (Zone-Empire).cbz
|
||||||
|
Ultimate X-Men #038.cbr
|
||||||
|
Excalibur 022 (2021) (Digital) (Zone-Empire).cbz
|
||||||
|
New Avengers 025 (2006) (Fixed) (Team-DCP).cbr
|
||||||
|
T06.2 - Topkapi.pdf
|
||||||
|
Thor Corps 2 of 4.cbr
|
||||||
|
Shang-Chi - Brothers & Sisters Infinity Comic 003 (2021) (Digital-Mobile) (Infinity-Empire) (WebP).cbz
|
||||||
|
X-Men To Serve And Protect 01 of 04 2010 .cbr
|
||||||
|
08A - Blue Beetle 020.cbz
|
||||||
|
The Joker Presents - A Puzzlebox Director's Cut 013 (2021) (digital) (Son of Ultron-Empire).cbz
|
||||||
|
Alice Matheson - T01 - Jour Z.cbz
|
@ -5,13 +5,13 @@ using System.Text.RegularExpressions;
|
|||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using BenchmarkDotNet.Order;
|
using BenchmarkDotNet.Order;
|
||||||
|
|
||||||
namespace API.Benchmark
|
namespace API.Benchmark;
|
||||||
|
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||||
|
[RankColumn]
|
||||||
|
public class ParserBenchmarks
|
||||||
{
|
{
|
||||||
[MemoryDiagnoser]
|
|
||||||
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
|
||||||
[RankColumn]
|
|
||||||
public class ParserBenchmarks
|
|
||||||
{
|
|
||||||
private readonly IList<string> _names;
|
private readonly IList<string> _names;
|
||||||
|
|
||||||
private static readonly Regex NormalizeRegex = new Regex(@"[^a-zA-Z0-9]",
|
private static readonly Regex NormalizeRegex = new Regex(@"[^a-zA-Z0-9]",
|
||||||
@ -75,5 +75,4 @@ namespace API.Benchmark
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,14 @@
|
|||||||
using BenchmarkDotNet.Running;
|
using BenchmarkDotNet.Running;
|
||||||
|
|
||||||
namespace API.Benchmark
|
namespace API.Benchmark;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// To build this, cd into API.Benchmark directory and run
|
|
||||||
/// dotnet build -c Release
|
|
||||||
/// then copy the outputted dll
|
|
||||||
/// dotnet copied_string\API.Benchmark.dll
|
|
||||||
/// </summary>
|
|
||||||
public static class Program
|
|
||||||
{
|
|
||||||
private static void Main(string[] args)
|
|
||||||
{
|
|
||||||
//BenchmarkRunner.Run<ParseScannedFilesBenchmarks>();
|
|
||||||
//BenchmarkRunner.Run<TestBenchmark>();
|
|
||||||
//BenchmarkRunner.Run<ParserBenchmarks>();
|
|
||||||
BenchmarkRunner.Run<EpubBenchmark>();
|
|
||||||
|
|
||||||
}
|
/// <summary>
|
||||||
}
|
/// To build this, cd into API.Benchmark directory and run
|
||||||
|
/// dotnet build -c Release
|
||||||
|
/// then copy the outputted dll
|
||||||
|
/// dotnet copied_string\API.Benchmark.dll
|
||||||
|
/// </summary>
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
private static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
|
||||||
}
|
}
|
||||||
|
@ -6,16 +6,16 @@ using API.Extensions;
|
|||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using BenchmarkDotNet.Order;
|
using BenchmarkDotNet.Order;
|
||||||
|
|
||||||
namespace API.Benchmark
|
namespace API.Benchmark;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is used as a scratchpad for testing
|
||||||
|
/// </summary>
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||||
|
[RankColumn]
|
||||||
|
public class TestBenchmark
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// This is used as a scratchpad for testing
|
|
||||||
/// </summary>
|
|
||||||
[MemoryDiagnoser]
|
|
||||||
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
|
||||||
[RankColumn]
|
|
||||||
public class TestBenchmark
|
|
||||||
{
|
|
||||||
private static IEnumerable<VolumeDto> GenerateVolumes(int max)
|
private static IEnumerable<VolumeDto> GenerateVolumes(int max)
|
||||||
{
|
{
|
||||||
var random = new Random();
|
var random = new Random();
|
||||||
@ -62,5 +62,4 @@ namespace API.Benchmark
|
|||||||
SortSpecialChapters(volumes);
|
SortSpecialChapters(volumes);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.7" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.10" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||||
<PackageReference Include="NSubstitute" Version="4.4.0" />
|
<PackageReference Include="NSubstitute" Version="4.4.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="17.0.24" />
|
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="17.2.3" />
|
||||||
<PackageReference Include="xunit" Version="2.4.2" />
|
<PackageReference Include="xunit" Version="2.4.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
118
API.Tests/BasicTest.cs
Normal file
118
API.Tests/BasicTest.cs
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
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.Services;
|
||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace API.Tests;
|
||||||
|
|
||||||
|
public abstract class BasicTest
|
||||||
|
{
|
||||||
|
private readonly DbConnection _connection;
|
||||||
|
protected readonly DataContext _context;
|
||||||
|
protected readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
|
|
||||||
|
protected const string CacheDirectory = "C:/kavita/config/cache/";
|
||||||
|
protected const string CoverImageDirectory = "C:/kavita/config/covers/";
|
||||||
|
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 TempDirectory = "C:/kavita/config/temp/";
|
||||||
|
|
||||||
|
protected BasicTest()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.TotalLogs).SingleAsync();
|
||||||
|
setting.Value = "10";
|
||||||
|
|
||||||
|
_context.ServerSetting.Update(setting);
|
||||||
|
|
||||||
|
_context.Library.Add(new Library()
|
||||||
|
{
|
||||||
|
Name = "Manga",
|
||||||
|
Folders = new List<FolderPath>()
|
||||||
|
{
|
||||||
|
new FolderPath()
|
||||||
|
{
|
||||||
|
Path = "C:/data/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return await _context.SaveChangesAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task ResetDb()
|
||||||
|
{
|
||||||
|
_context.Series.RemoveRange(_context.Series.ToList());
|
||||||
|
_context.Users.RemoveRange(_context.Users.ToList());
|
||||||
|
_context.AppUserBookmark.RemoveRange(_context.AppUserBookmark.ToList());
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected 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(LogDirectory);
|
||||||
|
fileSystem.AddDirectory(TempDirectory);
|
||||||
|
fileSystem.AddDirectory("C:/data/");
|
||||||
|
|
||||||
|
return fileSystem;
|
||||||
|
}
|
||||||
|
}
|
@ -2,10 +2,10 @@
|
|||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Comparers
|
namespace API.Tests.Comparers;
|
||||||
|
|
||||||
|
public class ChapterSortComparerTest
|
||||||
{
|
{
|
||||||
public class ChapterSortComparerTest
|
|
||||||
{
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(new[] {1, 2, 0}, new[] {1, 2, 0})]
|
[InlineData(new[] {1, 2, 0}, new[] {1, 2, 0})]
|
||||||
[InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})]
|
[InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})]
|
||||||
@ -15,5 +15,4 @@ namespace API.Tests.Comparers
|
|||||||
Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparer()).ToArray());
|
Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparer()).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -2,10 +2,10 @@
|
|||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Comparers
|
namespace API.Tests.Comparers;
|
||||||
|
|
||||||
|
public class StringLogicalComparerTest
|
||||||
{
|
{
|
||||||
public class StringLogicalComparerTest
|
|
||||||
{
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(
|
[InlineData(
|
||||||
new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
|
new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
|
||||||
@ -30,5 +30,4 @@ namespace API.Tests.Comparers
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
using API.Helpers.Converters;
|
using API.Helpers.Converters;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Converters
|
namespace API.Tests.Converters;
|
||||||
|
|
||||||
|
public class CronConverterTests
|
||||||
{
|
{
|
||||||
public class CronConverterTests
|
|
||||||
{
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("daily", "0 0 * * *")]
|
[InlineData("daily", "0 0 * * *")]
|
||||||
[InlineData("disabled", "0 0 31 2 *")]
|
[InlineData("disabled", "0 0 31 2 *")]
|
||||||
@ -15,5 +15,4 @@ namespace API.Tests.Converters
|
|||||||
{
|
{
|
||||||
Assert.Equal(expected, CronConverter.ConvertToCronNotation(input));
|
Assert.Equal(expected, CronConverter.ConvertToCronNotation(input));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -35,4 +35,62 @@ public class ComicInfoTests
|
|||||||
Assert.Equal(AgeRating.RatingPending, ComicInfo.ConvertAgeRatingToEnum("rating pending"));
|
Assert.Equal(AgeRating.RatingPending, ComicInfo.ConvertAgeRatingToEnum("rating pending"));
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region CalculatedCount
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CalculatedCount_ReturnsVolumeCount()
|
||||||
|
{
|
||||||
|
var ci = new ComicInfo()
|
||||||
|
{
|
||||||
|
Number = "5",
|
||||||
|
Volume = "10",
|
||||||
|
Count = 10
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(5, ci.CalculatedCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CalculatedCount_ReturnsNoCountWhenCountNotSet()
|
||||||
|
{
|
||||||
|
var ci = new ComicInfo()
|
||||||
|
{
|
||||||
|
Number = "5",
|
||||||
|
Volume = "10",
|
||||||
|
Count = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(5, ci.CalculatedCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CalculatedCount_ReturnsNumberCount()
|
||||||
|
{
|
||||||
|
var ci = new ComicInfo()
|
||||||
|
{
|
||||||
|
Number = "5",
|
||||||
|
Volume = "",
|
||||||
|
Count = 10
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(5, ci.CalculatedCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CalculatedCount_ReturnsNumberCount_OnlyWholeNumber()
|
||||||
|
{
|
||||||
|
var ci = new ComicInfo()
|
||||||
|
{
|
||||||
|
Number = "5.7",
|
||||||
|
Volume = "",
|
||||||
|
Count = 10
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(5, ci.CalculatedCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
using API.Data;
|
using API.Data;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Entities
|
namespace API.Tests.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for <see cref="API.Entities.Series"/>
|
||||||
|
/// </summary>
|
||||||
|
public class SeriesTest
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Tests for <see cref="API.Entities.Series"/>
|
|
||||||
/// </summary>
|
|
||||||
public class SeriesTest
|
|
||||||
{
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Darker than Black")]
|
[InlineData("Darker than Black")]
|
||||||
public void CreateSeries(string name)
|
public void CreateSeries(string name)
|
||||||
@ -23,5 +23,4 @@ namespace API.Tests.Entities
|
|||||||
Assert.Equal(name, series.OriginalName);
|
Assert.Equal(name, series.OriginalName);
|
||||||
Assert.Equal(key, series.NormalizedName);
|
Assert.Equal(key, series.NormalizedName);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
@ -6,10 +7,10 @@ using API.Extensions;
|
|||||||
using API.Parser;
|
using API.Parser;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Extensions
|
namespace API.Tests.Extensions;
|
||||||
|
|
||||||
|
public class ChapterListExtensionsTests
|
||||||
{
|
{
|
||||||
public class ChapterListExtensionsTests
|
|
||||||
{
|
|
||||||
private static Chapter CreateChapter(string range, string number, MangaFile file, bool isSpecial)
|
private static Chapter CreateChapter(string range, string number, MangaFile file, bool isSpecial)
|
||||||
{
|
{
|
||||||
return new Chapter()
|
return new Chapter()
|
||||||
@ -108,7 +109,6 @@ namespace API.Tests.Extensions
|
|||||||
var actualChapter = chapterList.GetChapterByRange(info);
|
var actualChapter = chapterList.GetChapterByRange(info);
|
||||||
|
|
||||||
Assert.Equal(chapterList[0], actualChapter);
|
Assert.Equal(chapterList[0], actualChapter);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region GetFirstChapterWithFiles
|
#region GetFirstChapterWithFiles
|
||||||
@ -141,5 +141,46 @@ namespace API.Tests.Extensions
|
|||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region MinimumReleaseYear
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MinimumReleaseYear_ZeroIfNoChapters()
|
||||||
|
{
|
||||||
|
var chapterList = new List<Chapter>();
|
||||||
|
|
||||||
|
Assert.Equal(0, chapterList.MinimumReleaseYear());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MinimumReleaseYear_ZeroIfNoValidDates()
|
||||||
|
{
|
||||||
|
var chapterList = new List<Chapter>()
|
||||||
|
{
|
||||||
|
CreateChapter("detective comics", "0", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true),
|
||||||
|
CreateChapter("detective comics", "0", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true)
|
||||||
|
};
|
||||||
|
|
||||||
|
chapterList[0].ReleaseDate = new DateTime(10, 1, 1);
|
||||||
|
chapterList[1].ReleaseDate = DateTime.MinValue;
|
||||||
|
|
||||||
|
Assert.Equal(0, chapterList.MinimumReleaseYear());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MinimumReleaseYear_MinValidReleaseYear()
|
||||||
|
{
|
||||||
|
var chapterList = new List<Chapter>()
|
||||||
|
{
|
||||||
|
CreateChapter("detective comics", "0", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true),
|
||||||
|
CreateChapter("detective comics", "0", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true)
|
||||||
|
};
|
||||||
|
|
||||||
|
chapterList[0].ReleaseDate = new DateTime(2002, 1, 1);
|
||||||
|
chapterList[1].ReleaseDate = new DateTime(2012, 2, 1);
|
||||||
|
|
||||||
|
Assert.Equal(2002, chapterList.MinimumReleaseYear());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
using System.Linq;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using API.Data.Misc;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -132,4 +135,33 @@ public class EnumerableExtensionsTests
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true, 2)]
|
||||||
|
[InlineData(false, 1)]
|
||||||
|
public void RestrictAgainstAgeRestriction_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount)
|
||||||
|
{
|
||||||
|
var items = new List<RecentlyAddedSeries>()
|
||||||
|
{
|
||||||
|
new RecentlyAddedSeries()
|
||||||
|
{
|
||||||
|
AgeRating = AgeRating.Teen,
|
||||||
|
},
|
||||||
|
new RecentlyAddedSeries()
|
||||||
|
{
|
||||||
|
AgeRating = AgeRating.Unknown,
|
||||||
|
},
|
||||||
|
new RecentlyAddedSeries()
|
||||||
|
{
|
||||||
|
AgeRating = AgeRating.X18Plus,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var filtered = items.RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||||
|
{
|
||||||
|
AgeRating = AgeRating.Teen,
|
||||||
|
IncludeUnknowns = includeUnknowns
|
||||||
|
});
|
||||||
|
Assert.Equal(expectedCount, filtered.Count());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@ using System.IO;
|
|||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Extensions
|
namespace API.Tests.Extensions;
|
||||||
|
|
||||||
|
public class FileInfoExtensionsTests
|
||||||
{
|
{
|
||||||
public class FileInfoExtensionsTests
|
|
||||||
{
|
|
||||||
private static readonly string TestDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Extensions/Test Data/");
|
private static readonly string TestDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Extensions/Test Data/");
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -29,5 +29,4 @@ namespace API.Tests.Extensions
|
|||||||
File.AppendAllLines(filepath, new[] { DateTime.Now.ToString(CultureInfo.InvariantCulture) });
|
File.AppendAllLines(filepath, new[] { DateTime.Now.ToString(CultureInfo.InvariantCulture) });
|
||||||
Assert.True(new FileInfo(filepath).HasFileBeenModifiedSince(date));
|
Assert.True(new FileInfo(filepath).HasFileBeenModifiedSince(date));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,15 +5,16 @@ using API.Entities.Enums;
|
|||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Parser;
|
using API.Parser;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using API.Tests.Helpers;
|
using API.Tests.Helpers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Extensions
|
namespace API.Tests.Extensions;
|
||||||
|
|
||||||
|
public class ParserInfoListExtensions
|
||||||
{
|
{
|
||||||
public class ParserInfoListExtensions
|
|
||||||
{
|
|
||||||
private readonly IDefaultParser _defaultParser;
|
private readonly IDefaultParser _defaultParser;
|
||||||
public ParserInfoListExtensions()
|
public ParserInfoListExtensions()
|
||||||
{
|
{
|
||||||
@ -49,5 +50,4 @@ namespace API.Tests.Extensions
|
|||||||
|
|
||||||
Assert.Equal(expectedHasInfo, infos.HasInfo(chapter));
|
Assert.Equal(expectedHasInfo, infos.HasInfo(chapter));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
284
API.Tests/Extensions/QueryableExtensionsTests.cs
Normal file
284
API.Tests/Extensions/QueryableExtensionsTests.cs
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using API.Data.Misc;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Metadata;
|
||||||
|
using API.Extensions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Extensions;
|
||||||
|
|
||||||
|
public class QueryableExtensionsTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true, 2)]
|
||||||
|
[InlineData(false, 1)]
|
||||||
|
public void RestrictAgainstAgeRestriction_Series_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount)
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||||
|
{
|
||||||
|
AgeRating = AgeRating.Teen,
|
||||||
|
IncludeUnknowns = includeUnknowns
|
||||||
|
});
|
||||||
|
Assert.Equal(expectedCount, filtered.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true, 2)]
|
||||||
|
[InlineData(false, 1)]
|
||||||
|
public void RestrictAgainstAgeRestriction_CollectionTag_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount)
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||||
|
{
|
||||||
|
AgeRating = AgeRating.Teen,
|
||||||
|
IncludeUnknowns = includeUnknowns
|
||||||
|
});
|
||||||
|
Assert.Equal(expectedCount, filtered.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true, 2)]
|
||||||
|
[InlineData(false, 1)]
|
||||||
|
public void RestrictAgainstAgeRestriction_Genre_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount)
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||||
|
{
|
||||||
|
AgeRating = AgeRating.Teen,
|
||||||
|
IncludeUnknowns = includeUnknowns
|
||||||
|
});
|
||||||
|
Assert.Equal(expectedCount, filtered.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true, 2)]
|
||||||
|
[InlineData(false, 1)]
|
||||||
|
public void RestrictAgainstAgeRestriction_Tag_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount)
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||||
|
{
|
||||||
|
AgeRating = AgeRating.Teen,
|
||||||
|
IncludeUnknowns = includeUnknowns
|
||||||
|
});
|
||||||
|
Assert.Equal(expectedCount, filtered.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true, 2)]
|
||||||
|
[InlineData(false, 1)]
|
||||||
|
public void RestrictAgainstAgeRestriction_Person_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount)
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||||
|
{
|
||||||
|
AgeRating = AgeRating.Teen,
|
||||||
|
IncludeUnknowns = includeUnknowns
|
||||||
|
});
|
||||||
|
Assert.Equal(expectedCount, filtered.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true, 2)]
|
||||||
|
[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
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction()
|
||||||
|
{
|
||||||
|
AgeRating = AgeRating.Teen,
|
||||||
|
IncludeUnknowns = includeUnknowns
|
||||||
|
});
|
||||||
|
Assert.Equal(expectedCount, filtered.Count());
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
using System.Linq;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using API.Comparators;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
@ -7,10 +9,10 @@ using API.Parser;
|
|||||||
using API.Services.Tasks.Scanner;
|
using API.Services.Tasks.Scanner;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Extensions
|
namespace API.Tests.Extensions;
|
||||||
|
|
||||||
|
public class SeriesExtensionsTests
|
||||||
{
|
{
|
||||||
public class SeriesExtensionsTests
|
|
||||||
{
|
|
||||||
[Theory]
|
[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_than_Black"}, true)]
|
[InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker_than_Black"}, true)]
|
||||||
@ -81,12 +83,286 @@ namespace API.Tests.Extensions
|
|||||||
NormalizedName = seriesInput.Length == 4 ? seriesInput[3] : API.Services.Tasks.Scanner.Parser.Parser.Normalize(seriesInput[0]),
|
NormalizedName = seriesInput.Length == 4 ? seriesInput[3] : API.Services.Tasks.Scanner.Parser.Parser.Normalize(seriesInput[0]),
|
||||||
Metadata = new SeriesMetadata()
|
Metadata = new SeriesMetadata()
|
||||||
};
|
};
|
||||||
var info = new ParserInfo();
|
var info = new ParserInfo
|
||||||
info.Series = parserSeries;
|
{
|
||||||
|
Series = parserSeries
|
||||||
|
};
|
||||||
|
|
||||||
Assert.Equal(expected, series.NameInParserInfo(info));
|
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",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal("Special 1", series.GetCoverImage());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal("Special 1", series.GetCoverImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
[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",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var vol in series.Volumes)
|
||||||
|
{
|
||||||
|
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal("Special 2", series.GetCoverImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
[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",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var vol in series.Volumes)
|
||||||
|
{
|
||||||
|
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal("Special 2", series.GetCoverImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
[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",
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var vol in series.Volumes)
|
||||||
|
{
|
||||||
|
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal("Volume 1", series.GetCoverImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
[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",
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var vol in series.Volumes)
|
||||||
|
{
|
||||||
|
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default)?.CoverImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal("Volume 1", series.GetCoverImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -165,7 +165,7 @@ public class CacheHelperTests
|
|||||||
FilePath = TestCoverArchive,
|
FilePath = TestCoverArchive,
|
||||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
LastModified = filesystemFile.LastWriteTime.DateTime
|
||||||
};
|
};
|
||||||
Assert.True(cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, false, file));
|
Assert.True(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -195,7 +195,7 @@ public class CacheHelperTests
|
|||||||
FilePath = TestCoverArchive,
|
FilePath = TestCoverArchive,
|
||||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
LastModified = filesystemFile.LastWriteTime.DateTime
|
||||||
};
|
};
|
||||||
Assert.True(cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, false, file));
|
Assert.True(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -225,15 +225,16 @@ public class CacheHelperTests
|
|||||||
FilePath = TestCoverArchive,
|
FilePath = TestCoverArchive,
|
||||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
LastModified = filesystemFile.LastWriteTime.DateTime
|
||||||
};
|
};
|
||||||
Assert.False(cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, true, file));
|
Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, true, file));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HasFileNotChangedSinceCreationOrLastScan_ModifiedSinceLastScan()
|
public void IsFileUnmodifiedSinceCreationOrLastScan_ModifiedSinceLastScan()
|
||||||
{
|
{
|
||||||
var filesystemFile = new MockFileData("")
|
var filesystemFile = new MockFileData("")
|
||||||
{
|
{
|
||||||
LastWriteTime = DateTimeOffset.Now
|
LastWriteTime = DateTimeOffset.Now,
|
||||||
|
CreationTime = DateTimeOffset.Now
|
||||||
};
|
};
|
||||||
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
||||||
{
|
{
|
||||||
@ -246,8 +247,8 @@ public class CacheHelperTests
|
|||||||
|
|
||||||
var chapter = new Chapter()
|
var chapter = new Chapter()
|
||||||
{
|
{
|
||||||
Created = filesystemFile.LastWriteTime.DateTime.Subtract(TimeSpan.FromMinutes(10)),
|
Created = DateTime.Now.Subtract(TimeSpan.FromMinutes(10)),
|
||||||
LastModified = filesystemFile.LastWriteTime.DateTime.Subtract(TimeSpan.FromMinutes(10))
|
LastModified = DateTime.Now.Subtract(TimeSpan.FromMinutes(10))
|
||||||
};
|
};
|
||||||
|
|
||||||
var file = new MangaFile()
|
var file = new MangaFile()
|
||||||
@ -255,7 +256,7 @@ public class CacheHelperTests
|
|||||||
FilePath = Path.Join(TestCoverImageDirectory, TestCoverArchive),
|
FilePath = Path.Join(TestCoverImageDirectory, TestCoverArchive),
|
||||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
LastModified = filesystemFile.LastWriteTime.DateTime
|
||||||
};
|
};
|
||||||
Assert.False(cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, false, file));
|
Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -276,8 +277,8 @@ public class CacheHelperTests
|
|||||||
|
|
||||||
var chapter = new Chapter()
|
var chapter = new Chapter()
|
||||||
{
|
{
|
||||||
Created = filesystemFile.LastWriteTime.DateTime.Subtract(TimeSpan.FromMinutes(10)),
|
Created = DateTime.Now.Subtract(TimeSpan.FromMinutes(10)),
|
||||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
LastModified = DateTime.Now
|
||||||
};
|
};
|
||||||
|
|
||||||
var file = new MangaFile()
|
var file = new MangaFile()
|
||||||
@ -285,7 +286,7 @@ public class CacheHelperTests
|
|||||||
FilePath = Path.Join(TestCoverImageDirectory, TestCoverArchive),
|
FilePath = Path.Join(TestCoverImageDirectory, TestCoverArchive),
|
||||||
LastModified = filesystemFile.LastWriteTime.DateTime
|
LastModified = filesystemFile.LastWriteTime.DateTime
|
||||||
};
|
};
|
||||||
Assert.False(cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, false, file));
|
Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,13 @@ using API.Entities;
|
|||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
|
|
||||||
namespace API.Tests.Helpers
|
namespace API.Tests.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to help quickly create DB entities for Unit Testing
|
||||||
|
/// </summary>
|
||||||
|
public static class EntityFactory
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Used to help quickly create DB entities for Unit Testing
|
|
||||||
/// </summary>
|
|
||||||
public static class EntityFactory
|
|
||||||
{
|
|
||||||
public static Series CreateSeries(string name)
|
public static Series CreateSeries(string name)
|
||||||
{
|
{
|
||||||
return new Series()
|
return new Series()
|
||||||
@ -79,5 +79,4 @@ namespace API.Tests.Helpers
|
|||||||
Promoted = promoted
|
Promoted = promoted
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,10 @@ using API.Entities.Enums;
|
|||||||
using API.Parser;
|
using API.Parser;
|
||||||
using API.Services.Tasks.Scanner;
|
using API.Services.Tasks.Scanner;
|
||||||
|
|
||||||
namespace API.Tests.Helpers
|
namespace API.Tests.Helpers;
|
||||||
|
|
||||||
|
public static class ParserInfoFactory
|
||||||
{
|
{
|
||||||
public static class ParserInfoFactory
|
|
||||||
{
|
|
||||||
public static ParserInfo CreateParsedInfo(string series, string volumes, string chapters, string filename, bool isSpecial)
|
public static ParserInfo CreateParsedInfo(string series, string volumes, string chapters, string filename, bool isSpecial)
|
||||||
{
|
{
|
||||||
return new ParserInfo()
|
return new ParserInfo()
|
||||||
@ -69,5 +69,4 @@ namespace API.Tests.Helpers
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace API.Tests.Helpers
|
namespace API.Tests.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given a -testcase.txt file, will generate a folder with fake archive or book files. These files are just renamed txt files.
|
||||||
|
/// <remarks>This currently is broken - you cannot create files from a unit test it seems</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public static class TestCaseGenerator
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Given a -testcase.txt file, will generate a folder with fake archive or book files. These files are just renamed txt files.
|
|
||||||
/// <remarks>This currently is broken - you cannot create files from a unit test it seems</remarks>
|
|
||||||
/// </summary>
|
|
||||||
public static class TestCaseGenerator
|
|
||||||
{
|
|
||||||
public static string GenerateFiles(string directory, string fileToExpand)
|
public static string GenerateFiles(string directory, string fileToExpand)
|
||||||
{
|
{
|
||||||
//var files = Directory.GetFiles(directory, fileToExpand);
|
//var files = Directory.GetFiles(directory, fileToExpand);
|
||||||
@ -49,5 +49,4 @@ namespace API.Tests.Helpers
|
|||||||
|
|
||||||
return newDirectory;
|
return newDirectory;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Parser
|
namespace API.Tests.Parser;
|
||||||
|
|
||||||
|
public class BookParserTests
|
||||||
{
|
{
|
||||||
public class BookParserTests
|
|
||||||
{
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", "Gifting The Wonderful World With Blessings!")]
|
[InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", "Gifting The Wonderful World With Blessings!")]
|
||||||
[InlineData("BBC Focus 00 The Science of Happiness 2nd Edition (2018)", "BBC Focus 00 The Science of Happiness 2nd Edition")]
|
[InlineData("BBC Focus 00 The Science of Happiness 2nd Edition (2018)", "BBC Focus 00 The Science of Happiness 2nd Edition")]
|
||||||
@ -39,5 +39,4 @@ namespace API.Tests.Parser
|
|||||||
// var actual = API.Parser.Parser.CssImportUrlRegex.Replace(input, "$1" + apiBase + "$2" + "$3");
|
// var actual = API.Parser.Parser.CssImportUrlRegex.Replace(input, "$1" + apiBase + "$2" + "$3");
|
||||||
// Assert.Equal(expected, actual);
|
// Assert.Equal(expected, actual);
|
||||||
// }
|
// }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using API.Parser;
|
using API.Parser;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace API.Tests.Parser
|
namespace API.Tests.Parser;
|
||||||
|
|
||||||
|
public class ComicParserTests
|
||||||
{
|
{
|
||||||
public class ComicParserTests
|
|
||||||
{
|
|
||||||
private readonly ITestOutputHelper _testOutputHelper;
|
private readonly ITestOutputHelper _testOutputHelper;
|
||||||
private readonly DefaultParser _defaultParser;
|
private readonly DefaultParser _defaultParser;
|
||||||
|
|
||||||
@ -77,6 +78,8 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Bd Fr-Aldebaran-Antares-t6", "Aldebaran-Antares")]
|
[InlineData("Bd Fr-Aldebaran-Antares-t6", "Aldebaran-Antares")]
|
||||||
[InlineData("Tintin - T22 Vol 714 pour Sydney", "Tintin")]
|
[InlineData("Tintin - T22 Vol 714 pour Sydney", "Tintin")]
|
||||||
[InlineData("Fables 2010 Vol. 1 Legends in Exile", "Fables 2010")]
|
[InlineData("Fables 2010 Vol. 1 Legends in Exile", "Fables 2010")]
|
||||||
|
[InlineData("Kebab Том 1 Глава 1", "Kebab")]
|
||||||
|
[InlineData("Манга Глава 1", "Манга")]
|
||||||
public void ParseComicSeriesTest(string filename, string expected)
|
public void ParseComicSeriesTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicSeries(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicSeries(filename));
|
||||||
@ -124,6 +127,9 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Chevaliers d'Héliopolis T3 - Rubedo, l'oeuvre au rouge (Jodorowsky & Jérémy)", "3")]
|
[InlineData("Chevaliers d'Héliopolis T3 - Rubedo, l'oeuvre au rouge (Jodorowsky & Jérémy)", "3")]
|
||||||
[InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", "0")]
|
[InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", "0")]
|
||||||
[InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", "1")]
|
[InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", "1")]
|
||||||
|
// Russian Tests
|
||||||
|
[InlineData("Kebab Том 1 Глава 3", "1")]
|
||||||
|
[InlineData("Манга Глава 2", "0")]
|
||||||
public void ParseComicVolumeTest(string filename, string expected)
|
public void ParseComicVolumeTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicVolume(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicVolume(filename));
|
||||||
@ -169,6 +175,10 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Batman Beyond 2016 - Chapter 001.cbz", "1")]
|
[InlineData("Batman Beyond 2016 - Chapter 001.cbz", "1")]
|
||||||
[InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", "1")]
|
[InlineData("Adventure Time (2012)/Adventure Time #1 (2012)", "1")]
|
||||||
[InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", "0")]
|
[InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", "0")]
|
||||||
|
[InlineData("Kebab Том 1 Глава 3", "3")]
|
||||||
|
[InlineData("Манга Глава 2", "2")]
|
||||||
|
[InlineData("Манга 2 Глава", "2")]
|
||||||
|
[InlineData("Манга Том 1 2 Глава", "2")]
|
||||||
public void ParseComicChapterTest(string filename, string expected)
|
public void ParseComicChapterTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicChapter(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseComicChapter(filename));
|
||||||
@ -188,9 +198,12 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Adventure Time 2013 Annual #001 (2013)", true)]
|
[InlineData("Adventure Time 2013 Annual #001 (2013)", true)]
|
||||||
[InlineData("Adventure Time 2013_Annual_#001 (2013)", true)]
|
[InlineData("Adventure Time 2013_Annual_#001 (2013)", true)]
|
||||||
[InlineData("Adventure Time 2013_-_Annual #001 (2013)", true)]
|
[InlineData("Adventure Time 2013_-_Annual #001 (2013)", true)]
|
||||||
public void ParseComicSpecialTest(string input, bool expected)
|
[InlineData("G.I. Joe - A Real American Hero Yearbook 004 Reprint (2021)", false)]
|
||||||
|
[InlineData("Mazebook 001", false)]
|
||||||
|
[InlineData("X-23 One Shot (2010)", true)]
|
||||||
|
[InlineData("Casus Belli v1 Hors-Série 21 - Mousquetaires et Sorcellerie", true)]
|
||||||
|
public void IsComicSpecialTest(string input, bool expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, !string.IsNullOrEmpty(API.Services.Tasks.Scanner.Parser.Parser.ParseComicSpecial(input)));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.IsComicSpecial(input));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using System.IO.Abstractions.TestingHelpers;
|
|||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Parser;
|
using API.Parser;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@ -77,6 +78,21 @@ public class DefaultParserTests
|
|||||||
Assert.Equal(expectedParseInfo, actual.Series);
|
Assert.Equal(expectedParseInfo, actual.Series);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/manga/Btooom!/Specials/Art Book.cbz", "Btooom!")]
|
||||||
|
public void ParseFromFallbackFolders_ShouldUseExistingSeriesName_NewScanLoop(string inputFile, string expectedParseInfo)
|
||||||
|
{
|
||||||
|
const string rootDirectory = "/manga/";
|
||||||
|
var fs = new MockFileSystem();
|
||||||
|
fs.AddDirectory(rootDirectory);
|
||||||
|
fs.AddFile(inputFile, new MockFileData(""));
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs);
|
||||||
|
var parser = new DefaultParser(ds);
|
||||||
|
var actual = parser.Parse(inputFile, rootDirectory);
|
||||||
|
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
|
||||||
|
Assert.Equal(expectedParseInfo, actual.Series);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
@ -87,6 +103,7 @@ public class DefaultParserTests
|
|||||||
{
|
{
|
||||||
const string rootPath = @"E:/Manga/";
|
const string rootPath = @"E:/Manga/";
|
||||||
var expected = new Dictionary<string, ParserInfo>();
|
var expected = new Dictionary<string, ParserInfo>();
|
||||||
|
|
||||||
var filepath = @"E:/Manga/Mujaki no Rakuen/Mujaki no Rakuen Vol12 ch76.cbz";
|
var filepath = @"E:/Manga/Mujaki no Rakuen/Mujaki no Rakuen Vol12 ch76.cbz";
|
||||||
expected.Add(filepath, new ParserInfo
|
expected.Add(filepath, new ParserInfo
|
||||||
{
|
{
|
||||||
@ -199,14 +216,6 @@ public class DefaultParserTests
|
|||||||
FullFilePath = filepath, IsSpecial = false
|
FullFilePath = filepath, IsSpecial = false
|
||||||
});
|
});
|
||||||
|
|
||||||
filepath = @"E:\Manga\Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub";
|
|
||||||
expected.Add(filepath, new ParserInfo
|
|
||||||
{
|
|
||||||
Series = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows", Volumes = "2.5", Edition = "",
|
|
||||||
Chapters = "0", Filename = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", Format = MangaFormat.Epub,
|
|
||||||
FullFilePath = filepath, IsSpecial = false
|
|
||||||
});
|
|
||||||
|
|
||||||
// If an image is cover exclusively, ignore it
|
// If an image is cover exclusively, ignore it
|
||||||
filepath = @"E:\Manga\Seraph of the End\cover.png";
|
filepath = @"E:\Manga\Seraph of the End\cover.png";
|
||||||
expected.Add(filepath, null);
|
expected.Add(filepath, null);
|
||||||
@ -219,11 +228,12 @@ public class DefaultParserTests
|
|||||||
FullFilePath = filepath, IsSpecial = false
|
FullFilePath = filepath, IsSpecial = false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Note: Fallback to folder will parse Monster #8 and get Monster
|
||||||
filepath = @"E:\Manga\Monster #8\Ch. 001-016 [MangaPlus] [Digital] [amit34521]\Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]\13.jpg";
|
filepath = @"E:\Manga\Monster #8\Ch. 001-016 [MangaPlus] [Digital] [amit34521]\Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]\13.jpg";
|
||||||
expected.Add(filepath, new ParserInfo
|
expected.Add(filepath, new ParserInfo
|
||||||
{
|
{
|
||||||
Series = "Monster #8", Volumes = "0", Edition = "",
|
Series = "Monster", Volumes = "0", Edition = "",
|
||||||
Chapters = "1", Filename = "13.jpg", Format = MangaFormat.Archive,
|
Chapters = "1", Filename = "13.jpg", Format = MangaFormat.Image,
|
||||||
FullFilePath = filepath, IsSpecial = false
|
FullFilePath = filepath, IsSpecial = false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -235,6 +245,29 @@ public class DefaultParserTests
|
|||||||
FullFilePath = filepath, IsSpecial = false
|
FullFilePath = filepath, IsSpecial = false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Vol19\ch186\Vol. 19 p106.gif";
|
||||||
|
expected.Add(filepath, new ParserInfo
|
||||||
|
{
|
||||||
|
Series = "Just Images the second", Volumes = "19", Edition = "",
|
||||||
|
Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image,
|
||||||
|
FullFilePath = filepath, IsSpecial = false
|
||||||
|
});
|
||||||
|
|
||||||
|
filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Blank Folder\Vol19\ch186\Vol. 19 p106.gif";
|
||||||
|
expected.Add(filepath, new ParserInfo
|
||||||
|
{
|
||||||
|
Series = "Just Images the second", Volumes = "19", Edition = "",
|
||||||
|
Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image,
|
||||||
|
FullFilePath = filepath, IsSpecial = false
|
||||||
|
});
|
||||||
|
|
||||||
|
filepath = @"E:\Manga\Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub";
|
||||||
|
expected.Add(filepath, new ParserInfo
|
||||||
|
{
|
||||||
|
Series = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows", Volumes = "2.5", Edition = "",
|
||||||
|
Chapters = "0", Filename = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", Format = MangaFormat.Epub,
|
||||||
|
FullFilePath = filepath, IsSpecial = false
|
||||||
|
});
|
||||||
|
|
||||||
foreach (var file in expected.Keys)
|
foreach (var file in expected.Keys)
|
||||||
{
|
{
|
||||||
@ -243,7 +276,7 @@ public class DefaultParserTests
|
|||||||
if (expectedInfo == null)
|
if (expectedInfo == null)
|
||||||
{
|
{
|
||||||
Assert.Null(actual);
|
Assert.Null(actual);
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
Assert.NotNull(actual);
|
Assert.NotNull(actual);
|
||||||
_testOutputHelper.WriteLine($"Validating {file}");
|
_testOutputHelper.WriteLine($"Validating {file}");
|
||||||
@ -383,7 +416,7 @@ public class DefaultParserTests
|
|||||||
if (expectedInfo == null)
|
if (expectedInfo == null)
|
||||||
{
|
{
|
||||||
Assert.Null(actual);
|
Assert.Null(actual);
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
Assert.NotNull(actual);
|
Assert.NotNull(actual);
|
||||||
_testOutputHelper.WriteLine($"Validating {file}");
|
_testOutputHelper.WriteLine($"Validating {file}");
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace API.Tests.Parser
|
namespace API.Tests.Parser;
|
||||||
|
|
||||||
|
public class MangaParserTests
|
||||||
{
|
{
|
||||||
public class MangaParserTests
|
|
||||||
{
|
|
||||||
private readonly ITestOutputHelper _testOutputHelper;
|
private readonly ITestOutputHelper _testOutputHelper;
|
||||||
|
|
||||||
public MangaParserTests(ITestOutputHelper testOutputHelper)
|
public MangaParserTests(ITestOutputHelper testOutputHelper)
|
||||||
@ -27,7 +28,7 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("vol_356-1", "356")] // Mangapy syntax
|
[InlineData("vol_356-1", "356")] // Mangapy syntax
|
||||||
[InlineData("No Volume", "0")]
|
[InlineData("No Volume", "0")]
|
||||||
[InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "1")]
|
[InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "1")]
|
||||||
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1")]
|
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1.1")]
|
||||||
[InlineData("Tonikaku Cawaii [Volume 11].cbz", "11")]
|
[InlineData("Tonikaku Cawaii [Volume 11].cbz", "11")]
|
||||||
[InlineData("[WS]_Ichiban_Ushiro_no_Daimaou_v02_ch10.zip", "2")]
|
[InlineData("[WS]_Ichiban_Ushiro_no_Daimaou_v02_ch10.zip", "2")]
|
||||||
[InlineData("[xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans]", "1")]
|
[InlineData("[xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans]", "1")]
|
||||||
@ -39,7 +40,6 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Ichinensei_ni_Nacchattara_v02_ch11_[Taruby]_v1.3.zip", "2")]
|
[InlineData("Ichinensei_ni_Nacchattara_v02_ch11_[Taruby]_v1.3.zip", "2")]
|
||||||
[InlineData("Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", "1")]
|
[InlineData("Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", "1")]
|
||||||
[InlineData("Dorohedoro v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")]
|
[InlineData("Dorohedoro v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")]
|
||||||
[InlineData("Dorohedoro v12 (2013) (Digital) (LostNerevarine-Empire).cbz", "12")]
|
|
||||||
[InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")]
|
[InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")]
|
||||||
[InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "0")]
|
[InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", "0")]
|
||||||
[InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1")]
|
[InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1")]
|
||||||
@ -69,10 +69,19 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("幽游白书完全版 第03卷 天下", "3")]
|
[InlineData("幽游白书完全版 第03卷 天下", "3")]
|
||||||
[InlineData("阿衰online 第1册", "1")]
|
[InlineData("阿衰online 第1册", "1")]
|
||||||
[InlineData("【TFO汉化&Petit汉化】迷你偶像漫画卷2第25话", "2")]
|
[InlineData("【TFO汉化&Petit汉化】迷你偶像漫画卷2第25话", "2")]
|
||||||
[InlineData("63권#200", "63")]
|
|
||||||
[InlineData("시즌34삽화2", "34")]
|
|
||||||
[InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1巻", "1")]
|
[InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1巻", "1")]
|
||||||
[InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1-3巻", "1-3")]
|
[InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1-3巻", "1-3")]
|
||||||
|
[InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", "3.5")]
|
||||||
|
[InlineData("Kebab Том 1 Глава 3", "1")]
|
||||||
|
[InlineData("Манга Глава 2", "0")]
|
||||||
|
[InlineData("Манга Тома 1-4", "1-4")]
|
||||||
|
[InlineData("Манга Том 1-4", "1-4")]
|
||||||
|
[InlineData("조선왕조실톡 106화", "106")]
|
||||||
|
[InlineData("죽음 13회", "13")]
|
||||||
|
[InlineData("동의보감 13장", "13")]
|
||||||
|
[InlineData("몰?루 아카이브 7.5권", "7.5")]
|
||||||
|
[InlineData("63권#200", "63")]
|
||||||
|
[InlineData("시즌34삽화2", "34")]
|
||||||
public void ParseVolumeTest(string filename, string expected)
|
public void ParseVolumeTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename));
|
||||||
@ -181,6 +190,11 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("諌山創] 進撃の巨人 第23巻", "諌山創] 進撃の巨人")]
|
[InlineData("諌山創] 進撃の巨人 第23巻", "諌山創] 進撃の巨人")]
|
||||||
[InlineData("(一般コミック) [奥浩哉] いぬやしき 第09巻", "いぬやしき")]
|
[InlineData("(一般コミック) [奥浩哉] いぬやしき 第09巻", "いぬやしき")]
|
||||||
[InlineData("Highschool of the Dead - 02", "Highschool of the Dead")]
|
[InlineData("Highschool of the Dead - 02", "Highschool of the Dead")]
|
||||||
|
[InlineData("Kebab Том 1 Глава 3", "Kebab")]
|
||||||
|
[InlineData("Манга Глава 2", "Манга")]
|
||||||
|
[InlineData("Манга Глава 2-2", "Манга")]
|
||||||
|
[InlineData("Манга Том 1 3-4 Глава", "Манга")]
|
||||||
|
[InlineData("Esquire 6권 2021년 10월호", "Esquire")]
|
||||||
public void ParseSeriesTest(string filename, string expected)
|
public void ParseSeriesTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename));
|
||||||
@ -195,7 +209,7 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "1-8")]
|
[InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "1-8")]
|
||||||
[InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "0")]
|
[InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", "0")]
|
||||||
[InlineData("c001", "1")]
|
[InlineData("c001", "1")]
|
||||||
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", "12")]
|
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", "0")]
|
||||||
[InlineData("Adding volume 1 with File: Ana Satsujin Vol. 1 Ch. 5 - Manga Box (gb).cbz", "5")]
|
[InlineData("Adding volume 1 with File: Ana Satsujin Vol. 1 Ch. 5 - Manga Box (gb).cbz", "5")]
|
||||||
[InlineData("Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz", "18")]
|
[InlineData("Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz", "18")]
|
||||||
[InlineData("Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip", "0-6")]
|
[InlineData("Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip", "0-6")]
|
||||||
@ -233,8 +247,7 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter.rar", "0")]
|
[InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter.rar", "0")]
|
||||||
[InlineData("Beelzebub_153b_RHS.zip", "153.5")]
|
[InlineData("Beelzebub_153b_RHS.zip", "153.5")]
|
||||||
[InlineData("Beelzebub_150-153b_RHS.zip", "150-153.5")]
|
[InlineData("Beelzebub_150-153b_RHS.zip", "150-153.5")]
|
||||||
[InlineData("Transferred to another world magical swordsman v1.1", "1")]
|
[InlineData("Transferred to another world magical swordsman v1.1", "0")]
|
||||||
[InlineData("Transferred to another world magical swordsman v1.2", "2")]
|
|
||||||
[InlineData("Kiss x Sis - Ch.15 - The Angst of a 15 Year Old Boy.cbz", "15")]
|
[InlineData("Kiss x Sis - Ch.15 - The Angst of a 15 Year Old Boy.cbz", "15")]
|
||||||
[InlineData("Kiss x Sis - Ch.12 - 1 , 2 , 3P!.cbz", "12")]
|
[InlineData("Kiss x Sis - Ch.12 - 1 , 2 , 3P!.cbz", "12")]
|
||||||
[InlineData("Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1", "1")]
|
[InlineData("Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1", "1")]
|
||||||
@ -257,8 +270,14 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Kaiju No. 8 036 (2021) (Digital)", "36")]
|
[InlineData("Kaiju No. 8 036 (2021) (Digital)", "36")]
|
||||||
[InlineData("Samurai Jack Vol. 01 - The threads of Time", "0")]
|
[InlineData("Samurai Jack Vol. 01 - The threads of Time", "0")]
|
||||||
[InlineData("【TFO汉化&Petit汉化】迷你偶像漫画第25话", "25")]
|
[InlineData("【TFO汉化&Petit汉化】迷你偶像漫画第25话", "25")]
|
||||||
|
[InlineData("자유록 13회#2", "13")]
|
||||||
[InlineData("이세계에서 고아원을 열었지만, 어째서인지 아무도 독립하려 하지 않는다 38-1화 ", "38")]
|
[InlineData("이세계에서 고아원을 열었지만, 어째서인지 아무도 독립하려 하지 않는다 38-1화 ", "38")]
|
||||||
[InlineData("[ハレム]ナナとカオル ~高校生のSMごっこ~ 第10話", "10")]
|
[InlineData("[ハレム]ナナとカオル ~高校生のSMごっこ~ 第10話", "10")]
|
||||||
|
[InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", "0")]
|
||||||
|
[InlineData("Kebab Том 1 Глава 3", "3")]
|
||||||
|
[InlineData("Манга Глава 2", "2")]
|
||||||
|
[InlineData("Манга 2 Глава", "2")]
|
||||||
|
[InlineData("Манга Том 1 2 Глава", "2")]
|
||||||
public void ParseChaptersTest(string filename, string expected)
|
public void ParseChaptersTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
||||||
@ -272,6 +291,7 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Wotakoi - Love is Hard for Otaku Omnibus v01 (2018) (Digital) (danke-Empire)", "Omnibus")]
|
[InlineData("Wotakoi - Love is Hard for Otaku Omnibus v01 (2018) (Digital) (danke-Empire)", "Omnibus")]
|
||||||
[InlineData("To Love Ru v01 Uncensored (Ch.001-007)", "Uncensored")]
|
[InlineData("To Love Ru v01 Uncensored (Ch.001-007)", "Uncensored")]
|
||||||
[InlineData("Chobits Omnibus Edition v01 [Dark Horse]", "Omnibus Edition")]
|
[InlineData("Chobits Omnibus Edition v01 [Dark Horse]", "Omnibus Edition")]
|
||||||
|
[InlineData("Chobits_Omnibus_Edition_v01_[Dark_Horse]", "Omnibus Edition")]
|
||||||
[InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "")]
|
[InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "")]
|
||||||
[InlineData("AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz", "")]
|
[InlineData("AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz", "")]
|
||||||
[InlineData("Love Hina Omnibus v05 (2015) (Digital-HD) (Asgard-Empire).cbz", "Omnibus")]
|
[InlineData("Love Hina Omnibus v05 (2015) (Digital-HD) (Asgard-Empire).cbz", "Omnibus")]
|
||||||
@ -294,9 +314,11 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Beastars SP01", false)]
|
[InlineData("Beastars SP01", false)]
|
||||||
[InlineData("The League of Extraordinary Gentlemen", false)]
|
[InlineData("The League of Extraordinary Gentlemen", false)]
|
||||||
[InlineData("The League of Extra-ordinary Gentlemen", false)]
|
[InlineData("The League of Extra-ordinary Gentlemen", false)]
|
||||||
public void ParseMangaSpecialTest(string input, bool expected)
|
[InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown].epub", true)]
|
||||||
|
[InlineData("Dr. Ramune - Mysterious Disease Specialist v01 (2020) (Digital) (danke-Empire).cbz", false)]
|
||||||
|
public void IsMangaSpecialTest(string input, bool expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, !string.IsNullOrEmpty(API.Services.Tasks.Scanner.Parser.Parser.ParseMangaSpecial(input)));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.IsMangaSpecial(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -308,14 +330,5 @@ namespace API.Tests.Parser
|
|||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseFormat(inputFile));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseFormat(inputFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown].epub", "Side Stories")]
|
|
||||||
public void ParseSpecialTest(string inputFile, string expected)
|
|
||||||
{
|
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseMangaSpecial(inputFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
using API.Parser;
|
using API.Parser;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Parser
|
namespace API.Tests.Parser;
|
||||||
|
|
||||||
|
public class ParserInfoTests
|
||||||
{
|
{
|
||||||
public class ParserInfoTests
|
|
||||||
{
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MergeFromTest()
|
public void MergeFromTest()
|
||||||
{
|
{
|
||||||
@ -106,5 +106,4 @@ namespace API.Tests.Parser
|
|||||||
Assert.Equal(expected.IsSpecial, actual.IsSpecial);
|
Assert.Equal(expected.IsSpecial, actual.IsSpecial);
|
||||||
Assert.Equal(expected.FullFilePath, actual.FullFilePath);
|
Assert.Equal(expected.FullFilePath, actual.FullFilePath);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,10 @@ using System.Linq;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using static API.Services.Tasks.Scanner.Parser.Parser;
|
using static API.Services.Tasks.Scanner.Parser.Parser;
|
||||||
|
|
||||||
namespace API.Tests.Parser
|
namespace API.Tests.Parser;
|
||||||
|
|
||||||
|
public class ParserTests
|
||||||
{
|
{
|
||||||
public class ParserTests
|
|
||||||
{
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Joe Shmo, Green Blue", "Joe Shmo, Green Blue")]
|
[InlineData("Joe Shmo, Green Blue", "Joe Shmo, Green Blue")]
|
||||||
[InlineData("Shmo, Joe", "Shmo, Joe")]
|
[InlineData("Shmo, Joe", "Shmo, Joe")]
|
||||||
@ -64,6 +64,11 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", false, "Kasumi Otoko no Ko v1.1")]
|
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", false, "Kasumi Otoko no Ko v1.1")]
|
||||||
[InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 04 (2019) (digital) (Son of Ultron-Empire)", true, "Batman - Detective Comics - Rebirth Deluxe Edition")]
|
[InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 04 (2019) (digital) (Son of Ultron-Empire)", true, "Batman - Detective Comics - Rebirth Deluxe Edition")]
|
||||||
[InlineData("Something - Full Color Edition", false, "Something - Full Color Edition")]
|
[InlineData("Something - Full Color Edition", false, "Something - Full Color Edition")]
|
||||||
|
[InlineData("Witchblade 089 (2005) (Bittertek-DCP) (Top Cow (Image Comics))", true, "Witchblade 089")]
|
||||||
|
[InlineData("(C99) Kami-sama Hiroimashita. (SSSS.GRIDMAN)", false, "Kami-sama Hiroimashita.")]
|
||||||
|
[InlineData("Dr. Ramune - Mysterious Disease Specialist v01 (2020) (Digital) (danke-Empire)", false, "Dr. Ramune - Mysterious Disease Specialist v01")]
|
||||||
|
[InlineData("Magic Knight Rayearth {Omnibus Edition}", false, "Magic Knight Rayearth {}")]
|
||||||
|
[InlineData("Magic Knight Rayearth {Omnibus Version}", false, "Magic Knight Rayearth { Version}")]
|
||||||
public void CleanTitleTest(string input, bool isComic, string expected)
|
public void CleanTitleTest(string input, bool isComic, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, CleanTitle(input, isComic));
|
Assert.Equal(expected, CleanTitle(input, isComic));
|
||||||
@ -138,6 +143,9 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("40", 40)]
|
[InlineData("40", 40)]
|
||||||
[InlineData("40a-040b", 0)]
|
[InlineData("40a-040b", 0)]
|
||||||
[InlineData("40.1_a", 0)]
|
[InlineData("40.1_a", 0)]
|
||||||
|
[InlineData("3.5", 3.5)]
|
||||||
|
[InlineData("3.5-4.0", 3.5)]
|
||||||
|
[InlineData("asdfasdf", 0.0)]
|
||||||
public void MinimumNumberFromRangeTest(string input, float expected)
|
public void MinimumNumberFromRangeTest(string input, float expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, MinNumberFromRange(input));
|
Assert.Equal(expected, MinNumberFromRange(input));
|
||||||
@ -151,6 +159,9 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("40", 40)]
|
[InlineData("40", 40)]
|
||||||
[InlineData("40a-040b", 0)]
|
[InlineData("40a-040b", 0)]
|
||||||
[InlineData("40.1_a", 0)]
|
[InlineData("40.1_a", 0)]
|
||||||
|
[InlineData("3.5", 3.5)]
|
||||||
|
[InlineData("3.5-4.0", 4.0)]
|
||||||
|
[InlineData("asdfasdf", 0.0)]
|
||||||
public void MaximumNumberFromRangeTest(string input, float expected)
|
public void MaximumNumberFromRangeTest(string input, float expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, MaxNumberFromRange(input));
|
Assert.Equal(expected, MaxNumberFromRange(input));
|
||||||
@ -162,7 +173,7 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Darker Than_Black", "darkerthanblack")]
|
[InlineData("Darker Than_Black", "darkerthanblack")]
|
||||||
[InlineData("Citrus", "citrus")]
|
[InlineData("Citrus", "citrus")]
|
||||||
[InlineData("Citrus+", "citrus+")]
|
[InlineData("Citrus+", "citrus+")]
|
||||||
[InlineData("Again!!!!", "again")]
|
[InlineData("Again", "again")]
|
||||||
[InlineData("카비타", "카비타")]
|
[InlineData("카비타", "카비타")]
|
||||||
[InlineData("06", "06")]
|
[InlineData("06", "06")]
|
||||||
[InlineData("", "")]
|
[InlineData("", "")]
|
||||||
@ -230,5 +241,52 @@ namespace API.Tests.Parser
|
|||||||
{
|
{
|
||||||
Assert.Equal(expected, NormalizePath(inputPath));
|
Assert.Equal(expected, NormalizePath(inputPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[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")]
|
||||||
|
[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)
|
||||||
|
{
|
||||||
|
Assert.Matches($@"^{BalancedParen}$", input);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[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")]
|
||||||
|
[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)
|
||||||
|
{
|
||||||
|
Assert.DoesNotMatch($@"^{BalancedParen}$", input);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[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")]
|
||||||
|
[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)
|
||||||
|
{
|
||||||
|
Assert.Matches($@"^{BalancedBrack}$", input);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[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")]
|
||||||
|
[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)
|
||||||
|
{
|
||||||
|
Assert.DoesNotMatch($@"^{BalancedBrack}$", input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,12 +140,12 @@ public class SeriesRepositoryTests
|
|||||||
|
|
||||||
[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.Archive, "The Idaten Deities Know Only Peace")] // Matching on localized name in DB
|
||||||
[InlineData("Heion Sedai no Idaten-tachi", "", MangaFormat.Pdf, null)]
|
[InlineData("Heion Sedai no Idaten-tachi", "", MangaFormat.Pdf, null)]
|
||||||
public async Task GetFullSeriesByAnyName_Should(string seriesName, string localizedName, string? expected)
|
public async Task GetFullSeriesByAnyName_Should(string seriesName, MangaFormat format, string localizedName, string? expected)
|
||||||
{
|
{
|
||||||
var firstSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
var firstSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||||
var series =
|
var series =
|
||||||
await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(seriesName, localizedName,
|
await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(seriesName, localizedName,
|
||||||
1, MangaFormat.Unknown);
|
1, format);
|
||||||
if (expected == null)
|
if (expected == null)
|
||||||
{
|
{
|
||||||
Assert.Null(series);
|
Assert.Null(series);
|
||||||
@ -157,4 +157,6 @@ public class SeriesRepositoryTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//public async Task
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,10 @@ using NSubstitute.Extensions;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace API.Tests.Services
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
|
public class ArchiveServiceTests
|
||||||
{
|
{
|
||||||
public class ArchiveServiceTests
|
|
||||||
{
|
|
||||||
private readonly ITestOutputHelper _testOutputHelper;
|
private readonly ITestOutputHelper _testOutputHelper;
|
||||||
private readonly ArchiveService _archiveService;
|
private readonly ArchiveService _archiveService;
|
||||||
private readonly ILogger<ArchiveService> _logger = Substitute.For<ILogger<ArchiveService>>();
|
private readonly ILogger<ArchiveService> _logger = Substitute.For<ILogger<ArchiveService>>();
|
||||||
@ -39,7 +39,7 @@ namespace API.Tests.Services
|
|||||||
{
|
{
|
||||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives");
|
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives");
|
||||||
var file = Path.Join(testDirectory, archivePath);
|
var file = Path.Join(testDirectory, archivePath);
|
||||||
using ZipArchive archive = ZipFile.OpenRead(file);
|
using var archive = ZipFile.OpenRead(file);
|
||||||
Assert.Equal(expected, _archiveService.ArchiveNeedsFlattening(archive));
|
Assert.Equal(expected, _archiveService.ArchiveNeedsFlattening(archive));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,17 +256,31 @@ namespace API.Tests.Services
|
|||||||
Assert.Equal("Junya Inoue", comicInfo.Writer);
|
Assert.Equal("Junya Inoue", comicInfo.Writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void ShouldHaveComicInfo_TopLevelFileOnly()
|
[InlineData("ComicInfo_duplicateInfos.zip")]
|
||||||
|
[InlineData("ComicInfo_duplicateInfos_reversed.zip")]
|
||||||
|
[InlineData("ComicInfo_duplicateInfos.rar")]
|
||||||
|
public void ShouldHaveComicInfo_TopLevelFileOnly(string filename)
|
||||||
{
|
{
|
||||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
||||||
var archive = Path.Join(testDirectory, "ComicInfo_duplicateInfos.zip");
|
var archive = Path.Join(testDirectory, filename);
|
||||||
|
|
||||||
var comicInfo = _archiveService.GetComicInfo(archive);
|
var comicInfo = _archiveService.GetComicInfo(archive);
|
||||||
Assert.NotNull(comicInfo);
|
Assert.NotNull(comicInfo);
|
||||||
Assert.Equal("BTOOOM!", comicInfo.Series);
|
Assert.Equal("BTOOOM!", comicInfo.Series);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShouldHaveComicInfo_OutsideRoot()
|
||||||
|
{
|
||||||
|
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
||||||
|
var archive = Path.Join(testDirectory, "ComicInfo_outside_root.zip");
|
||||||
|
|
||||||
|
var comicInfo = _archiveService.GetComicInfo(archive);
|
||||||
|
Assert.NotNull(comicInfo);
|
||||||
|
Assert.Equal("BTOOOM! - Duplicate", comicInfo.Series);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region CanParseComicInfo
|
#region CanParseComicInfo
|
||||||
@ -276,24 +290,41 @@ namespace API.Tests.Services
|
|||||||
{
|
{
|
||||||
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
||||||
var archive = Path.Join(testDirectory, "ComicInfo.zip");
|
var archive = Path.Join(testDirectory, "ComicInfo.zip");
|
||||||
var actual = _archiveService.GetComicInfo(archive);
|
var comicInfo = _archiveService.GetComicInfo(archive);
|
||||||
var expected = new ComicInfo()
|
|
||||||
{
|
|
||||||
Publisher = "Yen Press",
|
|
||||||
Genre = "Manga, Movies & TV",
|
|
||||||
Summary =
|
|
||||||
"By all counts, Ryouta Sakamoto is a loser when he's not holed up in his room, bombing things into oblivion in his favorite online action RPG. But his very own uneventful life is blown to pieces when he's abducted and taken to an uninhabited island, where he soon learns the hard way that he's being pitted against others just like him in a explosives-riddled death match! How could this be happening? Who's putting them up to this? And why!? The name, not to mention the objective, of this very real survival game is eerily familiar to Ryouta, who has mastered its virtual counterpart-BTOOOM! Can Ryouta still come out on top when he's playing for his life!?",
|
|
||||||
PageCount = 194,
|
|
||||||
LanguageISO = "en",
|
|
||||||
Notes = "Scraped metadata from Comixology [CMXDB450184]",
|
|
||||||
Series = "BTOOOM!",
|
|
||||||
Title = "v01",
|
|
||||||
Web = "https://www.comixology.com/BTOOOM/digital-comic/450184"
|
|
||||||
};
|
|
||||||
|
|
||||||
Assert.NotStrictEqual(expected, actual);
|
Assert.NotNull(comicInfo);
|
||||||
|
Assert.Equal("Yen Press", comicInfo.Publisher);
|
||||||
|
Assert.Equal("Manga, Movies & TV", comicInfo.Genre);
|
||||||
|
Assert.Equal("By all counts, Ryouta Sakamoto is a loser when he's not holed up in his room, bombing things into oblivion in his favorite online action RPG. But his very own uneventful life is blown to pieces when he's abducted and taken to an uninhabited island, where he soon learns the hard way that he's being pitted against others just like him in a explosives-riddled death match! How could this be happening? Who's putting them up to this? And why!? The name, not to mention the objective, of this very real survival game is eerily familiar to Ryouta, who has mastered its virtual counterpart-BTOOOM! Can Ryouta still come out on top when he's playing for his life!?",
|
||||||
|
comicInfo.Summary);
|
||||||
|
Assert.Equal(194, comicInfo.PageCount);
|
||||||
|
Assert.Equal("en", comicInfo.LanguageISO);
|
||||||
|
Assert.Equal("Scraped metadata from Comixology [CMXDB450184]", comicInfo.Notes);
|
||||||
|
Assert.Equal("BTOOOM!", comicInfo.Series);
|
||||||
|
Assert.Equal("v01", comicInfo.Title);
|
||||||
|
Assert.Equal("https://www.comixology.com/BTOOOM/digital-comic/450184", comicInfo.Web);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region CanParseComicInfo_DefaultNumberIsBlank
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanParseComicInfo_DefaultNumberIsBlank()
|
||||||
|
{
|
||||||
|
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
||||||
|
var archive = Path.Join(testDirectory, "ComicInfo2.zip");
|
||||||
|
var comicInfo = _archiveService.GetComicInfo(archive);
|
||||||
|
|
||||||
|
Assert.NotNull(comicInfo);
|
||||||
|
Assert.Equal("Hellboy", comicInfo.Series);
|
||||||
|
Assert.Equal("The Right Hand of Doom", comicInfo.Title);
|
||||||
|
Assert.Equal("", comicInfo.Number);
|
||||||
|
Assert.Equal(0, comicInfo.Count);
|
||||||
|
Assert.Equal("4", comicInfo.Volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region FindCoverImageFilename
|
#region FindCoverImageFilename
|
||||||
@ -326,5 +357,4 @@ namespace API.Tests.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -135,17 +135,9 @@ public class BackupServiceTests
|
|||||||
filesystem.AddFile($"{LogDirectory}kavita1.log", new MockFileData(""));
|
filesystem.AddFile($"{LogDirectory}kavita1.log", new MockFileData(""));
|
||||||
|
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var inMemorySettings = new Dictionary<string, string> {
|
var backupService = new BackupService(_logger, _unitOfWork, ds, _messageHub);
|
||||||
{"Logging:File:Path", "config/logs/kavita.log"},
|
|
||||||
{"Logging:File:MaxRollingFiles", "0"},
|
|
||||||
};
|
|
||||||
IConfiguration configuration = new ConfigurationBuilder()
|
|
||||||
.AddInMemoryCollection(inMemorySettings)
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
var backupService = new BackupService(_logger, _unitOfWork, ds, configuration, _messageHub);
|
var backupLogFiles = backupService.GetLogFiles(false).ToList();
|
||||||
|
|
||||||
var backupLogFiles = backupService.GetLogFiles(0, LogDirectory).ToList();
|
|
||||||
Assert.Single(backupLogFiles);
|
Assert.Single(backupLogFiles);
|
||||||
Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath($"{LogDirectory}kavita.log"), API.Services.Tasks.Scanner.Parser.Parser.NormalizePath(backupLogFiles.First()));
|
Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath($"{LogDirectory}kavita.log"), API.Services.Tasks.Scanner.Parser.Parser.NormalizePath(backupLogFiles.First()));
|
||||||
}
|
}
|
||||||
@ -155,20 +147,12 @@ public class BackupServiceTests
|
|||||||
{
|
{
|
||||||
var filesystem = CreateFileSystem();
|
var filesystem = CreateFileSystem();
|
||||||
filesystem.AddFile($"{LogDirectory}kavita.log", new MockFileData(""));
|
filesystem.AddFile($"{LogDirectory}kavita.log", new MockFileData(""));
|
||||||
filesystem.AddFile($"{LogDirectory}kavita1.log", new MockFileData(""));
|
filesystem.AddFile($"{LogDirectory}kavita20200213.log", new MockFileData(""));
|
||||||
|
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var inMemorySettings = new Dictionary<string, string> {
|
var backupService = new BackupService(_logger, _unitOfWork, ds, _messageHub);
|
||||||
{"Logging:File:Path", "config/logs/kavita.log"},
|
|
||||||
{"Logging:File:MaxRollingFiles", "1"},
|
|
||||||
};
|
|
||||||
IConfiguration configuration = new ConfigurationBuilder()
|
|
||||||
.AddInMemoryCollection(inMemorySettings)
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
var backupService = new BackupService(_logger, _unitOfWork, ds, configuration, _messageHub);
|
var backupLogFiles = backupService.GetLogFiles().Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList();
|
||||||
|
|
||||||
var backupLogFiles = backupService.GetLogFiles(1, LogDirectory).Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList();
|
|
||||||
Assert.NotEmpty(backupLogFiles.Where(file => file.Equals(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath($"{LogDirectory}kavita.log")) || file.Equals(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath($"{LogDirectory}kavita1.log"))));
|
Assert.NotEmpty(backupLogFiles.Where(file => file.Equals(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath($"{LogDirectory}kavita.log")) || file.Equals(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath($"{LogDirectory}kavita1.log"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,10 +5,10 @@ using Microsoft.Extensions.Logging;
|
|||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Services
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
|
public class BookServiceTests
|
||||||
{
|
{
|
||||||
public class BookServiceTests
|
|
||||||
{
|
|
||||||
private readonly IBookService _bookService;
|
private readonly IBookService _bookService;
|
||||||
private readonly ILogger<BookService> _logger = Substitute.For<ILogger<BookService>>();
|
private readonly ILogger<BookService> _logger = Substitute.For<ILogger<BookService>>();
|
||||||
|
|
||||||
@ -54,5 +54,4 @@ namespace API.Tests.Services
|
|||||||
Assert.Equal("Roger Starbuck,Junya Inoue", comicInfo.Writer);
|
Assert.Equal("Roger Starbuck,Junya Inoue", comicInfo.Writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -410,7 +410,7 @@ public class BookmarkServiceTests
|
|||||||
#region Misc
|
#region Misc
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ShouldNotDeleteBookmarkOnChapterDeletion()
|
public async Task ShouldNotDeleteBookmark_OnChapterDeletion()
|
||||||
{
|
{
|
||||||
var filesystem = CreateFileSystem();
|
var filesystem = CreateFileSystem();
|
||||||
filesystem.AddFile($"{CacheDirectory}1/0001.jpg", new MockFileData("123"));
|
filesystem.AddFile($"{CacheDirectory}1/0001.jpg", new MockFileData("123"));
|
||||||
@ -462,8 +462,6 @@ public class BookmarkServiceTests
|
|||||||
|
|
||||||
|
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var bookmarkService = Create(ds);
|
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks);
|
|
||||||
|
|
||||||
var vol = await _unitOfWork.VolumeRepository.GetVolumeAsync(1);
|
var vol = await _unitOfWork.VolumeRepository.GetVolumeAsync(1);
|
||||||
vol.Chapters = new List<Chapter>();
|
vol.Chapters = new List<Chapter>();
|
||||||
@ -475,5 +473,72 @@ public class BookmarkServiceTests
|
|||||||
Assert.NotNull(await _unitOfWork.UserRepository.GetBookmarkAsync(1));
|
Assert.NotNull(await _unitOfWork.UserRepository.GetBookmarkAsync(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ShouldNotDeleteBookmark_OnVolumeDeletion()
|
||||||
|
{
|
||||||
|
var filesystem = CreateFileSystem();
|
||||||
|
filesystem.AddFile($"{CacheDirectory}1/0001.jpg", new MockFileData("123"));
|
||||||
|
filesystem.AddFile($"{BookmarkDirectory}1/1/0001.jpg", new MockFileData("123"));
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Series.Add(series);
|
||||||
|
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "Joe",
|
||||||
|
Bookmarks = new List<AppUserBookmark>()
|
||||||
|
{
|
||||||
|
new AppUserBookmark()
|
||||||
|
{
|
||||||
|
Page = 1,
|
||||||
|
ChapterId = 1,
|
||||||
|
FileName = $"1/1/0001.jpg",
|
||||||
|
SeriesId = 1,
|
||||||
|
VolumeId = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks);
|
||||||
|
Assert.NotEmpty(user.Bookmarks);
|
||||||
|
|
||||||
|
series.Volumes = new List<Volume>();
|
||||||
|
_unitOfWork.SeriesRepository.Update(series);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
|
Assert.Single(ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories));
|
||||||
|
Assert.NotNull(await _unitOfWork.UserRepository.GetBookmarkAsync(1));
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -20,10 +20,10 @@ using Microsoft.Extensions.Logging;
|
|||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Services
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
|
internal class MockReadingItemServiceForCacheService : IReadingItemService
|
||||||
{
|
{
|
||||||
internal class MockReadingItemServiceForCacheService : IReadingItemService
|
|
||||||
{
|
|
||||||
private readonly DirectoryService _directoryService;
|
private readonly DirectoryService _directoryService;
|
||||||
|
|
||||||
public MockReadingItemServiceForCacheService(DirectoryService directoryService)
|
public MockReadingItemServiceForCacheService(DirectoryService directoryService)
|
||||||
@ -60,9 +60,9 @@ namespace API.Tests.Services
|
|||||||
{
|
{
|
||||||
throw new System.NotImplementedException();
|
throw new System.NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public class CacheServiceTests
|
public class CacheServiceTests
|
||||||
{
|
{
|
||||||
private readonly ILogger<CacheService> _logger = Substitute.For<ILogger<CacheService>>();
|
private readonly ILogger<CacheService> _logger = Substitute.For<ILogger<CacheService>>();
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IHubContext<MessageHub> _messageHub = Substitute.For<IHubContext<MessageHub>>();
|
private readonly IHubContext<MessageHub> _messageHub = Substitute.For<IHubContext<MessageHub>>();
|
||||||
@ -516,5 +516,4 @@ namespace API.Tests.Services
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ public class CleanupServiceTests
|
|||||||
private const string CacheDirectory = "C:/kavita/config/cache/";
|
private const string CacheDirectory = "C:/kavita/config/cache/";
|
||||||
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
||||||
private const string BackupDirectory = "C:/kavita/config/backups/";
|
private const string BackupDirectory = "C:/kavita/config/backups/";
|
||||||
|
private const string LogDirectory = "C:/kavita/config/logs/";
|
||||||
private const string BookmarkDirectory = "C:/kavita/config/bookmarks/";
|
private const string BookmarkDirectory = "C:/kavita/config/bookmarks/";
|
||||||
|
|
||||||
|
|
||||||
@ -84,6 +85,9 @@ public class CleanupServiceTests
|
|||||||
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync();
|
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync();
|
||||||
setting.Value = BookmarkDirectory;
|
setting.Value = BookmarkDirectory;
|
||||||
|
|
||||||
|
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.TotalLogs).SingleAsync();
|
||||||
|
setting.Value = "10";
|
||||||
|
|
||||||
_context.ServerSetting.Update(setting);
|
_context.ServerSetting.Update(setting);
|
||||||
|
|
||||||
_context.Library.Add(new Library()
|
_context.Library.Add(new Library()
|
||||||
@ -347,7 +351,7 @@ public class CleanupServiceTests
|
|||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
||||||
ds);
|
ds);
|
||||||
cleanupService.CleanupCacheDirectory();
|
cleanupService.CleanupCacheAndTempDirectories();
|
||||||
Assert.Empty(ds.GetFiles(CacheDirectory, searchOption: SearchOption.AllDirectories));
|
Assert.Empty(ds.GetFiles(CacheDirectory, searchOption: SearchOption.AllDirectories));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,7 +365,7 @@ public class CleanupServiceTests
|
|||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
||||||
ds);
|
ds);
|
||||||
cleanupService.CleanupCacheDirectory();
|
cleanupService.CleanupCacheAndTempDirectories();
|
||||||
Assert.Empty(ds.GetFiles(CacheDirectory, searchOption: SearchOption.AllDirectories));
|
Assert.Empty(ds.GetFiles(CacheDirectory, searchOption: SearchOption.AllDirectories));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -412,6 +416,59 @@ public class CleanupServiceTests
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region CleanupLogs
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CleanupLogs_LeaveOneFile_SinceAllAreExpired()
|
||||||
|
{
|
||||||
|
var filesystem = CreateFileSystem();
|
||||||
|
foreach (var i in Enumerable.Range(1, 10))
|
||||||
|
{
|
||||||
|
var day = API.Services.Tasks.Scanner.Parser.Parser.PadZeros($"{i}");
|
||||||
|
filesystem.AddFile($"{LogDirectory}kavita202009{day}.log", new MockFileData("")
|
||||||
|
{
|
||||||
|
CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
|
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
||||||
|
ds);
|
||||||
|
await cleanupService.CleanupLogs();
|
||||||
|
Assert.Single(ds.GetFiles(LogDirectory, searchOption: SearchOption.AllDirectories));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CleanupLogs_LeaveLestExpired()
|
||||||
|
{
|
||||||
|
var filesystem = CreateFileSystem();
|
||||||
|
foreach (var i in Enumerable.Range(1, 9))
|
||||||
|
{
|
||||||
|
var day = API.Services.Tasks.Scanner.Parser.Parser.PadZeros($"{i}");
|
||||||
|
filesystem.AddFile($"{LogDirectory}kavita202009{day}.log", new MockFileData("")
|
||||||
|
{
|
||||||
|
CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31 - i))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
filesystem.AddFile($"{LogDirectory}kavita20200910.log", new MockFileData("")
|
||||||
|
{
|
||||||
|
CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31 - 10))
|
||||||
|
});
|
||||||
|
filesystem.AddFile($"{LogDirectory}kavita20200911.log", new MockFileData("")
|
||||||
|
{
|
||||||
|
CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31 - 11))
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
|
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
||||||
|
ds);
|
||||||
|
await cleanupService.CleanupLogs();
|
||||||
|
Assert.True(filesystem.File.Exists($"{LogDirectory}kavita20200911.log"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
// #region CleanupBookmarks
|
// #region CleanupBookmarks
|
||||||
//
|
//
|
||||||
// [Fact]
|
// [Fact]
|
||||||
|
80
API.Tests/Services/DeviceServiceTests.cs
Normal file
80
API.Tests/Services/DeviceServiceTests.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
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;
|
||||||
|
|
||||||
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
|
public class DeviceServiceTests : BasicTest
|
||||||
|
{
|
||||||
|
private readonly ILogger<DeviceService> _logger = Substitute.For<ILogger<DeviceService>>();
|
||||||
|
private readonly IDeviceService _deviceService;
|
||||||
|
|
||||||
|
public DeviceServiceTests() : base()
|
||||||
|
{
|
||||||
|
_deviceService = new DeviceService(_unitOfWork, _logger, Substitute.For<IEmailService>());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected new Task ResetDb()
|
||||||
|
{
|
||||||
|
_context.Users.RemoveRange(_context.Users.ToList());
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateDevice_Succeeds()
|
||||||
|
{
|
||||||
|
|
||||||
|
var user = new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
Devices = new List<Device>()
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Users.Add(user);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
var device = await _deviceService.Create(new CreateDeviceDto()
|
||||||
|
{
|
||||||
|
EmailAddress = "fake@kindle.com",
|
||||||
|
Name = "Test Kindle",
|
||||||
|
Platform = DevicePlatform.Kindle
|
||||||
|
}, user);
|
||||||
|
|
||||||
|
Assert.NotNull(device);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateDevice_ThrowsErrorWhenEmailDoesntMatchRules()
|
||||||
|
{
|
||||||
|
|
||||||
|
var user = new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
Devices = new List<Device>()
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Users.Add(user);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
var device = await _deviceService.Create(new CreateDeviceDto()
|
||||||
|
{
|
||||||
|
EmailAddress = "fake@gmail.com",
|
||||||
|
Name = "Test Kindle",
|
||||||
|
Platform = DevicePlatform.Kindle
|
||||||
|
}, user);
|
||||||
|
|
||||||
|
Assert.NotNull(device);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -10,11 +10,10 @@ using Microsoft.Extensions.Logging;
|
|||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Services
|
namespace API.Tests.Services;
|
||||||
{
|
|
||||||
|
|
||||||
public class DirectoryServiceTests
|
public class DirectoryServiceTests
|
||||||
{
|
{
|
||||||
private readonly ILogger<DirectoryService> _logger = Substitute.For<ILogger<DirectoryService>>();
|
private readonly ILogger<DirectoryService> _logger = Substitute.For<ILogger<DirectoryService>>();
|
||||||
|
|
||||||
|
|
||||||
@ -996,5 +995,20 @@ namespace API.Tests.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region GetLastWriteTime
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetLastWriteTime_ShouldReturnMaxTime_IfNoFiles()
|
||||||
|
{
|
||||||
|
const string dir = "C:/manga/";
|
||||||
|
var filesystem = new MockFileSystem();
|
||||||
|
filesystem.AddDirectory("C:/");
|
||||||
|
filesystem.AddDirectory(dir);
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
|
|
||||||
|
Assert.Equal(DateTime.MaxValue, ds.GetLastWriteTime(dir));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,10 @@ using System.IO.Abstractions.TestingHelpers;
|
|||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
|
||||||
namespace API.Tests.Services
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
|
public class MetadataServiceTests
|
||||||
{
|
{
|
||||||
public class MetadataServiceTests
|
|
||||||
{
|
|
||||||
private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives");
|
private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives");
|
||||||
private const string TestCoverImageFile = "thumbnail.jpg";
|
private const string TestCoverImageFile = "thumbnail.jpg";
|
||||||
private const string TestCoverArchive = @"c:\file in folder.zip";
|
private const string TestCoverArchive = @"c:\file in folder.zip";
|
||||||
@ -38,5 +38,4 @@ namespace API.Tests.Services
|
|||||||
var fileService = new FileService(fileSystem);
|
var fileService = new FileService(fileSystem);
|
||||||
_cacheHelper = new CacheHelper(fileService);
|
_cacheHelper = new CacheHelper(fileService);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ using API.Entities.Enums;
|
|||||||
using API.Parser;
|
using API.Parser;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks.Scanner;
|
using API.Services.Tasks.Scanner;
|
||||||
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using API.Tests.Helpers;
|
using API.Tests.Helpers;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
@ -52,7 +53,7 @@ internal class MockReadingItemService : IReadingItemService
|
|||||||
|
|
||||||
public void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1)
|
public void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1)
|
||||||
{
|
{
|
||||||
throw new System.NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ParserInfo Parse(string path, string rootPath, LibraryType type)
|
public ParserInfo Parse(string path, string rootPath, LibraryType type)
|
||||||
@ -244,11 +245,11 @@ public class ParseScannedFilesTests
|
|||||||
|
|
||||||
var parsedSeries = new Dictionary<ParsedSeries, IList<ParserInfo>>();
|
var parsedSeries = new Dictionary<ParsedSeries, IList<ParserInfo>>();
|
||||||
|
|
||||||
void TrackFiles(Tuple<bool, IList<ParserInfo>> parsedInfo)
|
Task TrackFiles(Tuple<bool, IList<ParserInfo>> parsedInfo)
|
||||||
{
|
{
|
||||||
var skippedScan = parsedInfo.Item1;
|
var skippedScan = parsedInfo.Item1;
|
||||||
var parsedFiles = parsedInfo.Item2;
|
var parsedFiles = parsedInfo.Item2;
|
||||||
if (parsedFiles.Count == 0) return;
|
if (parsedFiles.Count == 0) return Task.CompletedTask;
|
||||||
|
|
||||||
var foundParsedSeries = new ParsedSeries()
|
var foundParsedSeries = new ParsedSeries()
|
||||||
{
|
{
|
||||||
@ -258,6 +259,7 @@ public class ParseScannedFilesTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
parsedSeries.Add(foundParsedSeries, parsedFiles);
|
parsedSeries.Add(foundParsedSeries, parsedFiles);
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -471,6 +471,53 @@ public class ReaderServiceTests
|
|||||||
Assert.Equal("21", actualChapter.Range);
|
Assert.Equal("21", actualChapter.Range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetNextChapterIdAsync_ShouldRollIntoNextVolumeWithFloat()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("1.5", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||||
|
|
||||||
|
|
||||||
|
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||||
|
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||||
|
Assert.Equal("21", actualChapter.Range);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetNextChapterIdAsync_ShouldRollIntoChaptersFromVolume()
|
public async Task GetNextChapterIdAsync_ShouldRollIntoChaptersFromVolume()
|
||||||
{
|
{
|
||||||
@ -895,6 +942,107 @@ public class ReaderServiceTests
|
|||||||
Assert.Equal("1", actualChapter.Range);
|
Assert.Equal("1", actualChapter.Range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPrevChapterIdAsync_ShouldGetPrevVolume_WithFloatVolume()
|
||||||
|
{
|
||||||
|
// V1 -> V2
|
||||||
|
await ResetDb();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("1.5", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||||
|
|
||||||
|
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 3, 5, 1);
|
||||||
|
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||||
|
Assert.Equal("22", actualChapter.Range);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPrevChapterIdAsync_ShouldGetPrevVolume_2()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
|
||||||
|
_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>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("40", false, new List<MangaFile>(), 1),
|
||||||
|
EntityFactory.CreateChapter("50", false, new List<MangaFile>(), 1),
|
||||||
|
EntityFactory.CreateChapter("60", false, new List<MangaFile>(), 1),
|
||||||
|
EntityFactory.CreateChapter("Some Special Title", true, new List<MangaFile>(), 1),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("1997", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("2001", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("21", false, new List<MangaFile>(), 1),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("2005", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("31", false, new List<MangaFile>(), 1),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||||
|
|
||||||
|
// prevChapter should be id from ch.21 from volume 2001
|
||||||
|
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 4, 7, 1);
|
||||||
|
|
||||||
|
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||||
|
Assert.NotNull(actualChapter);
|
||||||
|
Assert.Equal("21", actualChapter.Range);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetPrevChapterIdAsync_ShouldRollIntoPrevVolume()
|
public async Task GetPrevChapterIdAsync_ShouldRollIntoPrevVolume()
|
||||||
{
|
{
|
||||||
@ -2172,4 +2320,132 @@ public class ReaderServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region MarkVolumesUntilAsRead
|
||||||
|
[Fact]
|
||||||
|
public async Task MarkVolumesUntilAsRead_ShouldMarkVolumesAsRead()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_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>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("10", false, new List<MangaFile>(), 1),
|
||||||
|
EntityFactory.CreateChapter("20", false, new List<MangaFile>(), 1),
|
||||||
|
EntityFactory.CreateChapter("30", false, new List<MangaFile>(), 1),
|
||||||
|
EntityFactory.CreateChapter("Some Special Title", true, new List<MangaFile>(), 1),
|
||||||
|
}),
|
||||||
|
|
||||||
|
EntityFactory.CreateVolume("1997", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("0", false, new List<MangaFile>(), 1),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("2002", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("0", false, new List<MangaFile>(), 1),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("2003", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("0", false, new List<MangaFile>(), 1),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
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.MarkVolumesUntilAsRead(user, 1, 2002);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Validate loose leaf chapters don't get marked as read
|
||||||
|
Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)));
|
||||||
|
Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1)));
|
||||||
|
Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1)));
|
||||||
|
|
||||||
|
// Validate that volumes 1997 and 2002 both have their respective chapter 0 marked as read
|
||||||
|
Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1)).PagesRead);
|
||||||
|
Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(6, 1)).PagesRead);
|
||||||
|
// Validate that the chapter 0 of the following volume (2003) is not read
|
||||||
|
Assert.Null(await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(7, 1));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MarkVolumesUntilAsRead_ShouldMarkChapterBasedVolumesAsRead()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_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>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("10", false, new List<MangaFile>(), 1),
|
||||||
|
EntityFactory.CreateChapter("20", false, new List<MangaFile>(), 1),
|
||||||
|
EntityFactory.CreateChapter("30", false, new List<MangaFile>(), 1),
|
||||||
|
EntityFactory.CreateChapter("Some Special Title", true, 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("2003", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("3", false, new List<MangaFile>(), 1),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
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.MarkVolumesUntilAsRead(user, 1, 2002);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Validate loose leaf chapters don't get marked as read
|
||||||
|
Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)));
|
||||||
|
Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1)));
|
||||||
|
Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1)));
|
||||||
|
|
||||||
|
// Validate volumes chapter 0 have read status
|
||||||
|
Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1)).PagesRead);
|
||||||
|
Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(6, 1)).PagesRead);
|
||||||
|
Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,13 @@ using System.IO.Abstractions.TestingHelpers;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
|
using API.Data.Repositories;
|
||||||
|
using API.DTOs.ReadingLists;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
using API.SignalR;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -79,9 +82,10 @@ public class ReadingListServiceTests
|
|||||||
|
|
||||||
private async Task ResetDb()
|
private async Task ResetDb()
|
||||||
{
|
{
|
||||||
_context.Series.RemoveRange(_context.Series.ToList());
|
_context.AppUser.RemoveRange(_context.AppUser);
|
||||||
|
_context.Series.RemoveRange(_context.Series);
|
||||||
await _context.SaveChangesAsync();
|
_context.ReadingList.RemoveRange(_context.ReadingList);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MockFileSystem CreateFileSystem()
|
private static MockFileSystem CreateFileSystem()
|
||||||
@ -99,11 +103,373 @@ public class ReadingListServiceTests
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region UpdateReadingListItemPosition
|
||||||
|
|
||||||
#region RemoveFullyReadItems
|
[Fact]
|
||||||
|
public async Task UpdateReadingListItemPosition_MoveLastToFirst_TwoItemsShouldShift()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
ReadingLists = new List<ReadingList>(),
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
new Library()
|
||||||
|
{
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Metadata = DbFactory.SeriesMetadata(new List<CollectionTag>()),
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Name = "0",
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "1",
|
||||||
|
AgeRating = AgeRating.Everyone,
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "2",
|
||||||
|
AgeRating = AgeRating.X18Plus
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "3",
|
||||||
|
AgeRating = AgeRating.X18Plus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists);
|
||||||
|
var readingList = new ReadingList();
|
||||||
|
user.ReadingLists = new List<ReadingList>()
|
||||||
|
{
|
||||||
|
readingList
|
||||||
|
};
|
||||||
|
|
||||||
|
await _readingListService.AddChaptersToReadingList(1, new List<int>() {1, 2, 3}, readingList);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
Assert.Equal(3, readingList.Items.Count);
|
||||||
|
|
||||||
|
await _readingListService.UpdateReadingListItemPosition(new UpdateReadingListPosition()
|
||||||
|
{
|
||||||
|
FromPosition = 2, ToPosition = 0, ReadingListId = 1, ReadingListItemId = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Assert.Equal(3, readingList.Items.Count);
|
||||||
|
Assert.Equal(0, readingList.Items.Single(i => i.ChapterId == 3).Order);
|
||||||
|
Assert.Equal(1, readingList.Items.Single(i => i.ChapterId == 1).Order);
|
||||||
|
Assert.Equal(2, readingList.Items.Single(i => i.ChapterId == 2).Order);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Implement all methods here
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region DeleteReadingListItem
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteReadingListItem_DeleteFirstItem_SecondShouldBecomeFirst()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
ReadingLists = new List<ReadingList>(),
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
new Library()
|
||||||
|
{
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Metadata = DbFactory.SeriesMetadata(new List<CollectionTag>()),
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Name = "0",
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "1",
|
||||||
|
AgeRating = AgeRating.Everyone
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "2",
|
||||||
|
AgeRating = AgeRating.X18Plus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists);
|
||||||
|
var readingList = new ReadingList();
|
||||||
|
user.ReadingLists = new List<ReadingList>()
|
||||||
|
{
|
||||||
|
readingList
|
||||||
|
};
|
||||||
|
|
||||||
|
await _readingListService.AddChaptersToReadingList(1, new List<int>() {1, 2}, readingList);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
Assert.Equal(2, readingList.Items.Count);
|
||||||
|
|
||||||
|
await _readingListService.DeleteReadingListItem(new UpdateReadingListPosition()
|
||||||
|
{
|
||||||
|
ReadingListId = 1, ReadingListItemId = 1
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(1, readingList.Items.Count);
|
||||||
|
Assert.Equal(2, readingList.Items.First().ChapterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region RemoveFullyReadItems
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoveFullyReadItems_RemovesAllFullyReadItems()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
ReadingLists = new List<ReadingList>(),
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
new Library()
|
||||||
|
{
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Metadata = DbFactory.SeriesMetadata(new List<CollectionTag>()),
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Name = "0",
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "1",
|
||||||
|
AgeRating = AgeRating.Everyone,
|
||||||
|
Pages = 1
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "2",
|
||||||
|
AgeRating = AgeRating.X18Plus,
|
||||||
|
Pages = 1
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "3",
|
||||||
|
AgeRating = AgeRating.X18Plus,
|
||||||
|
Pages = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists | AppUserIncludes.Progress);
|
||||||
|
var readingList = new ReadingList();
|
||||||
|
user.ReadingLists = new List<ReadingList>()
|
||||||
|
{
|
||||||
|
readingList
|
||||||
|
};
|
||||||
|
|
||||||
|
await _readingListService.AddChaptersToReadingList(1, new List<int>() {1, 2, 3}, readingList);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
Assert.Equal(3, readingList.Items.Count);
|
||||||
|
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(),
|
||||||
|
Substitute.For<IEventHub>());
|
||||||
|
// Mark 2 as fully read
|
||||||
|
await readerService.MarkChaptersAsRead(user, 1,
|
||||||
|
(await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(new List<int>() {2})).ToList());
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
await _readingListService.RemoveFullyReadItems(1, user);
|
||||||
|
|
||||||
|
|
||||||
|
Assert.Equal(2, readingList.Items.Count);
|
||||||
|
Assert.DoesNotContain(readingList.Items, i => i.Id == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region CalculateAgeRating
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CalculateAgeRating_ShouldUpdateToUnknown_IfNoneSet()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
ReadingLists = new List<ReadingList>(),
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
new Library()
|
||||||
|
{
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Metadata = DbFactory.SeriesMetadata(new List<CollectionTag>()),
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Name = "0",
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "1",
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "2",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists);
|
||||||
|
var readingList = new ReadingList();
|
||||||
|
user.ReadingLists = new List<ReadingList>()
|
||||||
|
{
|
||||||
|
readingList
|
||||||
|
};
|
||||||
|
|
||||||
|
await _readingListService.AddChaptersToReadingList(1, new List<int>() {1, 2}, readingList);
|
||||||
|
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
await _readingListService.CalculateReadingListAgeRating(readingList);
|
||||||
|
Assert.Equal(AgeRating.Unknown, readingList.AgeRating);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CalculateAgeRating_ShouldUpdateToMax()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
ReadingLists = new List<ReadingList>(),
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
new Library()
|
||||||
|
{
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Metadata = DbFactory.SeriesMetadata(new List<CollectionTag>()),
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Name = "0",
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "1",
|
||||||
|
},
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Number = "2",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists);
|
||||||
|
var readingList = new ReadingList();
|
||||||
|
user.ReadingLists = new List<ReadingList>()
|
||||||
|
{
|
||||||
|
readingList
|
||||||
|
};
|
||||||
|
|
||||||
|
await _readingListService.AddChaptersToReadingList(1, new List<int>() {1, 2}, readingList);
|
||||||
|
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
await _readingListService.CalculateReadingListAgeRating(readingList);
|
||||||
|
Assert.Equal(AgeRating.Unknown, readingList.AgeRating);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,10 @@ using API.Services.Tasks.Scanner;
|
|||||||
using API.Tests.Helpers;
|
using API.Tests.Helpers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Services
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
|
public class ScannerServiceTests
|
||||||
{
|
{
|
||||||
public class ScannerServiceTests
|
|
||||||
{
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FindSeriesNotOnDisk_Should_Remove1()
|
public void FindSeriesNotOnDisk_Should_Remove1()
|
||||||
{
|
{
|
||||||
@ -128,5 +128,4 @@ namespace API.Tests.Services
|
|||||||
// TODO: I want a test for UpdateSeries where if I have chapter 10 and now it's mapping into Vol 2 Chapter 10,
|
// 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)
|
// if I can do it without deleting the underlying chapter (aka id change)
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -8,9 +9,10 @@ using API.Data.Repositories;
|
|||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.DTOs.CollectionTags;
|
using API.DTOs.CollectionTags;
|
||||||
using API.DTOs.Metadata;
|
using API.DTOs.Metadata;
|
||||||
using API.DTOs.Reader;
|
using API.DTOs.SeriesDetail;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Metadata;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
@ -22,10 +24,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using NSubstitute.Extensions;
|
|
||||||
using NSubstitute.ReceivedExtensions;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Sdk;
|
|
||||||
|
|
||||||
namespace API.Tests.Services;
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
@ -85,19 +84,19 @@ public class SeriesServiceTests
|
|||||||
|
|
||||||
_context.ServerSetting.Update(setting);
|
_context.ServerSetting.Update(setting);
|
||||||
|
|
||||||
var lib = new Library()
|
// var lib = new Library()
|
||||||
{
|
// {
|
||||||
Name = "Manga", Folders = new List<FolderPath>() {new FolderPath() {Path = "C:/data/"}}
|
// Name = "Manga", Folders = new List<FolderPath>() {new FolderPath() {Path = "C:/data/"}}
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
_context.AppUser.Add(new AppUser()
|
// _context.AppUser.Add(new AppUser()
|
||||||
{
|
// {
|
||||||
UserName = "majora2007",
|
// UserName = "majora2007",
|
||||||
Libraries = new List<Library>()
|
// Libraries = new List<Library>()
|
||||||
{
|
// {
|
||||||
lib
|
// lib
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
return await _context.SaveChangesAsync() > 0;
|
return await _context.SaveChangesAsync() > 0;
|
||||||
}
|
}
|
||||||
@ -109,6 +108,7 @@ public class SeriesServiceTests
|
|||||||
_context.Genre.RemoveRange(_context.Genre.ToList());
|
_context.Genre.RemoveRange(_context.Genre.ToList());
|
||||||
_context.CollectionTag.RemoveRange(_context.CollectionTag.ToList());
|
_context.CollectionTag.RemoveRange(_context.CollectionTag.ToList());
|
||||||
_context.Person.RemoveRange(_context.Person.ToList());
|
_context.Person.RemoveRange(_context.Person.ToList());
|
||||||
|
_context.Library.RemoveRange(_context.Library.ToList());
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
@ -126,6 +126,26 @@ public class SeriesServiceTests
|
|||||||
return fileSystem;
|
return fileSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static UpdateRelatedSeriesDto CreateRelationsDto(Series series)
|
||||||
|
{
|
||||||
|
return new UpdateRelatedSeriesDto()
|
||||||
|
{
|
||||||
|
SeriesId = series.Id,
|
||||||
|
Prequels = new List<int>(),
|
||||||
|
Adaptations = new List<int>(),
|
||||||
|
Characters = new List<int>(),
|
||||||
|
Contains = new List<int>(),
|
||||||
|
Doujinshis = new List<int>(),
|
||||||
|
Others = new List<int>(),
|
||||||
|
Sequels = new List<int>(),
|
||||||
|
AlternativeSettings = new List<int>(),
|
||||||
|
AlternativeVersions = new List<int>(),
|
||||||
|
SideStories = new List<int>(),
|
||||||
|
SpinOffs = new List<int>(),
|
||||||
|
Editions = new List<int>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region SeriesDetail
|
#region SeriesDetail
|
||||||
@ -135,13 +155,22 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
|
{
|
||||||
|
AppUsers = new List<AppUser>()
|
||||||
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Manga,
|
|
||||||
},
|
|
||||||
Volumes = new List<Volume>()
|
Volumes = new List<Volume>()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
@ -160,8 +189,11 @@ public class SeriesServiceTests
|
|||||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
var expectedRanges = new[] {"Omake", "Something SP02"};
|
var expectedRanges = new[] {"Omake", "Something SP02"};
|
||||||
@ -177,13 +209,22 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
Name = "Test LIb",
|
Name = "Test LIb",
|
||||||
Type = LibraryType.Manga,
|
Type = LibraryType.Manga,
|
||||||
},
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
Volumes = new List<Volume>()
|
Volumes = new List<Volume>()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
@ -202,6 +243,8 @@ public class SeriesServiceTests
|
|||||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
@ -220,13 +263,22 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
Name = "Test LIb",
|
Name = "Test LIb",
|
||||||
Type = LibraryType.Manga,
|
Type = LibraryType.Manga,
|
||||||
},
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
Volumes = new List<Volume>()
|
Volumes = new List<Volume>()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
@ -243,6 +295,8 @@ public class SeriesServiceTests
|
|||||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
@ -261,13 +315,22 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
Name = "Test LIb",
|
Name = "Test LIb",
|
||||||
Type = LibraryType.Manga,
|
Type = LibraryType.Manga,
|
||||||
},
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
Volumes = new List<Volume>()
|
Volumes = new List<Volume>()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
@ -284,6 +347,8 @@ public class SeriesServiceTests
|
|||||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
@ -305,13 +370,22 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
Name = "Test LIb",
|
Name = "Test LIb",
|
||||||
Type = LibraryType.Book,
|
Type = LibraryType.Book,
|
||||||
},
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
Volumes = new List<Volume>()
|
Volumes = new List<Volume>()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||||
@ -323,8 +397,11 @@ public class SeriesServiceTests
|
|||||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
||||||
@ -339,13 +416,22 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
Name = "Test LIb",
|
Name = "Test LIb",
|
||||||
Type = LibraryType.Book,
|
Type = LibraryType.Book,
|
||||||
},
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
Volumes = new List<Volume>()
|
Volumes = new List<Volume>()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
@ -357,8 +443,12 @@ public class SeriesServiceTests
|
|||||||
EntityFactory.CreateChapter("Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub", false, new List<MangaFile>()),
|
EntityFactory.CreateChapter("Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub", false, new List<MangaFile>()),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
||||||
@ -379,13 +469,22 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
|
{
|
||||||
|
AppUsers = new List<AppUser>()
|
||||||
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Book,
|
|
||||||
},
|
|
||||||
Volumes = new List<Volume>()
|
Volumes = new List<Volume>()
|
||||||
{
|
{
|
||||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||||
@ -401,14 +500,17 @@ public class SeriesServiceTests
|
|||||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
||||||
Assert.Equal("1", detail.Volumes.ElementAt(0).Name);
|
Assert.Equal("Volume 1", detail.Volumes.ElementAt(0).Name);
|
||||||
Assert.Equal("1.2", detail.Volumes.ElementAt(1).Name);
|
Assert.Equal("Volume 1.2", detail.Volumes.ElementAt(1).Name);
|
||||||
Assert.Equal("2", detail.Volumes.ElementAt(2).Name);
|
Assert.Equal("Volume 2", detail.Volumes.ElementAt(2).Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -422,28 +524,34 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
Name = "Test LIb",
|
Name = "Test LIb",
|
||||||
Type = LibraryType.Manga,
|
Type = LibraryType.Manga,
|
||||||
},
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
Volumes = new List<Volume>()
|
Volumes = new List<Volume>()
|
||||||
{
|
{
|
||||||
new Volume()
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
{
|
{
|
||||||
Chapters = new List<Chapter>()
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||||
{
|
}),
|
||||||
new Chapter()
|
|
||||||
{
|
|
||||||
Pages = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
@ -470,23 +578,28 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
Name = "Test LIb",
|
Name = "Test LIb",
|
||||||
Type = LibraryType.Manga,
|
Type = LibraryType.Manga,
|
||||||
},
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
Volumes = new List<Volume>()
|
Volumes = new List<Volume>()
|
||||||
{
|
{
|
||||||
new Volume()
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
{
|
{
|
||||||
Chapters = new List<Chapter>()
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||||
{
|
}),
|
||||||
new Chapter()
|
|
||||||
{
|
|
||||||
Pages = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -536,23 +649,28 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
Name = "Test LIb",
|
Name = "Test LIb",
|
||||||
Type = LibraryType.Manga,
|
Type = LibraryType.Manga,
|
||||||
},
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
Volumes = new List<Volume>()
|
Volumes = new List<Volume>()
|
||||||
{
|
{
|
||||||
new Volume()
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
{
|
{
|
||||||
Chapters = new List<Chapter>()
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||||
{
|
}),
|
||||||
new Chapter()
|
|
||||||
{
|
|
||||||
Pages = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -583,23 +701,28 @@ public class SeriesServiceTests
|
|||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
|
|
||||||
_context.Series.Add(new Series()
|
_context.Library.Add(new Library()
|
||||||
{
|
{
|
||||||
Name = "Test",
|
AppUsers = new List<AppUser>()
|
||||||
Library = new Library() {
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
Name = "Test LIb",
|
Name = "Test LIb",
|
||||||
Type = LibraryType.Manga,
|
Type = LibraryType.Manga,
|
||||||
},
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
Volumes = new List<Volume>()
|
Volumes = new List<Volume>()
|
||||||
{
|
{
|
||||||
new Volume()
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
{
|
{
|
||||||
Chapters = new List<Chapter>()
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||||
{
|
}),
|
||||||
new Chapter()
|
|
||||||
{
|
|
||||||
Pages = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -626,18 +749,6 @@ public class SeriesServiceTests
|
|||||||
|
|
||||||
#region UpdateSeriesMetadata
|
#region UpdateSeriesMetadata
|
||||||
|
|
||||||
private void SetupUpdateSeriesMetadataDb()
|
|
||||||
{
|
|
||||||
_context.Series.Add(new Series()
|
|
||||||
{
|
|
||||||
Name = "Test",
|
|
||||||
Library = new Library() {
|
|
||||||
Name = "Test LIb",
|
|
||||||
Type = LibraryType.Book,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task UpdateSeriesMetadata_ShouldCreateEmptyMetadata_IfDoesntExist()
|
public async Task UpdateSeriesMetadata_ShouldCreateEmptyMetadata_IfDoesntExist()
|
||||||
{
|
{
|
||||||
@ -909,6 +1020,41 @@ public class SeriesServiceTests
|
|||||||
Assert.True(series.Metadata.GenresLocked);
|
Assert.True(series.Metadata.GenresLocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateSeriesMetadata_ShouldNotUpdateReleaseYear_IfLessThan1000()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
var s = new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library()
|
||||||
|
{
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
},
|
||||||
|
Metadata = DbFactory.SeriesMetadata(new List<CollectionTag>())
|
||||||
|
};
|
||||||
|
_context.Series.Add(s);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto()
|
||||||
|
{
|
||||||
|
SeriesMetadata = new SeriesMetadataDto()
|
||||||
|
{
|
||||||
|
SeriesId = 1,
|
||||||
|
ReleaseYear = 100,
|
||||||
|
},
|
||||||
|
CollectionTags = new List<CollectionTagDto>()
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(success);
|
||||||
|
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||||
|
Assert.NotNull(series.Metadata);
|
||||||
|
Assert.Equal(0, series.Metadata.ReleaseYear);
|
||||||
|
Assert.False(series.Metadata.ReleaseYearLocked);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region GetFirstChapterForMetadata
|
#region GetFirstChapterForMetadata
|
||||||
@ -992,4 +1138,388 @@ public class SeriesServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region SeriesRelation
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateRelatedSeries_ShouldAddAllRelations()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_context.Library.Add(new Library()
|
||||||
|
{
|
||||||
|
AppUsers = new List<AppUser>()
|
||||||
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Prequels",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Sequels",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related);
|
||||||
|
// Add relations
|
||||||
|
var addRelationDto = CreateRelationsDto(series1);
|
||||||
|
addRelationDto.Adaptations.Add(2);
|
||||||
|
addRelationDto.Sequels.Add(3);
|
||||||
|
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
||||||
|
Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId);
|
||||||
|
Assert.Equal(3, series1.Relations.Single(s => s.TargetSeriesId == 3).TargetSeriesId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateRelatedSeries_DeleteAllRelations()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_context.Library.Add(new Library()
|
||||||
|
{
|
||||||
|
AppUsers = new List<AppUser>()
|
||||||
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Prequels",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Sequels",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related);
|
||||||
|
// Add relations
|
||||||
|
var addRelationDto = CreateRelationsDto(series1);
|
||||||
|
addRelationDto.Adaptations.Add(2);
|
||||||
|
addRelationDto.Sequels.Add(3);
|
||||||
|
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
||||||
|
Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId);
|
||||||
|
Assert.Equal(3, series1.Relations.Single(s => s.TargetSeriesId == 3).TargetSeriesId);
|
||||||
|
|
||||||
|
// Remove relations
|
||||||
|
var removeRelationDto = CreateRelationsDto(series1);
|
||||||
|
await _seriesService.UpdateRelatedSeries(removeRelationDto);
|
||||||
|
Assert.Empty(series1.Relations.Where(s => s.TargetSeriesId == 1));
|
||||||
|
Assert.Empty(series1.Relations.Where(s => s.TargetSeriesId == 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateRelatedSeries_ShouldNotAllowDuplicates()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_context.Library.Add(new Library()
|
||||||
|
{
|
||||||
|
AppUsers = new List<AppUser>()
|
||||||
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Prequels",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related);
|
||||||
|
var relation = new SeriesRelation()
|
||||||
|
{
|
||||||
|
Series = series1,
|
||||||
|
SeriesId = series1.Id,
|
||||||
|
TargetSeriesId = 2, // Target series id
|
||||||
|
RelationKind = RelationKind.Prequel
|
||||||
|
|
||||||
|
};
|
||||||
|
// Manually create a relation
|
||||||
|
series1.Relations.Add(relation);
|
||||||
|
|
||||||
|
// Create a new dto with the previous relation as well
|
||||||
|
var relationDto = CreateRelationsDto(series1);
|
||||||
|
relationDto.Adaptations.Add(2);
|
||||||
|
|
||||||
|
await _seriesService.UpdateRelatedSeries(relationDto);
|
||||||
|
// Expected is only one instance of the relation (hence not duping)
|
||||||
|
Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetRelatedSeries_EditionPrequelSequel_ShouldNotHaveParent()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_context.Library.Add(new Library()
|
||||||
|
{
|
||||||
|
AppUsers = new List<AppUser>()
|
||||||
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Editions",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Prequels",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Sequels",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Adaption",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related);
|
||||||
|
// Add relations
|
||||||
|
var addRelationDto = CreateRelationsDto(series1);
|
||||||
|
addRelationDto.Editions.Add(2);
|
||||||
|
addRelationDto.Prequels.Add(3);
|
||||||
|
addRelationDto.Sequels.Add(4);
|
||||||
|
addRelationDto.Adaptations.Add(5);
|
||||||
|
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
||||||
|
|
||||||
|
|
||||||
|
Assert.Empty(_seriesService.GetRelatedSeries(1, 2).Result.Parent);
|
||||||
|
Assert.Empty(_seriesService.GetRelatedSeries(1, 3).Result.Parent);
|
||||||
|
Assert.Empty(_seriesService.GetRelatedSeries(1, 4).Result.Parent);
|
||||||
|
Assert.NotEmpty(_seriesService.GetRelatedSeries(1, 5).Result.Parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeriesRelation_ShouldAllowDeleteOnLibrary()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_context.Library.Add(new Library()
|
||||||
|
{
|
||||||
|
AppUsers = new List<AppUser>()
|
||||||
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Prequels",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Sequels",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related);
|
||||||
|
// Add relations
|
||||||
|
var addRelationDto = CreateRelationsDto(series1);
|
||||||
|
addRelationDto.Adaptations.Add(2);
|
||||||
|
addRelationDto.Sequels.Add(3);
|
||||||
|
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
||||||
|
|
||||||
|
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1);
|
||||||
|
_unitOfWork.LibraryRepository.Delete(library);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
Assert.False(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Null(await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeriesRelation_ShouldAllowDeleteOnLibrary_WhenSeriesCrossLibraries()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
_context.Library.Add(new Library()
|
||||||
|
{
|
||||||
|
AppUsers = new List<AppUser>()
|
||||||
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series",
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
new Volume()
|
||||||
|
{
|
||||||
|
Chapters = new List<Chapter>()
|
||||||
|
{
|
||||||
|
new Chapter()
|
||||||
|
{
|
||||||
|
Files = new List<MangaFile>()
|
||||||
|
{
|
||||||
|
new MangaFile()
|
||||||
|
{
|
||||||
|
Pages = 1,
|
||||||
|
FilePath = "fake file"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Prequels",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Sequels",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.Library.Add(new Library()
|
||||||
|
{
|
||||||
|
AppUsers = new List<AppUser>()
|
||||||
|
{
|
||||||
|
new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Name = "Test LIb 2",
|
||||||
|
Type = LibraryType.Book,
|
||||||
|
Series = new List<Series>()
|
||||||
|
{
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series 2",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Prequels 2",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
},
|
||||||
|
new Series()
|
||||||
|
{
|
||||||
|
Name = "Test Series Sequels 2",
|
||||||
|
Volumes = new List<Volume>(){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related);
|
||||||
|
// Add relations
|
||||||
|
var addRelationDto = CreateRelationsDto(series1);
|
||||||
|
addRelationDto.Adaptations.Add(4); // cross library link
|
||||||
|
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
||||||
|
|
||||||
|
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1, LibraryIncludes.Series);
|
||||||
|
_unitOfWork.LibraryRepository.Delete(library);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
Assert.False(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Null(await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
733
API.Tests/Services/TachiyomiServiceTests.cs
Normal file
733
API.Tests/Services/TachiyomiServiceTests.cs
Normal file
@ -0,0 +1,733 @@
|
|||||||
|
namespace API.Tests.Services;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data.Common;
|
||||||
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Data;
|
||||||
|
using Data.Repositories;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Helpers;
|
||||||
|
using API.Services;
|
||||||
|
using SignalR;
|
||||||
|
using Helpers;
|
||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class TachiyomiServiceTests
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
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 TachiyomiServiceTests()
|
||||||
|
{
|
||||||
|
var contextOptions = new DbContextOptionsBuilder().UseSqlite(CreateInMemoryDatabase()).Options;
|
||||||
|
|
||||||
|
_context = new DataContext(contextOptions);
|
||||||
|
Task.Run(SeedDb).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
|
||||||
|
_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);
|
||||||
|
|
||||||
|
_context.Library.Add(new Library()
|
||||||
|
{
|
||||||
|
Name = "Manga", Folders = new List<FolderPath>() {new FolderPath() {Path = "C:/data/"}}
|
||||||
|
});
|
||||||
|
return await _context.SaveChangesAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ResetDb()
|
||||||
|
{
|
||||||
|
_context.Series.RemoveRange(_context.Series.ToList());
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MockFileSystem CreateFileSystem()
|
||||||
|
{
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
fileSystem.Directory.SetCurrentDirectory("C:/kavita/");
|
||||||
|
fileSystem.AddDirectory("C:/kavita/config/");
|
||||||
|
fileSystem.AddDirectory(CacheDirectory);
|
||||||
|
fileSystem.AddDirectory(CoverImageDirectory);
|
||||||
|
fileSystem.AddDirectory(BackupDirectory);
|
||||||
|
fileSystem.AddDirectory(DataDirectory);
|
||||||
|
|
||||||
|
return fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region GetLatestChapter
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetLatestChapter_ShouldReturnChapter_NoProgress()
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
library
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
|
||||||
|
Assert.Null(latestChapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetLatestChapter_ShouldReturnMaxChapter_CompletelyRead()
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
library
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
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 _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||||
|
|
||||||
|
Assert.Equal("96", latestChapter.Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetLatestChapter_ShouldReturnHighestChapter_Progress()
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
library
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
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 _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||||
|
|
||||||
|
Assert.Equal("21", latestChapter.Number);
|
||||||
|
}
|
||||||
|
[Fact]
|
||||||
|
public async Task GetLatestChapter_ShouldReturnEncodedVolume_Progress()
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
library
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
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 _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||||
|
Assert.Equal("0.0001", latestChapter.Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetLatestChapter_ShouldReturnEncodedVolume_Progress2()
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
library
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
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 _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||||
|
Assert.Equal("0.0003", latestChapter.Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetLatestChapter_ShouldReturnEncodedYearlyVolume_Progress()
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
library
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
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 _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||||
|
Assert.Equal("0.2002", latestChapter.Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region MarkChaptersUntilAsRead
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MarkChaptersUntilAsRead_ShouldReturnChapter_NoProgress()
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
library
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
|
||||||
|
Assert.Null(latestChapter);
|
||||||
|
}
|
||||||
|
[Fact]
|
||||||
|
public async Task MarkChaptersUntilAsRead_ShouldReturnMaxChapter_CompletelyRead()
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
library
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
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 _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||||
|
|
||||||
|
Assert.Equal("96", latestChapter.Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MarkChaptersUntilAsRead_ShouldReturnHighestChapter_Progress()
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
library
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
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 _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||||
|
|
||||||
|
Assert.Equal("21", latestChapter.Number);
|
||||||
|
}
|
||||||
|
[Fact]
|
||||||
|
public async Task MarkChaptersUntilAsRead_ShouldReturnEncodedVolume_Progress()
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007",
|
||||||
|
Libraries = new List<Library>()
|
||||||
|
{
|
||||||
|
library
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
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 _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var latestChapter = await tachiyomiService.GetLatestChapter(1, 1);
|
||||||
|
Assert.Equal("0.0001", latestChapter.Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -6,6 +6,8 @@
|
|||||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<TieredPGO>true</TieredPGO>
|
||||||
|
<TieredCompilation>true</TieredCompilation>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
@ -45,43 +47,52 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
|
||||||
<PackageReference Include="Docnet.Core" Version="2.4.0-alpha.4" />
|
<PackageReference Include="Docnet.Core" Version="2.4.0-alpha.4" />
|
||||||
<PackageReference Include="ExCSS" Version="4.1.0" />
|
<PackageReference Include="ExCSS" Version="4.1.0" />
|
||||||
<PackageReference Include="Flurl" Version="3.0.6" />
|
<PackageReference Include="Flurl" Version="3.0.6" />
|
||||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||||
<PackageReference Include="Hangfire" Version="1.7.30" />
|
<PackageReference Include="Hangfire" Version="1.7.31" />
|
||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.30" />
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.31" />
|
||||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||||
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.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.2" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.43" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
|
||||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.10">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.1" />
|
||||||
<PackageReference Include="NetVips" Version="2.2.0" />
|
<PackageReference Include="NetVips" Version="2.2.0" />
|
||||||
<PackageReference Include="NetVips.Native" Version="8.13.0" />
|
<PackageReference Include="NetVips.Native" Version="8.13.1" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.1.5" />
|
<PackageReference Include="NReco.Logging.File" Version="1.1.5" />
|
||||||
|
<PackageReference Include="Serilog" Version="2.12.0" />
|
||||||
|
<PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
|
||||||
|
<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" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.AspNetCore.SignalR" Version="0.4.0" />
|
||||||
|
<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="SharpCompress" Version="0.32.2" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.43.0.51858">
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.47.0.55603">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.22.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.24.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions" Version="17.0.24" />
|
<PackageReference Include="System.IO.Abstractions" Version="17.2.3" />
|
||||||
<PackageReference Include="VersOne.Epub" Version="3.1.2" />
|
<PackageReference Include="VersOne.Epub" Version="3.3.0-alpha1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
namespace API.Archive
|
namespace API.Archive;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents which library should handle opening this library
|
||||||
|
/// </summary>
|
||||||
|
public enum ArchiveLibrary
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Represents which library should handle opening this library
|
|
||||||
/// </summary>
|
|
||||||
public enum ArchiveLibrary
|
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The underlying archive cannot be opened
|
/// The underlying archive cannot be opened
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -17,5 +17,4 @@
|
|||||||
/// The underlying archive can be opened by default .NET
|
/// The underlying archive can be opened by default .NET
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Default = 2
|
Default = 2
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace API.Comparators
|
namespace API.Comparators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sorts chapters based on their Number. Uses natural ordering of doubles.
|
||||||
|
/// </summary>
|
||||||
|
public class ChapterSortComparer : IComparer<double>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Sorts chapters based on their Number. Uses natural ordering of doubles.
|
|
||||||
/// </summary>
|
|
||||||
public class ChapterSortComparer : IComparer<double>
|
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Normal sort for 2 doubles. 0 always comes last
|
/// Normal sort for 2 doubles. 0 always comes last
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -25,17 +25,17 @@ namespace API.Comparators
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static readonly ChapterSortComparer Default = new ChapterSortComparer();
|
public static readonly ChapterSortComparer Default = new ChapterSortComparer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is a special case comparer used exclusively for sorting chapters within a single Volume for reading order.
|
/// This is a special case comparer used exclusively for sorting chapters within a single Volume for reading order.
|
||||||
/// <example>
|
/// <example>
|
||||||
/// Volume 10 has "Series - Vol 10" and "Series - Vol 10 Chapter 81". In this case, for reading order, the order is Vol 10, Vol 10 Chapter 81.
|
/// Volume 10 has "Series - Vol 10" and "Series - Vol 10 Chapter 81". In this case, for reading order, the order is Vol 10, Vol 10 Chapter 81.
|
||||||
/// This is represented by Chapter 0, Chapter 81.
|
/// This is represented by Chapter 0, Chapter 81.
|
||||||
/// </example>
|
/// </example>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ChapterSortComparerZeroFirst : IComparer<double>
|
public class ChapterSortComparerZeroFirst : IComparer<double>
|
||||||
{
|
{
|
||||||
public int Compare(double x, double y)
|
public int Compare(double x, double y)
|
||||||
{
|
{
|
||||||
if (x == 0.0 && y == 0.0) return 0;
|
if (x == 0.0 && y == 0.0) return 0;
|
||||||
@ -48,10 +48,10 @@ namespace API.Comparators
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static readonly ChapterSortComparerZeroFirst Default = new ChapterSortComparerZeroFirst();
|
public static readonly ChapterSortComparerZeroFirst Default = new ChapterSortComparerZeroFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SortComparerZeroLast : IComparer<double>
|
public class SortComparerZeroLast : IComparer<double>
|
||||||
{
|
{
|
||||||
public int Compare(double x, double y)
|
public int Compare(double x, double y)
|
||||||
{
|
{
|
||||||
if (x == 0.0 && y == 0.0) return 0;
|
if (x == 0.0 && y == 0.0) return 0;
|
||||||
@ -62,5 +62,4 @@ namespace API.Comparators
|
|||||||
|
|
||||||
return x.CompareTo(y);
|
return x.CompareTo(y);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
|
||||||
namespace API.Comparators
|
namespace API.Comparators;
|
||||||
|
|
||||||
|
public class NumericComparer : IComparer
|
||||||
{
|
{
|
||||||
public class NumericComparer : IComparer
|
|
||||||
{
|
|
||||||
|
|
||||||
public int Compare(object x, object y)
|
public int Compare(object x, object y)
|
||||||
{
|
{
|
||||||
@ -13,5 +13,4 @@ namespace API.Comparators
|
|||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
using static System.Char;
|
using static System.Char;
|
||||||
|
|
||||||
namespace API.Comparators
|
namespace API.Comparators;
|
||||||
|
|
||||||
|
public static class StringLogicalComparer
|
||||||
{
|
{
|
||||||
public static class StringLogicalComparer
|
|
||||||
{
|
|
||||||
public static int Compare(string s1, string s2)
|
public static int Compare(string s1, string s2)
|
||||||
{
|
{
|
||||||
//get rid of special cases
|
//get rid of special cases
|
||||||
@ -126,5 +126,4 @@ namespace API.Comparators
|
|||||||
if(end >= s.Length) break;
|
if(end >= s.Length) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,12 +1,12 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
namespace API.Constants
|
namespace API.Constants;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Role-based Security
|
||||||
|
/// </summary>
|
||||||
|
public static class PolicyConstants
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Role-based Security
|
|
||||||
/// </summary>
|
|
||||||
public static class PolicyConstants
|
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Admin User. Has all privileges
|
/// Admin User. Has all privileges
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -23,8 +23,15 @@ namespace API.Constants
|
|||||||
/// Used to give a user ability to change their own password
|
/// Used to give a user ability to change their own password
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ChangePasswordRole = "Change Password";
|
public const string ChangePasswordRole = "Change Password";
|
||||||
|
/// <summary>
|
||||||
|
/// Used to give a user ability to bookmark files on the server
|
||||||
|
/// </summary>
|
||||||
|
public const string BookmarkRole = "Bookmark";
|
||||||
|
/// <summary>
|
||||||
|
/// Used to give a user ability to Change Restrictions on their account
|
||||||
|
/// </summary>
|
||||||
|
public const string ChangeRestrictionRole = "Change Restriction";
|
||||||
|
|
||||||
public static readonly ImmutableArray<string> ValidRoles =
|
public static readonly ImmutableArray<string> ValidRoles =
|
||||||
ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole);
|
ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -12,13 +12,13 @@ using API.DTOs.Account;
|
|||||||
using API.DTOs.Email;
|
using API.DTOs.Email;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Enums.UserPreferences;
|
|
||||||
using API.Errors;
|
using API.Errors;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -26,13 +26,13 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All Account matters
|
||||||
|
/// </summary>
|
||||||
|
public class AccountController : BaseApiController
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// All Account matters
|
|
||||||
/// </summary>
|
|
||||||
public class AccountController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly UserManager<AppUser> _userManager;
|
private readonly UserManager<AppUser> _userManager;
|
||||||
private readonly SignInManager<AppUser> _signInManager;
|
private readonly SignInManager<AppUser> _signInManager;
|
||||||
private readonly ITokenService _tokenService;
|
private readonly ITokenService _tokenService;
|
||||||
@ -188,15 +188,6 @@ namespace API.Controllers
|
|||||||
|
|
||||||
if (user == null) return Unauthorized("Invalid username");
|
if (user == null) return Unauthorized("Invalid username");
|
||||||
|
|
||||||
// Check if the user has an email, if not, inform them so they can migrate
|
|
||||||
var validPassword = await _signInManager.UserManager.CheckPasswordAsync(user, loginDto.Password);
|
|
||||||
if (string.IsNullOrEmpty(user.Email) && !user.EmailConfirmed && validPassword)
|
|
||||||
{
|
|
||||||
_logger.LogCritical("User {UserName} does not have an email. Providing a one time migration", user.UserName);
|
|
||||||
return Unauthorized(
|
|
||||||
"You are missing an email on your account. Please wait while we migrate your account.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _signInManager
|
var result = await _signInManager
|
||||||
.CheckPasswordSignInAsync(user, loginDto.Password, true);
|
.CheckPasswordSignInAsync(user, loginDto.Password, true);
|
||||||
|
|
||||||
@ -256,6 +247,7 @@ namespace API.Controllers
|
|||||||
[HttpGet("roles")]
|
[HttpGet("roles")]
|
||||||
public ActionResult<IList<string>> GetRoles()
|
public ActionResult<IList<string>> GetRoles()
|
||||||
{
|
{
|
||||||
|
// TODO: This should be moved to ServerController
|
||||||
return typeof(PolicyConstants)
|
return typeof(PolicyConstants)
|
||||||
.GetFields(BindingFlags.Public | BindingFlags.Static)
|
.GetFields(BindingFlags.Public | BindingFlags.Static)
|
||||||
.Where(f => f.FieldType == typeof(string))
|
.Where(f => f.FieldType == typeof(string))
|
||||||
@ -285,6 +277,116 @@ namespace API.Controllers
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initiates the flow to update a user's email address. The email address is not changed in this API. A confirmation link is sent/dumped which will
|
||||||
|
/// validate the email. It must be confirmed for the email to update.
|
||||||
|
/// </summary>
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
if (user == null) return Unauthorized("You do not have permission");
|
||||||
|
|
||||||
|
if (dto == null || string.IsNullOrEmpty(dto.Email)) return BadRequest("Invalid payload");
|
||||||
|
|
||||||
|
// Validate no other users exist with this email
|
||||||
|
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);
|
||||||
|
if (existingUserEmail != null)
|
||||||
|
{
|
||||||
|
return BadRequest("You cannot share emails across multiple accounts");
|
||||||
|
}
|
||||||
|
|
||||||
|
// All validations complete, generate a new token and email it to the user at the new address. Confirm email link will update the email
|
||||||
|
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||||
|
if (string.IsNullOrEmpty(token))
|
||||||
|
{
|
||||||
|
_logger.LogError("There was an issue generating a token for the email");
|
||||||
|
return BadRequest("There was an issue creating a confirmation email token. See logs.");
|
||||||
|
}
|
||||||
|
|
||||||
|
user.EmailConfirmed = false;
|
||||||
|
user.ConfirmationToken = token;
|
||||||
|
await _userManager.UpdateAsync(user);
|
||||||
|
|
||||||
|
// Send a confirmation email
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var emailLink = GenerateEmailLink(user.ConfirmationToken, "confirm-email-update", dto.Email);
|
||||||
|
_logger.LogCritical("[Update Email]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||||
|
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||||
|
var accessible = await _emailService.CheckIfAccessible(host);
|
||||||
|
if (accessible)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Email the old address of the update change
|
||||||
|
await _emailService.SendEmailChangeEmail(new ConfirmationEmailDto()
|
||||||
|
{
|
||||||
|
EmailAddress = string.IsNullOrEmpty(user.Email) ? dto.Email : user.Email,
|
||||||
|
InstallId = BuildInfo.Version.ToString(),
|
||||||
|
InvitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName,
|
||||||
|
ServerConfirmationLink = emailLink
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
/* Swallow exception */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new InviteUserResponse
|
||||||
|
{
|
||||||
|
EmailLink = string.Empty,
|
||||||
|
EmailSent = accessible
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "There was an error during invite user flow, unable to send an email");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("update/age-restriction")]
|
||||||
|
public async Task<ActionResult> UpdateAgeRestriction(UpdateAgeRestrictionDto dto)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating;
|
||||||
|
user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns;
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
|
if (!_unitOfWork.HasChanges()) return Ok();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "There was an error updating the age restriction");
|
||||||
|
return BadRequest("There was an error updating the age restriction");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update the user account. This can only affect Username, Email (will require confirming), Roles, and Library access.
|
/// Update the user account. This can only affect Username, Email (will require confirming), Roles, and Library access.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -310,14 +412,6 @@ namespace API.Controllers
|
|||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.Email.Equals(dto.Email))
|
|
||||||
{
|
|
||||||
// Validate username change
|
|
||||||
var errors = await _accountService.ValidateEmail(dto.Email);
|
|
||||||
if (errors.Any()) return BadRequest("Email already registered");
|
|
||||||
// NOTE: This needs to be handled differently, like save it in a temp variable in DB until email is validated. For now, I wont allow it
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update roles
|
// Update roles
|
||||||
var existingRoles = await _userManager.GetRolesAsync(user);
|
var existingRoles = await _userManager.GetRolesAsync(user);
|
||||||
var hasAdminRole = dto.Roles.Contains(PolicyConstants.AdminRole);
|
var hasAdminRole = dto.Roles.Contains(PolicyConstants.AdminRole);
|
||||||
@ -363,6 +457,11 @@ namespace API.Controllers
|
|||||||
lib.AppUsers.Add(user);
|
lib.AppUsers.Add(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating;
|
||||||
|
user.AgeRestrictionIncludeUnknowns = hasAdminRole || dto.AgeRestriction.IncludeUnknowns;
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync())
|
if (!_unitOfWork.HasChanges() || 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);
|
||||||
@ -404,10 +503,13 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult<string>> InviteUser(InviteUserDto dto)
|
public async Task<ActionResult<string>> InviteUser(InviteUserDto dto)
|
||||||
{
|
{
|
||||||
var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
if (adminUser == null) return Unauthorized("You need to login");
|
if (adminUser == null) return Unauthorized("You are not permitted");
|
||||||
|
|
||||||
_logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email);
|
_logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email);
|
||||||
|
|
||||||
// Check if there is an existing invite
|
// Check if there is an existing invite
|
||||||
|
if (!string.IsNullOrEmpty(dto.Email))
|
||||||
|
{
|
||||||
dto.Email = dto.Email.Trim();
|
dto.Email = dto.Email.Trim();
|
||||||
var emailValidationErrors = await _accountService.ValidateEmail(dto.Email);
|
var emailValidationErrors = await _accountService.ValidateEmail(dto.Email);
|
||||||
if (emailValidationErrors.Any())
|
if (emailValidationErrors.Any())
|
||||||
@ -417,6 +519,7 @@ namespace API.Controllers
|
|||||||
return BadRequest($"User is already registered as {invitedUser.UserName}");
|
return BadRequest($"User is already registered as {invitedUser.UserName}");
|
||||||
return BadRequest("User is already invited under this email and has yet to accepted invite.");
|
return BadRequest("User is already invited under this email and has yet to accepted invite.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create a new user
|
// Create a new user
|
||||||
var user = new AppUser()
|
var user = new AppUser()
|
||||||
@ -471,6 +574,9 @@ namespace API.Controllers
|
|||||||
lib.AppUsers.Add(user);
|
lib.AppUsers.Add(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating;
|
||||||
|
user.AgeRestrictionIncludeUnknowns = hasAdminRole || dto.AgeRestriction.IncludeUnknowns;
|
||||||
|
|
||||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||||
if (string.IsNullOrEmpty(token))
|
if (string.IsNullOrEmpty(token))
|
||||||
{
|
{
|
||||||
@ -575,6 +681,44 @@ namespace API.Controllers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Final step in email update change. Given a confirmation token and the email, this will finish the email change.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This will force connected clients to re-authenticate</remarks>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("confirm-email-update")]
|
||||||
|
public async Task<ActionResult> ConfirmEmailUpdate(ConfirmEmailUpdateDto dto)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByConfirmationToken(dto.Token);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid Email Token");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await ConfirmEmailToken(dto.Token, user)) return BadRequest("Invalid Email Token");
|
||||||
|
|
||||||
|
|
||||||
|
_logger.LogInformation("User is updating email from {OldEmail} to {NewEmail}", user.Email, dto.Email);
|
||||||
|
var result = await _userManager.SetEmailAsync(user, dto.Email);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
_logger.LogError("Unable to update email for users: {Errors}", result.Errors.Select(e => e.Description));
|
||||||
|
return BadRequest("Unable to update email for user. Check logs");
|
||||||
|
}
|
||||||
|
user.ConfirmationToken = null;
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Perform Login code
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("confirm-password-reset")]
|
[HttpPost("confirm-password-reset")]
|
||||||
public async Task<ActionResult<string>> ConfirmForgotPassword(ConfirmPasswordResetDto dto)
|
public async Task<ActionResult<string>> ConfirmForgotPassword(ConfirmPasswordResetDto dto)
|
||||||
@ -619,15 +763,15 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
var roles = await _userManager.GetRolesAsync(user);
|
var roles = await _userManager.GetRolesAsync(user);
|
||||||
|
|
||||||
|
|
||||||
if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole))
|
if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole))
|
||||||
return Unauthorized("You are not permitted to this operation.");
|
return Unauthorized("You are not permitted to this operation.");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(user.Email) || !user.EmailConfirmed)
|
||||||
|
return BadRequest("You do not have an email on account or it has not been confirmed");
|
||||||
|
|
||||||
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||||
var emailLink = GenerateEmailLink(token, "confirm-reset-password", user.Email);
|
var emailLink = GenerateEmailLink(token, "confirm-reset-password", user.Email);
|
||||||
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||||
_logger.LogCritical("[Forgot Password]: Token {UserName}: {Token}", user.UserName, token);
|
|
||||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||||
if (await _emailService.CheckIfAccessible(host))
|
if (await _emailService.CheckIfAccessible(host))
|
||||||
{
|
{
|
||||||
@ -643,6 +787,15 @@ namespace API.Controllers
|
|||||||
return Ok("Your server is not accessible. The Link to reset your password is in the logs.");
|
return Ok("Your server is not accessible. The Link to reset your password is in the logs.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("email-confirmed")]
|
||||||
|
public async Task<ActionResult<bool>> IsEmailConfirmed()
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
return Ok(user.EmailConfirmed);
|
||||||
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("confirm-migration-email")]
|
[HttpPost("confirm-migration-email")]
|
||||||
public async Task<ActionResult<UserDto>> ConfirmMigrationEmail(ConfirmMigrationEmailDto dto)
|
public async Task<ActionResult<UserDto>> ConfirmMigrationEmail(ConfirmMigrationEmailDto dto)
|
||||||
@ -773,5 +926,4 @@ namespace API.Controllers
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
public class AdminController : BaseApiController
|
||||||
{
|
{
|
||||||
public class AdminController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly UserManager<AppUser> _userManager;
|
private readonly UserManager<AppUser> _userManager;
|
||||||
|
|
||||||
public AdminController(UserManager<AppUser> userManager)
|
public AdminController(UserManager<AppUser> userManager)
|
||||||
@ -26,5 +26,4 @@ namespace API.Controllers
|
|||||||
var users = await _userManager.GetUsersInRoleAsync("Admin");
|
var users = await _userManager.GetUsersInRoleAsync("Admin");
|
||||||
return users.Count > 0;
|
return users.Count > 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class BaseApiController : ControllerBase
|
||||||
{
|
{
|
||||||
[ApiController]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
[Authorize]
|
|
||||||
public class BaseApiController : ControllerBase
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -13,10 +13,10 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using VersOne.Epub;
|
using VersOne.Epub;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
public class BookController : BaseApiController
|
||||||
{
|
{
|
||||||
public class BookController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly IBookService _bookService;
|
private readonly IBookService _bookService;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
@ -61,13 +61,10 @@ namespace API.Controllers
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MangaFormat.Image:
|
case MangaFormat.Image:
|
||||||
break;
|
|
||||||
case MangaFormat.Archive:
|
case MangaFormat.Archive:
|
||||||
break;
|
|
||||||
case MangaFormat.Unknown:
|
case MangaFormat.Unknown:
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException();
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(new BookInfoDto()
|
return Ok(new BookInfoDto()
|
||||||
@ -159,5 +156,4 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,13 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// APIs for Collections
|
||||||
|
/// </summary>
|
||||||
|
public class CollectionController : BaseApiController
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// APIs for Collections
|
|
||||||
/// </summary>
|
|
||||||
public class CollectionController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IEventHub _eventHub;
|
private readonly IEventHub _eventHub;
|
||||||
|
|
||||||
@ -41,7 +41,8 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||||
}
|
}
|
||||||
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
|
|
||||||
|
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(user.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -56,9 +57,10 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
queryString ??= "";
|
queryString ??= "";
|
||||||
queryString = queryString.Replace(@"%", string.Empty);
|
queryString = queryString.Replace(@"%", string.Empty);
|
||||||
if (queryString.Length == 0) return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
if (queryString.Length == 0) return await GetAllTags();
|
||||||
|
|
||||||
return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString);
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString, user.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -188,5 +190,4 @@ namespace API.Controllers
|
|||||||
|
|
||||||
return BadRequest("Something went wrong. Please try again.");
|
return BadRequest("Something went wrong. Please try again.");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
114
API/Controllers/DeviceController.cs
Normal file
114
API/Controllers/DeviceController.cs
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.Data.Repositories;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Responsible interacting and creating Devices
|
||||||
|
/// </summary>
|
||||||
|
public class DeviceController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IDeviceService _deviceService;
|
||||||
|
private readonly IEmailService _emailService;
|
||||||
|
private readonly IEventHub _eventHub;
|
||||||
|
|
||||||
|
public DeviceController(IUnitOfWork unitOfWork, IDeviceService deviceService, IEmailService emailService, IEventHub eventHub)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_deviceService = deviceService;
|
||||||
|
_emailService = emailService;
|
||||||
|
_eventHub = eventHub;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpPost("create")]
|
||||||
|
public async Task<ActionResult> CreateOrUpdateDevice(CreateDeviceDto dto)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
|
||||||
|
var device = await _deviceService.Create(dto, user);
|
||||||
|
|
||||||
|
if (device == null) return BadRequest("There was an error when creating the device");
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("update")]
|
||||||
|
public async Task<ActionResult> UpdateDevice(UpdateDeviceDto dto)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
|
||||||
|
var device = await _deviceService.Update(dto, user);
|
||||||
|
|
||||||
|
if (device == null) return BadRequest("There was an error when updating the device");
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the device from the user
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deviceId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpDelete]
|
||||||
|
public async Task<ActionResult> DeleteDevice(int deviceId)
|
||||||
|
{
|
||||||
|
if (deviceId <= 0) return BadRequest("Not a valid deviceId");
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
|
||||||
|
if (await _deviceService.Delete(user, deviceId)) return Ok();
|
||||||
|
|
||||||
|
return BadRequest("Could not delete device");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<IEnumerable<DeviceDto>>> GetDevices()
|
||||||
|
{
|
||||||
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
|
return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("send-to")]
|
||||||
|
public async Task<ActionResult> SendToDevice(SendToDeviceDto dto)
|
||||||
|
{
|
||||||
|
if (dto.ChapterIds.Any(i => i < 0)) return BadRequest("ChapterIds must be greater than 0");
|
||||||
|
if (dto.DeviceId < 0) return BadRequest("DeviceId must be greater than 0");
|
||||||
|
|
||||||
|
if (await _emailService.IsDefaultEmailService())
|
||||||
|
return BadRequest("Send to device cannot be used with Kavita's email service. Please configure your own.");
|
||||||
|
|
||||||
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
|
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, MessageFactory.SendingToDeviceEvent($"Transferring files to your device", "started"), userId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var success = await _deviceService.SendTo(dto.ChapterIds, dto.DeviceId);
|
||||||
|
if (success) return Ok();
|
||||||
|
}
|
||||||
|
catch (KavitaException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice, MessageFactory.SendingToDeviceEvent($"Transferring files to your device", "ended"), userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest("There was an error sending the file to the device");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -16,14 +16,14 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All APIs related to downloading entities from the system. Requires Download Role or Admin Role.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy="RequireDownloadRole")]
|
||||||
|
public class DownloadController : BaseApiController
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// All APIs related to downloading entities from the system. Requires Download Role or Admin Role.
|
|
||||||
/// </summary>
|
|
||||||
[Authorize(Policy="RequireDownloadRole")]
|
|
||||||
public class DownloadController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IArchiveService _archiveService;
|
private readonly IArchiveService _archiveService;
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
@ -31,10 +31,12 @@ namespace API.Controllers
|
|||||||
private readonly IEventHub _eventHub;
|
private readonly IEventHub _eventHub;
|
||||||
private readonly ILogger<DownloadController> _logger;
|
private readonly ILogger<DownloadController> _logger;
|
||||||
private readonly IBookmarkService _bookmarkService;
|
private readonly IBookmarkService _bookmarkService;
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
private const string DefaultContentType = "application/octet-stream";
|
private const string DefaultContentType = "application/octet-stream";
|
||||||
|
|
||||||
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService,
|
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService,
|
||||||
IDownloadService downloadService, IEventHub eventHub, ILogger<DownloadController> logger, IBookmarkService bookmarkService)
|
IDownloadService downloadService, IEventHub eventHub, ILogger<DownloadController> logger, IBookmarkService bookmarkService,
|
||||||
|
IAccountService accountService)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_archiveService = archiveService;
|
_archiveService = archiveService;
|
||||||
@ -43,6 +45,7 @@ namespace API.Controllers
|
|||||||
_eventHub = eventHub;
|
_eventHub = eventHub;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_bookmarkService = bookmarkService;
|
_bookmarkService = bookmarkService;
|
||||||
|
_accountService = accountService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -109,7 +112,7 @@ namespace API.Controllers
|
|||||||
private async Task<bool> HasDownloadPermission()
|
private async Task<bool> HasDownloadPermission()
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
return await _downloadService.HasDownloadPermission(user);
|
return await _accountService.HasDownloadPermission(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ActionResult GetFirstFileDownload(IEnumerable<MangaFile> files)
|
private ActionResult GetFirstFileDownload(IEnumerable<MangaFile> files)
|
||||||
@ -218,5 +221,4 @@ namespace API.Controllers
|
|||||||
return PhysicalFile(filePath, DefaultContentType, filename, true);
|
return PhysicalFile(filePath, DefaultContentType, filename, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
using System.IO;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ namespace API.Controllers;
|
|||||||
public class HealthController : BaseApiController
|
public class HealthController : BaseApiController
|
||||||
{
|
{
|
||||||
|
|
||||||
[HttpGet()]
|
[HttpGet]
|
||||||
public ActionResult GetHealth()
|
public ActionResult GetHealth()
|
||||||
{
|
{
|
||||||
return Ok("Ok");
|
return Ok("Ok");
|
||||||
|
@ -7,14 +7,14 @@ using API.Services;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Responsible for servicing up images stored in Kavita for entities
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class ImageController : BaseApiController
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Responsible for servicing up images stored in Kavita for entities
|
|
||||||
/// </summary>
|
|
||||||
[AllowAnonymous]
|
|
||||||
public class ImageController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
|
|
||||||
@ -149,5 +149,4 @@ namespace API.Controllers
|
|||||||
|
|
||||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ using API.DTOs.Search;
|
|||||||
using API.DTOs.System;
|
using API.DTOs.System;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Metadata;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks.Scanner;
|
using API.Services.Tasks.Scanner;
|
||||||
@ -22,11 +23,11 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using TaskScheduler = API.Services.TaskScheduler;
|
using TaskScheduler = API.Services.TaskScheduler;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
public class LibraryController : BaseApiController
|
||||||
{
|
{
|
||||||
[Authorize]
|
|
||||||
public class LibraryController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
private readonly ILogger<LibraryController> _logger;
|
private readonly ILogger<LibraryController> _logger;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
@ -112,10 +113,11 @@ namespace API.Controllers
|
|||||||
return Ok(_directoryService.ListDirectory(path));
|
return Ok(_directoryService.ListDirectory(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
|
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
|
||||||
{
|
{
|
||||||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosAsync());
|
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("jump-bar")]
|
[HttpGet("jump-bar")]
|
||||||
@ -196,12 +198,6 @@ namespace API.Controllers
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("libraries")]
|
|
||||||
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibrariesForUser()
|
|
||||||
{
|
|
||||||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given a valid path, will invoke either a Scan Series or Scan Library. If the folder does not exist within Kavita, the request will be ignored
|
/// Given a valid path, will invoke either a Scan Series or Scan Library. If the folder does not exist within Kavita, the request will be ignored
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -213,9 +209,11 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(dto.ApiKey);
|
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(dto.ApiKey);
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||||
|
|
||||||
// Validate user has Admin privileges
|
// Validate user has Admin privileges
|
||||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||||
if (!isAdmin) return BadRequest("API key must belong to an admin");
|
if (!isAdmin) return BadRequest("API key must belong to an admin");
|
||||||
|
|
||||||
if (dto.FolderPath.Contains("..")) return BadRequest("Invalid Path");
|
if (dto.FolderPath.Contains("..")) return BadRequest("Invalid Path");
|
||||||
|
|
||||||
dto.FolderPath = Services.Tasks.Scanner.Parser.Parser.NormalizePath(dto.FolderPath);
|
dto.FolderPath = Services.Tasks.Scanner.Parser.Parser.NormalizePath(dto.FolderPath);
|
||||||
@ -250,11 +248,24 @@ namespace API.Controllers
|
|||||||
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
|
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
|
||||||
{
|
{
|
||||||
// TODO: Figure out how to cancel a job
|
// TODO: Figure out how to cancel a job
|
||||||
|
|
||||||
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
|
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
|
||||||
return BadRequest(
|
return BadRequest(
|
||||||
"You cannot delete a library while a scan is in progress. Please wait for scan to continue then try to delete");
|
"You cannot delete a library while a scan is in progress. Please wait for scan to continue then try to delete");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
SeriesIncludes.Related))
|
||||||
|
{
|
||||||
|
s.Relations = new List<SeriesRelation>();
|
||||||
|
_unitOfWork.SeriesRepository.Update(s);
|
||||||
|
}
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
_unitOfWork.LibraryRepository.Delete(library);
|
_unitOfWork.LibraryRepository.Delete(library);
|
||||||
|
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
if (chapterIds.Any())
|
if (chapterIds.Any())
|
||||||
@ -317,28 +328,10 @@ namespace API.Controllers
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("search")]
|
|
||||||
public async Task<ActionResult<SearchResultGroupDto>> Search(string queryString)
|
|
||||||
{
|
|
||||||
queryString = Uri.UnescapeDataString(queryString).Trim().Replace(@"%", string.Empty).Replace(":", string.Empty);
|
|
||||||
|
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
|
||||||
// Get libraries user has access to
|
|
||||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
|
|
||||||
|
|
||||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
|
||||||
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(user.Id, isAdmin, libraries.Select(l => l.Id).ToArray(), queryString);
|
|
||||||
|
|
||||||
return Ok(series);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("type")]
|
[HttpGet("type")]
|
||||||
public async Task<ActionResult<LibraryType>> GetLibraryType(int libraryId)
|
public async Task<ActionResult<LibraryType>> GetLibraryType(int libraryId)
|
||||||
{
|
{
|
||||||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId));
|
return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ using API.DTOs;
|
|||||||
using API.DTOs.Filtering;
|
using API.DTOs.Filtering;
|
||||||
using API.DTOs.Metadata;
|
using API.DTOs.Metadata;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Extensions;
|
||||||
using Kavita.Common.Extensions;
|
using Kavita.Common.Extensions;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -31,15 +32,18 @@ public class MetadataController : BaseApiController
|
|||||||
[HttpGet("genres")]
|
[HttpGet("genres")]
|
||||||
public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds)
|
public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds)
|
||||||
{
|
{
|
||||||
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||||
if (ids != null && ids.Count > 0)
|
if (ids != null && ids.Count > 0)
|
||||||
{
|
{
|
||||||
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids));
|
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync());
|
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches people from the instance
|
/// Fetches people from the instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -48,12 +52,13 @@ public class MetadataController : BaseApiController
|
|||||||
[HttpGet("people")]
|
[HttpGet("people")]
|
||||||
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryIds)
|
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryIds)
|
||||||
{
|
{
|
||||||
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||||
if (ids != null && ids.Count > 0)
|
if (ids != null && ids.Count > 0)
|
||||||
{
|
{
|
||||||
return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids));
|
return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, userId));
|
||||||
}
|
}
|
||||||
return Ok(await _unitOfWork.PersonRepository.GetAllPeople());
|
return Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -64,19 +69,22 @@ public class MetadataController : BaseApiController
|
|||||||
[HttpGet("tags")]
|
[HttpGet("tags")]
|
||||||
public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
|
public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
|
||||||
{
|
{
|
||||||
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||||
if (ids != null && ids.Count > 0)
|
if (ids != null && ids.Count > 0)
|
||||||
{
|
{
|
||||||
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids));
|
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, userId));
|
||||||
}
|
}
|
||||||
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosAsync());
|
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosAsync(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches all age ratings from the instance
|
/// Fetches all age ratings from the instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
|
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
|
||||||
|
/// <remarks>This API is cached for 1 hour, varying by libraryIds</remarks>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
[ResponseCache(CacheProfileName = "5Minute", VaryByQueryKeys = new [] {"libraryIds"})]
|
||||||
[HttpGet("age-ratings")]
|
[HttpGet("age-ratings")]
|
||||||
public async Task<ActionResult<IList<AgeRatingDto>>> GetAllAgeRatings(string? libraryIds)
|
public async Task<ActionResult<IList<AgeRatingDto>>> GetAllAgeRatings(string? libraryIds)
|
||||||
{
|
{
|
||||||
@ -90,14 +98,16 @@ public class MetadataController : BaseApiController
|
|||||||
{
|
{
|
||||||
Title = t.ToDescription(),
|
Title = t.ToDescription(),
|
||||||
Value = t
|
Value = t
|
||||||
}));
|
}).Where(r => r.Value > AgeRating.NotApplicable));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches all publication status' from the instance
|
/// Fetches all publication status' from the instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryIds">String separated libraryIds or null for all publication status</param>
|
/// <param name="libraryIds">String separated libraryIds or null for all publication status</param>
|
||||||
|
/// <remarks>This API is cached for 1 hour, varying by libraryIds</remarks>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
[ResponseCache(CacheProfileName = "5Minute", VaryByQueryKeys = new [] {"libraryIds"})]
|
||||||
[HttpGet("publication-status")]
|
[HttpGet("publication-status")]
|
||||||
public ActionResult<IList<AgeRatingDto>> GetAllPublicationStatus(string? libraryIds)
|
public ActionResult<IList<AgeRatingDto>> GetAllPublicationStatus(string? libraryIds)
|
||||||
{
|
{
|
||||||
@ -115,8 +125,9 @@ public class MetadataController : BaseApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches all age ratings from the instance
|
/// Fetches all age languages from the libraries passed (or if none passed, all in the server)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>This does not perform RBS for the user if they have Library access due to the non-sensitive nature of languages</remarks>
|
||||||
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
|
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("languages")]
|
[HttpGet("languages")]
|
||||||
@ -128,15 +139,8 @@ public class MetadataController : BaseApiController
|
|||||||
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
|
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
var englishTag = CultureInfo.GetCultureInfo("en");
|
|
||||||
return Ok(new List<LanguageDto>()
|
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync());
|
||||||
{
|
|
||||||
new ()
|
|
||||||
{
|
|
||||||
Title = englishTag.DisplayName,
|
|
||||||
IsoCode = englishTag.IetfLanguageTag
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("all-languages")]
|
[HttpGet("all-languages")]
|
||||||
|
@ -32,6 +32,7 @@ public class OpdsController : BaseApiController
|
|||||||
private readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
private readonly IReaderService _readerService;
|
private readonly IReaderService _readerService;
|
||||||
private readonly ISeriesService _seriesService;
|
private readonly ISeriesService _seriesService;
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
|
|
||||||
|
|
||||||
private readonly XmlSerializer _xmlSerializer;
|
private readonly XmlSerializer _xmlSerializer;
|
||||||
@ -65,7 +66,8 @@ public class OpdsController : BaseApiController
|
|||||||
|
|
||||||
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
|
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
|
||||||
IDirectoryService directoryService, ICacheService cacheService,
|
IDirectoryService directoryService, ICacheService cacheService,
|
||||||
IReaderService readerService, ISeriesService seriesService)
|
IReaderService readerService, ISeriesService seriesService,
|
||||||
|
IAccountService accountService)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_downloadService = downloadService;
|
_downloadService = downloadService;
|
||||||
@ -73,6 +75,7 @@ public class OpdsController : BaseApiController
|
|||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_readerService = readerService;
|
_readerService = readerService;
|
||||||
_seriesService = seriesService;
|
_seriesService = seriesService;
|
||||||
|
_accountService = accountService;
|
||||||
|
|
||||||
_xmlSerializer = new XmlSerializer(typeof(Feed));
|
_xmlSerializer = new XmlSerializer(typeof(Feed));
|
||||||
_xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription));
|
_xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription));
|
||||||
@ -193,8 +196,8 @@ public class OpdsController : BaseApiController
|
|||||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||||
|
|
||||||
IList<CollectionTagDto> tags = isAdmin ? (await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync()).ToList()
|
IEnumerable<CollectionTagDto> tags = isAdmin ? (await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync())
|
||||||
: (await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync()).ToList();
|
: (await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(userId));
|
||||||
|
|
||||||
|
|
||||||
var feed = CreateFeed("All Collections", $"{apiKey}/collections", apiKey);
|
var feed = CreateFeed("All Collections", $"{apiKey}/collections", apiKey);
|
||||||
@ -236,7 +239,7 @@ public class OpdsController : BaseApiController
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
tags = await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
|
tags = await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var tag = tags.SingleOrDefault(t => t.Id == collectionId);
|
var tag = tags.SingleOrDefault(t => t.Id == collectionId);
|
||||||
@ -619,7 +622,7 @@ public class OpdsController : BaseApiController
|
|||||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
return BadRequest("OPDS is not enabled on this server");
|
return BadRequest("OPDS is not enabled on this server");
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey));
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey));
|
||||||
if (!await _downloadService.HasDownloadPermission(user))
|
if (!await _accountService.HasDownloadPermission(user))
|
||||||
{
|
{
|
||||||
return BadRequest("User does not have download permissions");
|
return BadRequest("User does not have download permissions");
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,10 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
public class PluginController : BaseApiController
|
||||||
{
|
{
|
||||||
public class PluginController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ITokenService _tokenService;
|
private readonly ITokenService _tokenService;
|
||||||
private readonly ILogger<PluginController> _logger;
|
private readonly ILogger<PluginController> _logger;
|
||||||
@ -46,5 +46,4 @@ namespace API.Controllers
|
|||||||
ApiKey = user.ApiKey,
|
ApiKey = user.ApiKey,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,40 +6,48 @@ using System.Threading.Tasks;
|
|||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.Filtering;
|
||||||
using API.DTOs.Reader;
|
using API.DTOs.Reader;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
|
using API.SignalR;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For all things regarding reading, mainly focusing on non-Book related entities
|
||||||
|
/// </summary>
|
||||||
|
public class ReaderController : BaseApiController
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// For all things regarding reading, mainly focusing on non-Book related entities
|
|
||||||
/// </summary>
|
|
||||||
public class ReaderController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ILogger<ReaderController> _logger;
|
private readonly ILogger<ReaderController> _logger;
|
||||||
private readonly IReaderService _readerService;
|
private readonly IReaderService _readerService;
|
||||||
private readonly IBookmarkService _bookmarkService;
|
private readonly IBookmarkService _bookmarkService;
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
|
private readonly IEventHub _eventHub;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ReaderController(ICacheService cacheService,
|
public ReaderController(ICacheService cacheService,
|
||||||
IUnitOfWork unitOfWork, ILogger<ReaderController> logger,
|
IUnitOfWork unitOfWork, ILogger<ReaderController> logger,
|
||||||
IReaderService readerService, IBookmarkService bookmarkService)
|
IReaderService readerService, IBookmarkService bookmarkService,
|
||||||
|
IAccountService accountService, IEventHub eventHub)
|
||||||
{
|
{
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_readerService = readerService;
|
_readerService = readerService;
|
||||||
_bookmarkService = bookmarkService;
|
_bookmarkService = bookmarkService;
|
||||||
|
_accountService = accountService;
|
||||||
|
_eventHub = eventHub;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -268,8 +276,6 @@ namespace API.Controllers
|
|||||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||||
await _readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters);
|
await _readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters);
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
return Ok();
|
return Ok();
|
||||||
@ -290,8 +296,9 @@ namespace API.Controllers
|
|||||||
|
|
||||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||||
await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
|
await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||||
_unitOfWork.UserRepository.Update(user);
|
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, markVolumeReadDto.SeriesId,
|
||||||
|
markVolumeReadDto.VolumeId, 0, chapters.Sum(c => c.Pages)));
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
@ -319,15 +326,14 @@ namespace API.Controllers
|
|||||||
chapterIds.Add(chapterId);
|
chapterIds.Add(chapterId);
|
||||||
}
|
}
|
||||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
||||||
await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters);
|
await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters.ToList());
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return BadRequest("Could not save progress");
|
return BadRequest("Could not save progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,9 +354,7 @@ namespace API.Controllers
|
|||||||
chapterIds.Add(chapterId);
|
chapterIds.Add(chapterId);
|
||||||
}
|
}
|
||||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
||||||
await _readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters);
|
await _readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters.ToList());
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
@ -377,8 +381,6 @@ namespace API.Controllers
|
|||||||
await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
return Ok();
|
return Ok();
|
||||||
@ -404,8 +406,6 @@ namespace API.Controllers
|
|||||||
await _readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters);
|
await _readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
return Ok();
|
return Ok();
|
||||||
@ -526,7 +526,7 @@ namespace API.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="chapterId"></param>
|
/// <param name="chapterId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("get-bookmarks")]
|
[HttpGet("chapter-bookmarks")]
|
||||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarks(int chapterId)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||||
@ -537,13 +537,15 @@ namespace API.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a list of all bookmarked pages for a User
|
/// Returns a list of all bookmarked pages for a User
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="filterDto">Only supports SeriesNameQuery</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("get-all-bookmarks")]
|
[HttpPost("all-bookmarks")]
|
||||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks()
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks(FilterDto filterDto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||||
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||||
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id));
|
|
||||||
|
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id, filterDto));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -626,7 +628,7 @@ namespace API.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="volumeId"></param>
|
/// <param name="volumeId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("get-volume-bookmarks")]
|
[HttpGet("volume-bookmarks")]
|
||||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForVolume(int volumeId)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||||
@ -639,7 +641,7 @@ namespace API.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seriesId"></param>
|
/// <param name="seriesId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("get-series-bookmarks")]
|
[HttpGet("series-bookmarks")]
|
||||||
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetBookmarksForSeries(int seriesId)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||||
@ -651,27 +653,31 @@ namespace API.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Bookmarks a page against a Chapter
|
/// Bookmarks a page against a Chapter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>This has a side effect of caching the chapter files to disk</remarks>
|
||||||
/// <param name="bookmarkDto"></param>
|
/// <param name="bookmarkDto"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost("bookmark")]
|
[HttpPost("bookmark")]
|
||||||
public async Task<ActionResult> BookmarkPage(BookmarkDto bookmarkDto)
|
public async Task<ActionResult> BookmarkPage(BookmarkDto bookmarkDto)
|
||||||
{
|
{
|
||||||
// Don't let user save past total pages.
|
// Don't let user save past total pages.
|
||||||
bookmarkDto.Page = await _readerService.CapPageToChapter(bookmarkDto.ChapterId, bookmarkDto.Page);
|
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||||
|
if (user == null) return new UnauthorizedResult();
|
||||||
|
|
||||||
|
if (!await _accountService.HasBookmarkPermission(user))
|
||||||
|
return BadRequest("You do not have permission to bookmark");
|
||||||
|
|
||||||
var chapter = await _cacheService.Ensure(bookmarkDto.ChapterId);
|
var chapter = await _cacheService.Ensure(bookmarkDto.ChapterId);
|
||||||
if (chapter == null) return BadRequest("Could not find cached image. Reload and try again.");
|
if (chapter == null) return BadRequest("Could not find cached image. Reload and try again.");
|
||||||
|
|
||||||
|
bookmarkDto.Page = _readerService.CapPageToChapter(chapter, bookmarkDto.Page);
|
||||||
var path = _cacheService.GetCachedPagePath(chapter, bookmarkDto.Page);
|
var path = _cacheService.GetCachedPagePath(chapter, bookmarkDto.Page);
|
||||||
|
|
||||||
if (await _bookmarkService.BookmarkPage(user, bookmarkDto, path))
|
if (!await _bookmarkService.BookmarkPage(user, bookmarkDto, path)) return BadRequest("Could not save bookmark");
|
||||||
{
|
|
||||||
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
|
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
return BadRequest("Could not save bookmark");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a bookmarked page for a Chapter
|
/// Removes a bookmarked page for a Chapter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -681,17 +687,18 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult> UnBookmarkPage(BookmarkDto bookmarkDto)
|
public async Task<ActionResult> UnBookmarkPage(BookmarkDto bookmarkDto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||||
if (user.Bookmarks == null) return Ok();
|
if (user == null) return new UnauthorizedResult();
|
||||||
|
if (user.Bookmarks.IsNullOrEmpty()) return Ok();
|
||||||
|
|
||||||
if (await _bookmarkService.RemoveBookmarkPage(user, bookmarkDto))
|
if (!await _accountService.HasBookmarkPermission(user))
|
||||||
{
|
return BadRequest("You do not have permission to unbookmark");
|
||||||
|
|
||||||
|
if (!await _bookmarkService.RemoveBookmarkPage(user, bookmarkDto))
|
||||||
|
return BadRequest("Could not remove bookmark");
|
||||||
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
|
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
return BadRequest("Could not remove bookmark");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the next logical chapter from the series.
|
/// Returns the next logical chapter from the series.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -702,6 +709,7 @@ namespace API.Controllers
|
|||||||
/// <param name="volumeId"></param>
|
/// <param name="volumeId"></param>
|
||||||
/// <param name="currentChapterId"></param>
|
/// <param name="currentChapterId"></param>
|
||||||
/// <returns>chapter id for next manga</returns>
|
/// <returns>chapter id for next manga</returns>
|
||||||
|
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new string[] { "seriesId", "volumeId", "currentChapterId"})]
|
||||||
[HttpGet("next-chapter")]
|
[HttpGet("next-chapter")]
|
||||||
public async Task<ActionResult<int>> GetNextChapter(int seriesId, int volumeId, int currentChapterId)
|
public async Task<ActionResult<int>> GetNextChapter(int seriesId, int volumeId, int currentChapterId)
|
||||||
{
|
{
|
||||||
@ -720,6 +728,7 @@ namespace API.Controllers
|
|||||||
/// <param name="volumeId"></param>
|
/// <param name="volumeId"></param>
|
||||||
/// <param name="currentChapterId"></param>
|
/// <param name="currentChapterId"></param>
|
||||||
/// <returns>chapter id for next manga</returns>
|
/// <returns>chapter id for next manga</returns>
|
||||||
|
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new string[] { "seriesId", "volumeId", "currentChapterId"})]
|
||||||
[HttpGet("prev-chapter")]
|
[HttpGet("prev-chapter")]
|
||||||
public async Task<ActionResult<int>> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId)
|
public async Task<ActionResult<int>> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId)
|
||||||
{
|
{
|
||||||
@ -756,5 +765,4 @@ namespace API.Controllers
|
|||||||
return _readerService.GetTimeEstimate(0, pagesLeft, false);
|
return _readerService.GetTimeEstimate(0, pagesLeft, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -13,15 +13,14 @@ using API.SignalR;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
public class ReadingListController : BaseApiController
|
||||||
{
|
{
|
||||||
[Authorize]
|
|
||||||
public class ReadingListController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IEventHub _eventHub;
|
private readonly IEventHub _eventHub;
|
||||||
private readonly IReadingListService _readingListService;
|
private readonly IReadingListService _readingListService;
|
||||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
|
||||||
|
|
||||||
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService)
|
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService)
|
||||||
{
|
{
|
||||||
@ -45,10 +44,11 @@ namespace API.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns reading lists (paginated) for a given user.
|
/// Returns reading lists (paginated) for a given user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="includePromoted">Defaults to true</param>
|
/// <param name="includePromoted">Include Promoted Reading Lists along with user's Reading Lists. Defaults to true</param>
|
||||||
|
/// <param name="userParams">Pagination parameters</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost("lists")]
|
[HttpPost("lists")]
|
||||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams, [FromQuery] bool includePromoted = true)
|
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams, bool includePromoted = true)
|
||||||
{
|
{
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, includePromoted,
|
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, includePromoted,
|
||||||
@ -216,18 +216,24 @@ namespace API.Controllers
|
|||||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(dto.Title))
|
dto.Title = dto.Title.Trim();
|
||||||
{
|
|
||||||
readingList.Title = dto.Title; // Should I check if this is unique?
|
|
||||||
readingList.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(readingList.Title);
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrEmpty(dto.Title))
|
if (!string.IsNullOrEmpty(dto.Title))
|
||||||
{
|
{
|
||||||
readingList.Summary = dto.Summary;
|
readingList.Summary = dto.Summary;
|
||||||
|
|
||||||
|
if (!readingList.Title.Equals(dto.Title))
|
||||||
|
{
|
||||||
|
var hasExisting = user.ReadingLists.Any(l => l.Title.Equals(dto.Title));
|
||||||
|
if (hasExisting)
|
||||||
|
{
|
||||||
|
return BadRequest("A list of this name already exists");
|
||||||
|
}
|
||||||
|
readingList.Title = dto.Title;
|
||||||
|
readingList.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(readingList.Title);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readingList.Promoted = dto.Promoted;
|
readingList.Promoted = dto.Promoted;
|
||||||
|
|
||||||
readingList.CoverImageLocked = dto.CoverImageLocked;
|
readingList.CoverImageLocked = dto.CoverImageLocked;
|
||||||
|
|
||||||
if (!dto.CoverImageLocked)
|
if (!dto.CoverImageLocked)
|
||||||
@ -491,5 +497,4 @@ namespace API.Controllers
|
|||||||
|
|
||||||
return Ok(-1);
|
return Ok(-1);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
|
67
API/Controllers/SearchController.cs
Normal file
67
API/Controllers/SearchController.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.DTOs;
|
||||||
|
using API.DTOs.Search;
|
||||||
|
using API.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Responsible for the Search interface from the UI
|
||||||
|
/// </summary>
|
||||||
|
public class SearchController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
|
public SearchController(IUnitOfWork unitOfWork)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the series for the MangaFile id. If the user does not have access (shouldn't happen by the UI),
|
||||||
|
/// then null is returned
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mangaFileId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("series-for-mangafile")]
|
||||||
|
public async Task<ActionResult<SeriesDto>> GetSeriesForMangaFile(int mangaFileId)
|
||||||
|
{
|
||||||
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
|
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the series for the Chapter id. If the user does not have access (shouldn't happen by the UI),
|
||||||
|
/// then null is returned
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("series-for-chapter")]
|
||||||
|
public async Task<ActionResult<SeriesDto>> GetSeriesForChapter(int chapterId)
|
||||||
|
{
|
||||||
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
|
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("search")]
|
||||||
|
public async Task<ActionResult<SearchResultGroupDto>> Search(string queryString)
|
||||||
|
{
|
||||||
|
queryString = Uri.UnescapeDataString(queryString).Trim().Replace(@"%", string.Empty).Replace(":", string.Empty);
|
||||||
|
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
// Get libraries user has access to
|
||||||
|
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
|
||||||
|
|
||||||
|
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||||
|
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(user.Id, isAdmin, libraries.Select(l => l.Id).ToArray(), queryString);
|
||||||
|
|
||||||
|
return Ok(series);
|
||||||
|
}
|
||||||
|
}
|
@ -19,10 +19,10 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
public class SeriesController : BaseApiController
|
||||||
{
|
{
|
||||||
public class SeriesController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly ILogger<SeriesController> _logger;
|
private readonly ILogger<SeriesController> _logger;
|
||||||
private readonly ITaskScheduler _taskScheduler;
|
private readonly ITaskScheduler _taskScheduler;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
@ -194,6 +194,7 @@ namespace API.Controllers
|
|||||||
return BadRequest("There was an error with updating the series");
|
return BadRequest("There was an error with updating the series");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ResponseCache(CacheProfileName = "Instant")]
|
||||||
[HttpPost("recently-added")]
|
[HttpPost("recently-added")]
|
||||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||||
{
|
{
|
||||||
@ -211,6 +212,7 @@ namespace API.Controllers
|
|||||||
return Ok(series);
|
return Ok(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ResponseCache(CacheProfileName = "Instant")]
|
||||||
[HttpPost("recently-updated-series")]
|
[HttpPost("recently-updated-series")]
|
||||||
public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChapters()
|
public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChapters()
|
||||||
{
|
{
|
||||||
@ -242,6 +244,7 @@ namespace API.Controllers
|
|||||||
/// <param name="userParams"></param>
|
/// <param name="userParams"></param>
|
||||||
/// <param name="libraryId">Default of 0 meaning all libraries</param>
|
/// <param name="libraryId">Default of 0 meaning all libraries</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
[ResponseCache(CacheProfileName = "Instant")]
|
||||||
[HttpPost("on-deck")]
|
[HttpPost("on-deck")]
|
||||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetOnDeck(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetOnDeck(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||||
{
|
{
|
||||||
@ -363,10 +366,13 @@ namespace API.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ageRating"></param>
|
/// <param name="ageRating"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
/// <remarks>This is cached for an hour</remarks>
|
||||||
|
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] {"ageRating"})]
|
||||||
[HttpGet("age-rating")]
|
[HttpGet("age-rating")]
|
||||||
public ActionResult<string> GetAgeRating(int ageRating)
|
public ActionResult<string> GetAgeRating(int ageRating)
|
||||||
{
|
{
|
||||||
var val = (AgeRating) ageRating;
|
var val = (AgeRating) ageRating;
|
||||||
|
if (val == AgeRating.NotApplicable) return "No Restriction";
|
||||||
|
|
||||||
return Ok(val.ToDescription());
|
return Ok(val.ToDescription());
|
||||||
}
|
}
|
||||||
@ -377,6 +383,7 @@ namespace API.Controllers
|
|||||||
/// <param name="seriesId"></param>
|
/// <param name="seriesId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <remarks>Do not rely on this API externally. May change without hesitation. </remarks>
|
/// <remarks>Do not rely on this API externally. May change without hesitation. </remarks>
|
||||||
|
[ResponseCache(CacheProfileName = "5Minute", VaryByQueryKeys = new [] {"seriesId"})]
|
||||||
[HttpGet("series-detail")]
|
[HttpGet("series-detail")]
|
||||||
public async Task<ActionResult<SeriesDetailDto>> GetSeriesDetailBreakdown(int seriesId)
|
public async Task<ActionResult<SeriesDetailDto>> GetSeriesDetailBreakdown(int seriesId)
|
||||||
{
|
{
|
||||||
@ -384,31 +391,7 @@ namespace API.Controllers
|
|||||||
return await _seriesService.GetSeriesDetail(seriesId, userId);
|
return await _seriesService.GetSeriesDetail(seriesId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the series for the MangaFile id. If the user does not have access (shouldn't happen by the UI),
|
|
||||||
/// then null is returned
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mangaFileId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpGet("series-for-mangafile")]
|
|
||||||
public async Task<ActionResult<SeriesDto>> GetSeriesForMangaFile(int mangaFileId)
|
|
||||||
{
|
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
|
||||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the series for the Chapter id. If the user does not have access (shouldn't happen by the UI),
|
|
||||||
/// then null is returned
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="chapterId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpGet("series-for-chapter")]
|
|
||||||
public async Task<ActionResult<SeriesDto>> GetSeriesForChapter(int chapterId)
|
|
||||||
{
|
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
|
||||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches the related series for a given series
|
/// Fetches the related series for a given series
|
||||||
@ -433,7 +416,7 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult<RelatedSeriesDto>> GetAllRelatedSeries(int seriesId)
|
public async Task<ActionResult<RelatedSeriesDto>> GetAllRelatedSeries(int seriesId)
|
||||||
{
|
{
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
return Ok(await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId));
|
return Ok(await _seriesService.GetRelatedSeries(userId, seriesId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -446,53 +429,12 @@ namespace API.Controllers
|
|||||||
[HttpPost("update-related")]
|
[HttpPost("update-related")]
|
||||||
public async Task<ActionResult> UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
|
public async Task<ActionResult> UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
|
||||||
{
|
{
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related);
|
if (await _seriesService.UpdateRelatedSeries(dto))
|
||||||
|
{
|
||||||
UpdateRelationForKind(dto.Adaptations, series.Relations.Where(r => r.RelationKind == RelationKind.Adaptation).ToList(), series, RelationKind.Adaptation);
|
return Ok();
|
||||||
UpdateRelationForKind(dto.Characters, series.Relations.Where(r => r.RelationKind == RelationKind.Character).ToList(), series, RelationKind.Character);
|
}
|
||||||
UpdateRelationForKind(dto.Contains, series.Relations.Where(r => r.RelationKind == RelationKind.Contains).ToList(), series, RelationKind.Contains);
|
|
||||||
UpdateRelationForKind(dto.Others, series.Relations.Where(r => r.RelationKind == RelationKind.Other).ToList(), series, RelationKind.Other);
|
|
||||||
UpdateRelationForKind(dto.SideStories, series.Relations.Where(r => r.RelationKind == RelationKind.SideStory).ToList(), series, RelationKind.SideStory);
|
|
||||||
UpdateRelationForKind(dto.SpinOffs, series.Relations.Where(r => r.RelationKind == RelationKind.SpinOff).ToList(), series, RelationKind.SpinOff);
|
|
||||||
UpdateRelationForKind(dto.AlternativeSettings, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeSetting).ToList(), series, RelationKind.AlternativeSetting);
|
|
||||||
UpdateRelationForKind(dto.AlternativeVersions, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeVersion).ToList(), series, RelationKind.AlternativeVersion);
|
|
||||||
UpdateRelationForKind(dto.Doujinshis, series.Relations.Where(r => r.RelationKind == RelationKind.Doujinshi).ToList(), series, RelationKind.Doujinshi);
|
|
||||||
UpdateRelationForKind(dto.Prequels, series.Relations.Where(r => r.RelationKind == RelationKind.Prequel).ToList(), series, RelationKind.Prequel);
|
|
||||||
UpdateRelationForKind(dto.Sequels, series.Relations.Where(r => r.RelationKind == RelationKind.Sequel).ToList(), series, RelationKind.Sequel);
|
|
||||||
|
|
||||||
if (!_unitOfWork.HasChanges()) return Ok();
|
|
||||||
if (await _unitOfWork.CommitAsync()) return Ok();
|
|
||||||
|
|
||||||
|
|
||||||
return BadRequest("There was an issue updating relationships");
|
return BadRequest("There was an issue updating relationships");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move this to a Service and Unit Test it
|
|
||||||
private void UpdateRelationForKind(ICollection<int> dtoTargetSeriesIds, IEnumerable<SeriesRelation> adaptations, Series series, RelationKind kind)
|
|
||||||
{
|
|
||||||
foreach (var adaptation in adaptations.Where(adaptation => !dtoTargetSeriesIds.Contains(adaptation.TargetSeriesId)))
|
|
||||||
{
|
|
||||||
// If the seriesId isn't in dto, it means we've removed or reclassified
|
|
||||||
series.Relations.Remove(adaptation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point, we only have things to add
|
|
||||||
foreach (var targetSeriesId in dtoTargetSeriesIds)
|
|
||||||
{
|
|
||||||
// This ensures we don't allow any duplicates to be added
|
|
||||||
if (series.Relations.SingleOrDefault(r =>
|
|
||||||
r.RelationKind == kind && r.TargetSeriesId == targetSeriesId) !=
|
|
||||||
null) continue;
|
|
||||||
|
|
||||||
series.Relations.Add(new SeriesRelation()
|
|
||||||
{
|
|
||||||
Series = series,
|
|
||||||
SeriesId = series.Id,
|
|
||||||
TargetSeriesId = targetSeriesId,
|
|
||||||
RelationKind = kind
|
|
||||||
});
|
|
||||||
_unitOfWork.SeriesRepository.Update(series);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ using API.DTOs.Jobs;
|
|||||||
using API.DTOs.Stats;
|
using API.DTOs.Stats;
|
||||||
using API.DTOs.Update;
|
using API.DTOs.Update;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
|
using API.Logging;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
@ -20,14 +21,13 @@ using Microsoft.Extensions.Hosting;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using TaskScheduler = System.Threading.Tasks.TaskScheduler;
|
using TaskScheduler = System.Threading.Tasks.TaskScheduler;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
public class ServerController : BaseApiController
|
||||||
{
|
{
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
|
||||||
public class ServerController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly IHostApplicationLifetime _applicationLifetime;
|
private readonly IHostApplicationLifetime _applicationLifetime;
|
||||||
private readonly ILogger<ServerController> _logger;
|
private readonly ILogger<ServerController> _logger;
|
||||||
private readonly IConfiguration _config;
|
|
||||||
private readonly IBackupService _backupService;
|
private readonly IBackupService _backupService;
|
||||||
private readonly IArchiveService _archiveService;
|
private readonly IArchiveService _archiveService;
|
||||||
private readonly IVersionUpdaterService _versionUpdaterService;
|
private readonly IVersionUpdaterService _versionUpdaterService;
|
||||||
@ -36,13 +36,12 @@ namespace API.Controllers
|
|||||||
private readonly IEmailService _emailService;
|
private readonly IEmailService _emailService;
|
||||||
private readonly IBookmarkService _bookmarkService;
|
private readonly IBookmarkService _bookmarkService;
|
||||||
|
|
||||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
|
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger,
|
||||||
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
||||||
ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService)
|
ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService)
|
||||||
{
|
{
|
||||||
_applicationLifetime = applicationLifetime;
|
_applicationLifetime = applicationLifetime;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_config = config;
|
|
||||||
_backupService = backupService;
|
_backupService = backupService;
|
||||||
_archiveService = archiveService;
|
_archiveService = archiveService;
|
||||||
_versionUpdaterService = versionUpdaterService;
|
_versionUpdaterService = versionUpdaterService;
|
||||||
@ -73,7 +72,20 @@ namespace API.Controllers
|
|||||||
public ActionResult ClearCache()
|
public ActionResult ClearCache()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("{UserName} is clearing cache of server from admin dashboard", User.GetUsername());
|
_logger.LogInformation("{UserName} is clearing cache of server from admin dashboard", User.GetUsername());
|
||||||
_cleanupService.CleanupCacheDirectory();
|
_cleanupService.CleanupCacheAndTempDirectories();
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs an ad-hoc cleanup of Want To Read, by removing want to read series for users, where the series are fully read and in Completed publication status.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("cleanup-want-to-read")]
|
||||||
|
public ActionResult CleanupWantToRead()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("{UserName} is clearing running want to read cleanup from admin dashboard", User.GetUsername());
|
||||||
|
RecurringJob.TriggerJob(API.Services.TaskScheduler.RemoveFromWantToReadTaskId);
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
@ -86,7 +98,7 @@ namespace API.Controllers
|
|||||||
public ActionResult BackupDatabase()
|
public ActionResult BackupDatabase()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("{UserName} is backing up database of server from admin dashboard", User.GetUsername());
|
_logger.LogInformation("{UserName} is backing up database of server from admin dashboard", User.GetUsername());
|
||||||
RecurringJob.Trigger("backup");
|
RecurringJob.TriggerJob(API.Services.TaskScheduler.BackupTaskId);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +126,7 @@ namespace API.Controllers
|
|||||||
[HttpGet("logs")]
|
[HttpGet("logs")]
|
||||||
public ActionResult GetLogs()
|
public ActionResult GetLogs()
|
||||||
{
|
{
|
||||||
var files = _backupService.GetLogFiles(_config.GetMaxRollingFiles(), _config.GetLoggingFileName());
|
var files = _backupService.GetLogFiles();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var zipPath = _archiveService.CreateZipForDownload(files, "logs");
|
var zipPath = _archiveService.CreateZipForDownload(files, "logs");
|
||||||
@ -155,7 +167,7 @@ namespace API.Controllers
|
|||||||
[HttpGet("jobs")]
|
[HttpGet("jobs")]
|
||||||
public ActionResult<IEnumerable<JobDto>> GetJobs()
|
public ActionResult<IEnumerable<JobDto>> GetJobs()
|
||||||
{
|
{
|
||||||
var recurringJobs = Hangfire.JobStorage.Current.GetConnection().GetRecurringJobs().Select(
|
var recurringJobs = JobStorage.Current.GetConnection().GetRecurringJobs().Select(
|
||||||
dto =>
|
dto =>
|
||||||
new JobDto() {
|
new JobDto() {
|
||||||
Id = dto.Id,
|
Id = dto.Id,
|
||||||
@ -170,5 +182,4 @@ namespace API.Controllers
|
|||||||
return Ok(recurringJobs);
|
return Ok(recurringJobs);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ using API.DTOs.Settings;
|
|||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers.Converters;
|
using API.Helpers.Converters;
|
||||||
|
using API.Logging;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks.Scanner;
|
using API.Services.Tasks.Scanner;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
@ -20,10 +21,10 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
public class SettingsController : BaseApiController
|
||||||
{
|
{
|
||||||
public class SettingsController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly ILogger<SettingsController> _logger;
|
private readonly ILogger<SettingsController> _logger;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ITaskScheduler _taskScheduler;
|
private readonly ITaskScheduler _taskScheduler;
|
||||||
@ -159,7 +160,7 @@ namespace API.Controllers
|
|||||||
if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
|
if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
|
||||||
{
|
{
|
||||||
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
|
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
|
||||||
Configuration.LogLevel = updateSettingsDto.LoggingLevel;
|
LogLevelOptions.SwitchLogLevel(updateSettingsDto.LoggingLevel);
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,6 +223,16 @@ namespace API.Controllers
|
|||||||
_unitOfWork.SettingsRepository.Update(setting);
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setting.Key == ServerSettingKey.TotalLogs && updateSettingsDto.TotalLogs + string.Empty != setting.Value)
|
||||||
|
{
|
||||||
|
if (updateSettingsDto.TotalLogs > 30 || updateSettingsDto.TotalLogs < 1)
|
||||||
|
{
|
||||||
|
return BadRequest("Total Logs must be between 1 and 30");
|
||||||
|
}
|
||||||
|
setting.Value = updateSettingsDto.TotalLogs + string.Empty;
|
||||||
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
|
}
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
|
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
|
||||||
{
|
{
|
||||||
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
|
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
|
||||||
@ -300,5 +311,4 @@ namespace API.Controllers
|
|||||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
return Ok(settingsDto.EnableOpds);
|
return Ok(settingsDto.EnableOpds);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,9 @@
|
|||||||
using System.Collections.Generic;
|
using System.Threading.Tasks;
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using API.Comparators;
|
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using AutoMapper;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
@ -21,14 +15,12 @@ namespace API.Controllers;
|
|||||||
public class TachiyomiController : BaseApiController
|
public class TachiyomiController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IReaderService _readerService;
|
private readonly ITachiyomiService _tachiyomiService;
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
public TachiyomiController(IUnitOfWork unitOfWork, IReaderService readerService, IMapper mapper)
|
public TachiyomiController(IUnitOfWork unitOfWork, ITachiyomiService tachiyomiService)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_readerService = readerService;
|
_tachiyomiService = tachiyomiService;
|
||||||
_mapper = mapper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -39,53 +31,9 @@ public class TachiyomiController : BaseApiController
|
|||||||
[HttpGet("latest-chapter")]
|
[HttpGet("latest-chapter")]
|
||||||
public async Task<ActionResult<ChapterDto>> GetLatestChapter(int seriesId)
|
public async Task<ActionResult<ChapterDto>> GetLatestChapter(int seriesId)
|
||||||
{
|
{
|
||||||
|
if (seriesId < 1) return BadRequest("seriesId must be greater than 0");
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
|
return Ok(await _tachiyomiService.GetLatestChapter(seriesId, userId));
|
||||||
var currentChapter = await _readerService.GetContinuePoint(seriesId, userId);
|
|
||||||
|
|
||||||
var prevChapterId =
|
|
||||||
await _readerService.GetPrevChapterIdAsync(seriesId, currentChapter.VolumeId, currentChapter.Id, userId);
|
|
||||||
|
|
||||||
// If prevChapterId is -1, this means either nothing is read or everything is read.
|
|
||||||
if (prevChapterId == -1)
|
|
||||||
{
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
|
||||||
var userHasProgress = series.PagesRead != 0 && series.PagesRead < series.Pages;
|
|
||||||
|
|
||||||
// If the user doesn't have progress, then return null, which the extension will catch as 204 (no content) and report nothing as read
|
|
||||||
if (!userHasProgress) return null;
|
|
||||||
|
|
||||||
// Else return the max chapter to Tachiyomi so it can consider everything read
|
|
||||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(seriesId)).ToImmutableList();
|
|
||||||
var looseLeafChapterVolume = volumes.FirstOrDefault(v => v.Number == 0);
|
|
||||||
if (looseLeafChapterVolume == null)
|
|
||||||
{
|
|
||||||
var volumeChapter = _mapper.Map<ChapterDto>(volumes.Last().Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparerZeroFirst.Default).Last());
|
|
||||||
return Ok(new ChapterDto()
|
|
||||||
{
|
|
||||||
Number = $"{int.Parse(volumeChapter.Number) / 100f}"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastChapter = looseLeafChapterVolume.Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default).Last();
|
|
||||||
return Ok(_mapper.Map<ChapterDto>(lastChapter));
|
|
||||||
}
|
|
||||||
|
|
||||||
// There is progress, we now need to figure out the highest volume or chapter and return that.
|
|
||||||
var prevChapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId);
|
|
||||||
var volumeWithProgress = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId);
|
|
||||||
// We only encode for single-file volumes
|
|
||||||
if (volumeWithProgress.Number != 0 && volumeWithProgress.Chapters.Count == 1)
|
|
||||||
{
|
|
||||||
// The progress is on a volume, encode it as a fake chapterDTO
|
|
||||||
return Ok(new ChapterDto()
|
|
||||||
{
|
|
||||||
Number = $"{volumeWithProgress.Number / 100f}"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progress is just on a chapter, return as is
|
|
||||||
return Ok(prevChapter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -97,34 +45,6 @@ public class TachiyomiController : BaseApiController
|
|||||||
public async Task<ActionResult<bool>> MarkChaptersUntilAsRead(int seriesId, float chapterNumber)
|
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);
|
||||||
user.Progresses ??= new List<AppUserProgress>();
|
return Ok(await _tachiyomiService.MarkChaptersUntilAsRead(user, seriesId, chapterNumber));
|
||||||
|
|
||||||
switch (chapterNumber)
|
|
||||||
{
|
|
||||||
// When Tachiyomi sync's progress, if there is no current progress in Tachiyomi, 0.0f is sent.
|
|
||||||
// Due to the encoding for volumes, this marks all chapters in volume 0 (loose chapters) as read.
|
|
||||||
// Hence we catch and return early, so we ignore the request.
|
|
||||||
case 0.0f:
|
|
||||||
return true;
|
|
||||||
case < 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);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
await _readerService.MarkChaptersUntilAsRead(user, seriesId, chapterNumber);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
|
|
||||||
if (!_unitOfWork.HasChanges()) return Ok(true);
|
|
||||||
if (await _unitOfWork.CommitAsync()) return Ok(true);
|
|
||||||
|
|
||||||
await _unitOfWork.RollbackAsync();
|
|
||||||
return Ok(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ public class ThemeController : BaseApiController
|
|||||||
_taskScheduler = taskScheduler;
|
_taskScheduler = taskScheduler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ResponseCache(CacheProfileName = "10Minute")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<IEnumerable<SiteThemeDto>>> GetThemes()
|
public async Task<ActionResult<IEnumerable<SiteThemeDto>>> GetThemes()
|
||||||
|
@ -12,14 +12,14 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NetVips;
|
using NetVips;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
public class UploadController : BaseApiController
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
|
||||||
public class UploadController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IImageService _imageService;
|
private readonly IImageService _imageService;
|
||||||
private readonly ILogger<UploadController> _logger;
|
private readonly ILogger<UploadController> _logger;
|
||||||
@ -49,8 +49,8 @@ namespace API.Controllers
|
|||||||
[HttpPost("upload-by-url")]
|
[HttpPost("upload-by-url")]
|
||||||
public async Task<ActionResult<string>> GetImageFromFile(UploadUrlDto dto)
|
public async Task<ActionResult<string>> GetImageFromFile(UploadUrlDto dto)
|
||||||
{
|
{
|
||||||
var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace("/", "_").Replace(":", "_");
|
var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace('/', '_').Replace(':', '_');
|
||||||
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", "");
|
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", string.Empty);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var path = await dto.Url
|
var path = await dto.Url
|
||||||
@ -305,5 +305,4 @@ namespace API.Controllers
|
|||||||
return BadRequest("Unable to resetting cover lock for Chapter");
|
return BadRequest("Unable to resetting cover lock for Chapter");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,11 @@ using AutoMapper;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
public class UsersController : BaseApiController
|
||||||
{
|
{
|
||||||
[Authorize]
|
|
||||||
public class UsersController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
private readonly IEventHub _eventHub;
|
private readonly IEventHub _eventHub;
|
||||||
@ -78,6 +78,8 @@ namespace API.Controllers
|
|||||||
AppUserIncludes.UserPreferences);
|
AppUserIncludes.UserPreferences);
|
||||||
var existingPreferences = user.UserPreferences;
|
var existingPreferences = user.UserPreferences;
|
||||||
|
|
||||||
|
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
||||||
|
|
||||||
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
|
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
|
||||||
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
|
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
|
||||||
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
|
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
|
||||||
@ -92,7 +94,6 @@ namespace API.Controllers
|
|||||||
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
|
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
|
||||||
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
|
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
|
||||||
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
||||||
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
|
||||||
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
|
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
|
||||||
existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode;
|
existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode;
|
||||||
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
|
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
|
||||||
@ -101,6 +102,7 @@ namespace API.Controllers
|
|||||||
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
|
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
|
||||||
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
||||||
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
|
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
|
||||||
|
existingPreferences.NoTransitions = preferencesDto.NoTransitions;
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(existingPreferences);
|
_unitOfWork.UserRepository.Update(existingPreferences);
|
||||||
|
|
||||||
@ -120,5 +122,4 @@ namespace API.Controllers
|
|||||||
await _unitOfWork.UserRepository.GetPreferencesAsync(User.GetUsername()));
|
await _unitOfWork.UserRepository.GetPreferencesAsync(User.GetUsername()));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
16
API/DTOs/Account/AgeRestrictionDto.cs
Normal file
16
API/DTOs/Account/AgeRestrictionDto.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using API.Entities.Enums;
|
||||||
|
|
||||||
|
namespace API.DTOs.Account;
|
||||||
|
|
||||||
|
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;
|
||||||
|
/// <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;
|
||||||
|
}
|
11
API/DTOs/Account/ConfirmEmailUpdateDto.cs
Normal file
11
API/DTOs/Account/ConfirmEmailUpdateDto.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace API.DTOs.Account;
|
||||||
|
|
||||||
|
public class ConfirmEmailUpdateDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Email { get; set; }
|
||||||
|
[Required]
|
||||||
|
public string Token { get; set; }
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
|
||||||
namespace API.DTOs.Account;
|
namespace API.DTOs.Account;
|
||||||
|
|
||||||
@ -16,4 +17,8 @@ public class InviteUserDto
|
|||||||
/// A list of libraries to grant access to
|
/// A list of libraries to grant access to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Libraries { get; init; }
|
public IList<int> Libraries { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// An Age Rating which will limit the account to seeing everything equal to or below said rating.
|
||||||
|
/// </summary>
|
||||||
|
public AgeRestrictionDto AgeRestriction { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
namespace API.DTOs.Account
|
namespace API.DTOs.Account;
|
||||||
|
|
||||||
|
public class LoginDto
|
||||||
{
|
{
|
||||||
public class LoginDto
|
|
||||||
{
|
|
||||||
public string Username { get; init; }
|
public string Username { get; init; }
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace API.DTOs.Account
|
namespace API.DTOs.Account;
|
||||||
|
|
||||||
|
public class ResetPasswordDto
|
||||||
{
|
{
|
||||||
public class ResetPasswordDto
|
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Username of the User
|
/// The Username of the User
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -19,5 +19,4 @@ namespace API.DTOs.Account
|
|||||||
/// The old, existing password. If an admin is performing the change, this is not required. Otherwise, it is.
|
/// The old, existing password. If an admin is performing the change, this is not required. Otherwise, it is.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string OldPassword { get; init; }
|
public string OldPassword { get; init; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
12
API/DTOs/Account/UpdateAgeRestrictionDto.cs
Normal file
12
API/DTOs/Account/UpdateAgeRestrictionDto.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
|
||||||
|
namespace API.DTOs.Account;
|
||||||
|
|
||||||
|
public class UpdateAgeRestrictionDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public AgeRating AgeRating { get; set; }
|
||||||
|
[Required]
|
||||||
|
public bool IncludeUnknowns { get; set; }
|
||||||
|
}
|
6
API/DTOs/Account/UpdateEmailDto.cs
Normal file
6
API/DTOs/Account/UpdateEmailDto.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace API.DTOs.Account;
|
||||||
|
|
||||||
|
public class UpdateEmailDto
|
||||||
|
{
|
||||||
|
public string Email { get; set; }
|
||||||
|
}
|
14
API/DTOs/Account/UpdateEmailResponse.cs
Normal file
14
API/DTOs/Account/UpdateEmailResponse.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace API.DTOs.Account;
|
||||||
|
|
||||||
|
public class UpdateEmailResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Did the user not have an existing email
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This informs the user to check the new email address</remarks>
|
||||||
|
public bool HadNoExistingEmail { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Was an email sent (ie is this server accessible)
|
||||||
|
/// </summary>
|
||||||
|
public bool EmailSent { get; set; }
|
||||||
|
}
|
@ -1,4 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
|
||||||
|
|
||||||
namespace API.DTOs.Account;
|
namespace API.DTOs.Account;
|
||||||
|
|
||||||
@ -6,18 +9,16 @@ public record UpdateUserDto
|
|||||||
{
|
{
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// This field will not result in any change to the User model. Changing email is not supported.
|
|
||||||
/// </summary>
|
|
||||||
public string Email { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// List of Roles to assign to user. If admin not present, Pleb will be applied.
|
/// List of Roles to assign to user. If admin not present, Pleb will be applied.
|
||||||
/// If admin present, all libraries will be granted access and will ignore those from DTO.
|
/// If admin present, all libraries will be granted access and will ignore those from DTO.
|
||||||
/// </summary>
|
|
||||||
public IList<string> Roles { get; init; }
|
public IList<string> Roles { get; init; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of libraries to grant access to
|
/// A list of libraries to grant access to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<int> Libraries { get; init; }
|
public IList<int> Libraries { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// An Age Rating which will limit the account to seeing everything equal to or below said rating.
|
||||||
|
/// </summary>
|
||||||
|
public AgeRestrictionDto AgeRestriction { get; init; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,14 @@ using API.DTOs.Reader;
|
|||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
|
|
||||||
namespace API.DTOs
|
namespace API.DTOs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying
|
||||||
|
/// file (abstracted from type).
|
||||||
|
/// </summary>
|
||||||
|
public class ChapterDto : IHasReadTimeEstimate
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying
|
|
||||||
/// file (abstracted from type).
|
|
||||||
/// </summary>
|
|
||||||
public class ChapterDto : IHasReadTimeEstimate
|
|
||||||
{
|
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2".
|
/// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2".
|
||||||
@ -89,5 +89,4 @@ namespace API.DTOs
|
|||||||
public int MaxHoursToRead { get; set; }
|
public int MaxHoursToRead { get; set; }
|
||||||
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
|
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
|
||||||
public int AvgHoursToRead { get; set; }
|
public int AvgHoursToRead { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace API.DTOs.CollectionTags
|
namespace API.DTOs.CollectionTags;
|
||||||
|
|
||||||
|
public class CollectionTagBulkAddDto
|
||||||
{
|
{
|
||||||
public class CollectionTagBulkAddDto
|
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Collection Tag Id
|
/// Collection Tag Id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -14,5 +14,4 @@ namespace API.DTOs.CollectionTags
|
|||||||
/// Series Ids to add onto Collection Tag
|
/// Series Ids to add onto Collection Tag
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<int> SeriesIds { get; init; }
|
public IEnumerable<int> SeriesIds { get; init; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
namespace API.DTOs.CollectionTags
|
namespace API.DTOs.CollectionTags;
|
||||||
|
|
||||||
|
public class CollectionTagDto
|
||||||
{
|
{
|
||||||
public class CollectionTagDto
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string Summary { get; set; }
|
public string Summary { get; set; }
|
||||||
@ -11,5 +11,4 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string CoverImage { get; set; }
|
public string CoverImage { get; set; }
|
||||||
public bool CoverImageLocked { get; set; }
|
public bool CoverImageLocked { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace API.DTOs.CollectionTags
|
namespace API.DTOs.CollectionTags;
|
||||||
|
|
||||||
|
public class UpdateSeriesForTagDto
|
||||||
{
|
{
|
||||||
public class UpdateSeriesForTagDto
|
|
||||||
{
|
|
||||||
public CollectionTagDto Tag { get; init; }
|
public CollectionTagDto Tag { get; init; }
|
||||||
public IEnumerable<int> SeriesIdsToRemove { get; init; }
|
public IEnumerable<int> SeriesIdsToRemove { get; init; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
|
||||||
namespace API.DTOs
|
namespace API.DTOs;
|
||||||
|
|
||||||
|
public class CreateLibraryDto
|
||||||
{
|
{
|
||||||
public class CreateLibraryDto
|
|
||||||
{
|
|
||||||
[Required]
|
[Required]
|
||||||
public string Name { get; init; }
|
public string Name { get; init; }
|
||||||
[Required]
|
[Required]
|
||||||
@ -13,5 +13,4 @@ namespace API.DTOs
|
|||||||
[Required]
|
[Required]
|
||||||
[MinLength(1)]
|
[MinLength(1)]
|
||||||
public IEnumerable<string> Folders { get; init; }
|
public IEnumerable<string> Folders { get; init; }
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,9 +1,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace API.DTOs
|
namespace API.DTOs;
|
||||||
|
|
||||||
|
public class DeleteSeriesDto
|
||||||
{
|
{
|
||||||
public class DeleteSeriesDto
|
|
||||||
{
|
|
||||||
public IList<int> SeriesIds { get; set; }
|
public IList<int> SeriesIds { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
20
API/DTOs/Device/CreateDeviceDto.cs
Normal file
20
API/DTOs/Device/CreateDeviceDto.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using API.Entities.Enums.Device;
|
||||||
|
|
||||||
|
namespace API.DTOs.Device;
|
||||||
|
|
||||||
|
public class CreateDeviceDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Name { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Platform of the device. If not know, defaults to "Custom"
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public DevicePlatform Platform { get; set; }
|
||||||
|
[Required]
|
||||||
|
public string EmailAddress { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user