mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
v0.5.3 - Book Reader Enhancements, Related Series, and More! (#1272)
* Angular Upgrade (#1059) * Upgraded to Angular 12 * Bump ng-bootstrap for upgrade * Angular 13 upgrade, ng-bootstrap bump * Angular 13 upgrade (broken) * Angular 13 upgrade. CSS is broken completely * Angular 13 upgrade is complete. * Bump versions by dotnet-bump-version. * Added beta disclaimer (#1065) * Bump versions by dotnet-bump-version. * Auto approve migration emails if the password is correct. Change Email Link dump to Critical to ensure it makes it into the logs. (#1069) * Bump versions by dotnet-bump-version. * Adding discord roles (#1070) * Adding discord roles # Added - Added: Added Discord roles to automated build discord notification. * update * Bump versions by dotnet-bump-version. * Custom Theme Support (#1077) * Started the migration to bootstrap 5. Introduced a breakpoint system that bootstrap reflects for our screens. * sr only migrated * mr/ml -> me/ms * pl/pr -> ps/pe * btn-block * removed input-group-append * Added form-label to all labels * Added some style overrides for inputs * Replaced form-group with mb-3 * Ignore journal files * Update media to d-flex/flex-grow-1 * Fixed reading list detail page * For develop builds, don't inline critical styles * Fixed some downstream security issues * Fixed a layout issue in series detail * Fixed issue with btn-light not having background color. Updated layout for series detail metadata * Cleaned up nav search * Laid out the organization for custom theme components. Update _inputs.scss with variable overrides and depending on theme, it will just work. * Lots of theming work * Added inputs to the theme page * Login and input placeholder changes - Fixed login screen centering issue on all devices - Changed the format of the login screen - Change the input placeholder color * Added checkbox styles * Refactored tagbadges and removed some ngdeep selectors * Added nav bar component and refactored some styles into event widget * Cleaned nav events again and made dedicated popover body * Finished pagination component * Fixed up some styles with buttons * refactored dropdown component * Update accordion component * Refactored breadcrumbs and rating star. Fixed a missing style for cards * Fixed some styling issues on person badge, added modal component, and some global styles * Finished moving everything within dark to component files * Fixed up filter buttons, move card styles into a component theme, fixed slider style * Refactored library card and grouped typeahead * Updated normal typeahead component and reduced amount of ngdeep selector * Refactored grid breakpoints to be available by css variable, but it's hardcoded into the app * Ensure breakpoints are defined per theme * Fixed up some styling overrides and customization for nav links and alt button * Removed some deep styles, moved css out of splash container and brough back labels for login page * Finished css variable refactor * Refactored all the theme variable definitions into files for each theme. * Added back bootstrap overrides * Added a note about bootstrap theme colors being not-possible to swap out at runtime * Cleaned up some dead code * Implemented the ability to set a custom theme on the site. Cleaned up misc code throughout. * Additional changes - Fixed nav where "kavita" was not hiding correctly on small viewports - Fixed search bar to make the behavior more consistent - Fixed accordion buttons - Changed accordion buttons to be more responsive - Added radio button colors - Fixed radios on theme test page - Changed login and reset password card layouts to be more consistent. - Added primary color shade for when darker shading is needed. * Built a basic site, allow the user to apply different themes, refactored nav service code out. * Implemented the ability update a user's theme * Added unit tests for Scan and Get Content in SiteThemeService. * Fixed a bug in the login code and Pref code which wasn't joining on SiteTheme table. Wrote Unit tests and the UI component to manage current theme. * Implemented scan so that it manages custom themes with unit tests * Component updates - Repositioning style ordering - Adding indicator override - Adding select styles * SignlaR integration, some fixes when creating custom entities, one single migration. Just login functionality left. * More ui updated - Added .no-hover to prevent hover on elements where not needed - Changed all selects I could find to appropriate class - Changed up nav tabs to work more like bootstrap tabs than pills - Added padding to top of some containers to make styles consistent - Added ability to change navbar fontawesome icon colors - removed some unecessary inline styling - Changed radio button to appropriate class - Toned down primate color, a bit too bright for dark theme. - Added ability to change button fontawesome icon color * nav-tab fix for series-detail * Added themes folder to gitignore * Adding card overlay * Fixing up light theme * Everything is done. Only bug is that color-scheme isn't being set properly from css variable. * Checkboxes have pointer by default. Confirm/Confirm email use default (dark) theme by default * Fixed an error where color-scheme wasn't reflecting correctly on themes on first load * Fixed user preferences not available on login * Changing dual radios to switches and color tweaks * disabled primary APCA fix * button APCA fixes * Fixed some timing issues with first load and image service * Fixed swiper issues from upgrade * Changed themes to be scss files again and adjusted Seed code * Migrated carousel to css variables. Fixed a broken animation for search. * Cleaned up some backend smells * Fixed white border outline on nav tabs, added some variables for header * Nav bar has been css variable-ified * Added some basic eink stuff to make the app useable Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Fix an issue for first time running theme code, theme will not be available (#1081) * Bump versions by dotnet-bump-version. * Theme Cleanup (#1089) * Fixed e-ink theme not properly applying correctly * Fixed some seed changes. Changed card checkboxes to use our themed ones * Fixed recently added carousel not going to recently-added page * Fixed an issue where no results found would show when searching for a library name * Cleaned up list a bit, typeahead dropdown still needs work * Added a TODO to streamline series-card component * Removed ng-lazyload-image module since we don't use it. We use lazysizes * Darken card on hover * Fixing accordion focus style * ux pass updates - Fixed typeahead width - Fixed changelog download buttons - Fixed a select - Fixed various input box-shadows - Fixed all anchors to only have underline on hover - Added navtab hover and active effects * more ux pass - Fixed spacing on theme cards - Fixed some light theme issues - Exposed text-muted-color for theme card subtitle color * UX pass fixes - Changed back to bright green for primary on dark theme - Changed fa icon to black on e-ink * Merged changelog component * Fixed anchor buttons text decoration * Changed nav tabs to have a background color instead of open active state * When user is not authenticated, make sure we set default theme (dark) * Cleanup on carousel * Updated Users tab to use small buttons with icons to align with Library tab * Cleaned up brand to not underline, removed default link underline on hover in dropdown and pill tabs * Fixed collection detail posters not rendering Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Event Widget Update (#1098) * Took care of some notes in the code * Fixed an issue where Extra might get flagged as special too early, if in a word like Extraordinary * Moved Tag cleanup code into Scanner service. Added a SplitQuery to another heavy API. Refactored Scan loop to remove parallelism and use async instead. * Lots of rework on the codebase to support detailed messages and easier management of message sending. Need to take a break on this work. * Progress is being made, but slowly. Code is broken in this commit. * Progress is being made, but slowly. Code is broken in this commit. * Fixed merge issue * Fixed unit tests * CoverUpdate is now hooked into new ProgressEvent structure * Refactored code to remove custom observables and have everything use standard messages$ * Refactored a ton of instances to NotificationProgressEvent style and tons of the UI to respect that too. UI is still a bit buggy, but wholistically the work is done. * Working much better. Sometimes events come in too fast. Currently cover update progress doesn't display on UI * Fixed unit tests * Removed SignalREvent to minimize internal event types. Updated the UI to use progress bars. Finished SiteThemeService. * Merged metadata refresh progress events and changed library scan events to merge cleaner in the UI * Changed RefreshMetadataProgress to CoverUpdateProgress to reflect the event better. * Theme Cleanup (#1089) * Fixed e-ink theme not properly applying correctly * Fixed some seed changes. Changed card checkboxes to use our themed ones * Fixed recently added carousel not going to recently-added page * Fixed an issue where no results found would show when searching for a library name * Cleaned up list a bit, typeahead dropdown still needs work * Added a TODO to streamline series-card component * Removed ng-lazyload-image module since we don't use it. We use lazysizes * Darken card on hover * Fixing accordion focus style * ux pass updates - Fixed typeahead width - Fixed changelog download buttons - Fixed a select - Fixed various input box-shadows - Fixed all anchors to only have underline on hover - Added navtab hover and active effects * more ux pass - Fixed spacing on theme cards - Fixed some light theme issues - Exposed text-muted-color for theme card subtitle color * UX pass fixes - Changed back to bright green for primary on dark theme - Changed fa icon to black on e-ink * Merged changelog component * Fixed anchor buttons text decoration * Changed nav tabs to have a background color instead of open active state * When user is not authenticated, make sure we set default theme (dark) * Cleanup on carousel * Updated Users tab to use small buttons with icons to align with Library tab * Cleaned up brand to not underline, removed default link underline on hover in dropdown and pill tabs * Fixed collection detail posters not rendering Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Tweaked some of the emitting code * Some css, but pretty bad. Robbie please save me * Removed a todo * styling update * Only send filename on FileScanProgress * Some console.log spam cleanup * Various updates * Show events widget activity based on activeEvents * progress bar color updates * Code cleanup Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Scanner event hub fix (#1099) * Scanner event hub fix - Fixed an issue where the scanner would error when adding a new series because the series didn't have a library name yet. (develop) * Removing library.type * Bump versions by dotnet-bump-version. * Workflow update to add nightly versions (#1100) # Changed - Changed: Changed automated workflow to release individual nightly versions on dockerhub * Bump versions by dotnet-bump-version. * Updating GA to parse version (#1101) * Bump versions by dotnet-bump-version. * GA Fixes (#1103) **Strictly Repo Changes** # Fixed - Fixed: Fixed an issue where patch version was not being added to docker tag. * Bump versions by dotnet-bump-version. * Fixed specials being misaligned (#1106) # Fixed - Fixed: Fixed issue with specials not being properly aligned (develop) * Bump versions by dotnet-bump-version. * Bugfix/ux pass 2 (#1107) * Adding margin bottom to series detail tabs * Styling tag badges with green on dark - Added 3 new css vars * Removing underline from readmore * Fixing see more to be on one line * adding gutter to see more * Changing queue toasts to info * adding api key tooltip * Updating active accordion on user preference. * Fixing search bar and close btn position * Fixed a bug where entering book reader in dark mode then closing out, would leave you in a broken white state. * Fixed broken wiki links Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com> * Bump versions by dotnet-bump-version. * Bump versions by dotnet-bump-version. * Bump versions by dotnet-bump-version. * Series Detail Refactor (#1118) * Fixed a bug where reading list and collection's summary wouldn't render newlines * Moved all the logic in the UI for Series Detail into the backend (messy code). We are averaging 400ms max with much optimizations available. Next step is to refactor out of controller and provide unit tests. * Unit tests for CleanSpecialTitle * Laid out foundation for testing major code in SeriesController. * Refactored code so that read doesn't need to be disabled on page load. SeriesId doesn't need the series to actually load. * Removed old property from Volume * Changed tagbadge font size to rem. * Refactored some methods from SeriesController.cs into SeriesService.cs * UpdateRating unit tested * Wrote unit tests for SeriesDetail * Worked up some code where books are rendered only as volumes. However, looks like I will need to use Chapters to better support series_index as floats. * Refactored Series Detail to change Volume Name on Book libraries to have book name and series_index. * Some cleanup on the code * DeleteMultipleSeries test is hard. Going to skip. * Removed some debug code and make all tabs Books for Book library Type * Bump versions by dotnet-bump-version. * Tachiyomi Bugfix (#1119) * Updated the dependencies for .NET 6.0.2 * Fixed a bad prev chapter logic where we would bleed into chapters from last volume instead of specials. * Fixed the get prev chapter code to properly walk the order according to documentation and updated some bad test cases * Bump versions by dotnet-bump-version. * Series index fix (#1120) * Series index fix # Fixed - Fixed: Fixed an issue where epub series with index = 0 would be hidden on series detail page. * Removing unnecessary conditional * Bump versions by dotnet-bump-version. * Misc Bugfixes (#1123) * Fixed a bug where ComicInfo Count can be a float and we threw a parse error. * Fixed a bug in download bookmarks which didn't properly create the filepaths for copying. Refactored into a service with a unit test. In Scanner, repull genres, people and tags between chunk saves to ensure no unique constraint issues. * Fixed a bug where card detail layout wouldn't refresh the library name on the card between pages * Fixed an issue where a check to scrolling page back to top was missing in manga reader * Fixed a bug where cleaning up collection tags without Series was missing after editing a Series. * Cleaned up the styles for cover chooser * Added Regex support for "Series 001 (Digital) (somethingwith1234)" and removed support for "A Compendium of Ghosts - 031 - The Third Story_ Part 12" due to complexity in parsing. * Fixed a miscommunication on how Tachiyomi needs the API MarkChaptersUntilAsRead implemented. Now 0 chapter volumes will be marked. * Removed unneeded DI * Bump versions by dotnet-bump-version. * CopyFilesToDirectory will now allow for one duplicate copy over and put (2) (#1126) * Bump versions by dotnet-bump-version. * Stablize the Styles (#1128) * Fixed a bug where adding multiple series to reading list would throw an error on UI, but it was successful. * When a series has a reading list, we now show the connection on Series detail. * Removed all baseurl code from UI and not-connected component since we no longer use it. * Fixed tag badges not showing a border. Added last read time to the series detail page * Fixed up error interceptor to remove no-connection code * Changed implementation for series detail. Book libraries will never send chapters back. Volume 0 volumes will not be sent in volumes ever. Fixed up more renaming logic on books to send more accurate representations to the UI. * Cleaned up the selected tab and tab display logic * Fixed a bad where statement in reading lists for series * Fixed up tab logic again * Fixed a small margin on search backdrop * Made badge expander button smaller to align with badges * Fixed a few UIs due to .form-group and .form-row being removed * Updated Theme component page to help with style testing * Added more components to theme tester * Cleaned up some styling * Fixed opacity on search item hover * Bump versions by dotnet-bump-version. * Hacked in code so that we render an image instead of canvas for fit to screen to try out. (#1131) * Bump versions by dotnet-bump-version. * Metadata Editing from the UI! (#1135) * Added the skeleton code for layout, hooked up Age Rating, Publication Status, and Tags * Tweaked message of Scan service to Finished scan of to better indicate the total scan time * Hooked in foundation for person typeaheads * Fixed people not populating typeaheads on load * For manga/comics, when parsing, set the SeriesSort from ComicInfo if it exists. * Implemented the ability to override and create new genre tags. Code is ready to flush out the rest. * Ability to update metadata from the UI is hooked up. Next is locking. * Updated typeahead to allow for non-multiple usage. Implemented ability to update Language tag in Series Metadata. * Fixed a bug in GetContinuePoint for a case where we have Volumes, Loose Leaf chapters and no read progress. * Added ETag headers on Images to allow for better caching (bookmarks and images in manga reader) * Built out UI code to show locked indication to user * Implemented Series locking and refactored a lot of styles in typeahead to make the lock setting work, plus misc cleanup. * Added locked properties to dtos. Updated typeahead loading indicator to not interfere with close button if present * Hooked up locking flags in UI * Integrated regular field locking/unlocking * Removed some old code * Prevent input group from wrapping * Implemented some basic layout for metadata on volume/chapter card modal. Refactored out all metadata from Chapter object in terms of UI and put into a separate call to ensure speedy delivery and simplicity of code. * Refactored code to hide covers section if not an admin * Implemented ability to modify a chapter/volume cover from the detail modal * Removed a few variables and change cover image modal * Added bookmark to single chapter view * Put a temp fix in for a ngb v12 z-index bug (reported). Bumped ngb to 12.0 stable and fixed some small rendering bugs * loading buttons ftw * Lots of cleanup, looks like the story is finished * Changed action name from Info to Details * Style tweaks * Fixed an issue where Summary would assume it's locked due to a subscription firing on setting the model * Fixed some misc bugs * Code smells Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Manga Reader Refresh (#1137) * Refactored manga reader to use a regular image element for all cases except for split page rendering * Fixed a weird issue where ordering of routes broke redireciton in one case. * Added comments to a lot of the enums and refactored READER_MODE to be ReaderMode and much more clearer on function. * Added bookmark effect on image renderer * Implemented keyboard shortcut modal * Introduced the new layout mode into the manga reader, updated preferences, and updated bookmark to work for said functionality. Need to implement renderer now * Hooked in ability to show double pages but all the css is broken. Committing for help from Robbie. * Fixed an issue where Language tag in metadata edit wasn't being updated * Fixed up some styling on mobile for edit series detail * Some css fixes * Hooked in ability to set background color on reader (not implemented in reader). Optimized some code in ArchiveService to avoid extra memory allocations. * Hooked in background color, generated the migration * Fixed a bug when paging to cover images, full height would be used instead of full-width for cover images * New option in reader to show screen hints (on by default). You can disable in user preferences which will stop showing pagination overlay hints * Lots of fixes for double rendering mode * Bumped the amount of cached pages to 8 * Fixed an issue where dropdowns weren't being locked on form manipulation * Bump versions by dotnet-bump-version. * On Deck tweaks + Bugfixes (#1141) * Tweaked the On deck to only look for series that have progress in past 30 days. This number is just to test it out, it will be configurable later. Tweaked the layout of the dashboard to remove a redundant section. * Fixed a bug where archives with __MACOSX/ inside would break the reader during flattening. * Fixed a bug where confirm service rejection should have resolved as false. * Fixed an issue with checking if server is accessible with loopback and local ips * Bump versions by dotnet-bump-version. * Manga Reader Shakeout (#1142) * Fixed a unit test in ArchiveService * Image scaling fixes * removing test * Added new layout mode (enum only) and cleaned up manga reader and wrote extra documentation * Aligned code with cleanup * Adding reverse classes for manga reading * Disable options for layout modes that doesn't make sense. * Cleaned up manga reader menu items to link to preferences options directly * Work in progress, but rendering the correct page numbers for double. Need to rework caching logic so we can use existing image objects * Pagination logic is now properly increasing page number an extra when double layout mode * I can't figure out cachedImages to work properly with double pages, but doing it in a way where it handles downloading the image (and etag cache) + rendering the url, seems to work really well * Double original fix, also flex squish fix * Implemented last page on double which will load next chapter. Fixed a bug where if GetImage from ReaderController threw an error, the chapter directory would be emptied, but the folder itself wasn't deleted. * Fixed a bad if for double manga * double class fix * Cleanup up some console.logs * Adjusted the caching for images in a reading session so they cache for 2 mins * fixing webtoon image issue * Tweaked the caching of images to 10 mins for reading. Fixed a bug where after webtoon, single image layout would be selected. Tweaked logic for handling prev/next pages on chapter boundaries. * Fixed an issue where 2nd page would be skipped * Fixed an issue where 2nd page would be skipped * Fixed a skip page issue * Misc css fixes Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Misc Bugfixes and Cleanup (#1144) * Moved libraryType into chapter info * Fixed a bug where you could not reset cover on a series * Patched in relevant changes from another polish branch * Refactored invite user setup to shift the checking for accessibility to the backend and always show the link. This will help with users who have some unique setups in docker. * Refactored invite user to always print the url to setup a new account. * Single page renderer uses canvasImage rather than re-requesting and relying on cache * Fixed a rendering issue where fit to split on single on a cover wouldn't force width scaling just for that image * Fixed a rendering bug with split image functionality * Added title to copy button * Fixed a bug in GetContinuePoint when a chapter is added to an already read volume and a new chapter is added loose leaf. The loose leaf would be prioritized over the volume chapter. Refactored 2 methods from controller into service and unit tested. * Fixed a bug on opening a volume in series detail that had a chapter added to it after the volume (0 chapter) was read would cause a loose leaf chapter to be opened. * Added mark as read/actionables on Files in volume detail modal. Fixed a bug where we were showing the wrong page count in a volume detail modal. * Removed OnDeck page and replaced it with a pre-filtered All-Series. Hooked up the ability to pass read state to the filter via query params. Fixed some spacing on filter post bootstrap update. * Fixed up some poor documentation on FilterDto. * Bump versions by dotnet-bump-version. * Bugfixes and Cover Chooser Upgrades (#1146) * Fixed a bug where GetNextChapter would return a loose leaf chapter from a special when it should return nothing. * Fixed a bug in events widget when an update comes in after a user refreshes, the active event counter could get out of sync, thus showing "Nothing going on here" Refactored the events widget to be named appropriately. * Refactored code to have errors during threaded tasks propagate to the UI via events widget (css still needed). Removed ScanLibraryError in favor of generic Error event. * Fixed up some code and added ability to remove the event from events widget * Fixed a bug where modifiying certain fields, like summary, wouldn't lock the field * Fixed a few bugs where lock state was not being set in the DB correctly nor were certain combinations of locking fields and editing fields. * Removed debug code * Updated the discord alert to tag new group * Refactored cover upload to actually handle uploading a temp file via url on the backend so that users can user change cover by url. Fixed up some bugs that occured when chaning the image container in a previous PR. * Code cleanup * Cleaned up the css on the error items * Code cleanup * Bump versions by dotnet-bump-version. * Side nav (#1155) * adding back side-nav * Event Widget Update (#1098) * Took care of some notes in the code * Fixed an issue where Extra might get flagged as special too early, if in a word like Extraordinary * Moved Tag cleanup code into Scanner service. Added a SplitQuery to another heavy API. Refactored Scan loop to remove parallelism and use async instead. * Lots of rework on the codebase to support detailed messages and easier management of message sending. Need to take a break on this work. * Progress is being made, but slowly. Code is broken in this commit. * Progress is being made, but slowly. Code is broken in this commit. * Fixed merge issue * Fixed unit tests * CoverUpdate is now hooked into new ProgressEvent structure * Refactored code to remove custom observables and have everything use standard messages$ * Refactored a ton of instances to NotificationProgressEvent style and tons of the UI to respect that too. UI is still a bit buggy, but wholistically the work is done. * Working much better. Sometimes events come in too fast. Currently cover update progress doesn't display on UI * Fixed unit tests * Removed SignalREvent to minimize internal event types. Updated the UI to use progress bars. Finished SiteThemeService. * Merged metadata refresh progress events and changed library scan events to merge cleaner in the UI * Changed RefreshMetadataProgress to CoverUpdateProgress to reflect the event better. * Theme Cleanup (#1089) * Fixed e-ink theme not properly applying correctly * Fixed some seed changes. Changed card checkboxes to use our themed ones * Fixed recently added carousel not going to recently-added page * Fixed an issue where no results found would show when searching for a library name * Cleaned up list a bit, typeahead dropdown still needs work * Added a TODO to streamline series-card component * Removed ng-lazyload-image module since we don't use it. We use lazysizes * Darken card on hover * Fixing accordion focus style * ux pass updates - Fixed typeahead width - Fixed changelog download buttons - Fixed a select - Fixed various input box-shadows - Fixed all anchors to only have underline on hover - Added navtab hover and active effects * more ux pass - Fixed spacing on theme cards - Fixed some light theme issues - Exposed text-muted-color for theme card subtitle color * UX pass fixes - Changed back to bright green for primary on dark theme - Changed fa icon to black on e-ink * Merged changelog component * Fixed anchor buttons text decoration * Changed nav tabs to have a background color instead of open active state * When user is not authenticated, make sure we set default theme (dark) * Cleanup on carousel * Updated Users tab to use small buttons with icons to align with Library tab * Cleaned up brand to not underline, removed default link underline on hover in dropdown and pill tabs * Fixed collection detail posters not rendering Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Tweaked some of the emitting code * Some css, but pretty bad. Robbie please save me * Removed a todo * styling update * Only send filename on FileScanProgress * Some console.log spam cleanup * Various updates * Show events widget activity based on activeEvents * progress bar color updates * Code cleanup Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Scanner event hub fix (#1099) * Scanner event hub fix - Fixed an issue where the scanner would error when adding a new series because the series didn't have a library name yet. (develop) * Removing library.type * Bump versions by dotnet-bump-version. * Workflow update to add nightly versions (#1100) # Changed - Changed: Changed automated workflow to release individual nightly versions on dockerhub * Bump versions by dotnet-bump-version. * Updating GA to parse version (#1101) * Bump versions by dotnet-bump-version. * GA Fixes (#1103) **Strictly Repo Changes** # Fixed - Fixed: Fixed an issue where patch version was not being added to docker tag. * Bump versions by dotnet-bump-version. * Fixed specials being misaligned (#1106) # Fixed - Fixed: Fixed issue with specials not being properly aligned (develop) * Bump versions by dotnet-bump-version. * Bugfix/ux pass 2 (#1107) * Adding margin bottom to series detail tabs * Styling tag badges with green on dark - Added 3 new css vars * Removing underline from readmore * Fixing see more to be on one line * adding gutter to see more * Changing queue toasts to info * adding api key tooltip * Updating active accordion on user preference. * Fixing search bar and close btn position * Fixed a bug where entering book reader in dark mode then closing out, would leave you in a broken white state. * Fixed broken wiki links Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com> * Bump versions by dotnet-bump-version. * Series Detail Refactor (#1118) * Fixed a bug where reading list and collection's summary wouldn't render newlines * Moved all the logic in the UI for Series Detail into the backend (messy code). We are averaging 400ms max with much optimizations available. Next step is to refactor out of controller and provide unit tests. * Unit tests for CleanSpecialTitle * Laid out foundation for testing major code in SeriesController. * Refactored code so that read doesn't need to be disabled on page load. SeriesId doesn't need the series to actually load. * Removed old property from Volume * Changed tagbadge font size to rem. * Refactored some methods from SeriesController.cs into SeriesService.cs * UpdateRating unit tested * Wrote unit tests for SeriesDetail * Worked up some code where books are rendered only as volumes. However, looks like I will need to use Chapters to better support series_index as floats. * Refactored Series Detail to change Volume Name on Book libraries to have book name and series_index. * Some cleanup on the code * DeleteMultipleSeries test is hard. Going to skip. * Removed some debug code and make all tabs Books for Book library Type * Bump versions by dotnet-bump-version. * Tachiyomi Bugfix (#1119) * Updated the dependencies for .NET 6.0.2 * Fixed a bad prev chapter logic where we would bleed into chapters from last volume instead of specials. * Fixed the get prev chapter code to properly walk the order according to documentation and updated some bad test cases * Updated side nav to float a bit and added user settings to it. * Refactored the code to hide/show sidenav to be more angular and decoupled * Moved Changelog out of admin dashboard and into a dedicated page in user menu. Added a wiki link from user menu * Introduced a side nav item for rendering each item and refactored code to use it. * Added a filter of side nav when there are more than 10 libraries. Added some themeing overrides for side nav. * Cleaned up the template code for side nav item so if there is no link, we don't generate that html directive * Refactored side nav into a module and migrated a few pipes into a pipe module for easy re-use * Added companion bar on reading list and collection. Updated modules to load pages and make side nav items clickable as anchors, so new tab works. * Moved metadata filter into separate component/module and the button in the companion bar. Needs cleanup. * Finished cleanup and refactoring of metadata filter into separate component. Removed filtering from Collections as it doesn't work and wasn't hooked up. * Tweaked the css on carousel component * Added to library detail and series-detail * Fixes and css vars * Stop destroying sidenav, animaton timing * Integrated side nav on the rest of the pages * Navbar now collapses to icons * mobile sidenav start * more mobile fixes * mobile tweaks * light and e-ink theme updates * white and eink dropdown color fixes * plex inspired side-nav * theme fixes * Making spacing more uniform across app * More fixes * fixing spacing on cards * actionable fix for sidenav * no scroll on mobile when sidenav is open * hide sidenav on pages * Adding card spacing * Adding ability to remove sidenav when in a reader * tidying up sidenav toggles * side-nav mobile updates * fixing up other themes * overlay fixes * Cleaned up the code to make the observables have better names. Removed a bunch of pointless subscriptions. Cleaned up methods that werent needed. Added jsdocs to help ensure the understandability of the 2 states for the side nav. * Integrated a highlight effect on side nav. Fixed a ton of places where the nav was being hidden when it shouldn't. * Fixed where active state wasn't working on all urls * misc fixes - smaller hamburger - z-index fixes - active fixes * Revert "Merge branch 'develop' into feature/side-nav-upgrade" This reverts commit 76b0d15a984692874e0cb57e821686ea703144cf, reversing changes made to b3ed55395473aa35577500596a211ad22a42631b. * Fixing edit-series modal spacing * Give the ability to jump to a library from admin manage libraries page * Fixed a bug with highlighting active item on side nav * Moved localized series title to companion bar via subtitle * Removed old title * Fixed a bug where clicking a link would reload the whole app, styling fixes on filter, fixed issue with initial load not setting active state, adjusted styles on active style. * code cleanup Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Fixed a bug where companion bar would be pushing content to the right even when not visible. Updated nav service localstorage key. (#1159) * Bump versions by dotnet-bump-version. * Side Nav Fixes (#1161) * Fixed an issue where there was extra padding on top/bottom of readers when side nav was hidden. * Fixed a bug where fit to screen wasn't forcing width scaling * Added back total pages to many pages * Fixed the padding on series detail cards * Tweaked carousels to match series detail padding * Fixed an issue where large amount of libraries could have 2 highlighted at once due to how highlight logic works on routes. * Cleaned up some extra space in card detail layout due to moving title into compainion bar * Moved some gloabls to global and moved color-scheme to body tag * Moved scrollbar onto the body itself which helps with page jank on loading and fixes scrollbar not working with theme * Bump versions by dotnet-bump-version. * Fixed loose chapters marked as read for Tachiyomi (#1158) * Tachiyomi-related fixes * Created unit test for MarkAsReadAnythingUntil * Applied the requested changes. * More Bugfixes from Side Nav (#1162) * Fixed a bug where the bottom of the page could be cut off * Adjusted all the headings to h2, which looks better * Refactored GetSeriesDetail to actually map the names inside the code so the UI just displays. * Put in some basic improvements to OPDS by using Series Detail type layout, but this only reduces one click. * Fixed a bug where offset from scrollbar fix causes readers to be cutoff. * Bump versions by dotnet-bump-version. * Bump versions by dotnet-bump-version. * OPDS Rework (#1164) * Fixed a bug where the bottom of the page could be cut off * Adjusted all the headings to h2, which looks better * Refactored GetSeriesDetail to actually map the names inside the code so the UI just displays. * Put in some basic improvements to OPDS by using Series Detail type layout, but this only reduces one click. * Fixed a bug where offset from scrollbar fix causes readers to be cutoff. * Ensure the hamburger menu icon is aligned with side nav * Disable the image splitting dropdown in webtoon mode * Fixed broken progress/scroll code as we scroll on the body instead of window now * Fixed phone-hidden class not working due to a bad media query * Lots of changes to OPDS to provide a richer text experience. Uses Issues or Books based on library type. Cleans up the experience by providing Storyline from the get-go. * Updated OPDS-SE search description to include collections and reading lists. * Fixed up some title stuff * If a volume only has one file underneath it, flatten it and send a chapter as if it were the volume. * Code cleanup * Bump versions by dotnet-bump-version. * Feature/enhancements and more (#1166) * Moved libraryType into chapter info * Fixed a bug where you could not reset cover on a series * Patched in relevant changes from another polish branch * Refactored invite user setup to shift the checking for accessibility to the backend and always show the link. This will help with users who have some unique setups in docker. * Refactored invite user to always print the url to setup a new account. * Single page renderer uses canvasImage rather than re-requesting and relying on cache * Fixed a rendering issue where fit to split on single on a cover wouldn't force width scaling just for that image * Fixed a rendering bug with split image functionality * Added title to copy button * Fixed a bug in GetContinuePoint when a chapter is added to an already read volume and a new chapter is added loose leaf. The loose leaf would be prioritized over the volume chapter. Refactored 2 methods from controller into service and unit tested. * Fixed a bug on opening a volume in series detail that had a chapter added to it after the volume (0 chapter) was read would cause a loose leaf chapter to be opened. * Added mark as read/actionables on Files in volume detail modal. Fixed a bug where we were showing the wrong page count in a volume detail modal. * Removed OnDeck page and replaced it with a pre-filtered All-Series. Hooked up the ability to pass read state to the filter via query params. Fixed some spacing on filter post bootstrap update. * Fixed up some poor documentation on FilterDto. * Some string equals enhancements to reduce extra allocations * Fixed an issue when trying to download via a url, to remove query parameters to get the format * Made an optimization to Normalize method to reduce memory pressure by 100MB over the course of a scan (16k files) * Adjusted the styles on dashboard for first time setup and used a routerlink rather than href to avoid a fresh load. * Use framgment on router link * Hooked in the ability to search by release year (along with series optionally) and series will be returned back. * Fixed a bug in the filter format code where it was sending the wrong type * Only show clear all on typeahead when there are at least one selected item * Cleaned up the styles of the styles of the typeahead * Removed some dead code * Implemented the ability to filter against a series name. * Fixed filter top offset * Ensure that when we add or remove libraries, the side nav of users gets updated. * Tweaked the width on the mobile side nav * Close side nav on clicking overlay on mobile viewport * Don't show a pointer if the carousel section title is not actually selectable * Removed the User profile on the side nav so home is always first. Tweaked styles to match * Fixed up some poor documentation on FilterDto. * Fixed a bug where Latest read date wasn't being set due to an early short circuit. * When sending the chapter file, format the title of the FeedEntry more like Series Detail. * Removed dead code * Bump versions by dotnet-bump-version. * Bugfixes (#1177) * Fixed an underline on hover of pagination link * Ensure title of companion bar eats full width if there is no filter * If a user doesn't have the Download role, they will not be able to download over OPDS. * Fixed a bug where after going into webtoon reader mode then leaving, the bookmark effect would continue using the webtoon mode styling * Fixed a bug where continuous reader wasn't being triggered due to moving scrollbar to body and a floating point percision error on scroll top * Fixed how continuous trigger is shown so that we properly adjust scroll on the top (for prev chapter) * Fixed a bad merge that broke saving any edits to series metadata * When a epub key is not correct, even after we correct it, ignore the inlining of the style so the book is at least still readable. * Disabled double rendering (this feature is being postponed to a later release) * Disabled user setting and forced it to Single on any save * Removed cache directory from UpdateSettings validation as we don't allow changing it. * Fix security issue with url parse * After all migrations run, update the installed version in the Database. Send that installed version on the stat service. * Dependency bot to update some security stuff * Some misc code cleanup and fixes on the typeahead (still broken) * Bump versions by dotnet-bump-version. * New Feature Stats (#1179) * When searching, search against normalized names. * Added new stat fields * Bump versions by dotnet-bump-version. * Getting Ready for Release (#1180) * One more unit test for Tachiyomi * Removed some debug code in the manga reader menu * Fixed a typeahead bug where using Enter on add new item or selected options could cause items to disappear from selected state or other visual glitches * Actually fix the selection issue. We needed to filter out selected before we access element * Cleaned up collection detail page to align to new side nav design * Cleaned up some styling on the reading list page * Fixed a bug where side nav would not be visible on the main app due to some weird redirect logic * Fixed a bug where when paging to the last page, a page will be skipped and user will have to refresh manually to view * Fixed some styling bugs on drawer for light themes. Added missing pagination colors on light themes * On mobile screens, add some padding on series-detail page * Fixed a bad test case helper * Bump versions by dotnet-bump-version. * Release Shakeout Part 1 (#1184) * Have actionables on series detail action bar and in title to make it easier to use. * Fixed a bug where super long titles could render over the book content * Fixed a bug in get continue point where it wasn't working in an edge case * Bump versions by dotnet-bump-version. * Release Shakeout (#1186) * Cleaned up some styles on the progress bar in book reader * Fixed up some phone-hidden classes and added titles around the codebase. Stat reporting on first run now takes into account that admin user wont exist. * Fixed manage library page not updating last scan time when a notification event comes in. * Integrated SeriesSort ComicInfo tag (somehow it got missed) * Some minor style changes and no results found for bookmarks on chapter detail modal * Fixed the labels in action bar on book reader so Prev/Next are in same place * Cleaned up some responsive styles around images and reduced custom classes in light of new display classes on collection detail and series detail pages * Fixed an issue with webkit browsers and book reader where the scroll to would fail as the document wasn't fully rendered. A 10ms delay seems to fix the issue. * Cleaned up some code and filtering for collections. Collection detail is missing filtering functionality somehow, disabled the button and will add in future release * Correctly validate and show a message when a user is not an admin or has change password role when going through forget password flow. * Fixed a bug on manage libraries where library last scan didn't work on first scan of a library, due to there being no updated series. * Fixed a rendering issue with text being focused on confirm email page textboxes. Fixed a bug where when deleting a theme that was default, Kavita didn't reset Dark as the default theme. * Cleaned up the naming and styles for side nav active item hover * Fixed event widget to have correct styling on eink and light * Tried to fix a rendering issue on side nav for light themes, but can't figure it out * On light more, ensure switches are green * Fixed a bug where opening a page with a preselected filter, the filter toggle button would require 2 clicks to collapse * Reverted the revert of On Deck. * Improved the upload by url experience by sending a custom fail error to UI when a url returns 401. * When deleting a library, emit a series removed event for each series removed so user's dashboards/screens update. * Fixed an api throwing an error due to text being sent back instead of json. * Fixed a refresh bug with refreshing pending invites after deleting an invite. Ensure we always refresh pending invites even if user cancel's from invite, as they might invite, then hit cancel, where invite is still active. * Fixed a bug where invited users with + in the email would fail due to validation, but UI wouldn't properly inform user. * Bump versions by dotnet-bump-version. * Version bump for release (#1187) * Bump versions by dotnet-bump-version. * Tech Debt + Series Sort bugfix (#1192) * Code cleanup. When copying files, if the target file already exists, append (1), (2), etc onto the file (this is enhancing existing implementation to allow multiple numbers) * Added a ton of null checks to UpdateSeriesMetadata and made the code work on the rare case (not really possible) that SeriesMetadata doesn't exist. * Updated Genre code to use strings to ensure a better, more fault tolerant update experience. * More cleanup on the codebase * Fixed a bug where Series SortName was getting emptied on file scan * Fixed a bad copy * Fixed unit tests * Bump versions by dotnet-bump-version. * Post Release Shakeout (Hotfix Testing) (#1200) * Fixed an issue where when falling back to folder parsing, sometimes the folder name wouldn't parse well, like "Foo 50" which parses as "Foo". Now the fallback will check if we have a solid series parsed from filename before we attempt to parse a folder. * Ensure SortName is set during a scan loop even if locked and it's empty string. * Added some null checks for metadata update * Fixed a bug where Updating a series name with a name of an existing series wouldn't properly check for existing series. * Tweaked the logic of OnDeck to consider LastChapterCreated from all chapters in a series, not just those with progress. * Fixed a bug where the hamburger menu was still visible on login/registration page despite not functioning * Tweaked the logic of OnDeck to consider LastChapterCreated from all chapters in a series, not just those with progress. * Removed 2 unused packages from ui * Fixed some bugs around determining what the current installed version is in Announcements * Use AnyAsync for a query to improve performance * Fixed up some fallback code * Tests are finally fixed * Bump versions by dotnet-bump-version. * PDF Rendering on Pi (64bit) & Backup Fix (#1204) * Updated dependencies. SharpCompress has been updated to v2.1.0 which should fix pdf rendering on pi/arm64 devices. * Removed some dependencies not needed and updated the Backup code to account for themes and ensure everything gets copied every time. * Bump versions by dotnet-bump-version. * Hotfix Prep (#1211) * Patched cover image change that somehow got missed * Fixed a bug where clicking bottom action bar buttons on book reader wouldn't work correctly (would close drawer when trying to open) * Bump versions by dotnet-bump-version. * Fixed some missing merge stuff * On Deck + Misc Fixes and Changes (#1215) * Added playwright and started writing e2e tests. * To make things easy, disabled other browsers while I get confortable. Added a login flow (assumes my dev env) * More tests on login page * Lots more testing code, trying to figure out auth code. * Ensure we don't track DBs inside config * Added a new date property for when chapters are added to a series which helps with OnDeck calculations. Changed a lot of heavy api calls to use IEnumerable to stream repsonse to UI. * Fixed OnDeck with a new field for when last chapter was added on Series. This is a streamlined way to query. Updated Reading List with NormalizedTitle, CoverImage, CoverImageLocked. * Implemented the ability to read a random item in the reading list and for the reading list to be intact for order. * Tweaked the style for webtoon to not span the whole width, but use max width * When we update a cover image just send an event so we don't need to have logic for when updates occur * Fixed a bad name for entity type on cover updates * Aligned the edit collection tag modal to align with new tab design * Rewrote code for picking the first file for metadata to ensure it always picks the correct file, esp if the first chapter of a series starts with a float (1.1) * Refactored setting LastChapterAdded to ensure we do it on the Series. * Updated Chapter updating in scan loop to avoid nested for loop and an additional loop. * Fixed a bug where locked person fields wouldn't persist between scans. * Updated Contributing to reflect how to view the swagger api * Bump versions by dotnet-bump-version. * Fixes, Tweaks, and Series Filtering (#1217) * From previous fix, added the other locking conditions on the update series metadata. * Fixed a bug where custom series, collection tag, and reading list covers weren't being removed on cleanup. * Ensure reading list detail has a margin to align to the standard * Refactored some event stuff to use dedicated consts. Introduced a new event when users read something, which can update progress bars on cards. * Added recomended and library tags to the library detail page. This will eventually offer more custom analytics * Cleanup some code onc arousel * Adjusted scale to height/width css to better fit * Small css tweaks to better center images in the manga reader in both axis. This takes care of double page rendering as well. * When a special has a Title set in the metadata, on series detail page, show that on the card rather than filename. * Fixed a bug where when paging in manga reader, the scroll to top wasn't working due to changing where scrolling is done * More css goodness for rendering images in manga reader * Fixed a bug where clearing a typeahead externally wouldn't clear the x button * Fixed a bug where filering then using keyboard would select wrong option * Added a new sorting field for Last Chapter Added (new field) to get a similar on deck feel. * Tweaked recently updated to hit the NFR of 500ms (300ms fresh start) and still give a much better experience. * Refactored On deck to now go to all series and also sort by last updated. Recently Added Series now loads all series with sort by created. * Some tweaks on css for cover image chooser * Fixed a bug in pagination control where multiple pagination events could trigger on load and thus multiple requests for data on parent controller. * Updated edit series modal to show when the last chapter was added and when user last read it. * Implemented a highlight on the fitler button when a filter is active. * Refactored metadata filter screens to perserve the filters in the url and thus when navigating back and forth, it will retain. users should click side nav to reset the state. * Hide middle section on companion bar on phones * Cleaned up some prefilters and console.logs * Don't open drawer by default when a filter is active * Bump versions by dotnet-bump-version. * Filtering Bugfixes (#1220) * Cleaned up random strings and unified them in one place. * Implemented the ability to disable typeaheads * Refactored disable state to disable controls on filter * Fixed an overflow regression on title * Updated ComicInfo DTO which had some bad properties on it * Cleaned up some code around disabled typeaheads/filters * Fixed typeaheads causing resets to state and mucking up filter presets * Fixed state not refreshing between page loads * Fixed a bad parsing for My Charms Are Wasted on Kuroiwa Medaka - Ch. 37.5 - Volume Extras * Cleanup within the metadata filter to reuse logic and minimize extra loops. * Fixed a timing issue with typeahead and first load for people * Fixed a bug in Publication Status for a given library, which would fail due to not performing some of the query in memory. Removed a custom index on Series table that wasn't used and potentially caused constraint issues. * Added a wiki link for stats collections * Security bump * Fixed the regex * Bump versions by dotnet-bump-version. * Fixing duplicate chapter issue and adding unit test (#1221) * Fixing duplicate chapter issue and adding unit test * Update test name * Bump versions by dotnet-bump-version. * Fixes #1222 (#1223) Co-authored-by: mihaibargau <mihai.bargau@gmail.com> * Bump versions by dotnet-bump-version. * Bump versions by dotnet-bump-version. * Adding gif support (#1225) * Adding gif to accepted image extension and unit test * Revert "Adding gif to accepted image extension and unit test" This reverts commit d0df8239068ddc12f44aed752804b5db60243e44. * Adding gif support and unit test * unit test and event widget - updating unit test archives to temive unneeded gifs, causing failures - adding overflow to event widget * Bump versions by dotnet-bump-version. * Readable Bookmarks (#1228) * Moved bookmarks to it's own page on side nav and integrated actions. * Implemented the ability to read bookmarks in the manga reader. * Removed old bookmark components that aren't needed any longer. * Removed recently added component as we use all-series instead now * Removed bookmark tab from card detail * Fixed scroll to top not working and being missing * When opening the side nav on mobile with metadata filter already open, collapse the filter. * When on mobile viewports, when clicking an item from side nav, collapse it afterwards * Converted most of series detail to use the card detail layout, except storyline which has custom logic * Fixed unit test * Bump versions by dotnet-bump-version. * Fixed a bad index causing card details modal to fail to render (#1229) * Bump versions by dotnet-bump-version. * Linked Series (#1230) * Implemented the ability to link different series together through Edit Series. CSS pending. * Fixed up the css for related cards to show the relation * Working on making all tabs in edit seris modal save in one go. Taking a break. * Some fixes for Robbie to help with styling on * Linked series pill, center library * Centering library detail and related pill spacing - Library detail cards are now centered if total number of items is > 6 or if mobile. - Added ability to determine if mobile (viewport width <= 480px - Fixed related card spacing - Fixed related card pill spacing * Updating relation form spacing * Fixed a bug in card detail layout when there is no pagination, we create one in a way that all items render at once. * Only auto-close side nav on phones, not tablets * Fixed a bug where we had flipped state on sideNavCollapsed$ * Cleaned up some misleading comments * Implemented RBS back in and now if you have a relationship besides prequel/sequel, the target series will show a link back to it's parent. * Added Parentto pipe * Missed a relationship type Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Publication Status Enhancements (#1231) * Trim when reading some fields from ComicInfo. Adjusted css on the site to reduce nbsp * Added Cancelled as a publication status * Ensure we track volume number from ComicInfo for the count to determine publication status * Publication Status will now check against volume number or chapter number (parsed or comicinfo). The UI will now display the progress, ie) 10/15 and will show the series as completed with a green tag badge if the progress is 100%. * Tweaked the ordering of the tabs to make it more streamlined in the reading ordering of Kavita * Tweaked the logic for filling in tag badge * Added a new publication status of Ended for series that have finished releasing, but not all items are in Kavita * Added some fields to edit series modal * Bump versions by dotnet-bump-version. * Feature/misc (#1234) * Fixed a bug where publication status could show as filled in when total number is 0 but there is a max count. Add ComicInfo support for LocalizedSeries which will populate a Series LocalizedName. Fixed an issue in tag constraint issues. * Hooked in LocalizedSeries tag into merge step in scanner. * Hooked in LocalizedSeries from ComicInfo into Kavita and also use it to help during merge phase to avoid 2 different series, if one file is using the name of the localized series. * Reduced some extra string creation and updated epub library to ignore bad ToCs. * Bumped dependencies to latest. When an epub doesn't have a dc:date with publication event type, default back to just a normal dc:date tag. * Fixed a bug where webtoon reader would error out on first load due to how we passed the function to the reader * Reverted the centering code * Bump versions by dotnet-bump-version. * Library Recomendations (#1236) * Updated cover regex for finding cover images in archives to ignore back_cover or back-cover * Fixed an issue where Tags wouldn't save due to not pulling them from the DB. * Refactored All series to it's own lazy loaded module * Modularized Dashboard and library detail. Had to change main dashboard page to be libraries. Subject to change. * Refactored login component into registration module * Series Detail module created * Refactored nav stuff into it's own module, not lazy loaded, but self contained. * Refactored theme component into a dev only module so we don't incur load for temp testing modules * Finished off modularization code. Only missing thing is to re-introduce some dashboard functionality for library view. * Implemented a basic recommendation page for library detail * Bump versions by dotnet-bump-version. * Major Search Enhancements (#1238) * Pull progress information for some of the recommended stuff. * Fixed some redirection code from last PR * Implemented the ability to search for files in the search and open the series directly. * Fixed nav search bar expanding too much * Fixed a bug in nav module not having router so some links broke * Fixed an issue where with new localized series tag, merging could fail if the user had 2 series with the series and localized series. Added extra error handling for tracking series parsed from disk. * Fixed the slowness when typing in a typeahead by using auditTime vs debounceTime * Removed some cleaning of Edition tags from the Parser. Only Omnibus and Uncensored will be ignored when cleaning titles, Full Color, Full Contact, etc will now stay in the title for Series name. * Implemented ability to search against chapter's title (from epub or title in comicinfo). This should help users search for books in a series a lot easier. * Restrict each search type to 15 records only to keep query performant and UI useful. * Wrote some extra messaging on invite user flow around email. * Messaging update * Bump versions by dotnet-bump-version. * Misc Cleanup (#1242) * Updated the wording for Read in incognito, as 'in' was redundant * Added icons to the middle tabs for a mobile compaitible view * Fixed up the code for side nav to make the display much cleaner * Added icons to tabs * Styling polishing - Making pagination spacing uniform - Fixing onresize event - Making cards center justification on mobile only - fixing vertical alignment for companion bar icons - Fixing Issue where drawer buttons would sometimes not be visible. - Fixed vertical alignment issue with filter button * Fixing orientation change event * added fixed position to drawer - fixes styling issues * added total pages to series modal * Downgraded ExCSS package to a version that doesn't die on @page query selectors. * Cleaned up some code and wrote some bug markers in typeahead * Removed some padding top on companion bar * Aligned the top margin for card detail layout and series detail * Use a temp close button on book reader until new code is ready. Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Bugfix polishing (#1245) * Fixed a bug where volumes that are a range fail to generate series detail * Moved tags closer to genre instead of between different people * Optimized the query for On Deck * Adjusted mime types to map to cbX types instead of their generic compression methods. * Added wiki documentation into invite user flow and register admin user to help users understand email isn't required and they can host their own service. * Refactored the document height to be set and removed on nav service, so the book reader and manga reader aren't broken. * Refactored On Deck to first be completely streamed to UI, without having to do any processing in memory. Rewrote the query so that we sort by progress then chapter added. Progress is 30 days inclusive, chapter added is 7 days. * Fixed an issue where epub date parsing would sometimes fail when it's only a year or not a year at all * Fixed a bug where incognito mode would report progress * Fixed a bug where bulk selection in storyline tab wouldn't properly run the action on the correct chapters (if selecting from volume -> chapter). * Removed a - 1 from total page from card progress bar as the original bug was fixed some time ago * Fixed a bug where the logic for filtering out a progress event for current logged in user didn't check properly when user is logged out. * When a file doesn't exist and we are trying to read, throw a kavita exception to the UI layer and log. * Removed unneeded variable and added some jsdoc * Bump versions by dotnet-bump-version. * Book Reader Redesign with e-ink focus (#1246) * Refactored the drawer into offcanvas component. Had to write some hacks to emulate how bootstrap's javascript implementation works as ngBootstrap doesn't have a component yet. * Cleaned up some of the code * Rewrote drawer to align it with the new design * First pass, refactored table of content into it's own component * Refactored all of the settings logic into a separate component. Everything is broken. * More settings on on reactive form * More code cleanup on settings * Misc fixes around the drawer code. Fixed a bug where range sliders were inheriting background color of normal text inputs * Fixed dark mode with book reader. We now clear the theme from the main app so book reader is self-contained. Styles for dark mode are injected into the reading-section. Styles that were previously in scss are now only for the actual menu system. * Cleaned up drawer styling on header * Removed an ngIf statement for click to paginate * Tweaked the accent style to have smaller font size and adjusted style on light mode. Cleaned up some clearTimeout code in a further effort to streamline codebase. * Refactored Dark mode into a basic theme. Currently styles are hardcoded. * Patched book theme in from themes branch * Patched in the backend for Book Theme (not tested yet) * Fixed a bug in seeding code for book themes. Started integration of themes into the reader settings * Everything except managing themes is working. Themes are a bit shakey, having second thoughts if we should have them or not. * Reverted the ability to do custom user book themes. Code is stable with system themes. * Stablize the Styles (#1128) * Fixed a bug where adding multiple series to reading list would throw an error on UI, but it was successful. * When a series has a reading list, we now show the connection on Series detail. * Removed all baseurl code from UI and not-connected component since we no longer use it. * Fixed tag badges not showing a border. Added last read time to the series detail page * Fixed up error interceptor to remove no-connection code * Changed implementation for series detail. Book libraries will never send chapters back. Volume 0 volumes will not be sent in volumes ever. Fixed up more renaming logic on books to send more accurate representations to the UI. * Cleaned up the selected tab and tab display logic * Fixed a bad where statement in reading lists for series * Fixed up tab logic again * Fixed a small margin on search backdrop * Made badge expander button smaller to align with badges * Fixed a few UIs due to .form-group and .form-row being removed * Updated Theme component page to help with style testing * Added more components to theme tester * Cleaned up some styling * Fixed opacity on search item hover * Bump versions by dotnet-bump-version. * Tweaked the accordion styles for light mode * Set dark book theme as default. Refactored resetSettings to be much cleaner * Started the refactor to allow book themes to affect global css variables * Fixed some issues with my css variable declarations * Fixed a close model state update * Lots of work, but dark mode on the book reader is basically done. We have to code the themes much like the site themes * Some black theme enhancements * Started working on column layout in book reader. * Cleaned up the CSS on Reader Settings * Hooked up reading direction * Got column and double column layout working * Implemented some basic virtual paging and hooked in book color theme and layout mode into user preferences. * Migration wrote, can edit page layout and color theme on book reader. Removed book dark mode since no longer needed. Fixed a bug on login/register forms where when input is focused, text is white and not black. * When loading book reader, apply column layout. * Lots of work around 2 column layout, working on images not splitting. Still not working, committing so i can merge develop in and validate code with new manga reader. * Fixed images being split into 2 BUT regression on each page boundary, total reading height is smaller and smaller * Fixed some rendering bugs where toggling column layouts would shrink images on screen constantly. Fixed a bug where bottom bar wouldn't render on column layout in some conditions (this might need to be reworked) * Started progress on progress work * Updated .NET to 6.0.4 * Fixed a bug where DataContextModelSnapshot was being removed on build thus new migrations were broken. * Tweaked the code around progress saving so that we don't loose track of last scroll element on page load * Trying to restore progress, but stuck * Extra merge stuff * Fixed a bug where volumes that are a range fail to generate series detail * No gutters on whole app. Book reader backend now applies the image class automatically at the backend. * Added wiki documentation into invite user flow and register admin user to help users understand email isn't required and they can host their own service. * Removed bottom padding * Refactored the document height to be set and removed on nav service, so the book reader and manga reader aren't broken. * Fixed the height of the action bar to simplify logic and keep the code cleaner. Refactored book service image scoping to be much more streamlined and efficient * Fixed the height of action bar to 62px and adjusted code to use the hardcoded px. (code commented) * Removed commented out code from fixed action bar height * Progress restoration seems to be working * Code cleanup * Ensure the bottom action bar is at the bottom of the viewport on small pages * Fixed book fonts not setting properly and added OpenDyslexic font. * Fixed up some font issues * Updated drawer so all sections are open by default * Switched some LINQ to use MinBy * When navigating between pages and column layout, adjust the shift for the user. * Removed some debug code * Blacklist .qpkg folders and don't scan Recently-Snapshot or recycle folders. * Renamed the scale width to be scoped to kavita to avoid conflicts. * Refactored ngx-sliders out to use normal range instead. Changed up the preferences to separate image and book settinngs into own accordion. * updated user preferences for new migration options (not committed yet) * Removed some debug code * Remove console.logs * Migration committed, let's release this to users. * A lot of crazy code just to ensure that when you close drawer the toggle reflectst that state. * Bump versions by dotnet-bump-version. * Book Reader Bugfixes (#1254) * Fixed image scoping breaking and books not being able to load images * Cleaned up a lot of variables and added more jsdoc. After shifting the margin, we try to recover the column layout width,height, and scroll posisiton. * Tap to paginate is working on first load now * On resize, rescroll to attempt to avoid breakage * Fixed transparent background for action bar on white theme * Moved some lists to immutable arrays * Actually fixed backgournd now * Fixed some settings not updating in book reader on load * Put some code in place to test out opening menu with clicking on the document * Fixed settings not propagating to the reader * Fixing 2 column when loading annd ios mobile * Fixed an issue where paging to prev page would sometimes skip the first page. * Fixing previous page skipping first page of chapter * removing console logs * Save progress when we page * Click on document to show the side nav * Removed columns auto because it could render more columns than applicable. Don't explicitly call saveProgress on prev page, as we already do in another call. Adjusted the logic to calculate windowHeight and width to be the same throughout the reader. * Setting select fix and settings polish * Fixed awkward tooltip wording * Added a message for when there is nothing to show on recommended tab * Removed bug marker, there was no bug after all * Fixing book title truncation in action bar * When counting volumes or chapters that have range, count the max part of the range for publication status. * Fixing TOC rendering issue * Styling fixes - Fixed an issue where the image height in the book reader was the column height plus padding so it was breaking pagination calc. - Centered book reader setting pills - Made inactive setting pill into a ghost button - Fixed spacing across the reader settings drawer * Added a bit of code to allow us to disable buttons before we click for next chapter load * Removed titles from action bars * The next page button will now show as the primary color to indicate to the user what the next forward page is. * Added a view series to bookmark page and removed actions from header since it didn't work * Fixed a bug where pagination wasn't mutating url state * Lots of changes, code is kinda working. Added Immersive Mode, but didn't generate migration. Added concept of virtual pages with ability to see them. Math is still slightly off. Cleaned up prefetching code so we do it much earlier. Added some code that doesn't work to disable buttons with virtual paging included. * When turning immersive mode on, force tap to paginate * Refactored out the book reader state as it wasn't very beneficial * Fixed total virtual page calculation * Next/prev page seems to be working pretty well * Applied Robbie's virtual page logic and fixed a bug in prev page code * Changed the next page to use same virtual page logic * Getting back and forward working...somehow. * removing redundant code * Fixing book title overflow from new action bar changes * Polishing pagination styles * Changing chapter to section * Fixing up other book reader themes * Fixed the login header being off-center * Fixing styling to follow approach * Refactored the pagination buttons to properly call next/prev page based on reading direction * Drawer pagination buttons now respect when there is no chapters (prev/next) * Everything except disabling buttons when on last possible page working * Added Book Reader immersive mode migration * Disable next/prev buttons for continuous reading before we request next/prev chapter if there is no chapter. * Show a tooltip for the title * Fixed unit test Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * More Reader Bugfixes (#1255) * Added css to center images * Better scaling css * Removing vertical height css for actionbar calc * Fixed a bug where book settings couldn't be saved due to typo in model * fixing height across layouts * Fixed an issue where column mode would reset to user preference default between continuous reader loads * Fixing some more logic * Reading direction arrow keys now flip * Small code cleanup on Robbie's code Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Reader scroll area fix (#1257) * Changes to make the pagination area scrollable (not working, debug code for Robbie) * Adjusted the html to be easier to understand and more streamlined * Fixed inability to scroll in manga reader over pagination areas for everything but the bottom bar * Book reader now allows you to scroll over pagination area * Bump versions by dotnet-bump-version. * Adding code to make sure bottom actionbar is applied on layout change (#1258) * Bump versions by dotnet-bump-version. * Last batch of bugfixes (#1262) * Refactored code to show action bar instead of drawer in immersive mode * Card grid * adding margin for pagination gap * Fixed a rare routing case that wouldn't redirect * Fixed a bug where series detail would show blank filtering * Fixing image scaling and library card spacing * Refactored some methods to be static * Adding card grid to series detail * Fixed a bug with webtoon going to non-webtoon mode, resulting in black screen. * Ensure emails are trimmed when trying to invite. * Don't show More In if there is only 1 item in there on library recommended tab * Fixed some bugs around locking metadata fields where the correct param wasn't being sent to backend. * Added some UI error messaging when the email doesn't match the confirm-email (or rather any email in the system). * Fixed some pages where actions weren't working (library detail) and removed some actionable buttons where they didn't make sense * Refactored the series detail to use Robbie's new grid system. * some styling fixes * Styling fixes - Removing select border gap - fixing switches on lite theme - fixing search result text-light * better css var naming * changing search lite text color override * fixing as per feedback * Removing boolean from being visible in bookreader * Fixed some bugs in bulk operations not being visible on light/eink screens. Added --bulk-selection-highlight-text-color and --bulk-selection-text-color. Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Remove Light and E-Ink theme (#1263) * Refactored code to show action bar instead of drawer in immersive mode * Card grid * adding margin for pagination gap * Fixed a rare routing case that wouldn't redirect * Fixed a bug where series detail would show blank filtering * Fixing image scaling and library card spacing * Refactored some methods to be static * Adding card grid to series detail * Fixed a bug with webtoon going to non-webtoon mode, resulting in black screen. * Ensure emails are trimmed when trying to invite. * Don't show More In if there is only 1 item in there on library recommended tab * Fixed some bugs around locking metadata fields where the correct param wasn't being sent to backend. * Added some UI error messaging when the email doesn't match the confirm-email (or rather any email in the system). * Fixed some pages where actions weren't working (library detail) and removed some actionable buttons where they didn't make sense * Refactored the series detail to use Robbie's new grid system. * some styling fixes * Styling fixes - Removing select border gap - fixing switches on lite theme - fixing search result text-light * better css var naming * changing search lite text color override * fixing as per feedback * Removing boolean from being visible in bookreader * Fixed some bugs in bulk operations not being visible on light/eink screens. Added --bulk-selection-highlight-text-color and --bulk-selection-text-color. * Wrote basic code to remove other themes. Need a migration instead. * Added a migration to remove light/e-ink themes and migrate users over to Dark theme by default. * Fixed unit tests Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Release Shakeout (#1266) * Fixed an issue with fit to screen where spread images would fail to generate a paging area long enough. * Fixed pagination placement on original scaling * Fixed an issue with webtoon reader not reporting scroll events due to a fix from manga reader. * Fixing select on black book-reader theme * Fixing canvas split centering * Fixed a bug with white mode in book reader not rendering correctly. When bookmarking new pages after previously have viewing bookmarks for a series, ensure we clear out the temp cache else your new files wont be visible till next day. * Use grid on related tab * Clear bookmarks was not hooked up. Bulk add to collection didn't have label hidden * Fixed bug where filter might stay open between pages * Fixed typo on relationship for Adaptation * Contains was missing from series relation modal * Tweaked some methods and wording on reading list page * Cleaned up the phrasing when we abort a scan. * Fixed issue where typeahead wasn't reopening and it wasn't filtering selected options * Fixed some typeahead bugs and decreased interval for docker health check * Cleaned up and fixed some logic with receiving cover image update events Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Release Shakeout Part 2 (#1267) * Fixed manga reader and removed debug code * Removed some console.logs * Bump versions by dotnet-bump-version. * Updated the readme to reflect new UX and some tweaks to wordings. (#1270) * version bump Co-authored-by: ThePromidius <thepromidiusyt@gmail.com> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: mihaibargau <45738311+mihaibargau@users.noreply.github.com> Co-authored-by: mihaibargau <mihai.bargau@gmail.com>
This commit is contained in:
parent
fbf8b6c1dd
commit
f9b3f1aeb5
4
.gitignore
vendored
4
.gitignore
vendored
@ -517,10 +517,12 @@ UI/Web/dist/
|
||||
/API/config/kavita.db-shm
|
||||
/API/config/kavita.db-wal
|
||||
/API/config/kavita.db-journal
|
||||
/API/config/*.db
|
||||
/API/config/*.bak
|
||||
/API/config/*.backup
|
||||
/API/config/Hangfire.db
|
||||
/API/config/Hangfire-log.db
|
||||
API/config/covers/
|
||||
API/config/*.db
|
||||
API/config/stats/*
|
||||
API/config/stats/app_stats.json
|
||||
API/config/pre-metadata/
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities.Enums;
|
||||
using API.Parser;
|
||||
using API.Services;
|
||||
@ -46,7 +47,7 @@ namespace API.Benchmark
|
||||
/// Generate a list of Series and another list with
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public void MergeName()
|
||||
public async Task MergeName()
|
||||
{
|
||||
var libraryPath = Path.Join(Directory.GetCurrentDirectory(),
|
||||
"../../../Services/Test Data/ScannerService/Manga");
|
||||
@ -61,7 +62,7 @@ namespace API.Benchmark
|
||||
Title = "A Town Where You Live",
|
||||
Volumes = "1"
|
||||
};
|
||||
_parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new [] {libraryPath}, "Manga");
|
||||
await _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new [] {libraryPath}, "Manga");
|
||||
_parseScannedFiles.MergeName(p1);
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="NSubstitute" Version="4.3.0" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="16.1.25" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="17.0.3" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
@ -14,4 +14,11 @@ public class ChapterSortComparerZeroFirstTests
|
||||
{
|
||||
Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerZeroFirst()).ToArray());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new[] {1.0, 0.5, 0.3}, new[] {0.3, 0.5, 1.0})]
|
||||
public void ChapterSortComparerZeroFirstTest_Doubles(double[] input, double[] expected)
|
||||
{
|
||||
Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerZeroFirst()).ToArray());
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,10 @@ public class NumericComparerTests
|
||||
new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
|
||||
new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"}
|
||||
)]
|
||||
[InlineData(
|
||||
new[] {"x1.0.jpg", "0.5.jpg", "0.3.jpg"},
|
||||
new[] {"0.3.jpg", "0.5.jpg", "x1.0.jpg",}
|
||||
)]
|
||||
public void NumericComparerTest(string[] input, string[] expected)
|
||||
{
|
||||
var nc = new NumericComparer();
|
||||
|
@ -16,7 +16,7 @@ public class ComicInfoTests
|
||||
[InlineData("Early Childhood", AgeRating.EarlyChildhood)]
|
||||
[InlineData("Everyone 10+", AgeRating.Everyone10Plus)]
|
||||
[InlineData("M", AgeRating.Mature)]
|
||||
[InlineData("MA 15+", AgeRating.Mature15Plus)]
|
||||
[InlineData("MA15+", AgeRating.Mature15Plus)]
|
||||
[InlineData("Mature 17+", AgeRating.Mature17Plus)]
|
||||
[InlineData("Rating Pending", AgeRating.RatingPending)]
|
||||
[InlineData("X18+", AgeRating.X18Plus)]
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
@ -83,6 +83,34 @@ namespace API.Tests.Extensions
|
||||
Assert.Equal(chapterList[0], actualChapter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChapterByRange_On_Duplicate_Files_Test_Should_Not_Error()
|
||||
{
|
||||
var info = new ParserInfo()
|
||||
{
|
||||
Chapters = "0",
|
||||
Edition = "",
|
||||
Format = MangaFormat.Archive,
|
||||
FullFilePath = "/manga/detective comics #001.cbz",
|
||||
Filename = "detective comics #001.cbz",
|
||||
IsSpecial = true,
|
||||
Series = "detective comics",
|
||||
Title = "detective comics",
|
||||
Volumes = "0"
|
||||
};
|
||||
|
||||
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)
|
||||
};
|
||||
|
||||
var actualChapter = chapterList.GetChapterByRange(info);
|
||||
|
||||
Assert.Equal(chapterList[0], actualChapter);
|
||||
|
||||
}
|
||||
|
||||
#region GetFirstChapterWithFiles
|
||||
|
||||
[Fact]
|
||||
|
@ -9,67 +9,6 @@ namespace API.Tests.Extensions;
|
||||
|
||||
public class VolumeListExtensionsTests
|
||||
{
|
||||
#region FirstWithChapters
|
||||
|
||||
[Fact]
|
||||
public void FirstWithChapters_ReturnsVolumeWithChapters()
|
||||
{
|
||||
var volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false),
|
||||
EntityFactory.CreateChapter("2", false),
|
||||
}),
|
||||
};
|
||||
|
||||
Assert.Equal(volumes[1].Number, volumes.FirstWithChapters(false).Number);
|
||||
Assert.Equal(volumes[1].Number, volumes.FirstWithChapters(true).Number);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstWithChapters_Book()
|
||||
{
|
||||
var volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("3", false),
|
||||
EntityFactory.CreateChapter("4", false),
|
||||
}),
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false),
|
||||
EntityFactory.CreateChapter("0", true),
|
||||
}),
|
||||
};
|
||||
|
||||
Assert.Equal(volumes[0].Number, volumes.FirstWithChapters(true).Number);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstWithChapters_NonBook()
|
||||
{
|
||||
var volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("3", false),
|
||||
EntityFactory.CreateChapter("4", false),
|
||||
}),
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false),
|
||||
EntityFactory.CreateChapter("0", true),
|
||||
}),
|
||||
};
|
||||
|
||||
Assert.Equal(volumes[0].Number, volumes.FirstWithChapters(false).Number);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetCoverImage
|
||||
|
||||
[Fact]
|
||||
|
@ -31,7 +31,7 @@ namespace API.Tests.Helpers
|
||||
return new Volume()
|
||||
{
|
||||
Name = volumeNumber,
|
||||
Number = (int) API.Parser.Parser.MinimumNumberFromRange(volumeNumber),
|
||||
Number = (int) API.Parser.Parser.MinNumberFromRange(volumeNumber),
|
||||
Pages = pages,
|
||||
Chapters = chaps
|
||||
};
|
||||
@ -43,7 +43,7 @@ namespace API.Tests.Helpers
|
||||
{
|
||||
IsSpecial = isSpecial,
|
||||
Range = range,
|
||||
Number = API.Parser.Parser.MinimumNumberFromRange(range) + string.Empty,
|
||||
Number = API.Parser.Parser.MinNumberFromRange(range) + string.Empty,
|
||||
Files = files ?? new List<MangaFile>(),
|
||||
Pages = pageCount,
|
||||
|
||||
|
@ -122,7 +122,7 @@ public class DefaultParserTests
|
||||
filepath = @"E:\Manga\Tenjo Tenge (Color)\Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz";
|
||||
expected.Add(filepath, new ParserInfo
|
||||
{
|
||||
Series = "Tenjo Tenge", Volumes = "1", Edition = "Full Contact Edition",
|
||||
Series = "Tenjo Tenge {Full Contact Edition}", Volumes = "1", Edition = "",
|
||||
Chapters = "0", Filename = "Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", Format = MangaFormat.Archive,
|
||||
FullFilePath = filepath
|
||||
});
|
||||
|
@ -169,6 +169,8 @@ namespace API.Tests.Parser
|
||||
[InlineData("Love Hina - Volume 01 [Scans].pdf", "Love Hina")]
|
||||
[InlineData("It's Witching Time! 001 (Digital) (Anonymous1234)", "It's Witching Time!")]
|
||||
[InlineData("Zettai Karen Children v02 c003 - The Invisible Guardian (2) [JS Scans]", "Zettai Karen Children")]
|
||||
[InlineData("My Charms Are Wasted on Kuroiwa Medaka - Ch. 37.5 - Volume Extras", "My Charms Are Wasted on Kuroiwa Medaka")]
|
||||
[InlineData("Highschool of the Dead - Full Color Edition v02 [Uasaha] (Yen Press)", "Highschool of the Dead - Full Color Edition")]
|
||||
public void ParseSeriesTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename));
|
||||
@ -252,13 +254,13 @@ namespace API.Tests.Parser
|
||||
|
||||
[Theory]
|
||||
[InlineData("Tenjou Tenge Omnibus", "Omnibus")]
|
||||
[InlineData("Tenjou Tenge {Full Contact Edition}", "Full Contact Edition")]
|
||||
[InlineData("Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", "Full Contact Edition")]
|
||||
[InlineData("Tenjou Tenge {Full Contact Edition}", "")]
|
||||
[InlineData("Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", "")]
|
||||
[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("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("AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz", "Full Color")]
|
||||
[InlineData("AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz", "")]
|
||||
[InlineData("Love Hina Omnibus v05 (2015) (Digital-HD) (Asgard-Empire).cbz", "Omnibus")]
|
||||
public void ParseEditionTest(string input, string expected)
|
||||
{
|
||||
|
@ -63,6 +63,7 @@ namespace API.Tests.Parser
|
||||
[InlineData("- The Title", false, "The Title")]
|
||||
[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("Something - Full Color Edition", false, "Something - Full Color Edition")]
|
||||
public void CleanTitleTest(string input, bool isComic, string expected)
|
||||
{
|
||||
Assert.Equal(expected, CleanTitle(input, isComic));
|
||||
@ -139,7 +140,7 @@ namespace API.Tests.Parser
|
||||
[InlineData("40.1_a", 0)]
|
||||
public void MinimumNumberFromRangeTest(string input, float expected)
|
||||
{
|
||||
Assert.Equal(expected, MinimumNumberFromRange(input));
|
||||
Assert.Equal(expected, MinNumberFromRange(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -152,7 +153,7 @@ namespace API.Tests.Parser
|
||||
[InlineData("40.1_a", 0)]
|
||||
public void MaximumNumberFromRangeTest(string input, float expected)
|
||||
{
|
||||
Assert.Equal(expected, MaximumNumberFromRange(input));
|
||||
Assert.Equal(expected, MaxNumberFromRange(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -179,6 +180,7 @@ namespace API.Tests.Parser
|
||||
[InlineData(".test.jpg", false)]
|
||||
[InlineData("!test.jpg", true)]
|
||||
[InlineData("test.webp", true)]
|
||||
[InlineData("test.gif", true)]
|
||||
public void IsImageTest(string filename, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, IsImage(filename));
|
||||
@ -197,6 +199,7 @@ namespace API.Tests.Parser
|
||||
[InlineData("ch1/cover.png", true)]
|
||||
[InlineData("ch1/backcover.png", false)]
|
||||
[InlineData("backcover.png", false)]
|
||||
[InlineData("back_cover.png", false)]
|
||||
public void IsCoverImageTest(string inputPath, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, IsCoverImage(inputPath));
|
||||
|
@ -389,7 +389,7 @@ public class BookmarkServiceTests
|
||||
VolumeId = 1
|
||||
}, $"{CacheDirectory}1/0001.jpg");
|
||||
|
||||
var files = await bookmarkService.GetBookmarkFilesById(1, new[] {1});
|
||||
var files = await bookmarkService.GetBookmarkFilesById(new[] {1});
|
||||
var actualFiles = ds.GetFiles(BookmarkDirectory, searchOption: SearchOption.AllDirectories);
|
||||
Assert.Equal(files.Select(API.Parser.Parser.NormalizePath).ToList(), actualFiles.Select(API.Parser.Parser.NormalizePath).ToList());
|
||||
}
|
||||
|
@ -157,7 +157,8 @@ namespace API.Tests.Services
|
||||
filesystem.AddDirectory($"{CacheDirectory}1/");
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cleanupService = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(), Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds));
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
|
||||
await ResetDB();
|
||||
var s = DbFactory.Series("Test");
|
||||
@ -240,7 +241,8 @@ namespace API.Tests.Services
|
||||
filesystem.AddFile($"{CacheDirectory}3/003.jpg", new MockFileData(""));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cleanupService = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(), Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds));
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
|
||||
cleanupService.CleanupChapters(new []{1, 3});
|
||||
Assert.Empty(ds.GetFiles(CacheDirectory, searchOption:SearchOption.AllDirectories));
|
||||
@ -260,7 +262,8 @@ namespace API.Tests.Services
|
||||
filesystem.AddFile($"{DataDirectory}2.epub", new MockFileData(""));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cs = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(), Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds));
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
|
||||
var c = new Chapter()
|
||||
{
|
||||
@ -311,7 +314,8 @@ namespace API.Tests.Services
|
||||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cs = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(), Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds));
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
|
||||
// Flatten to prepare for how GetFullPath expects
|
||||
ds.Flatten($"{CacheDirectory}1/");
|
||||
@ -362,7 +366,8 @@ namespace API.Tests.Services
|
||||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cs = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(), Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds));
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
|
||||
// Flatten to prepare for how GetFullPath expects
|
||||
ds.Flatten($"{CacheDirectory}1/");
|
||||
@ -408,7 +413,8 @@ namespace API.Tests.Services
|
||||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cs = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(), Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds));
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
|
||||
// Flatten to prepare for how GetFullPath expects
|
||||
ds.Flatten($"{CacheDirectory}1/");
|
||||
@ -460,7 +466,8 @@ namespace API.Tests.Services
|
||||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cs = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(), Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds));
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
|
||||
// Flatten to prepare for how GetFullPath expects
|
||||
ds.Flatten($"{CacheDirectory}1/");
|
||||
|
@ -11,6 +11,7 @@ using API.Entities.Enums;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Data.Sqlite;
|
||||
@ -125,23 +126,23 @@ public class CleanupServiceTests
|
||||
public async Task DeleteSeriesCoverImages_ShouldDeleteAll()
|
||||
{
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{CoverImageDirectory}series_01.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}series_03.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}series_1000.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1)}.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(3)}.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData(""));
|
||||
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
|
||||
var s = DbFactory.Series("Test 1");
|
||||
s.CoverImage = "series_01.jpg";
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
s = DbFactory.Series("Test 2");
|
||||
s.CoverImage = "series_03.jpg";
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
s = DbFactory.Series("Test 3");
|
||||
s.CoverImage = "series_1000.jpg";
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(1000)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
|
||||
@ -158,20 +159,20 @@ public class CleanupServiceTests
|
||||
public async Task DeleteSeriesCoverImages_ShouldNotDeleteLinkedFiles()
|
||||
{
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{CoverImageDirectory}series_01.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}series_03.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}series_1000.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1)}.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(3)}.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData(""));
|
||||
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
|
||||
// Add 2 series with cover images
|
||||
var s = DbFactory.Series("Test 1");
|
||||
s.CoverImage = "series_01.jpg";
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
s = DbFactory.Series("Test 2");
|
||||
s.CoverImage = "series_03.jpg";
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
|
||||
@ -242,9 +243,9 @@ public class CleanupServiceTests
|
||||
public async Task DeleteTagCoverImages_ShouldNotDeleteLinkedFiles()
|
||||
{
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{CoverImageDirectory}tag_01.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}tag_02.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}tag_1000.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1)}.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(2)}.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1000)}.jpg", new MockFileData(""));
|
||||
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
@ -255,9 +256,9 @@ public class CleanupServiceTests
|
||||
s.Metadata.CollectionTags.Add(new CollectionTag()
|
||||
{
|
||||
Title = "Something",
|
||||
CoverImage ="tag_01.jpg"
|
||||
CoverImage = $"{ImageService.GetCollectionTagFormat(1)}.jpg"
|
||||
});
|
||||
s.CoverImage = "series_01.jpg";
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
|
||||
@ -266,9 +267,9 @@ public class CleanupServiceTests
|
||||
s.Metadata.CollectionTags.Add(new CollectionTag()
|
||||
{
|
||||
Title = "Something 2",
|
||||
CoverImage ="tag_02.jpg"
|
||||
CoverImage = $"{ImageService.GetCollectionTagFormat(2)}.jpg"
|
||||
});
|
||||
s.CoverImage = "series_03.jpg";
|
||||
s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg";
|
||||
s.LibraryId = 1;
|
||||
_context.Series.Add(s);
|
||||
|
||||
@ -285,6 +286,49 @@ public class CleanupServiceTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region DeleteReadingListCoverImages
|
||||
[Fact]
|
||||
public async Task DeleteReadingListCoverImages_ShouldNotDeleteLinkedFiles()
|
||||
{
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(1)}.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(2)}.jpg", new MockFileData(""));
|
||||
filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(3)}.jpg", new MockFileData(""));
|
||||
|
||||
// Delete all Series to reset state
|
||||
await ResetDB();
|
||||
|
||||
_context.Users.Add(new AppUser()
|
||||
{
|
||||
UserName = "Joe",
|
||||
ReadingLists = new List<ReadingList>()
|
||||
{
|
||||
new ReadingList()
|
||||
{
|
||||
Title = "Something",
|
||||
NormalizedTitle = API.Parser.Parser.Normalize("Something"),
|
||||
CoverImage = $"{ImageService.GetReadingListFormat(1)}.jpg"
|
||||
},
|
||||
new ReadingList()
|
||||
{
|
||||
Title = "Something 2",
|
||||
NormalizedTitle = API.Parser.Parser.Normalize("Something 2"),
|
||||
CoverImage = $"{ImageService.GetReadingListFormat(2)}.jpg"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
||||
ds);
|
||||
|
||||
await cleanupService.DeleteReadingListCoverImages();
|
||||
|
||||
Assert.Equal(2, ds.GetFiles(CoverImageDirectory).Count());
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CleanupCacheDirectory
|
||||
|
||||
[Fact]
|
||||
@ -320,7 +364,7 @@ public class CleanupServiceTests
|
||||
#region CleanupBackups
|
||||
|
||||
[Fact]
|
||||
public void CleanupBackups_LeaveOneFile_SinceAllAreExpired()
|
||||
public async Task CleanupBackups_LeaveOneFile_SinceAllAreExpired()
|
||||
{
|
||||
var filesystem = CreateFileSystem();
|
||||
var filesystemFile = new MockFileData("")
|
||||
@ -334,12 +378,12 @@ public class CleanupServiceTests
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
||||
ds);
|
||||
cleanupService.CleanupBackups();
|
||||
await cleanupService.CleanupBackups();
|
||||
Assert.Single(ds.GetFiles(BackupDirectory, searchOption: SearchOption.AllDirectories));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CleanupBackups_LeaveLestExpired()
|
||||
public async Task CleanupBackups_LeaveLestExpired()
|
||||
{
|
||||
var filesystem = CreateFileSystem();
|
||||
var filesystemFile = new MockFileData("")
|
||||
@ -356,7 +400,7 @@ public class CleanupServiceTests
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
||||
ds);
|
||||
cleanupService.CleanupBackups();
|
||||
await cleanupService.CleanupBackups();
|
||||
Assert.True(filesystem.File.Exists($"{BackupDirectory}randomfile.zip"));
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ namespace API.Tests.Services
|
||||
{
|
||||
private readonly ILogger<DirectoryService> _logger = Substitute.For<ILogger<DirectoryService>>();
|
||||
|
||||
|
||||
#region TraverseTreeParallelForEach
|
||||
[Fact]
|
||||
public void TraverseTreeParallelForEach_JustArchives_ShouldBe28()
|
||||
@ -575,19 +576,22 @@ namespace API.Tests.Services
|
||||
[Fact]
|
||||
public void CopyFilesToDirectory_ShouldAppendWhenTargetFileExists()
|
||||
{
|
||||
|
||||
const string testDirectory = "/manga/";
|
||||
var fileSystem = new MockFileSystem();
|
||||
fileSystem.AddFile($"{testDirectory}file.zip", new MockFileData(""));
|
||||
fileSystem.AddFile($"/manga/output/file (1).zip", new MockFileData(""));
|
||||
fileSystem.AddFile($"/manga/output/file (2).zip", new MockFileData(""));
|
||||
fileSystem.AddFile(MockUnixSupport.Path($"{testDirectory}file.zip"), new MockFileData(""));
|
||||
fileSystem.AddFile(MockUnixSupport.Path($"/manga/output/file (1).zip"), new MockFileData(""));
|
||||
fileSystem.AddFile(MockUnixSupport.Path($"/manga/output/file (2).zip"), new MockFileData(""));
|
||||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
ds.CopyFilesToDirectory(new []{$"{testDirectory}file.zip"}, "/manga/output/");
|
||||
ds.CopyFilesToDirectory(new []{$"{testDirectory}file.zip"}, "/manga/output/");
|
||||
ds.CopyFilesToDirectory(new []{MockUnixSupport.Path($"{testDirectory}file.zip")}, "/manga/output/");
|
||||
ds.CopyFilesToDirectory(new []{MockUnixSupport.Path($"{testDirectory}file.zip")}, "/manga/output/");
|
||||
var outputFiles = ds.GetFiles("/manga/output/").Select(API.Parser.Parser.NormalizePath).ToList();
|
||||
Assert.Equal(4, outputFiles.Count()); // we have 2 already there and 2 copies
|
||||
// For some reason, this has C:/ on directory even though everything is emulated
|
||||
Assert.True(outputFiles.Contains(API.Parser.Parser.NormalizePath("/manga/output/file (3).zip")) || outputFiles.Contains(API.Parser.Parser.NormalizePath("C:/manga/output/file (3).zip")));
|
||||
// For some reason, this has C:/ on directory even though everything is emulated (System.IO.Abstractions issue, not changing)
|
||||
// https://github.com/TestableIO/System.IO.Abstractions/issues/831
|
||||
Assert.True(outputFiles.Contains(API.Parser.Parser.NormalizePath("/manga/output/file (3).zip"))
|
||||
|| outputFiles.Contains(API.Parser.Parser.NormalizePath("C:/manga/output/file (3).zip")));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -11,6 +11,7 @@ using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using API.Tests.Helpers;
|
||||
using AutoMapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
@ -147,7 +148,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
Assert.Equal(0, await readerService.CapPageToChapter(1, -1));
|
||||
Assert.Equal(1, await readerService.CapPageToChapter(1, 10));
|
||||
@ -191,7 +192,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var successful = await readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
@ -240,7 +241,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var successful = await readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
@ -310,7 +311,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumes(1);
|
||||
readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
@ -360,7 +361,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList();
|
||||
readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
@ -420,7 +421,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 1, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
@ -466,7 +467,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
@ -508,7 +509,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 4, 1);
|
||||
@ -551,7 +552,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 4, 1);
|
||||
@ -587,7 +588,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
@ -628,7 +629,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
@ -669,7 +670,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
@ -708,7 +709,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
@ -751,7 +752,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 3, 1);
|
||||
@ -793,7 +794,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 3, 1);
|
||||
@ -846,7 +847,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 2, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
@ -892,7 +893,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
|
||||
@ -934,7 +935,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
|
||||
@ -972,7 +973,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
@ -1007,7 +1008,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
@ -1047,7 +1048,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
@ -1095,7 +1096,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2,5, 1);
|
||||
var chapterInfoDto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(prevChapter);
|
||||
@ -1137,7 +1138,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
@ -1178,7 +1179,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 4, 1);
|
||||
@ -1221,7 +1222,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
@ -1276,7 +1277,7 @@ public class ReaderServiceTests
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
|
||||
@ -1321,7 +1322,7 @@ public class ReaderServiceTests
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
// Save progress on first volume chapters and 1st of second volume
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
@ -1404,7 +1405,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
// Save progress on first volume and 1st chapter of second volume
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
@ -1470,7 +1471,7 @@ public class ReaderServiceTests
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
// Save progress on first volume chapters and 1st of second volume
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
@ -1538,7 +1539,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("1", nextChapter.Range);
|
||||
@ -1575,7 +1576,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
// Save progress on first volume chapters and 1st of second volume
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
@ -1640,7 +1641,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
// Save progress on first volume chapters and 1st of second volume
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress);
|
||||
@ -1681,7 +1682,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
// Save progress on first volume chapters and 1st of second volume
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
@ -1753,7 +1754,7 @@ public class ReaderServiceTests
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress);
|
||||
await readerService.MarkSeriesAsRead(user, 1);
|
||||
await _context.SaveChangesAsync();
|
||||
@ -1801,7 +1802,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkChaptersUntilAsRead(user, 1, 5);
|
||||
@ -1844,7 +1845,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkChaptersUntilAsRead(user, 1, 2.5f);
|
||||
@ -1888,7 +1889,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
await readerService.MarkChaptersUntilAsRead(user, 1, 2);
|
||||
@ -1947,7 +1948,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
|
||||
const int markReadUntilNumber = 47;
|
||||
@ -2027,7 +2028,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
await readerService.MarkSeriesAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1);
|
||||
await _context.SaveChangesAsync();
|
||||
@ -2078,7 +2079,7 @@ public class ReaderServiceTests
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList();
|
||||
readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
|
||||
|
@ -125,5 +125,6 @@ namespace API.Tests.Services
|
||||
// }
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace API.Tests.Services;
|
||||
|
||||
@ -703,6 +704,85 @@ public class SeriesServiceTests
|
||||
Assert.False(series.Metadata.GenresLocked); // GenreLocked is false unless the UI Explicitly says it should be locked
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateSeriesMetadata_ShouldAddNewPerson_NoExistingPeople()
|
||||
{
|
||||
await ResetDb();
|
||||
var s = new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Book,
|
||||
},
|
||||
Metadata = DbFactory.SeriesMetadata(new List<CollectionTag>())
|
||||
};
|
||||
var g = DbFactory.Person("Existing Person", PersonRole.Publisher);
|
||||
_context.Series.Add(s);
|
||||
|
||||
_context.Person.Add(g);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto()
|
||||
{
|
||||
SeriesMetadata = new SeriesMetadataDto()
|
||||
{
|
||||
SeriesId = 1,
|
||||
Publishers = new List<PersonDto>() {new () {Id = 0, Name = "Existing Person", Role = PersonRole.Publisher}},
|
||||
},
|
||||
CollectionTags = new List<CollectionTagDto>()
|
||||
});
|
||||
|
||||
Assert.True(success);
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||
Assert.NotNull(series.Metadata);
|
||||
Assert.True(series.Metadata.People.Select(g => g.Name).All(g => g == "Existing Person"));
|
||||
Assert.False(series.Metadata.PublisherLocked); // PublisherLocked is false unless the UI Explicitly says it should be locked
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateSeriesMetadata_ShouldAddNewPerson_ExistingPeople()
|
||||
{
|
||||
await ResetDb();
|
||||
var s = new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library()
|
||||
{
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Book,
|
||||
},
|
||||
Metadata = DbFactory.SeriesMetadata(new List<CollectionTag>())
|
||||
};
|
||||
var g = DbFactory.Person("Existing Person", PersonRole.Publisher);
|
||||
s.Metadata.People = new List<Person>() {DbFactory.Person("Existing Writer", PersonRole.Writer),
|
||||
DbFactory.Person("Existing Translator", PersonRole.Translator), DbFactory.Person("Existing Publisher 2", PersonRole.Publisher)};
|
||||
_context.Series.Add(s);
|
||||
|
||||
_context.Person.Add(g);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto()
|
||||
{
|
||||
SeriesMetadata = new SeriesMetadataDto()
|
||||
{
|
||||
SeriesId = 1,
|
||||
Publishers = new List<PersonDto>() {new () {Id = 0, Name = "Existing Person", Role = PersonRole.Publisher}},
|
||||
PublishersLocked = true
|
||||
},
|
||||
CollectionTags = new List<CollectionTagDto>()
|
||||
});
|
||||
|
||||
Assert.True(success);
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||
Assert.NotNull(series.Metadata);
|
||||
Assert.True(series.Metadata.People.Select(g => g.Name).All(g => g == "Existing Person"));
|
||||
Assert.True(series.Metadata.PublisherLocked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateSeriesMetadata_ShouldLockIfTold()
|
||||
{
|
||||
@ -745,4 +825,86 @@ public class SeriesServiceTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetFirstChapterForMetadata
|
||||
|
||||
private static Series CreateSeriesMock()
|
||||
{
|
||||
var files = new List<MangaFile>()
|
||||
{
|
||||
EntityFactory.CreateMangaFile("Test.cbz", MangaFormat.Archive, 1)
|
||||
};
|
||||
return 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("95", false, files, 1),
|
||||
EntityFactory.CreateChapter("96", false, files, 1),
|
||||
EntityFactory.CreateChapter("A Special Case", true, files, 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false, files, 1),
|
||||
EntityFactory.CreateChapter("2", false, files, 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("21", false, files, 1),
|
||||
EntityFactory.CreateChapter("22", false, files, 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, files, 1),
|
||||
EntityFactory.CreateChapter("32", false, files, 1),
|
||||
}),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFirstChapterForMetadata_Book_Test()
|
||||
{
|
||||
var series = CreateSeriesMock();
|
||||
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series, true);
|
||||
Assert.Same("1", firstChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFirstChapterForMetadata_NonBook_ShouldReturnVolume1()
|
||||
{
|
||||
var series = CreateSeriesMock();
|
||||
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series, false);
|
||||
Assert.Same("1", firstChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFirstChapterForMetadata_NonBook_ShouldReturnVolume1_WhenFirstChapterIsFloat()
|
||||
{
|
||||
var series = CreateSeriesMock();
|
||||
var files = new List<MangaFile>()
|
||||
{
|
||||
EntityFactory.CreateMangaFile("Test.cbz", MangaFormat.Archive, 1)
|
||||
};
|
||||
series.Volumes[1].Chapters = new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("2", false, files, 1),
|
||||
EntityFactory.CreateChapter("1.1", false, files, 1),
|
||||
EntityFactory.CreateChapter("1.2", false, files, 1),
|
||||
};
|
||||
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series, false);
|
||||
Assert.Same("1.1", firstChapter.Range);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ namespace API.Tests.Services;
|
||||
|
||||
public class SiteThemeServiceTests
|
||||
{
|
||||
private readonly ILogger<SiteThemeService> _logger = Substitute.For<ILogger<SiteThemeService>>();
|
||||
private readonly ILogger<ThemeService> _logger = Substitute.For<ILogger<ThemeService>>();
|
||||
private readonly IEventHub _messageHub = Substitute.For<IEventHub>();
|
||||
|
||||
private readonly DbConnection _connection;
|
||||
@ -87,7 +87,7 @@ public class SiteThemeServiceTests
|
||||
UserName = "Joe",
|
||||
UserPreferences = new AppUserPreferences
|
||||
{
|
||||
Theme = Seed.DefaultThemes[1]
|
||||
Theme = Seed.DefaultThemes[0]
|
||||
}
|
||||
});
|
||||
|
||||
@ -135,7 +135,7 @@ public class SiteThemeServiceTests
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
||||
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||
await siteThemeService.Scan();
|
||||
|
||||
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
|
||||
@ -148,7 +148,7 @@ public class SiteThemeServiceTests
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
||||
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||
await siteThemeService.Scan();
|
||||
|
||||
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
|
||||
@ -167,7 +167,7 @@ public class SiteThemeServiceTests
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
||||
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||
await siteThemeService.Scan();
|
||||
|
||||
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
|
||||
@ -188,7 +188,7 @@ public class SiteThemeServiceTests
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
||||
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||
|
||||
_context.SiteTheme.Add(new SiteTheme()
|
||||
{
|
||||
@ -213,7 +213,7 @@ public class SiteThemeServiceTests
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
||||
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||
|
||||
_context.SiteTheme.Add(new SiteTheme()
|
||||
{
|
||||
@ -241,7 +241,7 @@ public class SiteThemeServiceTests
|
||||
var filesystem = CreateFileSystem();
|
||||
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
||||
var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
|
||||
|
||||
_context.SiteTheme.Add(new SiteTheme()
|
||||
{
|
||||
|
Binary file not shown.
Binary file not shown.
@ -1,25 +0,0 @@
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
@ -40,39 +40,39 @@
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
||||
<PackageReference Include="Docnet.Core" Version="2.4.0-alpha.2" />
|
||||
<PackageReference Include="ExCSS" Version="4.1.0" />
|
||||
<PackageReference Include="Flurl" Version="3.0.4" />
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.2" />
|
||||
<PackageReference Include="Flurl" Version="3.0.5" />
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.3" />
|
||||
<PackageReference Include="Hangfire" Version="1.7.28" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.28" />
|
||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.42" />
|
||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
|
||||
<PackageReference Include="NetVips" Version="2.1.0" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.12.2" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.1.4" />
|
||||
<PackageReference Include="SharpCompress" Version="0.31.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.0" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.37.0.45539">
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.1" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.38.0.46746">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.17.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="16.1.25" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.0.3.1" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="17.0.3" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -94,16 +94,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Interfaces\IMetadataService.cs" />
|
||||
<Compile Remove="obj\**" />
|
||||
<Compile Remove="cache\**" />
|
||||
<Compile Remove="backups\**" />
|
||||
<Compile Remove="logs\**" />
|
||||
<Compile Remove="temp\**" />
|
||||
<Compile Remove="covers\**" />
|
||||
<Compile Remove="DTOs\Email\SmtpConfig.cs" />
|
||||
<Compile Remove="DTOs\Email\EmailOptionsDto.cs" />
|
||||
<Compile Remove="Helpers\Converters\SmtpConverter.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -353,6 +353,7 @@ namespace API.Controllers
|
||||
_logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email);
|
||||
|
||||
// Check if there is an existing invite
|
||||
dto.Email = dto.Email.Trim();
|
||||
var emailValidationErrors = await _accountService.ValidateEmail(dto.Email);
|
||||
if (emailValidationErrors.Any())
|
||||
{
|
||||
@ -454,6 +455,11 @@ namespace API.Controllers
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("The email does not match the registered email");
|
||||
}
|
||||
|
||||
// Validate Password and Username
|
||||
var validationErrors = new List<ApiException>();
|
||||
validationErrors.AddRange(await _accountService.ValidateUsername(dto.Username));
|
||||
|
@ -40,7 +40,7 @@ namespace API.Controllers
|
||||
if (dto.SeriesFormat == MangaFormat.Epub)
|
||||
{
|
||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath);
|
||||
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions);
|
||||
bookTitle = book.Title;
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ namespace API.Controllers
|
||||
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
|
||||
|
||||
var key = BookService.CleanContentKeys(file);
|
||||
if (!book.Content.AllFiles.ContainsKey(key)) return BadRequest("File was not found in book");
|
||||
@ -87,7 +87,7 @@ namespace API.Controllers
|
||||
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
|
||||
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
||||
|
||||
var navItems = await book.GetNavigationAsync();
|
||||
@ -211,8 +211,7 @@ namespace API.Controllers
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
var path = _cacheService.GetCachedEpubFile(chapter.Id, chapter);
|
||||
|
||||
|
||||
using var book = await EpubReader.OpenBookAsync(path);
|
||||
using var book = await EpubReader.OpenBookAsync(path, BookService.BookReaderOptions);
|
||||
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
||||
|
||||
var counter = 0;
|
||||
|
@ -157,7 +157,7 @@ namespace API.Controllers
|
||||
tag.CoverImageLocked = false;
|
||||
tag.CoverImage = string.Empty;
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(tag.Id, "collectionTag"), false);
|
||||
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
|
||||
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||
}
|
||||
|
||||
|
@ -171,7 +171,7 @@ namespace API.Controllers
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);
|
||||
|
||||
var files = await _bookmarkService.GetBookmarkFilesById(user.Id, downloadBookmarkDto.Bookmarks.Select(b => b.Id));
|
||||
var files = await _bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id));
|
||||
|
||||
var filename = $"{series.Name} - Bookmarks.zip";
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
|
@ -88,6 +88,22 @@ namespace API.Controllers
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns cover image for a Reading List
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("readinglist-cover")]
|
||||
public async Task<ActionResult> GetReadingListCoverImage(int readingListId)
|
||||
{
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
Response.AddCacheHeader(path);
|
||||
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns image for a given bookmark page
|
||||
/// </summary>
|
||||
|
@ -99,12 +99,12 @@ public class MetadataController : BaseApiController
|
||||
/// <param name="libraryIds">String separated libraryIds or null for all publication status</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("publication-status")]
|
||||
public async Task<ActionResult<IList<AgeRatingDto>>> GetAllPublicationStatus(string? libraryIds)
|
||||
public ActionResult<IList<AgeRatingDto>> GetAllPublicationStatus(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||
if (ids != null && ids.Count > 0)
|
||||
if (ids is {Count: > 0})
|
||||
{
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
|
||||
return Ok(_unitOfWork.SeriesRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
|
||||
}
|
||||
|
||||
return Ok(Enum.GetValues<PublicationStatus>().Select(t => new PublicationStatusDto()
|
||||
|
@ -389,12 +389,8 @@ public class OpdsController : BaseApiController
|
||||
var userParams = new UserParams()
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
PageSize = 20
|
||||
};
|
||||
var results = await _unitOfWork.SeriesRepository.GetOnDeck(userId, 0, userParams, _filterDto);
|
||||
var listResults = results.DistinctBy(s => s.Name).Skip((userParams.PageNumber - 1) * userParams.PageSize)
|
||||
.Take(userParams.PageSize).ToList();
|
||||
var pagedList = new PagedList<SeriesDto>(listResults, listResults.Count, userParams.PageNumber, userParams.PageSize);
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, 0, userParams, _filterDto);
|
||||
|
||||
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
||||
|
||||
|
@ -11,6 +11,8 @@ using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -25,23 +27,21 @@ namespace API.Controllers
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<ReaderController> _logger;
|
||||
private readonly IReaderService _readerService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly ICleanupService _cleanupService;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IEventHub _eventHub;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReaderController(ICacheService cacheService,
|
||||
IUnitOfWork unitOfWork, ILogger<ReaderController> logger,
|
||||
IReaderService readerService, IDirectoryService directoryService,
|
||||
ICleanupService cleanupService, IBookmarkService bookmarkService)
|
||||
IReaderService readerService, IBookmarkService bookmarkService,
|
||||
IEventHub eventHub)
|
||||
{
|
||||
_cacheService = cacheService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
_readerService = readerService;
|
||||
_directoryService = directoryService;
|
||||
_cleanupService = cleanupService;
|
||||
_bookmarkService = bookmarkService;
|
||||
_eventHub = eventHub;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -73,6 +73,41 @@ namespace API.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an image for a given bookmark series. Side effect: This will cache the bookmark images for reading.
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="apiKey">Api key for the user the bookmarks are on</param>
|
||||
/// <param name="page"></param>
|
||||
/// <remarks>We must use api key as bookmarks could be leaked to other users via the API</remarks>
|
||||
/// <returns></returns>
|
||||
[HttpGet("bookmark-image")]
|
||||
public async Task<ActionResult> GetBookmarkImage(int seriesId, string apiKey, int page)
|
||||
{
|
||||
if (page < 0) page = 0;
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
var totalPages = await _cacheService.CacheBookmarkForSeries(userId, seriesId);
|
||||
if (page > totalPages)
|
||||
{
|
||||
page = totalPages;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page);
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
|
||||
var format = Path.GetExtension(path).Replace(".", "");
|
||||
|
||||
Response.AddCacheHeader(path, TimeSpan.FromMinutes(10).Seconds);
|
||||
return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_cacheService.CleanupBookmarks(new []{ seriesId });
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
|
||||
/// </summary>
|
||||
@ -81,6 +116,7 @@ namespace API.Controllers
|
||||
[HttpGet("chapter-info")]
|
||||
public async Task<ActionResult<ChapterInfoDto>> GetChapterInfo(int chapterId)
|
||||
{
|
||||
if (chapterId <= 0) return null; // This can happen occasionally from UI, we should just ignore
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
||||
|
||||
@ -104,6 +140,28 @@ namespace API.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns various information about all bookmark files for a Series. Side effect: This will cache the bookmark images for reading.
|
||||
/// </summary>
|
||||
/// <param name="seriesId">Series Id for all bookmarks</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("bookmark-info")]
|
||||
public async Task<ActionResult<BookmarkInfoDto>> GetBookmarkInfo(int seriesId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
var totalPages = await _cacheService.CacheBookmarkForSeries(user.Id, seriesId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.None);
|
||||
|
||||
return Ok(new BookmarkInfoDto()
|
||||
{
|
||||
SeriesName = series.Name,
|
||||
SeriesFormat = series.Format,
|
||||
SeriesId = series.Id,
|
||||
LibraryId = series.LibraryId,
|
||||
Pages = totalPages,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("mark-read")]
|
||||
public async Task<ActionResult> MarkRead(MarkReadDto markReadDto)
|
||||
@ -111,16 +169,12 @@ namespace API.Controllers
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
return BadRequest("There was an issue saving progress");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Marks a Series as Unread (progress)
|
||||
/// </summary>
|
||||
@ -132,15 +186,11 @@ namespace API.Controllers
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
await _readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
return BadRequest("There was an issue saving progress");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks all chapters within a volume as unread
|
||||
/// </summary>
|
||||
@ -514,6 +564,7 @@ namespace API.Controllers
|
||||
|
||||
if (await _bookmarkService.BookmarkPage(user, bookmarkDto, path))
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -533,6 +584,7 @@ namespace API.Controllers
|
||||
|
||||
if (await _bookmarkService.RemoveBookmarkPage(user, bookmarkDto))
|
||||
{
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ using API.DTOs.ReadingLists;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.SignalR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers
|
||||
@ -14,11 +15,13 @@ namespace API.Controllers
|
||||
public class ReadingListController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||
|
||||
public ReadingListController(IUnitOfWork unitOfWork)
|
||||
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -208,12 +211,8 @@ namespace API.Controllers
|
||||
{
|
||||
return BadRequest("A list of this name already exists");
|
||||
}
|
||||
user.ReadingLists.Add(new ReadingList()
|
||||
{
|
||||
Promoted = false,
|
||||
Title = dto.Title,
|
||||
Summary = string.Empty
|
||||
});
|
||||
|
||||
user.ReadingLists.Add(DbFactory.ReadingList(dto.Title, string.Empty, false));
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return BadRequest("There was a problem creating list");
|
||||
|
||||
@ -233,9 +232,12 @@ namespace API.Controllers
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("List does not exist");
|
||||
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.Title))
|
||||
{
|
||||
readingList.Title = dto.Title; // Should I check if this is unique?
|
||||
readingList.NormalizedTitle = Parser.Parser.Normalize(readingList.Title);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(dto.Title))
|
||||
{
|
||||
@ -244,6 +246,19 @@ namespace API.Controllers
|
||||
|
||||
readingList.Promoted = dto.Promoted;
|
||||
|
||||
readingList.CoverImageLocked = dto.CoverImageLocked;
|
||||
|
||||
if (!dto.CoverImageLocked)
|
||||
{
|
||||
readingList.CoverImageLocked = false;
|
||||
readingList.CoverImage = string.Empty;
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
@ -455,14 +470,7 @@ namespace API.Controllers
|
||||
foreach (var chapter in chaptersForSeries)
|
||||
{
|
||||
if (existingChapterExists.Contains(chapter.Id)) continue;
|
||||
|
||||
readingList.Items.Add(new ReadingListItem()
|
||||
{
|
||||
Order = index,
|
||||
ChapterId = chapter.Id,
|
||||
SeriesId = seriesId,
|
||||
VolumeId = chapter.VolumeId
|
||||
});
|
||||
readingList.Items.Add(DbFactory.ReadingListItem(index, seriesId, chapter.VolumeId, chapter.Id));
|
||||
index += 1;
|
||||
}
|
||||
|
||||
|
90
API/Controllers/RecommendedController.cs
Normal file
90
API/Controllers/RecommendedController.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public class RecommendedController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public RecommendedController(IUnitOfWork unitOfWork)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Quick Reads are series that are less than 2K pages in total.
|
||||
/// </summary>
|
||||
/// <param name="libraryId">Library to restrict series to</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("quick-reads")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickReads(int libraryId, [FromQuery] UserParams userParams)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
|
||||
userParams ??= new UserParams();
|
||||
var series = await _unitOfWork.SeriesRepository.GetQuickReads(user.Id, libraryId, userParams);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Highly Rated based on other users ratings. Will pull series with ratings > 4.0, weighted by count of other users.
|
||||
/// </summary>
|
||||
/// <param name="libraryId">Library to restrict series to</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("highly-rated")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
|
||||
userParams ??= new UserParams();
|
||||
var series = await _unitOfWork.SeriesRepository.GetHighlyRated(user.Id, libraryId, userParams);
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series);
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chooses a random genre and shows series that are in that without reading progress
|
||||
/// </summary>
|
||||
/// <param name="libraryId">Library to restrict series to</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("more-in")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams userParams)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
|
||||
userParams ??= new UserParams();
|
||||
var series = await _unitOfWork.SeriesRepository.GetMoreIn(user.Id, libraryId, genreId, userParams);
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Series that are fully read by the user in no particular order
|
||||
/// </summary>
|
||||
/// <param name="libraryId">Library to restrict series to</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("rediscover")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetRediscover(int libraryId, [FromQuery] UserParams userParams)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
|
||||
userParams ??= new UserParams();
|
||||
var series = await _unitOfWork.SeriesRepository.GetRediscover(user.Id, libraryId, userParams);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
}
|
@ -6,8 +6,10 @@ using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
@ -214,13 +216,6 @@ namespace API.Controllers
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId));
|
||||
}
|
||||
|
||||
[HttpPost("recently-added-chapters")]
|
||||
public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChaptersAlt()
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyAddedChapters(userId));
|
||||
}
|
||||
|
||||
[HttpPost("all")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
@ -248,12 +243,8 @@ namespace API.Controllers
|
||||
[HttpPost("on-deck")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetOnDeck(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
// NOTE: This has to be done manually like this due to the DistinctBy requirement
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var results = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, filterDto);
|
||||
|
||||
var listResults = results.DistinctBy(s => s.Name).Skip((userParams.PageNumber - 1) * userParams.PageSize).Take(userParams.PageSize).ToList();
|
||||
var pagedList = new PagedList<SeriesDto>(listResults, listResults.Count, userParams.PageNumber, userParams.PageSize);
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, filterDto);
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, pagedList);
|
||||
|
||||
@ -346,5 +337,105 @@ namespace API.Controllers
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
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>
|
||||
/// Fetches the related series for a given series
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="relation">Type of Relationship to pull back</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("related")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRelatedSeries(int seriesId, RelationKind relation)
|
||||
{
|
||||
// Send back a custom DTO with each type or maybe sorted in some way
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(userId, seriesId, relation));
|
||||
}
|
||||
|
||||
[HttpGet("all-related")]
|
||||
public async Task<ActionResult<RelatedSeriesDto>> GetAllRelatedSeries(int seriesId)
|
||||
{
|
||||
// Send back a custom DTO with each type or maybe sorted in some way
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId));
|
||||
}
|
||||
|
||||
[Authorize(Policy="RequireAdminRole")]
|
||||
[HttpPost("update-related")]
|
||||
public async Task<ActionResult> UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related);
|
||||
|
||||
UpdateRelationForKind(dto.Adaptations, series.Relations.Where(r => r.RelationKind == RelationKind.Adaptation).ToList(), series, RelationKind.Adaptation);
|
||||
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");
|
||||
}
|
||||
|
||||
private void UpdateRelationForKind(IList<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Theme;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using Kavita.Common;
|
||||
@ -13,13 +14,13 @@ namespace API.Controllers;
|
||||
public class ThemeController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ISiteThemeService _siteThemeService;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
|
||||
public ThemeController(IUnitOfWork unitOfWork, ISiteThemeService siteThemeService, ITaskScheduler taskScheduler)
|
||||
public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, ITaskScheduler taskScheduler)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_siteThemeService = siteThemeService;
|
||||
_themeService = themeService;
|
||||
_taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
@ -39,9 +40,9 @@ public class ThemeController : BaseApiController
|
||||
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpPost("update-default")]
|
||||
public async Task<ActionResult> UpdateDefault(UpdateDefaultSiteThemeDto dto)
|
||||
public async Task<ActionResult> UpdateDefault(UpdateDefaultThemeDto dto)
|
||||
{
|
||||
await _siteThemeService.UpdateDefault(dto.ThemeId);
|
||||
await _themeService.UpdateDefault(dto.ThemeId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -54,7 +55,7 @@ public class ThemeController : BaseApiController
|
||||
{
|
||||
try
|
||||
{
|
||||
return Ok(await _siteThemeService.GetContent(themeId));
|
||||
return Ok(await _themeService.GetContent(themeId));
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ using API.Data;
|
||||
using API.DTOs.Uploads;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using Flurl.Http;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -24,16 +25,18 @@ namespace API.Controllers
|
||||
private readonly ILogger<UploadController> _logger;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IEventHub _eventHub;
|
||||
|
||||
/// <inheritdoc />
|
||||
public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger<UploadController> logger,
|
||||
ITaskScheduler taskScheduler, IDirectoryService directoryService)
|
||||
ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_imageService = imageService;
|
||||
_logger = logger;
|
||||
_taskScheduler = taskScheduler;
|
||||
_directoryService = directoryService;
|
||||
_eventHub = eventHub;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -99,6 +102,8 @@ namespace API.Controllers
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false);
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
@ -145,6 +150,8 @@ namespace API.Controllers
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@ -158,6 +165,53 @@ namespace API.Controllers
|
||||
return BadRequest("Unable to save cover image to Collection Tag");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces reading list cover image and locks it with a base64 encoded image
|
||||
/// </summary>
|
||||
/// <param name="uploadFileDto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[RequestSizeLimit(8_000_000)]
|
||||
[HttpPost("reading-list")]
|
||||
public async Task<ActionResult> UploadReadingListCoverImageFromUrl(UploadFileDto uploadFileDto)
|
||||
{
|
||||
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
|
||||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
readingList.CoverImage = filePath;
|
||||
readingList.CoverImageLocked = true;
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "There was an issue uploading cover image for Reading List {Id}", uploadFileDto.Id);
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Reading List");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
|
||||
/// </summary>
|
||||
@ -193,6 +247,10 @@ namespace API.Controllers
|
||||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(chapter.VolumeId, MessageFactoryEntityTypes.Volume), false);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
|
||||
MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -82,11 +82,13 @@ namespace API.Controllers
|
||||
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
|
||||
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
|
||||
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
|
||||
existingPreferences.BookReaderDarkMode = preferencesDto.BookReaderDarkMode;
|
||||
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
|
||||
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
|
||||
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
||||
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
||||
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
|
||||
existingPreferences.PageLayoutMode = preferencesDto.BookReaderLayoutMode;
|
||||
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
|
||||
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
|
||||
|
||||
// TODO: Remove this code - this overrides layout mode to be single until the mode is released
|
||||
|
@ -6,7 +6,7 @@ namespace API.DTOs.Account;
|
||||
public class InviteUserDto
|
||||
{
|
||||
[Required]
|
||||
public string Email { get; init; }
|
||||
public string Email { get; set; }
|
||||
/// <summary>
|
||||
/// 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.
|
||||
|
@ -99,6 +99,5 @@ namespace API.DTOs.Filtering
|
||||
/// An optional name string to filter by. Empty string will ignore.
|
||||
/// </summary>
|
||||
public string SeriesNameQuery { get; init; } = string.Empty;
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -5,4 +5,5 @@ public enum SortField
|
||||
SortName = 1,
|
||||
CreatedDate = 2,
|
||||
LastModifiedDate = 3,
|
||||
LastChapterAdded = 4
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ namespace API.DTOs
|
||||
{
|
||||
public class MangaFileDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string FilePath { get; init; }
|
||||
public int Pages { get; init; }
|
||||
public MangaFormat Format { get; init; }
|
||||
|
13
API/DTOs/Reader/BookmarkInfoDto.cs
Normal file
13
API/DTOs/Reader/BookmarkInfoDto.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.Reader;
|
||||
|
||||
public class BookmarkInfoDto
|
||||
{
|
||||
public string SeriesName { get; set; }
|
||||
public MangaFormat SeriesFormat { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
public LibraryType LibraryType { get; set; }
|
||||
public int Pages { get; set; }
|
||||
}
|
@ -9,5 +9,6 @@
|
||||
/// Reading lists that are promoted are only done by admins
|
||||
/// </summary>
|
||||
public bool Promoted { get; set; }
|
||||
public bool CoverImageLocked { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -6,5 +6,6 @@
|
||||
public string Title { get; set; }
|
||||
public string Summary { get; set; }
|
||||
public bool Promoted { get; set; }
|
||||
public bool CoverImageLocked { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -17,5 +17,8 @@ public class SearchResultGroupDto
|
||||
public IEnumerable<PersonDto> Persons { get; set; }
|
||||
public IEnumerable<GenreTagDto> Genres { get; set; }
|
||||
public IEnumerable<TagDto> Tags { get; set; }
|
||||
public IEnumerable<MangaFileDto> Files { get; set; }
|
||||
public IEnumerable<ChapterDto> Chapters { get; set; }
|
||||
|
||||
|
||||
}
|
||||
|
25
API/DTOs/SeriesDetail/RelatedSeriesDto.cs
Normal file
25
API/DTOs/SeriesDetail/RelatedSeriesDto.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.SeriesDetail;
|
||||
|
||||
public class RelatedSeriesDto
|
||||
{
|
||||
/// <summary>
|
||||
/// The parent relationship Series
|
||||
/// </summary>
|
||||
public int SourceSeriesId { get; set; }
|
||||
|
||||
public IEnumerable<SeriesDto> Sequels { get; set; }
|
||||
public IEnumerable<SeriesDto> Prequels { get; set; }
|
||||
public IEnumerable<SeriesDto> SpinOffs { get; set; }
|
||||
public IEnumerable<SeriesDto> Adaptations { get; set; }
|
||||
public IEnumerable<SeriesDto> SideStories { get; set; }
|
||||
public IEnumerable<SeriesDto> Characters { get; set; }
|
||||
public IEnumerable<SeriesDto> Contains { get; set; }
|
||||
public IEnumerable<SeriesDto> Others { get; set; }
|
||||
public IEnumerable<SeriesDto> AlternativeSettings { get; set; }
|
||||
public IEnumerable<SeriesDto> AlternativeVersions { get; set; }
|
||||
public IEnumerable<SeriesDto> Doujinshis { get; set; }
|
||||
public IEnumerable<SeriesDto> Parent { get; set; }
|
||||
}
|
19
API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs
Normal file
19
API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs.SeriesDetail;
|
||||
|
||||
public class UpdateRelatedSeriesDto
|
||||
{
|
||||
public int SeriesId { get; set; }
|
||||
public IList<int> Adaptations { get; set; }
|
||||
public IList<int> Characters { get; set; }
|
||||
public IList<int> Contains { get; set; }
|
||||
public IList<int> Others { get; set; }
|
||||
public IList<int> Prequels { get; set; }
|
||||
public IList<int> Sequels { get; set; }
|
||||
public IList<int> SideStories { get; set; }
|
||||
public IList<int> SpinOffs { get; set; }
|
||||
public IList<int> AlternativeSettings { get; set; }
|
||||
public IList<int> AlternativeVersions { get; set; }
|
||||
public IList<int> Doujinshis { get; set; }
|
||||
}
|
@ -22,6 +22,10 @@ namespace API.DTOs
|
||||
/// </summary>
|
||||
public DateTime LatestReadDate { get; set; }
|
||||
/// <summary>
|
||||
/// DateTime representing last time a chapter was added to the Series
|
||||
/// </summary>
|
||||
public DateTime LastChapterAdded { get; set; }
|
||||
/// <summary>
|
||||
/// Rating from logged in user. Calculated at API-time.
|
||||
/// </summary>
|
||||
public int UserRating { get; set; }
|
||||
|
@ -45,11 +45,11 @@ namespace API.DTOs
|
||||
/// </summary>
|
||||
public string Language { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Number in the TotalCount of issues
|
||||
/// Max number of issues/volumes in the series (Max of Volume/Issue field in ComicInfo)
|
||||
/// </summary>
|
||||
public int Count { get; set; }
|
||||
public int MaxCount { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// Total number of issues for the series
|
||||
/// Total number of issues/volumes for the series
|
||||
/// </summary>
|
||||
public int TotalCount { get; set; }
|
||||
/// <summary>
|
||||
@ -69,16 +69,16 @@ namespace API.DTOs
|
||||
public bool PublicationStatusLocked { get; set; }
|
||||
public bool GenresLocked { get; set; }
|
||||
public bool TagsLocked { get; set; }
|
||||
public bool WriterLocked { get; set; }
|
||||
public bool CharacterLocked { get; set; }
|
||||
public bool ColoristLocked { get; set; }
|
||||
public bool EditorLocked { get; set; }
|
||||
public bool InkerLocked { get; set; }
|
||||
public bool LettererLocked { get; set; }
|
||||
public bool PencillerLocked { get; set; }
|
||||
public bool PublisherLocked { get; set; }
|
||||
public bool TranslatorLocked { get; set; }
|
||||
public bool CoverArtistLocked { get; set; }
|
||||
public bool WritersLocked { get; set; }
|
||||
public bool CharactersLocked { get; set; }
|
||||
public bool ColoristsLocked { get; set; }
|
||||
public bool EditorsLocked { get; set; }
|
||||
public bool InkersLocked { get; set; }
|
||||
public bool LetterersLocked { get; set; }
|
||||
public bool PencillersLocked { get; set; }
|
||||
public bool PublishersLocked { get; set; }
|
||||
public bool TranslatorsLocked { get; set; }
|
||||
public bool CoverArtistsLocked { get; set; }
|
||||
|
||||
|
||||
public int SeriesId { get; set; }
|
||||
|
@ -4,6 +4,9 @@ using API.Services;
|
||||
|
||||
namespace API.DTOs.Theme;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a set of css overrides the user can upload to Kavita and will load into webui
|
||||
/// </summary>
|
||||
public class SiteThemeDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
@ -1,6 +1,6 @@
|
||||
namespace API.DTOs.Theme;
|
||||
|
||||
public class UpdateDefaultSiteThemeDto
|
||||
public class UpdateDefaultThemeDto
|
||||
{
|
||||
public int ThemeId { get; set; }
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using API.Entities;
|
||||
using API.DTOs.Theme;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs
|
||||
@ -74,5 +75,12 @@ namespace API.DTOs
|
||||
/// </summary>
|
||||
/// <remarks>Should default to Dark</remarks>
|
||||
public SiteTheme Theme { get; set; }
|
||||
public string BookReaderThemeName { get; set; }
|
||||
public BookPageLayoutMode BookReaderLayoutMode { get; set; }
|
||||
/// <summary>
|
||||
/// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this.
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to false</remarks>
|
||||
public bool BookReaderImmersiveMode { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ namespace API.Data
|
||||
public DbSet<Genre> Genre { get; set; }
|
||||
public DbSet<Tag> Tag { get; set; }
|
||||
public DbSet<SiteTheme> SiteTheme { get; set; }
|
||||
public DbSet<SeriesRelation> SeriesRelation { get; set; }
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
@ -59,10 +60,28 @@ namespace API.Data
|
||||
.WithOne(u => u.Role)
|
||||
.HasForeignKey(ur => ur.RoleId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Entity<SeriesRelation>()
|
||||
.HasOne(pt => pt.Series)
|
||||
.WithMany(p => p.Relations)
|
||||
.HasForeignKey(pt => pt.SeriesId)
|
||||
.OnDelete(DeleteBehavior.ClientCascade);
|
||||
|
||||
builder.Entity<SeriesRelation>()
|
||||
.HasOne(pt => pt.TargetSeries)
|
||||
.WithMany(t => t.RelationOf)
|
||||
.HasForeignKey(pt => pt.TargetSeriesId);
|
||||
|
||||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.BookThemeName)
|
||||
.HasDefaultValue("Dark");
|
||||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.BackgroundColor)
|
||||
.HasDefaultValue("#000000");
|
||||
}
|
||||
|
||||
|
||||
void OnEntityTracked(object sender, EntityTrackedEventArgs e)
|
||||
static void OnEntityTracked(object sender, EntityTrackedEventArgs e)
|
||||
{
|
||||
if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity)
|
||||
{
|
||||
@ -72,7 +91,7 @@ namespace API.Data
|
||||
|
||||
}
|
||||
|
||||
void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
|
||||
static void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
|
||||
{
|
||||
if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity)
|
||||
entity.LastModified = DateTime.Now;
|
||||
|
@ -35,7 +35,7 @@ namespace API.Data
|
||||
return new Volume()
|
||||
{
|
||||
Name = volumeNumber,
|
||||
Number = (int) Parser.Parser.MinimumNumberFromRange(volumeNumber),
|
||||
Number = (int) Parser.Parser.MinNumberFromRange(volumeNumber),
|
||||
Chapters = new List<Chapter>()
|
||||
};
|
||||
}
|
||||
@ -46,7 +46,7 @@ namespace API.Data
|
||||
var specialTitle = specialTreatment ? info.Filename : info.Chapters;
|
||||
return new Chapter()
|
||||
{
|
||||
Number = specialTreatment ? "0" : Parser.Parser.MinimumNumberFromRange(info.Chapters) + string.Empty,
|
||||
Number = specialTreatment ? "0" : Parser.Parser.MinNumberFromRange(info.Chapters) + string.Empty,
|
||||
Range = specialTreatment ? info.Filename : info.Chapters,
|
||||
Title = (specialTreatment && info.Format == MangaFormat.Epub)
|
||||
? info.Title
|
||||
@ -82,6 +82,29 @@ namespace API.Data
|
||||
};
|
||||
}
|
||||
|
||||
public static ReadingList ReadingList(string title, string summary, bool promoted)
|
||||
{
|
||||
return new ReadingList()
|
||||
{
|
||||
NormalizedTitle = API.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
|
||||
Title = title?.Trim(),
|
||||
Summary = summary?.Trim(),
|
||||
Promoted = promoted,
|
||||
Items = new List<ReadingListItem>()
|
||||
};
|
||||
}
|
||||
|
||||
public static ReadingListItem ReadingListItem(int index, int seriesId, int volumeId, int chapterId)
|
||||
{
|
||||
return new ReadingListItem()
|
||||
{
|
||||
Order = index,
|
||||
ChapterId = chapterId,
|
||||
SeriesId = seriesId,
|
||||
VolumeId = volumeId
|
||||
};
|
||||
}
|
||||
|
||||
public static Genre Genre(string name, bool external)
|
||||
{
|
||||
return new Genre()
|
||||
|
@ -14,6 +14,10 @@ namespace API.Data.Metadata
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Series { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Localized Series name. Not standard.
|
||||
/// </summary>
|
||||
public string LocalizedSeries { get; set; } = string.Empty;
|
||||
public string SeriesSort { get; set; } = string.Empty;
|
||||
public string Number { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
@ -47,11 +51,11 @@ namespace API.Data.Metadata
|
||||
/// </summary>
|
||||
public float UserRating { get; set; }
|
||||
|
||||
public string AlternateSeries { get; set; } = string.Empty;
|
||||
public string StoryArc { get; set; } = string.Empty;
|
||||
public string SeriesGroup { get; set; } = string.Empty;
|
||||
public string AlternativeSeries { get; set; } = string.Empty;
|
||||
public string AlternativeNumber { get; set; } = string.Empty;
|
||||
public string AlternateNumber { get; set; } = string.Empty;
|
||||
public int AlternateCount { get; set; } = 0;
|
||||
public string AlternateSeries { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// This is Epub only: calibre:title_sort
|
||||
@ -94,6 +98,10 @@ namespace API.Data.Metadata
|
||||
{
|
||||
if (info == null) return;
|
||||
|
||||
info.Series = info.Series.Trim();
|
||||
info.SeriesSort = info.SeriesSort.Trim();
|
||||
info.LocalizedSeries = info.LocalizedSeries.Trim();
|
||||
|
||||
info.Writer = Parser.Parser.CleanAuthor(info.Writer);
|
||||
info.Colorist = Parser.Parser.CleanAuthor(info.Colorist);
|
||||
info.Editor = Parser.Parser.CleanAuthor(info.Editor);
|
||||
|
56
API/Data/MigrateRemoveExtraThemes.cs
Normal file
56
API/Data/MigrateRemoveExtraThemes.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Services.Tasks;
|
||||
|
||||
namespace API.Data;
|
||||
|
||||
/// <summary>
|
||||
/// In v0.5.3, we removed Light and E-Ink themes. This migration will remove the themes from the DB and default anyone on
|
||||
/// null, E-Ink, or Light to Dark.
|
||||
/// </summary>
|
||||
public static class MigrateRemoveExtraThemes
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, IThemeService themeService)
|
||||
{
|
||||
Console.WriteLine("Removing Dark and E-Ink themes");
|
||||
|
||||
var themes = (await unitOfWork.SiteThemeRepository.GetThemes()).ToList();
|
||||
|
||||
if (themes.FirstOrDefault(t => t.Name.Equals("Light")) == null)
|
||||
{
|
||||
Console.WriteLine("Done. Nothing to do");
|
||||
return;
|
||||
}
|
||||
|
||||
var darkTheme = themes.Single(t => t.Name.Equals("Dark"));
|
||||
var lightTheme = themes.Single(t => t.Name.Equals("Light"));
|
||||
var eInkTheme = themes.Single(t => t.Name.Equals("E-Ink"));
|
||||
|
||||
|
||||
|
||||
// Update default theme if it's not Dark or a custom theme
|
||||
await themeService.UpdateDefault(darkTheme.Id);
|
||||
|
||||
// Update all users to Dark theme if they are on Light/E-Ink
|
||||
foreach (var pref in await unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(lightTheme.Id))
|
||||
{
|
||||
pref.Theme = darkTheme;
|
||||
}
|
||||
foreach (var pref in await unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(eInkTheme.Id))
|
||||
{
|
||||
pref.Theme = darkTheme;
|
||||
}
|
||||
|
||||
// Remove Light/E-Ink themes
|
||||
foreach (var siteTheme in themes.Where(t => t.Name.Equals("Light") || t.Name.Equals("E-Ink")))
|
||||
{
|
||||
unitOfWork.SiteThemeRepository.Remove(siteTheme);
|
||||
}
|
||||
// Commit and call it a day
|
||||
await unitOfWork.CommitAsync();
|
||||
|
||||
Console.WriteLine("Completed removing Dark and E-Ink themes");
|
||||
}
|
||||
|
||||
}
|
1469
API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs
generated
Normal file
1469
API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class SeriesLastChapterAddedAndReadingListNormalization : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastChapterAdded",
|
||||
table: "Series",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CoverImage",
|
||||
table: "ReadingList",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "CoverImageLocked",
|
||||
table: "ReadingList",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "NormalizedTitle",
|
||||
table: "ReadingList",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastChapterAdded",
|
||||
table: "Series");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CoverImage",
|
||||
table: "ReadingList");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CoverImageLocked",
|
||||
table: "ReadingList");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "NormalizedTitle",
|
||||
table: "ReadingList");
|
||||
}
|
||||
}
|
||||
}
|
1466
API/Data/Migrations/20220416211340_RemoveCustomIndex.Designer.cs
generated
Normal file
1466
API/Data/Migrations/20220416211340_RemoveCustomIndex.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
API/Data/Migrations/20220416211340_RemoveCustomIndex.cs
Normal file
25
API/Data/Migrations/20220416211340_RemoveCustomIndex.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class RemoveCustomIndex : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Series_Name_NormalizedName_LocalizedName_LibraryId_Format",
|
||||
table: "Series");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Series_Name_NormalizedName_LocalizedName_LibraryId_Format",
|
||||
table: "Series",
|
||||
columns: new[] { "Name", "NormalizedName", "LocalizedName", "LibraryId", "Format" },
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
1513
API/Data/Migrations/20220421214448_SeriesRelations.Designer.cs
generated
Normal file
1513
API/Data/Migrations/20220421214448_SeriesRelations.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
API/Data/Migrations/20220421214448_SeriesRelations.cs
Normal file
55
API/Data/Migrations/20220421214448_SeriesRelations.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class SeriesRelations : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SeriesRelation",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
RelationKind = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
TargetSeriesId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SeriesId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SeriesRelation", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesRelation_Series_SeriesId",
|
||||
column: x => x.SeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_SeriesRelation_Series_TargetSeriesId",
|
||||
column: x => x.TargetSeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesRelation_SeriesId",
|
||||
table: "SeriesRelation",
|
||||
column: "SeriesId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SeriesRelation_TargetSeriesId",
|
||||
table: "SeriesRelation",
|
||||
column: "TargetSeriesId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SeriesRelation");
|
||||
}
|
||||
}
|
||||
}
|
1513
API/Data/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs
generated
Normal file
1513
API/Data/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,48 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class ChangeCountToTotalCount : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_SeriesRelation_Series_SeriesId",
|
||||
table: "SeriesRelation");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Count",
|
||||
table: "SeriesMetadata",
|
||||
newName: "TotalCount");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_SeriesRelation_Series_SeriesId",
|
||||
table: "SeriesRelation",
|
||||
column: "SeriesId",
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_SeriesRelation_Series_SeriesId",
|
||||
table: "SeriesRelation");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "TotalCount",
|
||||
table: "SeriesMetadata",
|
||||
newName: "Count");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_SeriesRelation_Series_SeriesId",
|
||||
table: "SeriesRelation",
|
||||
column: "SeriesId",
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
1516
API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs
generated
Normal file
1516
API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class AddMaxCountToSeriesMetadata : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MaxCount",
|
||||
table: "SeriesMetadata",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxCount",
|
||||
table: "SeriesMetadata");
|
||||
}
|
||||
}
|
||||
}
|
1523
API/Data/Migrations/20220508162841_BookReaderUpdate.Designer.cs
generated
Normal file
1523
API/Data/Migrations/20220508162841_BookReaderUpdate.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
API/Data/Migrations/20220508162841_BookReaderUpdate.cs
Normal file
56
API/Data/Migrations/20220508162841_BookReaderUpdate.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class BookReaderUpdate : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "BookReaderDarkMode",
|
||||
table: "AppUserPreferences",
|
||||
newName: "PageLayoutMode");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "BackgroundColor",
|
||||
table: "AppUserPreferences",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
defaultValue: "#000000",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "BookThemeName",
|
||||
table: "AppUserPreferences",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
defaultValue: "Dark");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BookThemeName",
|
||||
table: "AppUserPreferences");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "PageLayoutMode",
|
||||
table: "AppUserPreferences",
|
||||
newName: "BookReaderDarkMode");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "BackgroundColor",
|
||||
table: "AppUserPreferences",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true,
|
||||
oldDefaultValue: "#000000");
|
||||
}
|
||||
}
|
||||
}
|
1526
API/Data/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs
generated
Normal file
1526
API/Data/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class BookReaderImmersiveMode : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "BookReaderImmersiveMode",
|
||||
table: "AppUserPreferences",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BookReaderImmersiveMode",
|
||||
table: "AppUserPreferences");
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.2");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.4");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
@ -166,10 +166,9 @@ namespace API.Data.Migrations
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BackgroundColor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("BookReaderDarkMode")
|
||||
.HasColumnType("INTEGER");
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("#000000");
|
||||
|
||||
b.Property<string>("BookReaderFontFamily")
|
||||
.HasColumnType("TEXT");
|
||||
@ -177,6 +176,9 @@ namespace API.Data.Migrations
|
||||
b.Property<int>("BookReaderFontSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("BookReaderImmersiveMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookReaderLineSpacing")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@ -189,9 +191,17 @@ namespace API.Data.Migrations
|
||||
b.Property<bool>("BookReaderTapToPaginate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BookThemeName")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("Dark");
|
||||
|
||||
b.Property<int>("LayoutMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PageLayoutMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PageSplitOption")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@ -520,9 +530,6 @@ namespace API.Data.Migrations
|
||||
b.Property<bool>("ColoristLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Count")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("CoverArtistLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@ -544,6 +551,9 @@ namespace API.Data.Migrations
|
||||
b.Property<bool>("LettererLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("PencillerLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@ -575,6 +585,9 @@ namespace API.Data.Migrations
|
||||
b.Property<bool>("TagsLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TotalCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("TranslatorLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@ -592,6 +605,30 @@ namespace API.Data.Migrations
|
||||
b.ToTable("SeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("RelationKind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TargetSeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.HasIndex("TargetSeriesId");
|
||||
|
||||
b.ToTable("SeriesRelation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Person", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@ -621,12 +658,21 @@ namespace API.Data.Migrations
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CoverImage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("CoverImageLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Promoted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@ -695,6 +741,9 @@ namespace API.Data.Migrations
|
||||
b.Property<int>("Format")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastChapterAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -732,9 +781,6 @@ namespace API.Data.Migrations
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
@ -1173,6 +1219,25 @@ namespace API.Data.Migrations
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
.WithMany("Relations")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.ClientCascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Series", "TargetSeries")
|
||||
.WithMany("RelationOf")
|
||||
.HasForeignKey("TargetSeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Series");
|
||||
|
||||
b.Navigation("TargetSeries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingList", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
@ -1442,6 +1507,10 @@ namespace API.Data.Migrations
|
||||
|
||||
b.Navigation("Ratings");
|
||||
|
||||
b.Navigation("RelationOf");
|
||||
|
||||
b.Navigation("Relations");
|
||||
|
||||
b.Navigation("Volumes");
|
||||
});
|
||||
|
||||
|
@ -84,7 +84,6 @@ public class CollectionTagRepository : ICollectionTagRepository
|
||||
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
.Select(c => c)
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
|
@ -26,6 +26,8 @@ public interface IReadingListRepository
|
||||
void BulkRemove(IEnumerable<ReadingListItem> items);
|
||||
void Update(ReadingList list);
|
||||
Task<int> Count();
|
||||
Task<string> GetCoverImageAsync(int readingListId);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
}
|
||||
|
||||
public class ReadingListRepository : IReadingListRepository
|
||||
@ -49,6 +51,24 @@ public class ReadingListRepository : IReadingListRepository
|
||||
return await _context.ReadingList.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GetCoverImageAsync(int readingListId)
|
||||
{
|
||||
return await _context.ReadingList
|
||||
.Where(c => c.Id == readingListId)
|
||||
.Select(c => c.CoverImage)
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<string>> GetAllCoverImagesAsync()
|
||||
{
|
||||
return await _context.ReadingList
|
||||
.Select(t => t.CoverImage)
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public void Remove(ReadingListItem item)
|
||||
{
|
||||
_context.ReadingListItem.Remove(item);
|
||||
|
@ -11,6 +11,7 @@ using API.DTOs.Filtering;
|
||||
using API.DTOs.Metadata;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.DTOs.Search;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
@ -20,10 +21,23 @@ using API.Services.Tasks;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Kavita.Common.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SQLitePCL;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
[Flags]
|
||||
public enum SeriesIncludes
|
||||
{
|
||||
None = 1,
|
||||
Volumes = 2,
|
||||
Metadata = 4,
|
||||
Related = 8,
|
||||
//Related = 16,
|
||||
//UserPreferences = 32
|
||||
}
|
||||
|
||||
internal class RecentlyAddedSeries
|
||||
{
|
||||
public int LibraryId { get; init; }
|
||||
@ -68,7 +82,7 @@ public interface ISeriesRepository
|
||||
Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId);
|
||||
Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId);
|
||||
Task<bool> DeleteSeriesAsync(int seriesId);
|
||||
Task<Series> GetSeriesByIdAsync(int seriesId);
|
||||
Task<Series> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata);
|
||||
Task<IList<Series>> GetSeriesByIdsAsync(IList<int> seriesIds);
|
||||
Task<int[]> GetChapterIdsForSeriesAsync(IList<int> seriesIds);
|
||||
Task<IDictionary<int, IList<int>>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds);
|
||||
@ -80,7 +94,7 @@ public interface ISeriesRepository
|
||||
/// <returns></returns>
|
||||
Task AddSeriesModifiers(int userId, List<SeriesDto> series);
|
||||
Task<string> GetSeriesCoverImageAsync(int seriesId);
|
||||
Task<IEnumerable<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter, bool cutoffOnDate = true);
|
||||
Task<PagedList<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter);
|
||||
Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter);
|
||||
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
||||
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
||||
@ -92,11 +106,18 @@ public interface ISeriesRepository
|
||||
Task<Series> GetFullSeriesForSeriesIdAsync(int seriesId);
|
||||
Task<Chunk> GetChunkInfo(int libraryId = 0);
|
||||
Task<IList<SeriesMetadata>> GetSeriesMetadataForIdsAsync(IEnumerable<int> seriesIds);
|
||||
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
|
||||
Task<IList<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId);
|
||||
Task<IList<RecentlyAddedItemDto>> GetRecentlyAddedChapters(int userId);
|
||||
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds); // TODO: Move to LibraryRepository
|
||||
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds); // TODO: Move to LibraryRepository
|
||||
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds); // TODO: Move to LibraryRepository
|
||||
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
|
||||
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
||||
Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind);
|
||||
Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams);
|
||||
Task<PagedList<SeriesDto>> GetHighlyRated(int userId, int libraryId, UserParams userParams);
|
||||
Task<PagedList<SeriesDto>> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams);
|
||||
Task<PagedList<SeriesDto>> GetRediscover(int userId, int libraryId, UserParams userParams);
|
||||
Task<SeriesDto> GetSeriesForMangaFile(int mangaFileId, int userId);
|
||||
Task<SeriesDto> GetSeriesForChapter(int chapterId, int userId);
|
||||
}
|
||||
|
||||
public class SeriesRepository : ISeriesRepository
|
||||
@ -232,7 +253,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
/// <summary>
|
||||
/// Gets all series
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="libraryId">Restricts to just one library</param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <param name="filter"></param>
|
||||
@ -269,7 +290,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
|
||||
public async Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, int[] libraryIds, string searchQuery)
|
||||
{
|
||||
|
||||
const int maxRecords = 15;
|
||||
var result = new SearchResultGroupDto();
|
||||
var searchQueryNormalized = Parser.Parser.Normalize(searchQuery);
|
||||
|
||||
@ -283,6 +304,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
.Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%"))
|
||||
.OrderBy(l => l.Name)
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
@ -290,7 +312,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
var hasYearInQuery = !string.IsNullOrEmpty(justYear);
|
||||
var yearComparison = hasYearInQuery ? int.Parse(justYear) : 0;
|
||||
|
||||
result.Series = await _context.Series
|
||||
result.Series = _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%")
|
||||
|| EF.Functions.Like(s.OriginalName, $"%{searchQuery}%")
|
||||
@ -301,14 +323,16 @@ public class SeriesRepository : ISeriesRepository
|
||||
.OrderBy(s => s.SortName)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<SearchResultDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
.AsEnumerable();
|
||||
|
||||
|
||||
result.ReadingLists = await _context.ReadingList
|
||||
.Where(rl => rl.AppUserId == userId || rl.Promoted)
|
||||
.Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%"))
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
@ -318,6 +342,8 @@ public class SeriesRepository : ISeriesRepository
|
||||
.Where(s => s.Promoted || isAdmin)
|
||||
.OrderBy(s => s.Title)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
@ -326,6 +352,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||
.SelectMany(sm => sm.People.Where(t => EF.Functions.Like(t.Name, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.Distinct()
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
@ -336,6 +363,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
.AsSplitQuery()
|
||||
.OrderBy(t => t.Title)
|
||||
.Distinct()
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
@ -345,9 +373,34 @@ public class SeriesRepository : ISeriesRepository
|
||||
.AsSplitQuery()
|
||||
.OrderBy(t => t.Title)
|
||||
.Distinct()
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
var fileIds = _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.AsSplitQuery()
|
||||
.SelectMany(s => s.Volumes)
|
||||
.SelectMany(v => v.Chapters)
|
||||
.SelectMany(c => c.Files.Select(f => f.Id));
|
||||
|
||||
result.Files = await _context.MangaFile
|
||||
.Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id))
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<MangaFileDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
result.Chapters = await _context.Chapter
|
||||
.Include(c => c.Files)
|
||||
.Where(c => EF.Functions.Like(c.TitleName, $"%{searchQuery}%"))
|
||||
.Where(c => c.Files.All(f => fileIds.Contains(f.Id)))
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<ChapterDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -377,19 +430,37 @@ public class SeriesRepository : ISeriesRepository
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Series> GetSeriesByIdAsync(int seriesId)
|
||||
public async Task<Series> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata)
|
||||
{
|
||||
return await _context.Series
|
||||
.Include(s => s.Volumes)
|
||||
.Include(s => s.Metadata)
|
||||
var query = _context.Series
|
||||
.Where(s => s.Id == seriesId)
|
||||
.AsSplitQuery();
|
||||
|
||||
if (includes.HasFlag(SeriesIncludes.Volumes))
|
||||
{
|
||||
query = query.Include(s => s.Volumes);
|
||||
}
|
||||
|
||||
if (includes.HasFlag(SeriesIncludes.Related))
|
||||
{
|
||||
query = query.Include(s => s.Relations)
|
||||
.ThenInclude(r => r.TargetSeries)
|
||||
.Include(s => s.RelationOf);
|
||||
}
|
||||
|
||||
if (includes.HasFlag(SeriesIncludes.Metadata))
|
||||
{
|
||||
query = query.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.CollectionTags)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Genres)
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.People)
|
||||
.Where(s => s.Id == seriesId)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync();
|
||||
.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.Tags);
|
||||
}
|
||||
|
||||
return await query.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -597,55 +668,48 @@ public class SeriesRepository : ISeriesRepository
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Series that the user has some partial progress on. Sorts based on activity. Sort first by User progress, but if a series
|
||||
/// has been updated recently, bump it to the front.
|
||||
/// Returns Series that the user has some partial progress on. Sorts based on activity. Sort first by User progress, then
|
||||
/// by when chapters have been added to series. Restricts progress in the past 30 days and chapters being added to last 7.
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="libraryId">Library to restrict to, if 0, will apply to all libraries</param>
|
||||
/// <param name="userParams">Pagination information</param>
|
||||
/// <param name="filter">Optional (default null) filter on query</param>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter, bool cutoffOnDate = true)
|
||||
public async Task<PagedList<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter)
|
||||
{
|
||||
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
|
||||
var query = (await CreateFilteredSearchQueryable(userId, libraryId, filter))
|
||||
.Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) =>
|
||||
new
|
||||
var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(7);
|
||||
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
|
||||
|
||||
var query = _context.Series
|
||||
.Where(s => usersSeriesIds.Contains(s.Id))
|
||||
.Select(s => new
|
||||
{
|
||||
Series = s,
|
||||
PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId)
|
||||
PagesRead = _context.AppUserProgresses.Where(p => p.SeriesId == s.Id && p.AppUserId == userId)
|
||||
.Sum(s1 => s1.PagesRead),
|
||||
progress.AppUserId,
|
||||
LastReadingProgress = _context.AppUserProgresses
|
||||
.Where(p => p.Id == progress.Id && p.AppUserId == userId)
|
||||
LatestReadDate = _context.AppUserProgresses
|
||||
.Where(p => p.SeriesId == s.Id && p.AppUserId == userId)
|
||||
.Max(p => p.LastModified),
|
||||
// This is only taking into account chapters that have progress on them, not all chapters in said series
|
||||
//LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created),
|
||||
LastChapterCreated = s.Volumes.SelectMany(v => v.Chapters).Max(c => c.Created)
|
||||
});
|
||||
if (cutoffOnDate)
|
||||
{
|
||||
query = query.Where(d => d.LastReadingProgress >= cutoffProgressPoint || d.LastChapterCreated >= cutoffProgressPoint);
|
||||
}
|
||||
|
||||
// I think I need another Join statement. The problem is the chapters are still limited to progress
|
||||
|
||||
|
||||
|
||||
var retSeries = query.Where(s => s.AppUserId == userId
|
||||
&& s.PagesRead > 0
|
||||
s.LastChapterAdded,
|
||||
})
|
||||
.Where(s => s.PagesRead > 0
|
||||
&& s.PagesRead < s.Series.Pages)
|
||||
.OrderByDescending(s => s.LastReadingProgress)
|
||||
.ThenByDescending(s => s.LastChapterCreated)
|
||||
.Where(d => d.LatestReadDate >= cutoffProgressPoint || d.LastChapterAdded >= cutoffLastAddedPoint).OrderByDescending(s => s.LatestReadDate)
|
||||
.ThenByDescending(s => s.LastChapterAdded)
|
||||
.Select(s => s.Series)
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking();
|
||||
|
||||
// Pagination does not work for this query as when we pull the data back, we get multiple rows of the same series. See controller for pagination code
|
||||
return await retSeries.ToListAsync();
|
||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
|
||||
private async Task<IQueryable<Series>> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter)
|
||||
{
|
||||
var userLibraries = await GetUserLibraries(libraryId, userId);
|
||||
@ -687,6 +751,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
SortField.SortName => query.OrderBy(s => s.SortName),
|
||||
SortField.CreatedDate => query.OrderBy(s => s.Created),
|
||||
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
|
||||
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
||||
_ => query
|
||||
};
|
||||
}
|
||||
@ -697,6 +762,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
SortField.SortName => query.OrderByDescending(s => s.SortName),
|
||||
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
|
||||
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
|
||||
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
||||
_ => query
|
||||
};
|
||||
}
|
||||
@ -888,106 +954,37 @@ public class SeriesRepository : ISeriesRepository
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds)
|
||||
public IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds)
|
||||
{
|
||||
return await _context.Series
|
||||
return _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Select(s => s.Metadata.PublicationStatus)
|
||||
.Distinct()
|
||||
.AsEnumerable()
|
||||
.Select(s => new PublicationStatusDto()
|
||||
{
|
||||
Value = s,
|
||||
Title = s.ToDescription()
|
||||
})
|
||||
.OrderBy(s => s.Title)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private static string RecentlyAddedItemTitle(RecentlyAddedSeries item)
|
||||
{
|
||||
switch (item.LibraryType)
|
||||
{
|
||||
case LibraryType.Book:
|
||||
return string.Empty;
|
||||
case LibraryType.Comic:
|
||||
return "Issue";
|
||||
case LibraryType.Manga:
|
||||
default:
|
||||
return "Chapter";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show all recently added chapters. Provide some mapping for chapter 0 -> Volume 1
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<RecentlyAddedItemDto>> GetRecentlyAddedChapters(int userId)
|
||||
{
|
||||
var ret = await GetRecentlyAddedChaptersQuery(userId);
|
||||
|
||||
var items = new List<RecentlyAddedItemDto>();
|
||||
foreach (var item in ret)
|
||||
{
|
||||
var dto = new RecentlyAddedItemDto()
|
||||
{
|
||||
LibraryId = item.LibraryId,
|
||||
LibraryType = item.LibraryType,
|
||||
SeriesId = item.SeriesId,
|
||||
SeriesName = item.SeriesName,
|
||||
Created = item.Created,
|
||||
Id = items.Count,
|
||||
Format = item.Format
|
||||
};
|
||||
|
||||
// Add title and Volume/Chapter Id
|
||||
var chapterTitle = RecentlyAddedItemTitle(item);
|
||||
string title;
|
||||
if (item.ChapterNumber.Equals(Parser.Parser.DefaultChapter))
|
||||
{
|
||||
if ((item.VolumeNumber + string.Empty).Equals(Parser.Parser.DefaultChapter))
|
||||
{
|
||||
title = item.ChapterTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
title = "Volume " + item.VolumeNumber;
|
||||
}
|
||||
|
||||
dto.VolumeId = item.VolumeId;
|
||||
}
|
||||
else
|
||||
{
|
||||
title = item.IsSpecial
|
||||
? item.ChapterRange
|
||||
: $"{chapterTitle} {item.ChapterRange}";
|
||||
dto.ChapterId = item.ChapterId;
|
||||
}
|
||||
|
||||
dto.Title = title;
|
||||
items.Add(dto);
|
||||
}
|
||||
|
||||
|
||||
return items;
|
||||
|
||||
.OrderBy(s => s.Title);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Return recently updated series, regardless of read progress, and group the number of volume or chapters added.
|
||||
/// </summary>
|
||||
/// <remarks>This provides 2 levels of pagination. Fetching the individual chapters only looks at 3000. Then when performing grouping
|
||||
/// in memory, we stop after 30 series. </remarks>
|
||||
/// <param name="userId">Used to ensure user has access to libraries</param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId)
|
||||
public async Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30)
|
||||
{
|
||||
var ret = await GetRecentlyAddedChaptersQuery(userId, 150);
|
||||
|
||||
|
||||
var seriesMap = new Dictionary<string, GroupedSeriesDto>();
|
||||
var index = 0;
|
||||
foreach (var item in ret)
|
||||
foreach (var item in await GetRecentlyAddedChaptersQuery(userId))
|
||||
{
|
||||
if (seriesMap.Keys.Count == pageSize) break;
|
||||
|
||||
if (seriesMap.ContainsKey(item.SeriesName))
|
||||
{
|
||||
seriesMap[item.SeriesName].Count += 1;
|
||||
@ -1003,16 +1000,212 @@ public class SeriesRepository : ISeriesRepository
|
||||
Created = item.Created,
|
||||
Id = index,
|
||||
Format = item.Format,
|
||||
Count = 1
|
||||
Count = 1,
|
||||
};
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return seriesMap.Values.ToList();
|
||||
return seriesMap.Values.AsEnumerable();
|
||||
}
|
||||
|
||||
private async Task<List<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 50)
|
||||
public async Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId);
|
||||
var usersSeriesIds = _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Select(s => s.Id);
|
||||
|
||||
var targetSeries = _context.SeriesRelation
|
||||
.Where(sr =>
|
||||
sr.SeriesId == seriesId && sr.RelationKind == kind && usersSeriesIds.Contains(sr.TargetSeriesId))
|
||||
.Include(sr => sr.TargetSeries)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.Select(sr => sr.TargetSeriesId);
|
||||
|
||||
return await _context.Series
|
||||
.Where(s => targetSeries.Contains(s.Id))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private IQueryable<int> GetLibraryIdsForUser(int userId)
|
||||
{
|
||||
return _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
.SelectMany(l => l.Libraries.Select(lib => lib.Id));
|
||||
}
|
||||
|
||||
public async Task<PagedList<SeriesDto>> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
|
||||
var query = _context.Series
|
||||
.Where(s => s.Metadata.Genres.Select(g => g.Id).Contains(genreId))
|
||||
.Where(s => usersSeriesIds.Contains(s.Id))
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
|
||||
|
||||
|
||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
public async Task<PagedList<SeriesDto>> GetRediscover(int userId, int libraryId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
||||
.Where(s => usersSeriesIds.Contains(s.SeriesId))
|
||||
.Select(p => p.SeriesId)
|
||||
.Distinct();
|
||||
|
||||
var query = _context.Series
|
||||
.Where(s => distinctSeriesIdsWithProgress.Contains(s.Id) &&
|
||||
_context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId)
|
||||
.Sum(s1 => s1.PagesRead) >= s.Pages)
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
|
||||
|
||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
public async Task<SeriesDto> GetSeriesForMangaFile(int mangaFileId, int userId)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId);
|
||||
return await _context.MangaFile
|
||||
.Where(m => m.Id == mangaFileId)
|
||||
.AsSplitQuery()
|
||||
.Select(f => f.Chapter)
|
||||
.Select(c => c.Volume)
|
||||
.Select(v => v.Series)
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<SeriesDto> GetSeriesForChapter(int chapterId, int userId)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId);
|
||||
return await _context.Chapter
|
||||
.Where(m => m.Id == chapterId)
|
||||
.AsSplitQuery()
|
||||
.Select(c => c.Volume)
|
||||
.Select(v => v.Series)
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<PagedList<SeriesDto>> GetHighlyRated(int userId, int libraryId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
var distinctSeriesIdsWithHighRating = _context.AppUserRating
|
||||
.Where(s => usersSeriesIds.Contains(s.SeriesId) && s.Rating > 4)
|
||||
.Select(p => p.SeriesId)
|
||||
.Distinct();
|
||||
|
||||
var query = _context.Series
|
||||
.Where(s => distinctSeriesIdsWithHighRating.Contains(s.Id))
|
||||
.AsSplitQuery()
|
||||
.OrderByDescending(s => _context.AppUserRating.Where(r => r.SeriesId == s.Id).Select(r => r.Rating).Average())
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
|
||||
|
||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
|
||||
public async Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
||||
.Where(s => usersSeriesIds.Contains(s.SeriesId))
|
||||
.Select(p => p.SeriesId)
|
||||
.Distinct();
|
||||
|
||||
|
||||
var query = _context.Series
|
||||
.Where(s => s.Pages < 2000 && !distinctSeriesIdsWithProgress.Contains(s.Id) &&
|
||||
usersSeriesIds.Contains(s.Id))
|
||||
.Where(s => s.Metadata.PublicationStatus != PublicationStatus.OnGoing)
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
|
||||
|
||||
|
||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all library ids for a user
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="libraryId">0 for no library filter</param>
|
||||
/// <returns></returns>
|
||||
private IQueryable<int> GetLibraryIdsForUser(int userId, int libraryId)
|
||||
{
|
||||
return _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId || libraryId == 0).Select(lib => lib.Id));
|
||||
}
|
||||
|
||||
public async Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
|
||||
return new RelatedSeriesDto()
|
||||
{
|
||||
SourceSeriesId = seriesId,
|
||||
Adaptations = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Adaptation),
|
||||
Characters = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Character),
|
||||
Prequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Prequel),
|
||||
Sequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Sequel),
|
||||
Contains = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Contains),
|
||||
SideStories = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SideStory),
|
||||
SpinOffs = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SpinOff),
|
||||
Others = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Other),
|
||||
AlternativeSettings = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeSetting),
|
||||
AlternativeVersions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeVersion),
|
||||
Doujinshis = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Doujinshi),
|
||||
Parent = await _context.Series
|
||||
.SelectMany(s =>
|
||||
s.RelationOf.Where(r => r.TargetSeriesId == seriesId
|
||||
&& usersSeriesIds.Contains(r.TargetSeriesId)
|
||||
&& r.RelationKind != RelationKind.Prequel
|
||||
&& r.RelationKind != RelationKind.Sequel)
|
||||
.Select(sr => sr.Series))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync()
|
||||
};
|
||||
}
|
||||
|
||||
private IQueryable<int> GetSeriesIdsForLibraryIds(IQueryable<int> libraryIds)
|
||||
{
|
||||
return _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Select(s => s.Id);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<SeriesDto>> GetRelatedSeriesQuery(int seriesId, IEnumerable<int> usersSeriesIds, RelationKind kind)
|
||||
{
|
||||
return await _context.Series.SelectMany(s =>
|
||||
s.Relations.Where(sr => sr.RelationKind == kind && sr.SeriesId == seriesId && usersSeriesIds.Contains(sr.TargetSeriesId))
|
||||
.Select(sr => sr.TargetSeries))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 3000)
|
||||
{
|
||||
var libraries = await _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
@ -1021,7 +1214,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
var libraryIds = libraries.Select(l => l.LibraryId).ToList();
|
||||
|
||||
var withinLastWeek = DateTime.Now - TimeSpan.FromDays(12);
|
||||
var ret = await _context.Chapter
|
||||
var ret = _context.Chapter
|
||||
.Where(c => c.Created >= withinLastWeek)
|
||||
.AsNoTracking()
|
||||
.Include(c => c.Volume)
|
||||
@ -1044,9 +1237,10 @@ public class SeriesRepository : ISeriesRepository
|
||||
VolumeNumber = c.Volume.Number,
|
||||
ChapterTitle = c.Title
|
||||
})
|
||||
.Take(maxRecords)
|
||||
//.Take(maxRecords)
|
||||
.AsSplitQuery()
|
||||
.Where(c => c.Created >= withinLastWeek && libraryIds.Contains(c.LibraryId))
|
||||
.ToListAsync();
|
||||
.AsEnumerable();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ public interface ISiteThemeRepository
|
||||
Task<SiteThemeDto> GetThemeDtoByName(string themeName);
|
||||
Task<SiteTheme> GetDefaultTheme();
|
||||
Task<IEnumerable<SiteTheme>> GetThemes();
|
||||
|
||||
Task<SiteTheme> GetThemeById(int themeId);
|
||||
}
|
||||
|
||||
|
@ -21,9 +21,10 @@ namespace API.Data
|
||||
/// <summary>
|
||||
/// Generated on Startup. Seed.SeedSettings must run before
|
||||
/// </summary>
|
||||
public static IList<ServerSetting> DefaultSettings;
|
||||
public static ImmutableArray<ServerSetting> DefaultSettings;
|
||||
|
||||
public static readonly IList<SiteTheme> DefaultThemes = new List<SiteTheme>
|
||||
public static readonly ImmutableArray<SiteTheme> DefaultThemes = ImmutableArray.Create(
|
||||
new List<SiteTheme>
|
||||
{
|
||||
new()
|
||||
{
|
||||
@ -32,24 +33,8 @@ namespace API.Data
|
||||
Provider = ThemeProvider.System,
|
||||
FileName = "dark.scss",
|
||||
IsDefault = true,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Light",
|
||||
NormalizedName = Parser.Parser.Normalize("Light"),
|
||||
Provider = ThemeProvider.System,
|
||||
FileName = "light.scss",
|
||||
IsDefault = false,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "E-Ink",
|
||||
NormalizedName = Parser.Parser.Normalize("E-Ink"),
|
||||
Provider = ThemeProvider.System,
|
||||
FileName = "e-ink.scss",
|
||||
IsDefault = false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}.ToArray());
|
||||
|
||||
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
|
||||
{
|
||||
@ -90,24 +75,32 @@ namespace API.Data
|
||||
public static async Task SeedSettings(DataContext context, IDirectoryService directoryService)
|
||||
{
|
||||
await context.Database.EnsureCreatedAsync();
|
||||
|
||||
DefaultSettings = new List<ServerSetting>()
|
||||
DefaultSettings = ImmutableArray.Create(new List<ServerSetting>()
|
||||
{
|
||||
new () {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory},
|
||||
new () {Key = ServerSettingKey.TaskScan, Value = "daily"},
|
||||
new () {Key = ServerSettingKey.LoggingLevel, Value = "Information"}, // Not used from DB, but DB is sync with appSettings.json
|
||||
new () {Key = ServerSettingKey.TaskBackup, Value = "daily"},
|
||||
new () {Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory)},
|
||||
new () {Key = ServerSettingKey.Port, Value = "5000"}, // Not used from DB, but DB is sync with appSettings.json
|
||||
new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
|
||||
new () {Key = ServerSettingKey.EnableOpds, Value = "false"},
|
||||
new () {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
|
||||
new () {Key = ServerSettingKey.BaseUrl, Value = "/"},
|
||||
new () {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()},
|
||||
new () {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
|
||||
new () {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
|
||||
new () {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
|
||||
};
|
||||
new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory},
|
||||
new() {Key = ServerSettingKey.TaskScan, Value = "daily"},
|
||||
new()
|
||||
{
|
||||
Key = ServerSettingKey.LoggingLevel, Value = "Information"
|
||||
}, // Not used from DB, but DB is sync with appSettings.json
|
||||
new() {Key = ServerSettingKey.TaskBackup, Value = "daily"},
|
||||
new()
|
||||
{
|
||||
Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory)
|
||||
},
|
||||
new()
|
||||
{
|
||||
Key = ServerSettingKey.Port, Value = "5000"
|
||||
}, // Not used from DB, but DB is sync with appSettings.json
|
||||
new() {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
|
||||
new() {Key = ServerSettingKey.EnableOpds, Value = "false"},
|
||||
new() {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
|
||||
new() {Key = ServerSettingKey.BaseUrl, Value = "/"},
|
||||
new() {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()},
|
||||
new() {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
|
||||
new() {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
|
||||
new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
|
||||
}.ToArray());
|
||||
|
||||
foreach (var defaultSetting in DefaultSettings)
|
||||
{
|
||||
|
@ -1,40 +0,0 @@
|
||||
#This Dockerfile pulls the latest git commit and builds Kavita from source
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0-focal AS builder
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
#Installs nodejs and npm
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#Builds app based on platform
|
||||
COPY build_target.sh /build_target.sh
|
||||
RUN /build_target.sh
|
||||
|
||||
#Production image
|
||||
FROM ubuntu:focal
|
||||
|
||||
#Move the output files to where they need to be
|
||||
COPY --from=builder /Projects/Kavita/_output/build/Kavita /kavita
|
||||
|
||||
#Installs program dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libicu-dev libssl1.1 pwgen \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#Creates the manga storage directory
|
||||
RUN mkdir /manga /kavita/data
|
||||
|
||||
RUN cp /kavita/appsettings.Development.json /kavita/appsettings.json \
|
||||
&& sed -i 's/Data source=kavita.db/Data source=data\/kavita.db/g' /kavita/appsettings.json
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
WORKDIR /kavita
|
||||
|
||||
ENTRYPOINT ["/bin/bash"]
|
||||
CMD ["/entrypoint.sh"]
|
@ -25,6 +25,7 @@ namespace API.Entities
|
||||
/// </example>
|
||||
/// </summary>
|
||||
public ReaderMode ReaderMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
||||
/// </summary>
|
||||
@ -42,10 +43,6 @@ namespace API.Entities
|
||||
/// </summary>
|
||||
public string BackgroundColor { get; set; } = "#000000";
|
||||
/// <summary>
|
||||
/// Book Reader Option: Should the background color be dark
|
||||
/// </summary>
|
||||
public bool BookReaderDarkMode { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Book Reader Option: Override extra Margin
|
||||
/// </summary>
|
||||
public int BookReaderMargin { get; set; } = 15;
|
||||
@ -74,7 +71,22 @@ namespace API.Entities
|
||||
/// </summary>
|
||||
/// <remarks>Should default to Dark</remarks>
|
||||
public SiteTheme Theme { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Book Reader Option: The color theme to decorate the book contents
|
||||
/// </summary>
|
||||
/// <remarks>Should default to Dark</remarks>
|
||||
public string BookThemeName { get; set; } = "Dark";
|
||||
/// <summary>
|
||||
/// Book Reader Option: The way a page from a book is rendered. Default is as book dictates, 1 column is fit to height,
|
||||
/// 2 column is fit to height, 2 columns
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to Default</remarks>
|
||||
public BookPageLayoutMode PageLayoutMode { get; set; } = BookPageLayoutMode.Default;
|
||||
/// <summary>
|
||||
/// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this.
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to false</remarks>
|
||||
public bool BookReaderImmersiveMode { get; set; } = false;
|
||||
|
||||
|
||||
public AppUser AppUser { get; set; }
|
||||
|
@ -27,7 +27,7 @@ public enum AgeRating
|
||||
KidsToAdults = 7,
|
||||
[Description("Teen")]
|
||||
Teen = 8,
|
||||
[Description("MA 15+")]
|
||||
[Description("MA15+")]
|
||||
Mature15Plus = 9,
|
||||
[Description("Mature 17+")]
|
||||
Mature17Plus = 10,
|
||||
|
13
API/Entities/Enums/BookPageLayoutMode.cs
Normal file
13
API/Entities/Enums/BookPageLayoutMode.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace API.Entities.Enums;
|
||||
|
||||
public enum BookPageLayoutMode
|
||||
{
|
||||
[Description("Default")]
|
||||
Default = 0,
|
||||
[Description("1 Column")]
|
||||
Column1 = 1,
|
||||
[Description("2 Column")]
|
||||
Column2 = 2
|
||||
}
|
@ -18,6 +18,16 @@ public enum PublicationStatus
|
||||
/// Publication has finished releasing
|
||||
/// </summary>
|
||||
[Description("Completed")]
|
||||
Completed = 2
|
||||
Completed = 2,
|
||||
/// <summary>
|
||||
/// Publication has been cancelled
|
||||
/// </summary>
|
||||
[Description("Cancelled")]
|
||||
Cancelled = 3,
|
||||
/// <summary>
|
||||
/// Publication has been completed, but Kavita doesn't have all issues/volumes accounted for
|
||||
/// </summary>
|
||||
[Description("Ended")]
|
||||
Ended = 4
|
||||
|
||||
}
|
||||
|
66
API/Entities/Enums/RelationKind.cs
Normal file
66
API/Entities/Enums/RelationKind.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace API.Entities.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a relationship between Series
|
||||
/// </summary>
|
||||
public enum RelationKind
|
||||
{
|
||||
/// <summary>
|
||||
/// Story that occurred before the original.
|
||||
/// </summary>
|
||||
[Description("Prequel")]
|
||||
Prequel = 1,
|
||||
/// <summary>
|
||||
/// Direct continuation of the story.
|
||||
/// </summary>
|
||||
[Description("Sequel")]
|
||||
Sequel = 2,
|
||||
/// <summary>
|
||||
/// Uses characters of a different series, but is not an alternate setting or story.
|
||||
/// </summary>
|
||||
[Description("Spin Off")]
|
||||
SpinOff = 3,
|
||||
/// <summary>
|
||||
/// Manga/Anime/Light Novel adaptation
|
||||
/// </summary>
|
||||
[Description("Adaptation")]
|
||||
Adaptation = 4,
|
||||
/// <summary>
|
||||
/// Takes place sometime during the parent storyline.
|
||||
/// </summary>
|
||||
[Description("Side Story")]
|
||||
SideStory = 5,
|
||||
/// <summary>
|
||||
/// When characters appear in both series, but is not a spin-off
|
||||
/// </summary>
|
||||
[Description("Character")]
|
||||
Character = 6,
|
||||
/// <summary>
|
||||
/// When the story contains another story, useful for One-Shots
|
||||
/// </summary>
|
||||
[Description("Contains")]
|
||||
Contains = 7,
|
||||
/// <summary>
|
||||
/// When nothing else fits
|
||||
/// </summary>
|
||||
[Description("Other")]
|
||||
Other = 8,
|
||||
/// <summary>
|
||||
/// Same universe/world/reality/timeline, completely different characters
|
||||
/// </summary>
|
||||
[Description("Alternative Setting")]
|
||||
AlternativeSetting = 9,
|
||||
/// <summary>
|
||||
/// Same setting, same characters, story is told differently
|
||||
/// </summary>
|
||||
[Description("Alternative Version")]
|
||||
AlternativeVersion = 10,
|
||||
/// <summary>
|
||||
/// Doujinshi or Fan work
|
||||
/// </summary>
|
||||
[Description("Doujinshi")]
|
||||
Doujinshi = 11
|
||||
|
||||
}
|
15
API/Entities/Interfaces/ITheme.cs
Normal file
15
API/Entities/Interfaces/ITheme.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using API.Entities.Enums.Theme;
|
||||
|
||||
namespace API.Entities.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// A theme in some kind
|
||||
/// </summary>
|
||||
public interface ITheme
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string NormalizedName { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
public ThemeProvider Provider { get; set; }
|
||||
}
|
@ -35,9 +35,13 @@ namespace API.Entities.Metadata
|
||||
/// </summary>
|
||||
public string Language { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Total number of issues in the series
|
||||
/// Total number of issues/volumes in the series
|
||||
/// </summary>
|
||||
public int Count { get; set; } = 0;
|
||||
public int TotalCount { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// Max number of issues/volumes in the series (Max of Volume/Issue field in ComicInfo)
|
||||
/// </summary>
|
||||
public int MaxCount { get; set; } = 0;
|
||||
public PublicationStatus PublicationStatus { get; set; }
|
||||
|
||||
// Locks
|
||||
|
25
API/Entities/Metadata/SeriesRelation.cs
Normal file
25
API/Entities/Metadata/SeriesRelation.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Entities.Metadata;
|
||||
|
||||
/// <summary>
|
||||
/// A relation flows between one series and another.
|
||||
/// Series ---kind---> target
|
||||
/// </summary>
|
||||
public class SeriesRelation
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public RelationKind RelationKind { get; set; }
|
||||
|
||||
public virtual Series TargetSeries { get; set; }
|
||||
/// <summary>
|
||||
/// A is Sequel to B. In this example, TargetSeries is A. B will hold the foreign key.
|
||||
/// </summary>
|
||||
public int TargetSeriesId { get; set; }
|
||||
|
||||
// Relationships
|
||||
public virtual Series Series { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
}
|
@ -11,11 +11,21 @@ namespace API.Entities
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string Title { get; set; }
|
||||
/// <summary>
|
||||
/// A normalized string used to check if the reading list already exists in the DB
|
||||
/// </summary>
|
||||
public string NormalizedTitle { get; set; }
|
||||
public string Summary { get; set; }
|
||||
/// <summary>
|
||||
/// Reading lists that are promoted are only done by admins
|
||||
/// </summary>
|
||||
public bool Promoted { get; set; }
|
||||
/// <summary>
|
||||
/// Absolute path to the (managed) image file
|
||||
/// </summary>
|
||||
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
||||
public string CoverImage { get; set; }
|
||||
public bool CoverImageLocked { get; set; }
|
||||
|
||||
public ICollection<ReadingListItem> Items { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
|
@ -3,13 +3,11 @@ using System.Collections.Generic;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Entities.Metadata;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Entities
|
||||
namespace API.Entities;
|
||||
|
||||
public class Series : IEntityDate
|
||||
{
|
||||
[Index(nameof(Name), nameof(NormalizedName), nameof(LocalizedName), nameof(LibraryId), nameof(Format), IsUnique = true)]
|
||||
public class Series : IEntityDate
|
||||
{
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// The UI visible Name of the Series. This may or may not be the same as the OriginalName
|
||||
@ -62,14 +60,27 @@ namespace API.Entities
|
||||
public bool SortNameLocked { get; set; }
|
||||
public bool LocalizedNameLocked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When a Chapter was last added onto the Series
|
||||
/// </summary>
|
||||
public DateTime LastChapterAdded { get; set; }
|
||||
|
||||
public SeriesMetadata Metadata { get; set; }
|
||||
|
||||
public ICollection<AppUserRating> Ratings { get; set; } = new List<AppUserRating>();
|
||||
public ICollection<AppUserProgress> Progress { get; set; } = new List<AppUserProgress>();
|
||||
|
||||
/// <summary>
|
||||
/// Relations to other Series, like Sequels, Prequels, etc
|
||||
/// </summary>
|
||||
/// <remarks>1 to Many relationship</remarks>
|
||||
public virtual ICollection<SeriesRelation> Relations { get; set; } = new List<SeriesRelation>();
|
||||
public virtual ICollection<SeriesRelation> RelationOf { get; set; } = new List<SeriesRelation>();
|
||||
|
||||
|
||||
// Relationships
|
||||
public List<Volume> Volumes { get; set; }
|
||||
public Library Library { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ namespace API.Entities;
|
||||
/// <summary>
|
||||
/// Represents a set of css overrides the user can upload to Kavita and will load into webui
|
||||
/// </summary>
|
||||
public class SiteTheme : IEntityDate
|
||||
public class SiteTheme : IEntityDate, ITheme
|
||||
{
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
@ -23,6 +23,7 @@ public class SiteTheme : IEntityDate
|
||||
/// File path to the content. Stored under <see cref="DirectoryService.SiteThemeDirectory"/>.
|
||||
/// Must be a .css file
|
||||
/// </summary>
|
||||
/// <remarks>System provided themes use an alternative location as they are packaged with the app</remarks>
|
||||
public string FileName { get; set; }
|
||||
/// <summary>
|
||||
/// Only one theme can have this. Will auto-set this as default for new user accounts
|
||||
|
@ -40,7 +40,7 @@ namespace API.Extensions
|
||||
services.AddScoped<IAccountService, AccountService>();
|
||||
services.AddScoped<IEmailService, EmailService>();
|
||||
services.AddScoped<IBookmarkService, BookmarkService>();
|
||||
services.AddScoped<ISiteThemeService, SiteThemeService>();
|
||||
services.AddScoped<IThemeService, ThemeService>();
|
||||
services.AddScoped<ISeriesService, SeriesService>();
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Entities;
|
||||
using API.Parser;
|
||||
@ -28,8 +28,8 @@ namespace API.Extensions
|
||||
{
|
||||
var specialTreatment = info.IsSpecialInfo();
|
||||
return specialTreatment
|
||||
? chapters.SingleOrDefault(c => c.Range == info.Filename || (c.Files.Select(f => f.FilePath).Contains(info.FullFilePath)))
|
||||
: chapters.SingleOrDefault(c => c.Range == info.Chapters);
|
||||
? chapters.FirstOrDefault(c => c.Range == info.Filename || (c.Files.Select(f => f.FilePath).Contains(info.FullFilePath)))
|
||||
: chapters.FirstOrDefault(c => c.Range == info.Chapters);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,14 +8,6 @@ namespace API.Extensions
|
||||
{
|
||||
public static class VolumeListExtensions
|
||||
{
|
||||
public static Volume FirstWithChapters(this IEnumerable<Volume> volumes, bool inBookSeries)
|
||||
{
|
||||
return inBookSeries
|
||||
? volumes.FirstOrDefault(v => v.Chapters.Any())
|
||||
: volumes.OrderBy(v => v.Number, new ChapterSortComparer())
|
||||
.FirstOrDefault(v => v.Chapters.Any());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the first Volume to get the cover image from. For a book with only a special, the special will be returned.
|
||||
/// If there are both specials and non-specials, then the first non-special will be returned.
|
||||
|
@ -6,6 +6,7 @@ using API.DTOs.Metadata;
|
||||
using API.DTOs.Reader;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.DTOs.Search;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.DTOs.Settings;
|
||||
using API.DTOs.Theme;
|
||||
using API.Entities;
|
||||
@ -96,13 +97,23 @@ namespace API.Helpers
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Editor)));
|
||||
|
||||
// CreateMap<SeriesRelation, RelatedSeriesDto>()
|
||||
// .ForMember(dest => dest.Adaptations,
|
||||
// opt =>
|
||||
// opt.MapFrom(src => src.Where(p => p.Role == PersonRole.Writer)))
|
||||
|
||||
CreateMap<AppUser, UserDto>();
|
||||
CreateMap<SiteTheme, SiteThemeDto>();
|
||||
CreateMap<AppUserPreferences, UserPreferencesDto>()
|
||||
.ForMember(dest => dest.Theme,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.Theme));
|
||||
opt.MapFrom(src => src.Theme))
|
||||
.ForMember(dest => dest.BookReaderThemeName,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.BookThemeName))
|
||||
.ForMember(dest => dest.BookReaderLayoutMode,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.PageLayoutMode));
|
||||
|
||||
|
||||
CreateMap<AppUserBookmark, BookmarkDto>();
|
||||
|
@ -64,16 +64,14 @@ public static class PersonHelper
|
||||
/// </summary>
|
||||
/// <param name="existingPeople"></param>
|
||||
/// <param name="removeAllExcept"></param>
|
||||
/// <param name="action">Callback for all entities that was removed</param>
|
||||
public static void KeepOnlySamePeopleBetweenLists(ICollection<Person> existingPeople, ICollection<Person> removeAllExcept, Action<Person> action = null)
|
||||
/// <param name="action">Callback for all entities that should be removed</param>
|
||||
public static void KeepOnlySamePeopleBetweenLists(IEnumerable<Person> existingPeople, ICollection<Person> removeAllExcept, Action<Person> action = null)
|
||||
{
|
||||
var existing = existingPeople.ToList();
|
||||
foreach (var person in existing)
|
||||
foreach (var person in existingPeople)
|
||||
{
|
||||
var existingPerson = removeAllExcept.FirstOrDefault(p => p.Role == person.Role && person.NormalizedName.Equals(p.NormalizedName));
|
||||
if (existingPerson == null)
|
||||
{
|
||||
existingPeople.Remove(person);
|
||||
action?.Invoke(person);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
@ -132,11 +132,11 @@ public class DefaultParser
|
||||
|
||||
if (!parsedVolume.Equals(Parser.DefaultVolume) || !parsedChapter.Equals(Parser.DefaultChapter))
|
||||
{
|
||||
if ((ret.Volumes.Equals(Parser.DefaultVolume) || string.IsNullOrEmpty(ret.Volumes)) && !parsedVolume.Equals(Parser.DefaultVolume))
|
||||
if ((string.IsNullOrEmpty(ret.Volumes) || ret.Volumes.Equals(Parser.DefaultVolume)) && !parsedVolume.Equals(Parser.DefaultVolume))
|
||||
{
|
||||
ret.Volumes = parsedVolume;
|
||||
}
|
||||
if ((ret.Chapters.Equals(Parser.DefaultChapter) || string.IsNullOrEmpty(ret.Chapters)) && !parsedChapter.Equals(Parser.DefaultChapter))
|
||||
if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters.Equals(Parser.DefaultChapter)) && !parsedChapter.Equals(Parser.DefaultChapter))
|
||||
{
|
||||
ret.Chapters = parsedChapter;
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ namespace API.Parser
|
||||
public const string DefaultVolume = "0";
|
||||
private static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp)";
|
||||
public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif)";
|
||||
public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt";
|
||||
public const string BookFileExtensions = @"\.epub|\.pdf";
|
||||
public const string MacOsMetadataFileStartsWith = @"._";
|
||||
@ -43,7 +43,7 @@ namespace API.Parser
|
||||
MatchOptions, RegexTimeout);
|
||||
|
||||
|
||||
private static readonly string XmlRegexExtensions = @"\.xml";
|
||||
private const string XmlRegexExtensions = @"\.xml";
|
||||
private static readonly Regex ImageRegex = new Regex(ImageFileExtensions,
|
||||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions,
|
||||
@ -54,7 +54,7 @@ namespace API.Parser
|
||||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex BookFileRegex = new Regex(BookFileExtensions,
|
||||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)((?<!back)cover|folder)(?![\w\d])",
|
||||
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)(?<!back)(?<!back_)(?<!back-)(cover|folder)(?![\w\d])",
|
||||
MatchOptions, RegexTimeout);
|
||||
|
||||
private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+]",
|
||||
@ -132,9 +132,9 @@ namespace API.Parser
|
||||
new Regex(
|
||||
@"(?<Series>.*)(?:, Chapter )(?<Chapter>\d+)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Please Go Home, Akutsu-San! - Chapter 038.5 - Volume Announcement.cbz
|
||||
// Please Go Home, Akutsu-San! - Chapter 038.5 - Volume Announcement.cbz, My Charms Are Wasted on Kuroiwa Medaka - Ch. 37.5 - Volume Extras
|
||||
new Regex(
|
||||
@"(?<Series>.*)(\s|_|-)(?!Vol)(\s|_|-)(?:Chapter)(\s|_|-)(?<Chapter>\d+)",
|
||||
@"(?<Series>.+?)(\s|_|-)(?!Vol)(\s|_|-)((?:Chapter)|(?:Ch\.))(\s|_|-)(?<Chapter>\d+)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// [dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz
|
||||
new Regex(
|
||||
@ -452,10 +452,6 @@ namespace API.Parser
|
||||
};
|
||||
private static readonly Regex[] MangaEditionRegex = {
|
||||
// Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz
|
||||
new Regex(
|
||||
@"(?<Edition>({|\(|\[).* Edition(}|\)|\]))",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz
|
||||
new Regex(
|
||||
@"(\b|_)(?<Edition>Omnibus(( |_)?Edition)?)(\b|_)?",
|
||||
MatchOptions, RegexTimeout),
|
||||
@ -463,10 +459,6 @@ namespace API.Parser
|
||||
new Regex(
|
||||
@"(\b|_)(?<Edition>Uncensored)(\b|_)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz
|
||||
new Regex(
|
||||
@"(\b|_)(?<Edition>Full(?: |_)Color)(\b|_)?",
|
||||
MatchOptions, RegexTimeout),
|
||||
};
|
||||
|
||||
private static readonly Regex[] CleanupRegex =
|
||||
@ -934,25 +926,7 @@ namespace API.Parser
|
||||
}
|
||||
|
||||
|
||||
public static float MaximumNumberFromRange(string range)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Regex.IsMatch(range, @"^[\d-.]+$"))
|
||||
{
|
||||
return (float) 0.0;
|
||||
}
|
||||
|
||||
var tokens = range.Replace("_", string.Empty).Split("-");
|
||||
return tokens.Max(float.Parse);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (float) 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
public static float MinimumNumberFromRange(string range)
|
||||
public static float MinNumberFromRange(string range)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -970,6 +944,24 @@ namespace API.Parser
|
||||
}
|
||||
}
|
||||
|
||||
public static float MaxNumberFromRange(string range)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Regex.IsMatch(range, @"^[\d-.]+$"))
|
||||
{
|
||||
return (float) 0.0;
|
||||
}
|
||||
|
||||
var tokens = range.Replace("_", string.Empty).Split("-");
|
||||
return tokens.Max(float.Parse);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (float) 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
public static string Normalize(string name)
|
||||
{
|
||||
return NormalizeRegex.Replace(name, string.Empty).ToLower();
|
||||
@ -1007,7 +999,7 @@ namespace API.Parser
|
||||
|
||||
public static bool HasBlacklistedFolderInPath(string path)
|
||||
{
|
||||
return path.Contains("__MACOSX") || path.StartsWith("@Recently-Snapshot") || path.StartsWith("@recycle") || path.StartsWith("._");
|
||||
return path.Contains("__MACOSX") || path.StartsWith("@Recently-Snapshot") || path.StartsWith("@recycle") || path.StartsWith("._") || path.Contains(".qpkg");
|
||||
}
|
||||
|
||||
|
||||
|
@ -22,6 +22,10 @@ namespace API.Parser
|
||||
/// </summary>
|
||||
public string SeriesSort { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// This can be filled in from ComicInfo.xml/Epub during scanning. Will update the LocalizedName field on <see cref="Entities.Series"/>
|
||||
/// </summary>
|
||||
public string LocalizedSeries { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Represents the parsed volumes from a file. By default, will be 0 which means that nothing could be parsed.
|
||||
/// If Volumes is 0 and Chapters is 0, the file is a special. If Chapters is non-zero, then no volume could be parsed.
|
||||
/// <example>Beastars Vol 3-4 will map to "3-4"</example>
|
||||
|
@ -4,6 +4,7 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
using API.Archive;
|
||||
@ -436,6 +437,12 @@ namespace API.Services
|
||||
|
||||
if (Directory.Exists(extractPath)) return;
|
||||
|
||||
if (!_directoryService.FileSystem.File.Exists(archivePath))
|
||||
{
|
||||
_logger.LogError("{Archive} does not exist on disk", archivePath);
|
||||
throw new KavitaException($"{archivePath} does not exist on disk");
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
|
@ -20,6 +20,7 @@ using Microsoft.IO;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using VersOne.Epub;
|
||||
using VersOne.Epub.Options;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
|
||||
namespace API.Services
|
||||
@ -59,6 +60,13 @@ namespace API.Services
|
||||
private readonly StylesheetParser _cssParser = new ();
|
||||
private static readonly RecyclableMemoryStreamManager StreamManager = new ();
|
||||
private const string CssScopeClass = ".book-content";
|
||||
public static readonly EpubReaderOptions BookReaderOptions = new()
|
||||
{
|
||||
PackageReaderOptions = new PackageReaderOptions()
|
||||
{
|
||||
IgnoreMissingToc = true
|
||||
}
|
||||
};
|
||||
|
||||
public BookService(ILogger<BookService> logger, IDirectoryService directoryService, IImageService imageService)
|
||||
{
|
||||
@ -148,8 +156,7 @@ namespace API.Services
|
||||
|
||||
public async Task<string> ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book)
|
||||
{
|
||||
// @Import statements will be handled by browser, so we must inline the css into the original file that request it, so they can be
|
||||
// Scoped
|
||||
// @Import statements will be handled by browser, so we must inline the css into the original file that request it, so they can be Scoped
|
||||
var prepend = filename.Length > 0 ? filename.Replace(Path.GetFileName(filename), string.Empty) : string.Empty;
|
||||
var importBuilder = new StringBuilder();
|
||||
foreach (Match match in Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml))
|
||||
@ -175,6 +182,7 @@ namespace API.Services
|
||||
|
||||
EscapeFontFamilyReferences(ref stylesheetHtml, apiBase, prepend);
|
||||
|
||||
|
||||
// Check if there are any background images and rewrite those urls
|
||||
EscapeCssImageReferences(ref stylesheetHtml, apiBase, book);
|
||||
|
||||
@ -237,68 +245,62 @@ namespace API.Services
|
||||
|
||||
private static void ScopeImages(HtmlDocument doc, EpubBookRef book, string apiBase)
|
||||
{
|
||||
var images = doc.DocumentNode.SelectNodes("//img");
|
||||
if (images != null)
|
||||
{
|
||||
var images = doc.DocumentNode.SelectNodes("//img")
|
||||
?? doc.DocumentNode.SelectNodes("//image");
|
||||
|
||||
if (images == null) return;
|
||||
|
||||
foreach (var image in images)
|
||||
{
|
||||
if (image.Name != "img") continue;
|
||||
|
||||
// Need to do for xlink:href
|
||||
string key = null;
|
||||
if (image.Attributes["src"] != null)
|
||||
{
|
||||
var imageFile = image.Attributes["src"].Value;
|
||||
if (!book.Content.Images.ContainsKey(imageFile))
|
||||
key = "src";
|
||||
}
|
||||
else if (image.Attributes["xlink:href"] != null)
|
||||
{
|
||||
// TODO: Refactor the Key code to a method to allow the hacks to be tested
|
||||
key = "xlink:href";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(key)) continue;
|
||||
|
||||
var imageFile = GetKeyForImage(book, image.Attributes[key].Value);
|
||||
image.Attributes.Remove(key);
|
||||
image.Attributes.Add(key, $"{apiBase}" + imageFile);
|
||||
|
||||
// Add a custom class that the reader uses to ensure images stay within reader
|
||||
image.AddClass("kavita-scale-width");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the image key associated with the file. Contains some basic fallback logic.
|
||||
/// </summary>
|
||||
/// <param name="book"></param>
|
||||
/// <param name="imageFile"></param>
|
||||
/// <returns></returns>
|
||||
private static string GetKeyForImage(EpubBookRef book, string imageFile)
|
||||
{
|
||||
if (book.Content.Images.ContainsKey(imageFile)) return imageFile;
|
||||
|
||||
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
|
||||
if (correctedKey != null)
|
||||
{
|
||||
imageFile = correctedKey;
|
||||
} else if (imageFile.StartsWith(".."))
|
||||
}
|
||||
else if (imageFile.StartsWith(".."))
|
||||
{
|
||||
// There are cases where the key is defined static like OEBPS/Images/1-4.jpg but reference is ../Images/1-4.jpg
|
||||
correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile.Replace("..", string.Empty)));
|
||||
correctedKey =
|
||||
book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile.Replace("..", string.Empty)));
|
||||
if (correctedKey != null)
|
||||
{
|
||||
imageFile = correctedKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
image.Attributes.Remove("src");
|
||||
image.Attributes.Add("src", $"{apiBase}" + imageFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
images = doc.DocumentNode.SelectNodes("//image");
|
||||
if (images != null)
|
||||
{
|
||||
foreach (var image in images)
|
||||
{
|
||||
if (image.Name != "image") continue;
|
||||
|
||||
if (image.Attributes["xlink:href"] != null)
|
||||
{
|
||||
var imageFile = image.Attributes["xlink:href"].Value;
|
||||
if (!book.Content.Images.ContainsKey(imageFile))
|
||||
{
|
||||
var correctedKey = book.Content.Images.Keys.SingleOrDefault(s => s.EndsWith(imageFile));
|
||||
if (correctedKey != null)
|
||||
{
|
||||
imageFile = correctedKey;
|
||||
}
|
||||
}
|
||||
|
||||
image.Attributes.Remove("xlink:href");
|
||||
image.Attributes.Add("xlink:href", $"{apiBase}" + imageFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
return imageFile;
|
||||
}
|
||||
|
||||
private static string PrepareFinalHtml(HtmlDocument doc, HtmlNode body)
|
||||
@ -317,12 +319,11 @@ namespace API.Services
|
||||
private static void RewriteAnchors(int page, HtmlDocument doc, Dictionary<string, int> mappings)
|
||||
{
|
||||
var anchors = doc.DocumentNode.SelectNodes("//a");
|
||||
if (anchors != null)
|
||||
{
|
||||
if (anchors == null) return;
|
||||
|
||||
foreach (var anchor in anchors)
|
||||
{
|
||||
BookService.UpdateLinks(anchor, mappings, page);
|
||||
}
|
||||
UpdateLinks(anchor, mappings, page);
|
||||
}
|
||||
}
|
||||
|
||||
@ -383,23 +384,44 @@ namespace API.Services
|
||||
|
||||
try
|
||||
{
|
||||
using var epubBook = EpubReader.OpenBook(filePath);
|
||||
using var epubBook = EpubReader.OpenBook(filePath, BookReaderOptions);
|
||||
var publicationDate =
|
||||
epubBook.Schema.Package.Metadata.Dates.FirstOrDefault(date => date.Event == "publication")?.Date;
|
||||
|
||||
if (string.IsNullOrEmpty(publicationDate))
|
||||
{
|
||||
publicationDate = epubBook.Schema.Package.Metadata.Dates.FirstOrDefault()?.Date;
|
||||
}
|
||||
var dateParsed = DateTime.TryParse(publicationDate, out var date);
|
||||
var year = 0;
|
||||
var month = 0;
|
||||
var day = 0;
|
||||
switch (dateParsed)
|
||||
{
|
||||
case true:
|
||||
year = date.Year;
|
||||
month = date.Month;
|
||||
day = date.Day;
|
||||
break;
|
||||
case false when !string.IsNullOrEmpty(publicationDate) && publicationDate.Length == 4:
|
||||
int.TryParse(publicationDate, out year);
|
||||
break;
|
||||
}
|
||||
|
||||
var info = new ComicInfo()
|
||||
{
|
||||
Summary = epubBook.Schema.Package.Metadata.Description,
|
||||
Writer = string.Join(",", epubBook.Schema.Package.Metadata.Creators.Select(c => Parser.Parser.CleanAuthor(c.Creator))),
|
||||
Publisher = string.Join(",", epubBook.Schema.Package.Metadata.Publishers),
|
||||
Month = !string.IsNullOrEmpty(publicationDate) ? DateTime.Parse(publicationDate).Month : 0,
|
||||
Day = !string.IsNullOrEmpty(publicationDate) ? DateTime.Parse(publicationDate).Day : 0,
|
||||
Year = !string.IsNullOrEmpty(publicationDate) ? DateTime.Parse(publicationDate).Year : 0,
|
||||
Month = month,
|
||||
Day = day,
|
||||
Year = year,
|
||||
Title = epubBook.Title,
|
||||
Genre = string.Join(",", epubBook.Schema.Package.Metadata.Subjects.Select(s => s.ToLower().Trim())),
|
||||
LanguageISO = epubBook.Schema.Package.Metadata.Languages.FirstOrDefault() ?? string.Empty
|
||||
|
||||
};
|
||||
ComicInfo.CleanComicInfo(info);
|
||||
|
||||
// Parse tags not exposed via Library
|
||||
foreach (var metadataItem in epubBook.Schema.Package.Metadata.MetaItems)
|
||||
{
|
||||
@ -450,7 +472,7 @@ namespace API.Services
|
||||
return docReader.GetPageCount();
|
||||
}
|
||||
|
||||
using var epubBook = EpubReader.OpenBook(filePath);
|
||||
using var epubBook = EpubReader.OpenBook(filePath, BookReaderOptions);
|
||||
return epubBook.Content.Html.Count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -504,7 +526,7 @@ namespace API.Services
|
||||
|
||||
try
|
||||
{
|
||||
using var epubBook = EpubReader.OpenBook(filePath);
|
||||
using var epubBook = EpubReader.OpenBook(filePath, BookReaderOptions);
|
||||
|
||||
// <meta content="The Dark Tower" name="calibre:series"/>
|
||||
// <meta content="Wolves of the Calla" name="calibre:title_sort"/>
|
||||
@ -669,8 +691,7 @@ namespace API.Services
|
||||
return GetPdfCoverImage(fileFilePath, fileName, outputDirectory);
|
||||
}
|
||||
|
||||
using var epubBook = EpubReader.OpenBook(fileFilePath);
|
||||
|
||||
using var epubBook = EpubReader.OpenBook(fileFilePath, BookReaderOptions);
|
||||
|
||||
try
|
||||
{
|
||||
|
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