mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
v0.5.4 - Double Page Layout, Estimated Reading Time, a new PDF Reader and Infinite Scroll?! (#1344)
* 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) * Bump versions by dotnet-bump-version. * version bump (#1271) * Bump versions by dotnet-bump-version. * UX Changes, Tasks, WebP, and More! (#1280) * When account updates occur for a user, send an event to them to tell them to refresh their account information (if they are on the site at the time). This way if we revoke permissions, the site will reactively adapt. * Some cleanup on the user preferences to remove some calls we don't need anymore. * Removed old bulk cleanup bookmark code as it's no longer needed. * Tweaked the messaging for stat collection to reflect what we collect now versus when this was initially implemented. * Implemented the ability for users to configure their servers to save bookmarks as webP. Reorganized the tabs for Admin dashboard to account for upcoming features. * Implemented the ability to bulk convert bookmarks (as many times as the user wants). Added a display of Reoccurring Jobs to the Tasks admin tab. Currently it's just placeholder, but will be enhanced further later in the release. * Tweaked the wording around the convert switch. * Moved System actions to the task tab * Added a controller just for Tachiyomi so we can have dedicated APIs for that client. Deprecated an existing API on the Reader route. * Fixed the unit tests * Bump versions by dotnet-bump-version. * Implemented the ability to read format tag and force special status. (#1284) * Bump versions by dotnet-bump-version. * Implemented the ability to parse some volume and chapter keywords for chinese. (#1285) * Bump versions by dotnet-bump-version. * Word Count (#1286) * Adding some code for Robbie * See more on series detail metadata area is now at the bottom on the section * Cleaned up subtitle headings to use a single class for offset with actionables * Added some markup for the new design, waiting for Robbie to finish it off * styling age-rating badge * Started hooking up basic analyze file service and hooks in the UI. Basic code to implement the count is implemented and in benchmarks. * Hooked up analyze ui to backend * Refactored Series Detail metadata area to use a new icon/title design * Cleaned up the new design * Pushing for robbie to do css * Massive performance improvement to scan series where we only need to scan folders reported that have series in them, rather than the whole library. * Removed theme page as we no longer need it. Added WordCount to DTOs so the UI can show them. Added new pipe to format numbers in compact mode. * Hooked up actual reading time based on user's words per hour * Refactor some magic numbers to consts * Hooked in progress reporting for series word count * Hooked up analyze files * Re-implemented time to read on comics * Removed the word Last Read * Show proper language name instead of iso tag on series detail page. Added some error handling on word count code. * Reworked error handling * Fixed some security vulnerabilities in npm. * Handle a case where there are no text nodes and instead of returning an empty list, htmlagilitypack returns null. * Tweaked the styles a bit on the icon-and-title * Code cleanup Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Tweaked when we calculate min reading time * Don't use plural if there is only 1 hour for reading * Fixed the logic for caluclating time to read on comics * Bump versions by dotnet-bump-version. * Drawers, Estimated Reading Time, Korean Parsing Support (#1297) * Started building out idea around detail drawer. Need code from word count to continue * Fixed the logic for caluclating time to read on comics * Adding styles * more styling fixes * Cleaned up the styles a bit more so it's at least functional. Not sure on the feature, might abandon. * Pulled Robbie's changes in and partially migrated them to the drawer. * Add offset overrides for offcanvas so it takes our header into account * Implemented a basic time left to finish the series (or at least what's in Kavita). Rough around the edges. * Cleaned up the drawer code. * Added Quick Catch ups to recommended page. Updated the timeout for scan tasks to ensure we don't run 2 at the same time. * Quick catchups implemented * Added preliminary support for Korean filename parsing. Reduced an array alloc that is called many thousands of times per scan. * Fixing drawer overflow * Fixed a calculation bug with average reading time. * Small spacing changes to drawer * Don't show estimated reading time if the user hasn't read anything * Bump eventsource from 1.1.1 to 2.0.2 in /UI/Web Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.1 to 2.0.2. - [Release notes](https://github.com/EventSource/eventsource/releases) - [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md) - [Commits](https://github.com/EventSource/eventsource/compare/v1.1.1...v2.0.2) --- updated-dependencies: - dependency-name: eventsource dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> * Added image to series detail drawer Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Time Estimation Cleanup (#1301) * Moved the calculation for time to read to the backend. Tweaked some logic around showing est time to complete. * Added debug logging to help pinpoint a duplicate issue in Kavita. * More combination logic is error checked in a special way for Robbie to reproduce an issue. * Migrated chapter detail card to use backend for time calculation. Ensure we take all chapters into account for volume time calcs * Tweaked messaging for some critical logs to include file * Ensure pages count uses comma separated number * Moved Hangfire annotations to interface level. Adjusted word count service to always recalculate when user requests via analyze series files. * Bump versions by dotnet-bump-version. * Jump Bar Testing (#1302) * Implemented a basic jump bar for the library view. This currently just interacts with existing pagination controls and is not inlined with infinite scroll yet. This is a first pass implementation. * Refactored time estimates into the reading service. * Cleaned up when the jump bar is shown to mimic pagination controls * Cleanup up code in reader service. * Scroll to card when selecting a jump key that is shown on the current page. * Ensure estimated times always has the smaller number on left hand side. * Fixed a bug with a missing vertical rule * Fixed an off by 1 pixel for search overlay * Bump versions by dotnet-bump-version. * Jumpbar Tweaks (#1305) * Adjusted the detail drawer to be slightly larger. * Attempted to shorten the jump bar on smaller screens. Robbie needs to take a look at this. * Adding plex-like styling to jumpbar * style fixes * style fixes * More fixes *sigh* * fix height issue * Fixing jumpbar on mobile * viewport height fix * added --primary-color-scrollbar for overflow across the app Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Fix for book cover extending across multiple pages (#1269) * Attaching book-reader scale style to parent instead of image * Fixing some width issues on images * Making sure max-height is respected * Fixing duplicate styles which caused excessive breaks * Updated the readme to reflect new UX and some tweaks to wordings. (#1270) * Bump versions by dotnet-bump-version. * version bump (#1271) * Bump versions by dotnet-bump-version. * UX Changes, Tasks, WebP, and More! (#1280) * When account updates occur for a user, send an event to them to tell them to refresh their account information (if they are on the site at the time). This way if we revoke permissions, the site will reactively adapt. * Some cleanup on the user preferences to remove some calls we don't need anymore. * Removed old bulk cleanup bookmark code as it's no longer needed. * Tweaked the messaging for stat collection to reflect what we collect now versus when this was initially implemented. * Implemented the ability for users to configure their servers to save bookmarks as webP. Reorganized the tabs for Admin dashboard to account for upcoming features. * Implemented the ability to bulk convert bookmarks (as many times as the user wants). Added a display of Reoccurring Jobs to the Tasks admin tab. Currently it's just placeholder, but will be enhanced further later in the release. * Tweaked the wording around the convert switch. * Moved System actions to the task tab * Added a controller just for Tachiyomi so we can have dedicated APIs for that client. Deprecated an existing API on the Reader route. * Fixed the unit tests * Bump versions by dotnet-bump-version. * Implemented the ability to read format tag and force special status. (#1284) * Bump versions by dotnet-bump-version. * Implemented the ability to parse some volume and chapter keywords for chinese. (#1285) * Bump versions by dotnet-bump-version. * Word Count (#1286) * Adding some code for Robbie * See more on series detail metadata area is now at the bottom on the section * Cleaned up subtitle headings to use a single class for offset with actionables * Added some markup for the new design, waiting for Robbie to finish it off * styling age-rating badge * Started hooking up basic analyze file service and hooks in the UI. Basic code to implement the count is implemented and in benchmarks. * Hooked up analyze ui to backend * Refactored Series Detail metadata area to use a new icon/title design * Cleaned up the new design * Pushing for robbie to do css * Massive performance improvement to scan series where we only need to scan folders reported that have series in them, rather than the whole library. * Removed theme page as we no longer need it. Added WordCount to DTOs so the UI can show them. Added new pipe to format numbers in compact mode. * Hooked up actual reading time based on user's words per hour * Refactor some magic numbers to consts * Hooked in progress reporting for series word count * Hooked up analyze files * Re-implemented time to read on comics * Removed the word Last Read * Show proper language name instead of iso tag on series detail page. Added some error handling on word count code. * Reworked error handling * Fixed some security vulnerabilities in npm. * Handle a case where there are no text nodes and instead of returning an empty list, htmlagilitypack returns null. * Tweaked the styles a bit on the icon-and-title * Code cleanup Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Tweaked when we calculate min reading time * Don't use plural if there is only 1 hour for reading * Fixed the logic for caluclating time to read on comics * Bump versions by dotnet-bump-version. * Drawers, Estimated Reading Time, Korean Parsing Support (#1297) * Started building out idea around detail drawer. Need code from word count to continue * Fixed the logic for caluclating time to read on comics * Adding styles * more styling fixes * Cleaned up the styles a bit more so it's at least functional. Not sure on the feature, might abandon. * Pulled Robbie's changes in and partially migrated them to the drawer. * Add offset overrides for offcanvas so it takes our header into account * Implemented a basic time left to finish the series (or at least what's in Kavita). Rough around the edges. * Cleaned up the drawer code. * Added Quick Catch ups to recommended page. Updated the timeout for scan tasks to ensure we don't run 2 at the same time. * Quick catchups implemented * Added preliminary support for Korean filename parsing. Reduced an array alloc that is called many thousands of times per scan. * Fixing drawer overflow * Fixed a calculation bug with average reading time. * Small spacing changes to drawer * Don't show estimated reading time if the user hasn't read anything * Bump eventsource from 1.1.1 to 2.0.2 in /UI/Web Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.1 to 2.0.2. - [Release notes](https://github.com/EventSource/eventsource/releases) - [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md) - [Commits](https://github.com/EventSource/eventsource/compare/v1.1.1...v2.0.2) --- updated-dependencies: - dependency-name: eventsource dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> * Added image to series detail drawer Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Time Estimation Cleanup (#1301) * Moved the calculation for time to read to the backend. Tweaked some logic around showing est time to complete. * Added debug logging to help pinpoint a duplicate issue in Kavita. * More combination logic is error checked in a special way for Robbie to reproduce an issue. * Migrated chapter detail card to use backend for time calculation. Ensure we take all chapters into account for volume time calcs * Tweaked messaging for some critical logs to include file * Ensure pages count uses comma separated number * Moved Hangfire annotations to interface level. Adjusted word count service to always recalculate when user requests via analyze series files. * Bump versions by dotnet-bump-version. * Jump Bar Testing (#1302) * Implemented a basic jump bar for the library view. This currently just interacts with existing pagination controls and is not inlined with infinite scroll yet. This is a first pass implementation. * Refactored time estimates into the reading service. * Cleaned up when the jump bar is shown to mimic pagination controls * Cleanup up code in reader service. * Scroll to card when selecting a jump key that is shown on the current page. * Ensure estimated times always has the smaller number on left hand side. * Fixed a bug with a missing vertical rule * Fixed an off by 1 pixel for search overlay * Bump versions by dotnet-bump-version. * Jumpbar Tweaks (#1305) * Adjusted the detail drawer to be slightly larger. * Attempted to shorten the jump bar on smaller screens. Robbie needs to take a look at this. * Adding plex-like styling to jumpbar * style fixes * style fixes * More fixes *sigh* * fix height issue * Fixing jumpbar on mobile * viewport height fix * added --primary-color-scrollbar for overflow across the app Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com> Co-authored-by: majora2007 <josephmajora@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Bugfix/reader issues (#1311) * Updated the design of icon and text to show a label. * Fixed a bug when fit to height and there is overflow on horizontal, the pagination area is stuck to the original width and after scrolling right, the pagination area doesn't move. * Attempt to fix a border showing on eink readers white mode with book reader * Removed debug code * Added back in pagination controls * fixing viewport overflow issue * Ensure volume detail drawer shows all pages for the volume, not just first chapter. Don't show release date when it's not a real date. Non-ongoing series will now show a different icon. * Fixing drawer viewport issue * Fix for book cover extending across multiple pages (#1269) * Attaching book-reader scale style to parent instead of image * Fixing some width issues on images * Making sure max-height is respected * Fixing duplicate styles which caused excessive breaks * Updated the readme to reflect new UX and some tweaks to wordings. (#1270) * Bump versions by dotnet-bump-version. * version bump (#1271) * Bump versions by dotnet-bump-version. * UX Changes, Tasks, WebP, and More! (#1280) * When account updates occur for a user, send an event to them to tell them to refresh their account information (if they are on the site at the time). This way if we revoke permissions, the site will reactively adapt. * Some cleanup on the user preferences to remove some calls we don't need anymore. * Removed old bulk cleanup bookmark code as it's no longer needed. * Tweaked the messaging for stat collection to reflect what we collect now versus when this was initially implemented. * Implemented the ability for users to configure their servers to save bookmarks as webP. Reorganized the tabs for Admin dashboard to account for upcoming features. * Implemented the ability to bulk convert bookmarks (as many times as the user wants). Added a display of Reoccurring Jobs to the Tasks admin tab. Currently it's just placeholder, but will be enhanced further later in the release. * Tweaked the wording around the convert switch. * Moved System actions to the task tab * Added a controller just for Tachiyomi so we can have dedicated APIs for that client. Deprecated an existing API on the Reader route. * Fixed the unit tests * Bump versions by dotnet-bump-version. * Implemented the ability to read format tag and force special status. (#1284) * Bump versions by dotnet-bump-version. * Implemented the ability to parse some volume and chapter keywords for chinese. (#1285) * Bump versions by dotnet-bump-version. * Word Count (#1286) * Adding some code for Robbie * See more on series detail metadata area is now at the bottom on the section * Cleaned up subtitle headings to use a single class for offset with actionables * Added some markup for the new design, waiting for Robbie to finish it off * styling age-rating badge * Started hooking up basic analyze file service and hooks in the UI. Basic code to implement the count is implemented and in benchmarks. * Hooked up analyze ui to backend * Refactored Series Detail metadata area to use a new icon/title design * Cleaned up the new design * Pushing for robbie to do css * Massive performance improvement to scan series where we only need to scan folders reported that have series in them, rather than the whole library. * Removed theme page as we no longer need it. Added WordCount to DTOs so the UI can show them. Added new pipe to format numbers in compact mode. * Hooked up actual reading time based on user's words per hour * Refactor some magic numbers to consts * Hooked in progress reporting for series word count * Hooked up analyze files * Re-implemented time to read on comics * Removed the word Last Read * Show proper language name instead of iso tag on series detail page. Added some error handling on word count code. * Reworked error handling * Fixed some security vulnerabilities in npm. * Handle a case where there are no text nodes and instead of returning an empty list, htmlagilitypack returns null. * Tweaked the styles a bit on the icon-and-title * Code cleanup Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Tweaked when we calculate min reading time * Don't use plural if there is only 1 hour for reading * Fixed the logic for caluclating time to read on comics * Bump versions by dotnet-bump-version. * Drawers, Estimated Reading Time, Korean Parsing Support (#1297) * Started building out idea around detail drawer. Need code from word count to continue * Fixed the logic for caluclating time to read on comics * Adding styles * more styling fixes * Cleaned up the styles a bit more so it's at least functional. Not sure on the feature, might abandon. * Pulled Robbie's changes in and partially migrated them to the drawer. * Add offset overrides for offcanvas so it takes our header into account * Implemented a basic time left to finish the series (or at least what's in Kavita). Rough around the edges. * Cleaned up the drawer code. * Added Quick Catch ups to recommended page. Updated the timeout for scan tasks to ensure we don't run 2 at the same time. * Quick catchups implemented * Added preliminary support for Korean filename parsing. Reduced an array alloc that is called many thousands of times per scan. * Fixing drawer overflow * Fixed a calculation bug with average reading time. * Small spacing changes to drawer * Don't show estimated reading time if the user hasn't read anything * Bump eventsource from 1.1.1 to 2.0.2 in /UI/Web Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.1 to 2.0.2. - [Release notes](https://github.com/EventSource/eventsource/releases) - [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md) - [Commits](https://github.com/EventSource/eventsource/compare/v1.1.1...v2.0.2) --- updated-dependencies: - dependency-name: eventsource dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> * Added image to series detail drawer Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Time Estimation Cleanup (#1301) * Moved the calculation for time to read to the backend. Tweaked some logic around showing est time to complete. * Added debug logging to help pinpoint a duplicate issue in Kavita. * More combination logic is error checked in a special way for Robbie to reproduce an issue. * Migrated chapter detail card to use backend for time calculation. Ensure we take all chapters into account for volume time calcs * Tweaked messaging for some critical logs to include file * Ensure pages count uses comma separated number * Moved Hangfire annotations to interface level. Adjusted word count service to always recalculate when user requests via analyze series files. * Bump versions by dotnet-bump-version. * Jump Bar Testing (#1302) * Implemented a basic jump bar for the library view. This currently just interacts with existing pagination controls and is not inlined with infinite scroll yet. This is a first pass implementation. * Refactored time estimates into the reading service. * Cleaned up when the jump bar is shown to mimic pagination controls * Cleanup up code in reader service. * Scroll to card when selecting a jump key that is shown on the current page. * Ensure estimated times always has the smaller number on left hand side. * Fixed a bug with a missing vertical rule * Fixed an off by 1 pixel for search overlay * Bump versions by dotnet-bump-version. * Jumpbar Tweaks (#1305) * Adjusted the detail drawer to be slightly larger. * Attempted to shorten the jump bar on smaller screens. Robbie needs to take a look at this. * Adding plex-like styling to jumpbar * style fixes * style fixes * More fixes *sigh* * fix height issue * Fixing jumpbar on mobile * viewport height fix * added --primary-color-scrollbar for overflow across the app Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com> Co-authored-by: majora2007 <josephmajora@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Close drawer when opening a chapter to read * The actionables were not working on chapters in the detail drawer for a volume. Added a distinct read action instead of having the user to have hidden knowledge you can click the cover images. * Debug code in. Fixed a bug where book reader pagination wasn't covering from top to bottom when immersive mode was on. Trying to better ensure images don't span multiple virtual pages in column mode. Changed icon/title so label is bolded * Updated some dependencies for security issues. Fixed up the fix for images on column view wrapping to next virtual page. Lots of css tweaks to the layout code to make it easier to work with. * When switching from column layout to default, scroll back to where the user was. * Fixed some overlap on default mode with bottom bar * Series Detail uses the cover image update mechanism builtin, instead of the old way to handle. Added some text when there is no summary on a volume/chapter. * Side nav filter now has clear button within field. Filter no longer shows when side nav is collapsed. Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * bugfix/user-preferences-accordion (#1307) * Add files via upload * Revert "Add files via upload" This reverts commit 446e3e0f046859a0695aedfe79f28965ad104864. Revert upload package.json * bugfix/user-preferences Fixed a bug with the BookReader panel id missing brackets which prevented the accordion from functioning correctly. * Bump versions by dotnet-bump-version. * Infinite Scroll + List View + Cover Upload Redesign (#1319) * Started with the redesign of the cover image chooser redesign to be less click intensive for volume/chapter images. Made some headings bold in card detail drawer. * Tweaked the styles * Moved where the info cards show * Added an ability to open a page settings drawer * Cleaned up some old code that isn't needed anymore. * Started implementing a list view. Refactored some title code to a dedicated component * List view implemented but way too many API calls. Either need caching or adjusting the SeriesDetail api. * Fixed a bug where if the progress bar didn't render on a card item while a download was in progress, the download indicator would be removed. * Large refactor to move a lot of the needed fields to the chapter and volume dtos for series detail. All fields are noted when only used in series detail. * Implemented cards for other tabs (except related) * Fixed the unit test which needed a mocked reader service call. * More cleanup around age rating and removing old code from the refactor. Commented out sorting till i feel motivated to work on that. * Some cleanup and restored cards as initial layout. Time to test this out and see if there is value add. * Added ability for Chapters tab to show the volume chapters belong to (if applicable) * Adding style fixes * Cover image updates, don't allow the first image (which is what is currently set) to respond to cover changes. Hide the ID field on list item for series detail. * Refactored the title for list item to be injectable * Cleaned up the selection code to make it less finicky on mobile when tap scrolling. * Refactored chapter tab to show volume as well on list view. * Ensure word count shows for Volumes * Started adding virtual scrolling, pushing up so Robbie can mess around * Started adding virtual scrolling, pushing up so Robbie can mess around * Fixed a bug where all chapters would come under specials * Show title data as accent if set. * Style fixes for virtual scroller * Restyling scroll * Implemented a way to show storyline with virtual scrolling * Show Word Count for chapters and cleaned up some logics. * I might have card layout working with virtual scroll code. * Some cleanup to hide more system like properties from info bar on series detail page. Fixed some missing time estimate info on storyline chapters. * Fixed a regression on series service when I integrated VolumeTitle. * Refactored read time to the backend. Added WordCount to the volume itself so we don't need to calculate on frontend. When asking to analyze files from a series, force the calculation. * Fixed SeriesDetail api code * Fixed up the code in the drawer to better update list/card mode * Basic infinite scroll implemented, however due to how we are updating the list to render, we are re-rending cards that haven't been touched. * Updated how we render and layout data for infinite scroll on library detail. It's almost there. * Started laying foundation for loading pages backwards. Removed lazy loading of images since we are now using virtual paging. * Hooked in some basic code to allow user to load a prev page with infinite scroll. * Fixed up series detail api and undid the non-lazy loaded images. Changed the router to help with this infinite loading on Firefox issue. * Fixed up some naming issues with Series Detail and added a new test. * This is an infinite scroll without pagination implementation. It is not fully done, but off to a good start. Virtual scroller with jump bar is working pretty well, def needs more polishing and tweaking. There are hacks in this implementation that need to be revisited. * Refactored code so that we don't use any pagination and load all results by default. * Misc code cleanup from build warnings. * Cleaned up some logic for how to display titles in list view. * More title cleanup for specials * Hooked up page layout to user preferences and renamed an existing user pref name to match the dto. * Swapped out everything but storyline with virtual-scroller over CDK * Removed CDK from series detail. * Default value for migration on page layout * Updating card layout for library detail page * fixing height for mobile * Moved scrollbar * Tweaked some styling for layouts when there is no data * Refactored the series cards into their own component to make it re-usable. * More tweaks on series info cards layout and enhanced a few pages with trackby functions. * Removed some dead code * Added download on series detail to actionables to fit in with new scroll strategy. * Fixed language not being updated and sent to the backend for series update. * Fixed a bad migration (if you ran any prior migration in this branch, you need to undo before you use this commit) * Adding sticky tabs * fixed mobile gap on sticky tab * Enhanced the card title for books to show number up front. * Adjusted the gutters on admin dashboard * Removed debug code * Removing duplicate book title * Cleaned up old references to cdk scroller * Implemented a basic jump bar scaling algorithm. Not perfect, but works pretty well. * Code smells Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Fixing sticky tabs overlapping title on longer titles (#1320) * Bump versions by dotnet-bump-version. * dynamic height for series detail (#1321) * Adding dynamic height function * pushing change requests * Moved method to getter * Changed carousel reel to onpush strat Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com> * Bump versions by dotnet-bump-version. * New PDF Reader (#1324) * Refactored all the code that opens the reader to use a unified function. Added new library and setup basic pdf reader route. * Progress saving is implemented. Targeting ES6 now. * Customized the toolbar to remove things we don't want, made the download button download with correct filename. Adjusted zoom setting to work well on first load regardless of device. * Stream the pdf file to the UI rather than handling the download ourselves. * Started implementing a custom toolbar. * Fixed up the jump bar calculations * Fixed filtering being broken * Pushing up for Robbie to cleanup the toolbar layout * Added an additional button. Working on logic while robbie takes styling * Tried to fix the code for robbie * Tweaks for fonts * Added button for book mode, but doesn't seem to work after renderer is built * Removed book mode * Removed the old image caching code for pdfs as it's not needed with new reader * Removed the interfaces to extract images from pdf. * Fixed original pagination area not scaling correctly * Integrated series remove events to library detail * Cleaned up the getter naming convention * Cleaned up some of the manga reader code to reduce cluter and improve re-use * Implemented Japanese parser support for volume and chapters. * Fixed a bug where resetting scroll in manga reader wasn't working * Fixed a bug where word count grew on each scan. * Removed unused variable * Ensure we calculate word count on files with their own cache timestamp * Adjusted size of reel headers * Put some code in for moving on original image with keyboard, but it's not in use. * Cleaned up the css for the pdf reader * Cleaned up the code * Tweaked the list item so we show scrollbar now when fully read * Bump versions by dotnet-bump-version. * Bugfix for sticky tabs on firefox (#1322) * Updating calcs for firefox * Removing unused code * Fixed up browser discrepencies * Review updates * Review updates * Added debouncing to scroll * Fixed a janky scroll issue with overscrolling * Cleaned up the code to use renderer and injectable document for SSR. * Removing sticky tabs Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com> * Bump versions by dotnet-bump-version. * Directory Picker Rework (#1325) * Started on the directory picker refactor. * Coded some basic working version. Needs styling and variable cleanup * code cleanup * Implemented the ability to expose swagger on non-development servers. * Implemented the ability to expose swagger on non-development servers. * Bump versions by dotnet-bump-version. * All Around Polish (#1328) * Added --card-list-item-bg-color for the card list items * Updated the card list item progress to match how cards render * Implemented the ability to configure how many backups are retained. * Fixed a bug where odd jump keys could cause a bad index error for jump bar * Commented out more code for the pagination route if we go with that. * Reverted a move of DisableConcurrentExecution to interface, as it seems to not work there. * Updated manga format utility code to pipes * Fixed bulk selection on series detail page * Fixed bulk selection on all other pages * Changed card item to OnPush * Updated image component to OnPush * Updated Series Card to OnPush * Updated Series Detail to OnPush * Lots of changes here. Integrated parentscroll support on card detail layout. Added jump bar (custom js implementation) on collection, reading list and all series pages. Updated UserParams to default to no pagination. Lots of cleanup all around * Updated some notes on a module use * Some code cleanup * Fixed up a broken test due to the mapper not being configured in the test. * Applied TabID pattern to edit collection tags * Applied css from series detail to collection detail page to remove double scrollbar * Implemented the ability to sort by Time To Read. * Throw an error to the UI when we extract an archive and it contains invalid characters in the filename for the Server OS. * Tweaked how the page scrolls for jumpbar on collection detail. We will have to polish another release * Cleaned up the styling on directory picker * Put some code in but it doesn't work for scroll to top on virtual scrolling. I'll do it later. * Fixed a container bug * Bump versions by dotnet-bump-version. * Fixed a bug where there was no pagination on the dashboard and it would take some time to load (#1329) * Bump versions by dotnet-bump-version. * Swagger, Tachiyomi, and some new settings (#1331) * Fixed up swagger generation * Updated Tachiyomi's latest-chapter to hopefully solve some sync issues. * Fixed #1279 with table of contents due to new EPubReader * When errors occur, show the event widget icon in red * Lots of documentation added and tweaked some wording around backups and swagger * For promidius * Return proper ChapterDTO * Hacks for Promidius * Cleanup code * No loose leaf, send max chapter * One more encode change * Implemented code per promiduius' requirements * Fixed a bug in the epub parsing where even if you had a series index and series group, but didn't have the series in the title, Kavita wouldn't group them properly. * Removed some extra comment * Implemented the ability to change a library's type after it's been setup. This displays a warning explaining the dangers of it. * Removed some whitespace * Blur descriptions based on read status for list item view to avoid spoilers * Tweaked placement of a tooltip due to new series detail styles * Hooked up a user preference for bluring unread summaries. Fixed a bug in refresh token where we would cause re-authentication when it shouldn't be needed. * Bump versions by dotnet-bump-version. * Add KnownProxies configuration (#1332) * Bump versions by dotnet-bump-version. * More Stat collection (#1337) * Ensure that Scan Series triggers a file analysis task. * Tweaked concurrency for Analyze Files * Implemented new stats tracking for upcoming performance release. * Bump versions by dotnet-bump-version. * Double Page Rendering (#1333) * First commit Enabling double layout mode (and minor fixes) * Second commit * bug-fix/side-nav-icon Side nav icons were not aligned to centred. * bug-fix/side-nav-icon Side navigation icons were not aligned centred. * Squashed commit of the following: commit d796bcdc0a084ff3ab187b8c6aca74805b68d463 Author: majora2007 <josephmajora@gmail.com> Date: Thu May 26 00:24:25 2022 +0000 Bump versions by dotnet-bump-version. commit 3c92b6d8a5250b8d52331d125a44290bab310697 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 25 19:11:01 2022 -0500 Fixed the logic for caluclating time to read on comics commit e7617862a597ec28d6623b6468d0d70d060bcbf0 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 25 17:49:45 2022 -0500 Don't use plural if there is only 1 hour for reading commit 713e20ebf4338e48591f01c3ccbdd2dd1bd8e4aa Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 25 17:48:50 2022 -0500 Tweaked when we calculate min reading time commit c0f7dd39a2e792ae1e385c15a5991bdf1e6bf76b Author: majora2007 <josephmajora@gmail.com> Date: Wed May 25 22:10:42 2022 +0000 Bump versions by dotnet-bump-version. commit c1490d6e86367377c11ccba568ddd9d206eaae87 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 25 16:53:39 2022 -0500 Word Count (#1286) * Adding some code for Robbie * See more on series detail metadata area is now at the bottom on the section * Cleaned up subtitle headings to use a single class for offset with actionables * Added some markup for the new design, waiting for Robbie to finish it off * styling age-rating badge * Started hooking up basic analyze file service and hooks in the UI. Basic code to implement the count is implemented and in benchmarks. * Hooked up analyze ui to backend * Refactored Series Detail metadata area to use a new icon/title design * Cleaned up the new design * Pushing for robbie to do css * Massive performance improvement to scan series where we only need to scan folders reported that have series in them, rather than the whole library. * Removed theme page as we no longer need it. Added WordCount to DTOs so the UI can show them. Added new pipe to format numbers in compact mode. * Hooked up actual reading time based on user's words per hour * Refactor some magic numbers to consts * Hooked in progress reporting for series word count * Hooked up analyze files * Re-implemented time to read on comics * Removed the word Last Read * Show proper language name instead of iso tag on series detail page. Added some error handling on word count code. * Reworked error handling * Fixed some security vulnerabilities in npm. * Handle a case where there are no text nodes and instead of returning an empty list, htmlagilitypack returns null. * Tweaked the styles a bit on the icon-and-title * Code cleanup Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> commit 0a70ac35dca3180b2b62927ddb22153945467c8a Author: majora2007 <josephmajora@gmail.com> Date: Wed May 25 19:39:11 2022 +0000 Bump versions by dotnet-bump-version. commit a9a1ec02cac0162b6d4dad5cb9c0b964cd11fded Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 25 14:25:53 2022 -0500 Implemented the ability to parse some volume and chapter keywords for chinese. (#1285) commit 1247f450e2c7001bcc4bb48ca16979656bd521c7 Author: majora2007 <josephmajora@gmail.com> Date: Wed May 25 17:21:56 2022 +0000 Bump versions by dotnet-bump-version. commit 1a128a3af435e5d54e13ceb189648f5bfbcacd39 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 25 12:05:16 2022 -0500 Implemented the ability to read format tag and force special status. (#1284) commit a0e1ba8d67a232357a43040ac4e886635f61cbf2 Author: majora2007 <josephmajora@gmail.com> Date: Mon May 23 23:30:19 2022 +0000 Bump versions by dotnet-bump-version. commit e0a2fc615f0ea2c363394c9596711d08043e1738 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Mon May 23 18:19:52 2022 -0500 UX Changes, Tasks, WebP, and More! (#1280) * When account updates occur for a user, send an event to them to tell them to refresh their account information (if they are on the site at the time). This way if we revoke permissions, the site will reactively adapt. * Some cleanup on the user preferences to remove some calls we don't need anymore. * Removed old bulk cleanup bookmark code as it's no longer needed. * Tweaked the messaging for stat collection to reflect what we collect now versus when this was initially implemented. * Implemented the ability for users to configure their servers to save bookmarks as webP. Reorganized the tabs for Admin dashboard to account for upcoming features. * Implemented the ability to bulk convert bookmarks (as many times as the user wants). Added a display of Reoccurring Jobs to the Tasks admin tab. Currently it's just placeholder, but will be enhanced further later in the release. * Tweaked the wording around the convert switch. * Moved System actions to the task tab * Added a controller just for Tachiyomi so we can have dedicated APIs for that client. Deprecated an existing API on the Reader route. * Fixed the unit tests commit dd83b6a9a1ee06dd342b96b3be5ca36c6d1ba8d6 Author: majora2007 <josephmajora@gmail.com> Date: Sat May 21 23:36:25 2022 +0000 Bump versions by dotnet-bump-version. commit 16eb1e3e8ed19f4b0857b1b14593448b072d48ca Author: Joseph Milazzo <josephmajora@gmail.com> Date: Sat May 21 18:24:18 2022 -0500 version bump (#1271) commit f4f91fa5a957f92b175d63b9af30ad5e5c94decf Author: majora2007 <josephmajora@gmail.com> Date: Sat May 21 23:03:49 2022 +0000 Bump versions by dotnet-bump-version. commit 1801afd4a7e28f0c9da669b61fd5970002a44c8f Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Sat May 21 17:52:57 2022 -0500 Updated the readme to reflect new UX and some tweaks to wordings. (#1270) commit bcad2e4a553e864b91dfef260527601cfd35da31 Author: majora2007 <josephmajora@gmail.com> Date: Sat May 21 02:18:06 2022 +0000 Bump versions by dotnet-bump-version. commit 81082508f2d81dbff00856f1b1988147f2f2127f Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Fri May 20 21:05:09 2022 -0500 Release Shakeout Part 2 (#1267) * Fixed manga reader and removed debug code * Removed some console.logs commit 92010379f165cf56307af37d448cfe6734d677ba Author: majora2007 <josephmajora@gmail.com> Date: Fri May 20 23:00:48 2022 +0000 Bump versions by dotnet-bump-version. commit a0623415647c276f6131f39172ae0dd2ffa6aee5 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Fri May 20 17:50:17 2022 -0500 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> commit 49d8a7c6cabc2cac2b2839b421af97854986f31e Author: majora2007 <josephmajora@gmail.com> Date: Thu May 19 12:24:13 2022 +0000 Bump versions by dotnet-bump-version. commit f2b1cd55f0e5b7f4f70c2a2029af6aa85225dc7e Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Thu May 19 07:14:18 2022 -0500 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> commit 876e19177e57e06a8919450e350eeefb1c2a0302 Author: majora2007 <josephmajora@gmail.com> Date: Thu May 19 00:46:22 2022 +0000 Bump versions by dotnet-bump-version. commit 1961b412688f90f7b099d1f086bd5629dd1515e1 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 18 19:31:49 2022 -0500 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> commit 6f23a3bc6d31ac1f0d0ca6e3d5a985e980d9649e Author: majora2007 <josephmajora@gmail.com> Date: Sun May 15 21:31:42 2022 +0000 Bump versions by dotnet-bump-version. commit 92fb185f2030331a5c26c7c70b06531b00157c21 Author: Robbie Davis <robbie@therobbiedavis.com> Date: Sun May 15 17:21:17 2022 -0400 Adding code to make sure bottom actionbar is applied on layout change (#1258) commit bb791b7789b047b39c7c687477eccb3bd2f24786 Author: majora2007 <josephmajora@gmail.com> Date: Sun May 15 19:44:47 2022 +0000 Bump versions by dotnet-bump-version. commit cdc49317705d7a74aa9ed0cb0dfaa291039ca7ad Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Sun May 15 14:34:53 2022 -0500 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 commit ccb6414e9e07366efd6a1195e7dde77eda869a48 Author: majora2007 <josephmajora@gmail.com> Date: Sat May 14 22:13:24 2022 +0000 Bump versions by dotnet-bump-version. commit d458a823ef6e3b779b6c5722db2b226597cb7793 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Sat May 14 17:02:58 2022 -0500 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> commit be5b997259f6442fcac8401a09db711971e257b2 Author: majora2007 <josephmajora@gmail.com> Date: Sat May 14 00:40:11 2022 +0000 Bump versions by dotnet-bump-version. commit f701f8e5999da4e7ce8f8e1b4c7a968953426a92 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Fri May 13 19:30:37 2022 -0500 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> commit dfcc2f081311113fa4cf1b96097ccc92d3677f54 Author: majora2007 <josephmajora@gmail.com> Date: Mon May 9 01:01:46 2022 +0000 Bump versions by dotnet-bump-version. commit 2723a6cd1061a0abd575b60b4240897fd99ce93a Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Sun May 8 19:52:15 2022 -0500 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. * bug-fix/typos and linting * Update API.csproj * Squashed commit of the following: commit d796bcdc0a084ff3ab187b8c6aca74805b68d463 Author: majora2007 <josephmajora@gmail.com> Date: Thu May 26 00:24:25 2022 +0000 Bump versions by dotnet-bump-version. commit 3c92b6d8a5250b8d52331d125a44290bab310697 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 25 19:11:01 2022 -0500 Fixed the logic for caluclating time to read on comics commit e7617862a597ec28d6623b6468d0d70d060bcbf0 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 25 17:49:45 2022 -0500 Don't use plural if there is only 1 hour for reading commit 713e20ebf4338e48591f01c3ccbdd2dd1bd8e4aa Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 25 17:48:50 2022 -0500 Tweaked when we calculate min reading time commit c0f7dd39a2e792ae1e385c15a5991bdf1e6bf76b Author: majora2007 <josephmajora@gmail.com> Date: Wed May 25 22:10:42 2022 +0000 Bump versions by dotnet-bump-version. commit c1490d6e86367377c11ccba568ddd9d206eaae87 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 25 16:53:39 2022 -0500 Word Count (#1286) * Adding some code for Robbie * See more on series detail metadata area is now at the bottom on the section * Cleaned up subtitle headings to use a single class for offset with actionables * Added some markup for the new design, waiting for Robbie to finish it off * styling age-rating badge * Started hooking up basic analyze file service and hooks in the UI. Basic code to implement the count is implemented and in benchmarks. * Hooked up analyze ui to backend * Refactored Series Detail metadata area to use a new icon/title design * Cleaned up the new design * Pushing for robbie to do css * Massive performance improvement to scan series where we only need to scan folders reported that have series in them, rather than the whole library. * Removed theme page as we no longer need it. Added WordCount to DTOs so the UI can show them. Added new pipe to format numbers in compact mode. * Hooked up actual reading time based on user's words per hour * Refactor some magic numbers to consts * Hooked in progress reporting for series word count * Hooked up analyze files * Re-implemented time to read on comics * Removed the word Last Read * Show proper language name instead of iso tag on series detail page. Added some error handling on word count code. * Reworked error handling * Fixed some security vulnerabilities in npm. * Handle a case where there are no text nodes and instead of returning an empty list, htmlagilitypack returns null. * Tweaked the styles a bit on the icon-and-title * Code cleanup Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> commit 0a70ac35dca3180b2b62927ddb22153945467c8a Author: majora2007 <josephmajora@gmail.com> Date: Wed May 25 19:39:11 2022 +0000 Bump versions by dotnet-bump-version. commit a9a1ec02cac0162b6d4dad5cb9c0b964cd11fded Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 25 14:25:53 2022 -0500 Implemented the ability to parse some volume and chapter keywords for chinese. (#1285) commit 1247f450e2c7001bcc4bb48ca16979656bd521c7 Author: majora2007 <josephmajora@gmail.com> Date: Wed May 25 17:21:56 2022 +0000 Bump versions by dotnet-bump-version. commit 1a128a3af435e5d54e13ceb189648f5bfbcacd39 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 25 12:05:16 2022 -0500 Implemented the ability to read format tag and force special status. (#1284) commit a0e1ba8d67a232357a43040ac4e886635f61cbf2 Author: majora2007 <josephmajora@gmail.com> Date: Mon May 23 23:30:19 2022 +0000 Bump versions by dotnet-bump-version. commit e0a2fc615f0ea2c363394c9596711d08043e1738 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Mon May 23 18:19:52 2022 -0500 UX Changes, Tasks, WebP, and More! (#1280) * When account updates occur for a user, send an event to them to tell them to refresh their account information (if they are on the site at the time). This way if we revoke permissions, the site will reactively adapt. * Some cleanup on the user preferences to remove some calls we don't need anymore. * Removed old bulk cleanup bookmark code as it's no longer needed. * Tweaked the messaging for stat collection to reflect what we collect now versus when this was initially implemented. * Implemented the ability for users to configure their servers to save bookmarks as webP. Reorganized the tabs for Admin dashboard to account for upcoming features. * Implemented the ability to bulk convert bookmarks (as many times as the user wants). Added a display of Reoccurring Jobs to the Tasks admin tab. Currently it's just placeholder, but will be enhanced further later in the release. * Tweaked the wording around the convert switch. * Moved System actions to the task tab * Added a controller just for Tachiyomi so we can have dedicated APIs for that client. Deprecated an existing API on the Reader route. * Fixed the unit tests commit dd83b6a9a1ee06dd342b96b3be5ca36c6d1ba8d6 Author: majora2007 <josephmajora@gmail.com> Date: Sat May 21 23:36:25 2022 +0000 Bump versions by dotnet-bump-version. commit 16eb1e3e8ed19f4b0857b1b14593448b072d48ca Author: Joseph Milazzo <josephmajora@gmail.com> Date: Sat May 21 18:24:18 2022 -0500 version bump (#1271) commit f4f91fa5a957f92b175d63b9af30ad5e5c94decf Author: majora2007 <josephmajora@gmail.com> Date: Sat May 21 23:03:49 2022 +0000 Bump versions by dotnet-bump-version. commit 1801afd4a7e28f0c9da669b61fd5970002a44c8f Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Sat May 21 17:52:57 2022 -0500 Updated the readme to reflect new UX and some tweaks to wordings. (#1270) commit bcad2e4a553e864b91dfef260527601cfd35da31 Author: majora2007 <josephmajora@gmail.com> Date: Sat May 21 02:18:06 2022 +0000 Bump versions by dotnet-bump-version. commit 81082508f2d81dbff00856f1b1988147f2f2127f Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Fri May 20 21:05:09 2022 -0500 Release Shakeout Part 2 (#1267) * Fixed manga reader and removed debug code * Removed some console.logs commit 92010379f165cf56307af37d448cfe6734d677ba Author: majora2007 <josephmajora@gmail.com> Date: Fri May 20 23:00:48 2022 +0000 Bump versions by dotnet-bump-version. commit a0623415647c276f6131f39172ae0dd2ffa6aee5 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Fri May 20 17:50:17 2022 -0500 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> commit 49d8a7c6cabc2cac2b2839b421af97854986f31e Author: majora2007 <josephmajora@gmail.com> Date: Thu May 19 12:24:13 2022 +0000 Bump versions by dotnet-bump-version. commit f2b1cd55f0e5b7f4f70c2a2029af6aa85225dc7e Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Thu May 19 07:14:18 2022 -0500 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> commit 876e19177e57e06a8919450e350eeefb1c2a0302 Author: majora2007 <josephmajora@gmail.com> Date: Thu May 19 00:46:22 2022 +0000 Bump versions by dotnet-bump-version. commit 1961b412688f90f7b099d1f086bd5629dd1515e1 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed May 18 19:31:49 2022 -0500 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> commit 6f23a3bc6d31ac1f0d0ca6e3d5a985e980d9649e Author: majora2007 <josephmajora@gmail.com> Date: Sun May 15 21:31:42 2022 +0000 Bump versions by dotnet-bump-version. commit 92fb185f2030331a5c26c7c70b06531b00157c21 Author: Robbie Davis <robbie@therobbiedavis.com> Date: Sun May 15 17:21:17 2022 -0400 Adding code to make sure bottom actionbar is applied on layout change (#1258) commit bb791b7789b047b39c7c687477eccb3bd2f24786 Author: majora2007 <josephmajora@gmail.com> Date: Sun May 15 19:44:47 2022 +0000 Bump versions by dotnet-bump-version. commit cdc49317705d7a74aa9ed0cb0dfaa291039ca7ad Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Sun May 15 14:34:53 2022 -0500 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 commit ccb6414e9e07366efd6a1195e7dde77eda869a48 Author: majora2007 <josephmajora@gmail.com> Date: Sat May 14 22:13:24 2022 +0000 Bump versions by dotnet-bump-version. commit d458a823ef6e3b779b6c5722db2b226597cb7793 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Sat May 14 17:02:58 2022 -0500 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> commit be5b997259f6442fcac8401a09db711971e257b2 Author: majora2007 <josephmajora@gmail.com> Date: Sat May 14 00:40:11 2022 +0000 Bump versions by dotnet-bump-version. commit f701f8e5999da4e7ce8f8e1b4c7a968953426a92 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Fri May 13 19:30:37 2022 -0500 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> commit dfcc2f081311113fa4cf1b96097ccc92d3677f54 Author: majora2007 <josephmajora@gmail.com> Date: Mon May 9 01:01:46 2022 +0000 Bump versions by dotnet-bump-version. commit 2723a6cd1061a0abd575b60b4240897fd99ce93a Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Sun May 8 19:52:15 2022 -0500 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. * bug-fix/typos & linting * Add files via upload * bug-fix/double layout html: image-1 <img> needed closing scss: full-height needed display: inline-block (not grid) * bug-fix/itemsPerPage Set items per page to 112 (%8=0, %7=0) * Revert "Add files via upload" This reverts commit 446e3e0f046859a0695aedfe79f28965ad104864. Revert upload package.json * bugfix/user-preferences Fixed a bug with the BookReader panel id missing brackets which prevented the accordion from functioning correctly. * Squashed commit of the following: commit ebbb3ec86b546bd01d17d5b631620de6ec6b7f54 Merge: 24d90527 2ed0aca8 Author: marcelo <mguimaraes@okanagan.bc.ca> Date: Fri Jun 17 21:15:42 2022 -0700 Merge remote-tracking branch 'upstream/develop' into develop commit 2ed0aca866eebf40edf13fe44e1a4cbd417ad47f Author: majora2007 <josephmajora@gmail.com> Date: Thu Jun 16 17:20:39 2022 +0000 Bump versions by dotnet-bump-version. commit 9c851b0f0e2e473e229456913ac026b6cb7ab5e1 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Thu Jun 16 12:08:09 2022 -0500 Directory Picker Rework (#1325) * Started on the directory picker refactor. * Coded some basic working version. Needs styling and variable cleanup * code cleanup * Implemented the ability to expose swagger on non-development servers. * Implemented the ability to expose swagger on non-development servers. commit 0f5a7ee6fac33767208901ec7bf6ef61c547e523 Author: majora2007 <josephmajora@gmail.com> Date: Thu Jun 16 14:09:35 2022 +0000 Bump versions by dotnet-bump-version. commit c8418d127c3a36cee16393ee70748f9fb0216efc Author: Robbie Davis <robbie@therobbiedavis.com> Date: Thu Jun 16 09:50:23 2022 -0400 Bugfix for sticky tabs on firefox (#1322) * Updating calcs for firefox * Removing unused code * Fixed up browser discrepencies * Review updates * Review updates * Added debouncing to scroll * Fixed a janky scroll issue with overscrolling * Cleaned up the code to use renderer and injectable document for SSR. * Removing sticky tabs Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com> commit 5b829af5312ca107d4d16ded6efb02d5475287b4 Author: majora2007 <josephmajora@gmail.com> Date: Wed Jun 15 21:57:45 2022 +0000 Bump versions by dotnet-bump-version. commit 3ab3a10ae79445ebf6d0fe2ecf3d1de989adb398 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Wed Jun 15 16:43:32 2022 -0500 New PDF Reader (#1324) * Refactored all the code that opens the reader to use a unified function. Added new library and setup basic pdf reader route. * Progress saving is implemented. Targeting ES6 now. * Customized the toolbar to remove things we don't want, made the download button download with correct filename. Adjusted zoom setting to work well on first load regardless of device. * Stream the pdf file to the UI rather than handling the download ourselves. * Started implementing a custom toolbar. * Fixed up the jump bar calculations * Fixed filtering being broken * Pushing up for Robbie to cleanup the toolbar layout * Added an additional button. Working on logic while robbie takes styling * Tried to fix the code for robbie * Tweaks for fonts * Added button for book mode, but doesn't seem to work after renderer is built * Removed book mode * Removed the old image caching code for pdfs as it's not needed with new reader * Removed the interfaces to extract images from pdf. * Fixed original pagination area not scaling correctly * Integrated series remove events to library detail * Cleaned up the getter naming convention * Cleaned up some of the manga reader code to reduce cluter and improve re-use * Implemented Japanese parser support for volume and chapters. * Fixed a bug where resetting scroll in manga reader wasn't working * Fixed a bug where word count grew on each scan. * Removed unused variable * Ensure we calculate word count on files with their own cache timestamp * Adjusted size of reel headers * Put some code in for moving on original image with keyboard, but it's not in use. * Cleaned up the css for the pdf reader * Cleaned up the code * Tweaked the list item so we show scrollbar now when fully read commit 384fac68c443c3c7aaa159f0944d400e236a5177 Author: majora2007 <josephmajora@gmail.com> Date: Tue Jun 14 15:18:27 2022 +0000 Bump versions by dotnet-bump-version. commit 78f0bad144b487bbe7dee3f6eecb9a827062c741 Author: Robbie Davis <robbie@therobbiedavis.com> Date: Tue Jun 14 11:01:06 2022 -0400 dynamic height for series detail (#1321) * Adding dynamic height function * pushing change requests * Moved method to getter * Changed carousel reel to onpush strat Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com> commit 1edf23d8c37a1bfde12e980b487e3d0636d411b6 Author: majora2007 <josephmajora@gmail.com> Date: Tue Jun 14 12:20:50 2022 +0000 Bump versions by dotnet-bump-version. commit 791e852596245447682c3fbe370048a31f578314 Author: Robbie Davis <robbie@therobbiedavis.com> Date: Tue Jun 14 08:07:28 2022 -0400 Fixing sticky tabs overlapping title on longer titles (#1320) commit 549b889639b9164ca1406e958667b8236a3fd6a4 Author: majora2007 <josephmajora@gmail.com> Date: Mon Jun 13 21:51:00 2022 +0000 Bump versions by dotnet-bump-version. commit bbc48a5f5b4742ca206ad5606c58ad789069fe2f Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Mon Jun 13 16:37:49 2022 -0500 Infinite Scroll + List View + Cover Upload Redesign (#1319) * Started with the redesign of the cover image chooser redesign to be less click intensive for volume/chapter images. Made some headings bold in card detail drawer. * Tweaked the styles * Moved where the info cards show * Added an ability to open a page settings drawer * Cleaned up some old code that isn't needed anymore. * Started implementing a list view. Refactored some title code to a dedicated component * List view implemented but way too many API calls. Either need caching or adjusting the SeriesDetail api. * Fixed a bug where if the progress bar didn't render on a card item while a download was in progress, the download indicator would be removed. * Large refactor to move a lot of the needed fields to the chapter and volume dtos for series detail. All fields are noted when only used in series detail. * Implemented cards for other tabs (except related) * Fixed the unit test which needed a mocked reader service call. * More cleanup around age rating and removing old code from the refactor. Commented out sorting till i feel motivated to work on that. * Some cleanup and restored cards as initial layout. Time to test this out and see if there is value add. * Added ability for Chapters tab to show the volume chapters belong to (if applicable) * Adding style fixes * Cover image updates, don't allow the first image (which is what is currently set) to respond to cover changes. Hide the ID field on list item for series detail. * Refactored the title for list item to be injectable * Cleaned up the selection code to make it less finicky on mobile when tap scrolling. * Refactored chapter tab to show volume as well on list view. * Ensure word count shows for Volumes * Started adding virtual scrolling, pushing up so Robbie can mess around * Started adding virtual scrolling, pushing up so Robbie can mess around * Fixed a bug where all chapters would come under specials * Show title data as accent if set. * Style fixes for virtual scroller * Restyling scroll * Implemented a way to show storyline with virtual scrolling * Show Word Count for chapters and cleaned up some logics. * I might have card layout working with virtual scroll code. * Some cleanup to hide more system like properties from info bar on series detail page. Fixed some missing time estimate info on storyline chapters. * Fixed a regression on series service when I integrated VolumeTitle. * Refactored read time to the backend. Added WordCount to the volume itself so we don't need to calculate on frontend. When asking to analyze files from a series, force the calculation. * Fixed SeriesDetail api code * Fixed up the code in the drawer to better update list/card mode * Basic infinite scroll implemented, however due to how we are updating the list to render, we are re-rending cards that haven't been touched. * Updated how we render and layout data for infinite scroll on library detail. It's almost there. * Started laying foundation for loading pages backwards. Removed lazy loading of images since we are now using virtual paging. * Hooked in some basic code to allow user to load a prev page with infinite scroll. * Fixed up series detail api and undid the non-lazy loaded images. Changed the router to help with this infinite loading on Firefox issue. * Fixed up some naming issues with Series Detail and added a new test. * This is an infinite scroll without pagination implementation. It is not fully done, but off to a good start. Virtual scroller with jump bar is working pretty well, def needs more polishing and tweaking. There are hacks in this implementation that need to be revisited. * Refactored code so that we don't use any pagination and load all results by default. * Misc code cleanup from build warnings. * Cleaned up some logic for how to display titles in list view. * More title cleanup for specials * Hooked up page layout to user preferences and renamed an existing user pref name to match the dto. * Swapped out everything but storyline with virtual-scroller over CDK * Removed CDK from series detail. * Default value for migration on page layout * Updating card layout for library detail page * fixing height for mobile * Moved scrollbar * Tweaked some styling for layouts when there is no data * Refactored the series cards into their own component to make it re-usable. * More tweaks on series info cards layout and enhanced a few pages with trackby functions. * Removed some dead code * Added download on series detail to actionables to fit in with new scroll strategy. * Fixed language not being updated and sent to the backend for series update. * Fixed a bad migration (if you ran any prior migration in this branch, you need to undo before you use this commit) * Adding sticky tabs * fixed mobile gap on sticky tab * Enhanced the card title for books to show number up front. * Adjusted the gutters on admin dashboard * Removed debug code * Removing duplicate book title * Cleaned up old references to cdk scroller * Implemented a basic jump bar scaling algorithm. Not perfect, but works pretty well. * Code smells Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> commit f0f0e23e88845dc36430df031305f9765aeaa26e Author: majora2007 <josephmajora@gmail.com> Date: Fri Jun 10 15:05:27 2022 +0000 Bump versions by dotnet-bump-version. commit 54e94bfcb22753c89c3ceee104130937e296b0d4 Author: Marcelo Guimarães Junior <75567460+magujun@users.noreply.github.com> Date: Fri Jun 10 07:52:42 2022 -0700 bugfix/user-preferences-accordion (#1307) * Add files via upload * Revert "Add files via upload" This reverts commit 446e3e0f046859a0695aedfe79f28965ad104864. Revert upload package.json * bugfix/user-preferences Fixed a bug with the BookReader panel id missing brackets which prevented the accordion from functioning correctly. commit 0124620e36c4db090e834d76199cd67c0e3bbe9e Author: majora2007 <josephmajora@gmail.com> Date: Wed Jun 8 00:06:51 2022 +0000 Bump versions by dotnet-bump-version. commit 77b8551620ff43475de5a32dd799ee70a4c97996 Author: Joseph Milazzo <joseph.v.milazzo@gmail.com> Date: Tue Jun 7 18:48:25 2022 -0500 … * Bump versions by dotnet-bump-version. * Release Shakeout (#1340) * Fixed a bug where analyze series would not force a re-analysis. Fixed a bug where if files weren't changed since last analysis, then series word count got reset to 0. * Fixed epub images not loading in detail drawer * Fixed a bug on double page layout where the reader would be wonky when moving to and from mobile layout. * package-lock.json updated * Cleaned up some wording * Moved a conditional on jump bar * Bugfix for Double Page Rendering skipping pages (#1339) * Bump versions by dotnet-bump-version. * Double (Manga) fixes . Fixed: *ngIf condition, last page loading Double . Added: isLoose, pageAmount, and CanvasImageNextDouble to keep track of double sequence breaks (nn/n/w) . Fixed: ShouldRenderReverseDouble and pageAmount conditions . Added: Setting isLoose on loadPage() . Added: canvasImageNextDouble in loadPage() Co-authored-by: majora2007 <josephmajora@gmail.com> * Added comments for Magunjun's PR. Co-authored-by: Marcelo Guimarães Junior <75567460+magujun@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Manga Reader Double Layout bugfixes (#1341) * Enforce some max heights on series detail page * Added some icon to layout mode to help the user understand how they work (Robbie needs to css it) * Adding split-double icon * Fixing reverse * Added lots of debug code, refactored documentation, and added some history for wide images * More prefetching code for wide images * Fixed the issue where sometimes paging backwards would skip an image * Fixed up a bug where occasionally on double (manga) paging backwards could skip a page. Fixed a bug on double where last page could get duplicated. * Don't update pageDimensionHistory since we don't need it * Forgot some changes Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Last Shakeout (#1342) * Fixed a bug where series estimate reading time could be calculated before we restore esisting time. * Cleaned up debug code for the reader * Fixed an issue where pagination areas on wide images wasn't proper height * Fixed a pagination height calc * Small change * Bump versions by dotnet-bump-version. * Version bump * Version bump (#1343) 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> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Marcelo Guimarães Junior <75567460+magujun@users.noreply.github.com> Co-authored-by: tjarls <tjarls@gmail.com> Co-authored-by: magujun <guimaraesjunior.marcelo@gmail.com> Co-authored-by: marcelo <mguimaraes@okanagan.bc.ca>
This commit is contained in:
parent
f9b3f1aeb5
commit
6d0b18c98f
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "pwa-chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Chrome against localhost",
|
||||||
|
"url": "http://localhost:5000",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -46,5 +46,6 @@
|
|||||||
"bold": false,
|
"bold": false,
|
||||||
"italic": false
|
"italic": false
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"diffEditor.wordWrap": "off"
|
||||||
}
|
}
|
68
API.Benchmark/EpubBenchmark.cs
Normal file
68
API.Benchmark/EpubBenchmark.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Services;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Order;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
using VersOne.Epub;
|
||||||
|
|
||||||
|
namespace API.Benchmark;
|
||||||
|
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
|
||||||
|
[RankColumn]
|
||||||
|
[SimpleJob(launchCount: 1, warmupCount: 3, targetCount: 5, invocationCount: 100, id: "Epub"), ShortRunJob]
|
||||||
|
public class EpubBenchmark
|
||||||
|
{
|
||||||
|
[Benchmark]
|
||||||
|
public static async Task GetWordCount_PassByString()
|
||||||
|
{
|
||||||
|
using var book = await EpubReader.OpenBookAsync("Data/book-test.epub", BookService.BookReaderOptions);
|
||||||
|
foreach (var bookFile in book.Content.Html.Values)
|
||||||
|
{
|
||||||
|
Console.WriteLine(GetBookWordCount_PassByString(await bookFile.ReadContentAsTextAsync()));
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public static async Task GetWordCount_PassByRef()
|
||||||
|
{
|
||||||
|
using var book = await EpubReader.OpenBookAsync("Data/book-test.epub", BookService.BookReaderOptions);
|
||||||
|
foreach (var bookFile in book.Content.Html.Values)
|
||||||
|
{
|
||||||
|
Console.WriteLine(await GetBookWordCount_PassByRef(bookFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetBookWordCount_PassByString(string fileContents)
|
||||||
|
{
|
||||||
|
var doc = new HtmlDocument();
|
||||||
|
doc.LoadHtml(fileContents);
|
||||||
|
var delimiter = new char[] {' '};
|
||||||
|
|
||||||
|
return doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]")
|
||||||
|
.Select(node => node.InnerText)
|
||||||
|
.Select(text => text.Split(delimiter, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Where(s => char.IsLetter(s[0])))
|
||||||
|
.Select(words => words.Count())
|
||||||
|
.Where(wordCount => wordCount > 0)
|
||||||
|
.Sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> GetBookWordCount_PassByRef(EpubContentFileRef bookFile)
|
||||||
|
{
|
||||||
|
var doc = new HtmlDocument();
|
||||||
|
doc.LoadHtml(await bookFile.ReadContentAsTextAsync());
|
||||||
|
var delimiter = new char[] {' '};
|
||||||
|
|
||||||
|
return doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]")
|
||||||
|
.Select(node => node.InnerText)
|
||||||
|
.Select(text => text.Split(delimiter, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Where(s => char.IsLetter(s[0])))
|
||||||
|
.Select(words => words.Count())
|
||||||
|
.Where(wordCount => wordCount > 0)
|
||||||
|
.Sum();
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,8 @@ namespace API.Benchmark
|
|||||||
{
|
{
|
||||||
//BenchmarkRunner.Run<ParseScannedFilesBenchmarks>();
|
//BenchmarkRunner.Run<ParseScannedFilesBenchmarks>();
|
||||||
//BenchmarkRunner.Run<TestBenchmark>();
|
//BenchmarkRunner.Run<TestBenchmark>();
|
||||||
BenchmarkRunner.Run<ParserBenchmarks>();
|
//BenchmarkRunner.Run<ParserBenchmarks>();
|
||||||
|
BenchmarkRunner.Run<EpubBenchmark>();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.5" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="4.3.0" />
|
<PackageReference Include="NSubstitute" Version="4.3.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="17.0.3" />
|
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="17.0.15" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
@ -66,6 +66,13 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "6")]
|
[InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "6")]
|
||||||
[InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz", "3")]
|
[InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz", "3")]
|
||||||
[InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03.5 Ch. 023.5 - Volume 3 Extras.cbz", "3.5")]
|
[InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03.5 Ch. 023.5 - Volume 3 Extras.cbz", "3.5")]
|
||||||
|
[InlineData("幽游白书完全版 第03卷 天下", "3")]
|
||||||
|
[InlineData("阿衰online 第1册", "1")]
|
||||||
|
[InlineData("【TFO汉化&Petit汉化】迷你偶像漫画卷2第25话", "2")]
|
||||||
|
[InlineData("63권#200", "63")]
|
||||||
|
[InlineData("시즌34삽화2", "34")]
|
||||||
|
[InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1巻", "1")]
|
||||||
|
[InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1-3巻", "1-3")]
|
||||||
public void ParseVolumeTest(string filename, string expected)
|
public void ParseVolumeTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Parser.Parser.ParseVolume(filename));
|
Assert.Equal(expected, API.Parser.Parser.ParseVolume(filename));
|
||||||
@ -246,6 +253,9 @@ namespace API.Tests.Parser
|
|||||||
[InlineData("Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", "0")]
|
[InlineData("Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", "0")]
|
||||||
[InlineData("Kaiju No. 8 036 (2021) (Digital)", "36")]
|
[InlineData("Kaiju No. 8 036 (2021) (Digital)", "36")]
|
||||||
[InlineData("Samurai Jack Vol. 01 - The threads of Time", "0")]
|
[InlineData("Samurai Jack Vol. 01 - The threads of Time", "0")]
|
||||||
|
[InlineData("【TFO汉化&Petit汉化】迷你偶像漫画第25话", "25")]
|
||||||
|
[InlineData("이세계에서 고아원을 열었지만, 어째서인지 아무도 독립하려 하지 않는다 38-1화 ", "38")]
|
||||||
|
[InlineData("[ハレム]ナナとカオル ~高校生のSMごっこ~ 第10話", "10")]
|
||||||
public void ParseChaptersTest(string filename, string expected)
|
public void ParseChaptersTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Parser.Parser.ParseChapter(filename));
|
Assert.Equal(expected, API.Parser.Parser.ParseChapter(filename));
|
||||||
|
@ -47,6 +47,12 @@ public class BookmarkServiceTests
|
|||||||
_unitOfWork = new UnitOfWork(_context, Substitute.For<IMapper>(), null);
|
_unitOfWork = new UnitOfWork(_context, Substitute.For<IMapper>(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BookmarkService Create(IDirectoryService ds)
|
||||||
|
{
|
||||||
|
return new BookmarkService(Substitute.For<ILogger<BookmarkService>>(), _unitOfWork, ds,
|
||||||
|
Substitute.For<IImageService>(), Substitute.For<IEventHub>());
|
||||||
|
}
|
||||||
|
|
||||||
#region Setup
|
#region Setup
|
||||||
|
|
||||||
private static DbConnection CreateInMemoryDatabase()
|
private static DbConnection CreateInMemoryDatabase()
|
||||||
@ -121,7 +127,8 @@ public class BookmarkServiceTests
|
|||||||
public async Task BookmarkPage_ShouldCopyTheFileAndUpdateDB()
|
public async Task BookmarkPage_ShouldCopyTheFileAndUpdateDB()
|
||||||
{
|
{
|
||||||
var filesystem = CreateFileSystem();
|
var filesystem = CreateFileSystem();
|
||||||
filesystem.AddFile($"{CacheDirectory}1/0001.jpg", new MockFileData("123"));
|
var file = $"{CacheDirectory}1/0001.jpg";
|
||||||
|
filesystem.AddFile(file, new MockFileData("123"));
|
||||||
|
|
||||||
// Delete all Series to reset state
|
// Delete all Series to reset state
|
||||||
await ResetDB();
|
await ResetDB();
|
||||||
@ -157,7 +164,7 @@ public class BookmarkServiceTests
|
|||||||
|
|
||||||
|
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var bookmarkService = new BookmarkService(Substitute.For<ILogger<BookmarkService>>(), _unitOfWork, ds);
|
var bookmarkService = Create(ds);
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks);
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks);
|
||||||
|
|
||||||
var result = await bookmarkService.BookmarkPage(user, new BookmarkDto()
|
var result = await bookmarkService.BookmarkPage(user, new BookmarkDto()
|
||||||
@ -166,7 +173,7 @@ public class BookmarkServiceTests
|
|||||||
Page = 1,
|
Page = 1,
|
||||||
SeriesId = 1,
|
SeriesId = 1,
|
||||||
VolumeId = 1
|
VolumeId = 1
|
||||||
}, $"{CacheDirectory}1/0001.jpg");
|
}, file);
|
||||||
|
|
||||||
|
|
||||||
Assert.True(result);
|
Assert.True(result);
|
||||||
@ -227,7 +234,7 @@ public class BookmarkServiceTests
|
|||||||
|
|
||||||
|
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var bookmarkService = new BookmarkService(Substitute.For<ILogger<BookmarkService>>(), _unitOfWork, ds);
|
var bookmarkService = Create(ds);
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks);
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks);
|
||||||
|
|
||||||
var result = await bookmarkService.RemoveBookmarkPage(user, new BookmarkDto()
|
var result = await bookmarkService.RemoveBookmarkPage(user, new BookmarkDto()
|
||||||
@ -319,7 +326,7 @@ public class BookmarkServiceTests
|
|||||||
|
|
||||||
|
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var bookmarkService = new BookmarkService(Substitute.For<ILogger<BookmarkService>>(), _unitOfWork, ds);
|
var bookmarkService = Create(ds);
|
||||||
|
|
||||||
await bookmarkService.DeleteBookmarkFiles(new [] {new AppUserBookmark()
|
await bookmarkService.DeleteBookmarkFiles(new [] {new AppUserBookmark()
|
||||||
{
|
{
|
||||||
@ -378,7 +385,7 @@ public class BookmarkServiceTests
|
|||||||
|
|
||||||
|
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
var bookmarkService = new BookmarkService(Substitute.For<ILogger<BookmarkService>>(), _unitOfWork, ds);
|
var bookmarkService = Create(ds);
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks);
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks);
|
||||||
|
|
||||||
await bookmarkService.BookmarkPage(user, new BookmarkDto()
|
await bookmarkService.BookmarkPage(user, new BookmarkDto()
|
||||||
|
@ -279,8 +279,8 @@ namespace API.Tests.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
cs.GetCachedEpubFile(1, c);
|
cs.GetCachedFile(c);
|
||||||
Assert.Same($"{DataDirectory}1.epub", cs.GetCachedEpubFile(1, c));
|
Assert.Same($"{DataDirectory}1.epub", cs.GetCachedFile(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -6,8 +6,11 @@ using System.IO.Abstractions.TestingHelpers;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
|
using API.DTOs.Settings;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Helpers;
|
||||||
|
using API.Helpers.Converters;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
@ -48,7 +51,10 @@ public class CleanupServiceTests
|
|||||||
_context = new DataContext(contextOptions);
|
_context = new DataContext(contextOptions);
|
||||||
Task.Run(SeedDb).GetAwaiter().GetResult();
|
Task.Run(SeedDb).GetAwaiter().GetResult();
|
||||||
|
|
||||||
_unitOfWork = new UnitOfWork(_context, Substitute.For<IMapper>(), null);
|
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
|
||||||
|
var mapper = config.CreateMapper();
|
||||||
|
|
||||||
|
_unitOfWork = new UnitOfWork(_context, mapper, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Setup
|
#region Setup
|
||||||
|
@ -8,6 +8,7 @@ using API.Data.Repositories;
|
|||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.DTOs.CollectionTags;
|
using API.DTOs.CollectionTags;
|
||||||
using API.DTOs.Metadata;
|
using API.DTOs.Metadata;
|
||||||
|
using API.DTOs.Reader;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
@ -21,6 +22,8 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
|
using NSubstitute.Extensions;
|
||||||
|
using NSubstitute.ReceivedExtensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Sdk;
|
using Xunit.Sdk;
|
||||||
|
|
||||||
@ -253,6 +256,50 @@ public class SeriesServiceTests
|
|||||||
Assert.Equal(2, detail.Volumes.Count());
|
Assert.Equal(2, detail.Volumes.Count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeriesDetail_ShouldReturnCorrectNaming_VolumeTitle()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||||
|
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
||||||
|
Assert.NotEmpty(detail.Chapters);
|
||||||
|
// volume 2 has a 0 chapter aka a single chapter that is represented as a volume. We don't show in Chapters area
|
||||||
|
Assert.Equal(3, detail.Chapters.Count());
|
||||||
|
|
||||||
|
Assert.NotEmpty(detail.Volumes);
|
||||||
|
Assert.Equal(2, detail.Volumes.Count());
|
||||||
|
|
||||||
|
Assert.Equal(string.Empty, detail.Chapters.First().VolumeTitle); // loose leaf chapter
|
||||||
|
Assert.Equal("Volume 3", detail.Chapters.Last().VolumeTitle); // volume based chapter
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SeriesDetail_ShouldReturnChaptersOnly_WhenBookLibrary()
|
public async Task SeriesDetail_ShouldReturnChaptersOnly_WhenBookLibrary()
|
||||||
{
|
{
|
||||||
@ -700,7 +747,7 @@ public class SeriesServiceTests
|
|||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||||
Assert.NotNull(series.Metadata);
|
Assert.NotNull(series.Metadata);
|
||||||
Assert.True(series.Metadata.Genres.Select(g => g.Title).All(g => g == "New Genre".SentenceCase()));
|
Assert.True(series.Metadata.Genres.Select(g1 => g1.Title).All(g2 => g2 == "New Genre".SentenceCase()));
|
||||||
Assert.False(series.Metadata.GenresLocked); // GenreLocked is false unless the UI Explicitly says it should be locked
|
Assert.False(series.Metadata.GenresLocked); // GenreLocked is false unless the UI Explicitly says it should be locked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ using API.Data;
|
|||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Enums.Theme;
|
using API.Entities.Enums.Theme;
|
||||||
|
using API.Entities.Enums.UserPreferences;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
|
@ -5,15 +5,17 @@
|
|||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
<DebugSymbols>false</DebugSymbols>
|
<DebugSymbols>false</DebugSymbols>
|
||||||
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
||||||
|
<DocumentationFile>bin\$(Configuration)\$(AssemblyName).xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<DocumentationFile>bin\Debug\API.xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(AssemblyName).xml</DocumentationFile>
|
||||||
<NoWarn>1701;1702;1591</NoWarn>
|
<NoWarn>1701;1702;1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
@ -40,39 +42,39 @@
|
|||||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
||||||
<PackageReference Include="Docnet.Core" Version="2.4.0-alpha.2" />
|
<PackageReference Include="Docnet.Core" Version="2.4.0-alpha.2" />
|
||||||
<PackageReference Include="ExCSS" Version="4.1.0" />
|
<PackageReference Include="ExCSS" Version="4.1.0" />
|
||||||
<PackageReference Include="Flurl" Version="3.0.5" />
|
<PackageReference Include="Flurl" Version="3.0.6" />
|
||||||
<PackageReference Include="Flurl.Http" Version="3.2.3" />
|
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||||
<PackageReference Include="Hangfire" Version="1.7.28" />
|
<PackageReference Include="Hangfire" Version="1.7.30" />
|
||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.28" />
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.30" />
|
||||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||||
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.42" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.43" />
|
||||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.6" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.6" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.6" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.4">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
|
||||||
<PackageReference Include="NetVips" Version="2.1.0" />
|
<PackageReference Include="NetVips" Version="2.1.0" />
|
||||||
<PackageReference Include="NetVips.Native" Version="8.12.2" />
|
<PackageReference Include="NetVips.Native" Version="8.12.2" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.1.4" />
|
<PackageReference Include="NReco.Logging.File" Version="1.1.5" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.31.0" />
|
<PackageReference Include="SharpCompress" Version="0.32.1" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.1" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.38.0.46746">
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.40.0.48530">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.17.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.20.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions" Version="17.0.3" />
|
<PackageReference Include="System.IO.Abstractions" Version="17.0.18" />
|
||||||
<PackageReference Include="VersOne.Epub" Version="3.1.0" />
|
<PackageReference Include="VersOne.Epub" Version="3.1.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -134,6 +136,9 @@
|
|||||||
</Content>
|
</Content>
|
||||||
<Content Remove="covers\**" />
|
<Content Remove="covers\**" />
|
||||||
<Content Remove="config\covers\**" />
|
<Content Remove="config\covers\**" />
|
||||||
|
<Content Update="bin\$(Configuration)\$(AssemblyName).xml">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -12,9 +12,11 @@ using API.DTOs.Account;
|
|||||||
using API.DTOs.Email;
|
using API.DTOs.Email;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Enums.UserPreferences;
|
||||||
using API.Errors;
|
using API.Errors;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
using API.SignalR;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -40,13 +42,16 @@ namespace API.Controllers
|
|||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly IEmailService _emailService;
|
private readonly IEmailService _emailService;
|
||||||
private readonly IHostEnvironment _environment;
|
private readonly IHostEnvironment _environment;
|
||||||
|
private readonly IEventHub _eventHub;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public AccountController(UserManager<AppUser> userManager,
|
public AccountController(UserManager<AppUser> userManager,
|
||||||
SignInManager<AppUser> signInManager,
|
SignInManager<AppUser> signInManager,
|
||||||
ITokenService tokenService, IUnitOfWork unitOfWork,
|
ITokenService tokenService, IUnitOfWork unitOfWork,
|
||||||
ILogger<AccountController> logger,
|
ILogger<AccountController> logger,
|
||||||
IMapper mapper, IAccountService accountService, IEmailService emailService, IHostEnvironment environment)
|
IMapper mapper, IAccountService accountService,
|
||||||
|
IEmailService emailService, IHostEnvironment environment,
|
||||||
|
IEventHub eventHub)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_signInManager = signInManager;
|
_signInManager = signInManager;
|
||||||
@ -57,6 +62,7 @@ namespace API.Controllers
|
|||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
_emailService = emailService;
|
_emailService = emailService;
|
||||||
_environment = environment;
|
_environment = environment;
|
||||||
|
_eventHub = eventHub;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -201,6 +207,11 @@ namespace API.Controllers
|
|||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refreshes the user's JWT token
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tokenRequestDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("refresh-token")]
|
[HttpPost("refresh-token")]
|
||||||
public async Task<ActionResult<TokenRequestDto>> RefreshToken([FromBody] TokenRequestDto tokenRequestDto)
|
public async Task<ActionResult<TokenRequestDto>> RefreshToken([FromBody] TokenRequestDto tokenRequestDto)
|
||||||
{
|
{
|
||||||
@ -289,6 +300,7 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
dto.Roles.Add(PolicyConstants.PlebRole);
|
dto.Roles.Add(PolicyConstants.PlebRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingRoles.Except(dto.Roles).Any() || dto.Roles.Except(existingRoles).Any())
|
if (existingRoles.Except(dto.Roles).Any() || dto.Roles.Except(existingRoles).Any())
|
||||||
{
|
{
|
||||||
var roles = dto.Roles;
|
var roles = dto.Roles;
|
||||||
@ -326,9 +338,9 @@ namespace API.Controllers
|
|||||||
lib.AppUsers.Add(user);
|
lib.AppUsers.Add(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_unitOfWork.HasChanges()) return Ok();
|
if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync())
|
||||||
if (await _unitOfWork.CommitAsync())
|
|
||||||
{
|
{
|
||||||
|
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -646,22 +658,13 @@ namespace API.Controllers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||||
//if (string.IsNullOrEmpty(token)) return BadRequest("There was an issue sending email");
|
|
||||||
user.Email = dto.Email;
|
user.Email = dto.Email;
|
||||||
if (!await ConfirmEmailToken(token, user)) return BadRequest("There was a critical error during migration");
|
if (!await ConfirmEmailToken(token, user)) return BadRequest("There was a critical error during migration");
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
//var emailLink = GenerateEmailLink(await _userManager.GenerateEmailConfirmationTokenAsync(user), "confirm-migration-email", user.Email);
|
|
||||||
// _logger.LogCritical("[Email Migration]: Email Link for {UserName}: {Link}", dto.Username, emailLink);
|
|
||||||
// // Always send an email, even if the user can't click it just to get them conformable with the system
|
|
||||||
// await _emailService.SendMigrationEmail(new EmailMigrationDto()
|
|
||||||
// {
|
|
||||||
// EmailAddress = dto.Email,
|
|
||||||
// Username = user.UserName,
|
|
||||||
// ServerConfirmationLink = emailLink
|
|
||||||
// });
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -14,6 +14,10 @@ namespace API.Controllers
|
|||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if an admin exists on the system. This is essentially a check to validate if the system has been setup.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("exists")]
|
[HttpGet("exists")]
|
||||||
public async Task<ActionResult<bool>> AdminExists()
|
public async Task<ActionResult<bool>> AdminExists()
|
||||||
{
|
{
|
||||||
@ -21,4 +25,4 @@ namespace API.Controllers
|
|||||||
return users.Count > 0;
|
return users.Count > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
@ -32,16 +33,43 @@ namespace API.Controllers
|
|||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves information for the PDF and Epub reader
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This only applies to Epub or PDF files</remarks>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("{chapterId}/book-info")]
|
[HttpGet("{chapterId}/book-info")]
|
||||||
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
|
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
|
||||||
{
|
{
|
||||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||||
var bookTitle = string.Empty;
|
var bookTitle = string.Empty;
|
||||||
if (dto.SeriesFormat == MangaFormat.Epub)
|
switch (dto.SeriesFormat)
|
||||||
{
|
{
|
||||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
case MangaFormat.Epub:
|
||||||
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions);
|
{
|
||||||
bookTitle = book.Title;
|
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||||
|
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions);
|
||||||
|
bookTitle = book.Title;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MangaFormat.Pdf:
|
||||||
|
{
|
||||||
|
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
|
||||||
|
if (string.IsNullOrEmpty(bookTitle))
|
||||||
|
{
|
||||||
|
// Override with filename
|
||||||
|
bookTitle = Path.GetFileNameWithoutExtension(mangaFile.FilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MangaFormat.Image:
|
||||||
|
break;
|
||||||
|
case MangaFormat.Archive:
|
||||||
|
break;
|
||||||
|
case MangaFormat.Unknown:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(new BookInfoDto()
|
return Ok(new BookInfoDto()
|
||||||
@ -59,6 +87,12 @@ namespace API.Controllers
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is an entry point to fetch resources from within an epub chapter/book.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <param name="file"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("{chapterId}/book-resources")]
|
[HttpGet("{chapterId}/book-resources")]
|
||||||
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
|
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
|
||||||
{
|
{
|
||||||
@ -78,7 +112,7 @@ namespace API.Controllers
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This will return a list of mappings from ID -> page num. ID will be the xhtml key and page num will be the reading order
|
/// This will return a list of mappings from ID -> page num. ID will be the xhtml key and page num will be the reading order
|
||||||
/// this is used to rewrite anchors in the book text so that we always load properly in FE
|
/// this is used to rewrite anchors in the book text so that we always load properly in our reader.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>This is essentially building the table of contents</remarks>
|
/// <remarks>This is essentially building the table of contents</remarks>
|
||||||
/// <param name="chapterId"></param>
|
/// <param name="chapterId"></param>
|
||||||
@ -205,11 +239,18 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This returns a single page within the epub book. All html will be rewritten to be scoped within our reader,
|
||||||
|
/// all css is scoped, etc.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <param name="page"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("{chapterId}/book-page")]
|
[HttpGet("{chapterId}/book-page")]
|
||||||
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
|
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
|
||||||
{
|
{
|
||||||
var chapter = await _cacheService.Ensure(chapterId);
|
var chapter = await _cacheService.Ensure(chapterId);
|
||||||
var path = _cacheService.GetCachedEpubFile(chapter.Id, chapter);
|
var path = _cacheService.GetCachedFile(chapter);
|
||||||
|
|
||||||
using var book = await EpubReader.OpenBookAsync(path, BookService.BookReaderOptions);
|
using var book = await EpubReader.OpenBookAsync(path, BookService.BookReaderOptions);
|
||||||
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
||||||
|
@ -18,6 +18,9 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// All APIs related to downloading entities from the system. Requires Download Role or Admin Role.
|
||||||
|
/// </summary>
|
||||||
[Authorize(Policy="RequireDownloadRole")]
|
[Authorize(Policy="RequireDownloadRole")]
|
||||||
public class DownloadController : BaseApiController
|
public class DownloadController : BaseApiController
|
||||||
{
|
{
|
||||||
@ -42,6 +45,11 @@ namespace API.Controllers
|
|||||||
_bookmarkService = bookmarkService;
|
_bookmarkService = bookmarkService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For a given volume, return the size in bytes
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="volumeId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("volume-size")]
|
[HttpGet("volume-size")]
|
||||||
public async Task<ActionResult<long>> GetVolumeSize(int volumeId)
|
public async Task<ActionResult<long>> GetVolumeSize(int volumeId)
|
||||||
{
|
{
|
||||||
@ -49,6 +57,11 @@ namespace API.Controllers
|
|||||||
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For a given chapter, return the size in bytes
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("chapter-size")]
|
[HttpGet("chapter-size")]
|
||||||
public async Task<ActionResult<long>> GetChapterSize(int chapterId)
|
public async Task<ActionResult<long>> GetChapterSize(int chapterId)
|
||||||
{
|
{
|
||||||
@ -56,6 +69,11 @@ namespace API.Controllers
|
|||||||
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For a series, return the size in bytes
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("series-size")]
|
[HttpGet("series-size")]
|
||||||
public async Task<ActionResult<long>> GetSeriesSize(int seriesId)
|
public async Task<ActionResult<long>> GetSeriesSize(int seriesId)
|
||||||
{
|
{
|
||||||
@ -64,7 +82,11 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downloads all chapters within a volume.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="volumeId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[Authorize(Policy="RequireDownloadRole")]
|
[Authorize(Policy="RequireDownloadRole")]
|
||||||
[HttpGet("volume")]
|
[HttpGet("volume")]
|
||||||
public async Task<ActionResult> DownloadVolume(int volumeId)
|
public async Task<ActionResult> DownloadVolume(int volumeId)
|
||||||
|
@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Responsible for servicing up images stored in the DB
|
/// Responsible for servicing up images stored in Kavita for entities
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ImageController : BaseApiController
|
public class ImageController : BaseApiController
|
||||||
{
|
{
|
||||||
|
@ -6,7 +6,9 @@ using System.Threading.Tasks;
|
|||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.JumpBar;
|
||||||
using API.DTOs.Search;
|
using API.DTOs.Search;
|
||||||
|
using API.DTOs.System;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
@ -88,11 +90,15 @@ namespace API.Controllers
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpGet("list")]
|
[HttpGet("list")]
|
||||||
public ActionResult<IEnumerable<string>> GetDirectories(string path)
|
public ActionResult<IEnumerable<DirectoryDto>> GetDirectories(string path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
{
|
{
|
||||||
return Ok(Directory.GetLogicalDrives());
|
return Ok(Directory.GetLogicalDrives().Select(d => new DirectoryDto()
|
||||||
|
{
|
||||||
|
Name = d,
|
||||||
|
FullPath = d
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Directory.Exists(path)) return BadRequest("This is not a valid path");
|
if (!Directory.Exists(path)) return BadRequest("This is not a valid path");
|
||||||
@ -106,6 +112,16 @@ namespace API.Controllers
|
|||||||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosAsync());
|
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("jump-bar")]
|
||||||
|
public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId)
|
||||||
|
{
|
||||||
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
|
if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, userId)) return BadRequest("User does not have access to library");
|
||||||
|
|
||||||
|
return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpPost("grant-access")]
|
[HttpPost("grant-access")]
|
||||||
public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto)
|
public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto)
|
||||||
@ -166,6 +182,14 @@ namespace API.Controllers
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[HttpPost("analyze")]
|
||||||
|
public ActionResult Analyze(int libraryId)
|
||||||
|
{
|
||||||
|
_taskScheduler.AnalyzeFilesForLibrary(libraryId, true);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("libraries")]
|
[HttpGet("libraries")]
|
||||||
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibrariesForUser()
|
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibrariesForUser()
|
||||||
{
|
{
|
||||||
@ -215,6 +239,12 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates an existing Library with new name, folders, and/or type.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Any folder or type change will invoke a scan.</remarks>
|
||||||
|
/// <param name="libraryForUserDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpPost("update")]
|
[HttpPost("update")]
|
||||||
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto libraryForUserDto)
|
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto libraryForUserDto)
|
||||||
@ -226,10 +256,13 @@ namespace API.Controllers
|
|||||||
library.Name = libraryForUserDto.Name;
|
library.Name = libraryForUserDto.Name;
|
||||||
library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
|
library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
|
||||||
|
|
||||||
|
var typeUpdate = library.Type != libraryForUserDto.Type;
|
||||||
|
library.Type = libraryForUserDto.Type;
|
||||||
|
|
||||||
_unitOfWork.LibraryRepository.Update(library);
|
_unitOfWork.LibraryRepository.Update(library);
|
||||||
|
|
||||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
|
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
|
||||||
if (originalFolders.Count != libraryForUserDto.Folders.Count())
|
if (originalFolders.Count != libraryForUserDto.Folders.Count() || typeUpdate)
|
||||||
{
|
{
|
||||||
_taskScheduler.ScanLibrary(library.Id);
|
_taskScheduler.ScanLibrary(library.Id);
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ public class MetadataController : BaseApiController
|
|||||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||||
if (ids != null && ids.Count > 0)
|
if (ids != null && ids.Count > 0)
|
||||||
{
|
{
|
||||||
return Ok(await _unitOfWork.SeriesRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids));
|
return Ok(await _unitOfWork.LibraryRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(Enum.GetValues<AgeRating>().Select(t => new AgeRatingDto()
|
return Ok(Enum.GetValues<AgeRating>().Select(t => new AgeRatingDto()
|
||||||
@ -104,7 +104,7 @@ public class MetadataController : BaseApiController
|
|||||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||||
if (ids is {Count: > 0})
|
if (ids is {Count: > 0})
|
||||||
{
|
{
|
||||||
return Ok(_unitOfWork.SeriesRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
|
return Ok(_unitOfWork.LibraryRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(Enum.GetValues<PublicationStatus>().Select(t => new PublicationStatusDto()
|
return Ok(Enum.GetValues<PublicationStatus>().Select(t => new PublicationStatusDto()
|
||||||
@ -125,7 +125,7 @@ public class MetadataController : BaseApiController
|
|||||||
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
|
||||||
if (ids is {Count: > 0})
|
if (ids is {Count: > 0})
|
||||||
{
|
{
|
||||||
return Ok(await _unitOfWork.SeriesRepository.GetAllLanguagesForLibrariesAsync(ids));
|
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
var englishTag = CultureInfo.GetCultureInfo("en");
|
var englishTag = CultureInfo.GetCultureInfo("en");
|
||||||
@ -149,4 +149,18 @@ public class MetadataController : BaseApiController
|
|||||||
IsoCode = c.IetfLanguageTag
|
IsoCode = c.IetfLanguageTag
|
||||||
}).Where(l => !string.IsNullOrEmpty(l.IsoCode));
|
}).Where(l => !string.IsNullOrEmpty(l.IsoCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns summary for the chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("chapter-summary")]
|
||||||
|
public async Task<ActionResult<string>> GetChapterSummary(int chapterId)
|
||||||
|
{
|
||||||
|
if (chapterId <= 0) return BadRequest("Chapter does not exist");
|
||||||
|
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||||
|
if (chapter == null) return BadRequest("Chapter does not exist");
|
||||||
|
return Ok(chapter.Summary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -604,6 +604,7 @@ public class OpdsController : BaseApiController
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads a file
|
/// Downloads a file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="apiKey">User's API Key</param>
|
||||||
/// <param name="seriesId"></param>
|
/// <param name="seriesId"></param>
|
||||||
/// <param name="volumeId"></param>
|
/// <param name="volumeId"></param>
|
||||||
/// <param name="chapterId"></param>
|
/// <param name="chapterId"></param>
|
||||||
|
@ -8,9 +8,9 @@ using API.Data.Repositories;
|
|||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.DTOs.Reader;
|
using API.DTOs.Reader;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks;
|
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -44,6 +44,34 @@ namespace API.Controllers
|
|||||||
_eventHub = eventHub;
|
_eventHub = eventHub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the PDF for the chapterId.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiKey">API Key for user to validate they have access</param>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("pdf")]
|
||||||
|
public async Task<ActionResult> GetPdf(int chapterId)
|
||||||
|
{
|
||||||
|
|
||||||
|
var chapter = await _cacheService.Ensure(chapterId);
|
||||||
|
if (chapter == null) return BadRequest("There was an issue finding pdf file for reading");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var path = _cacheService.GetCachedFile(chapter);
|
||||||
|
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"Pdf doesn't exist when it should.");
|
||||||
|
|
||||||
|
Response.AddCacheHeader(path, TimeSpan.FromMinutes(60).Seconds);
|
||||||
|
return PhysicalFile(path, "application/pdf", Path.GetFileName(path), true);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_cacheService.CleanupChapters(new []{ chapterId });
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns an image for a given chapter. Side effect: This will cache the chapter images for reading.
|
/// Returns an image for a given chapter. Side effect: This will cache the chapter images for reading.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -163,6 +191,11 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a Series as read. All volumes and chapters will be marked as read during this process.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="markReadDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("mark-read")]
|
[HttpPost("mark-read")]
|
||||||
public async Task<ActionResult> MarkRead(MarkReadDto markReadDto)
|
public async Task<ActionResult> MarkRead(MarkReadDto markReadDto)
|
||||||
{
|
{
|
||||||
@ -176,7 +209,7 @@ namespace API.Controllers
|
|||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks a Series as Unread (progress)
|
/// Marks a Series as Unread. All volumes and chapters will be marked as unread during this process.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="markReadDto"></param>
|
/// <param name="markReadDto"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@ -424,6 +457,7 @@ namespace API.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>This is built for Tachiyomi and is not expected to be called by any other place</remarks>
|
/// <remarks>This is built for Tachiyomi and is not expected to be called by any other place</remarks>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
[Obsolete("Deprecated. Use 'Tachiyomi/mark-chapter-until-as-read'")]
|
||||||
[HttpPost("mark-chapter-until-as-read")]
|
[HttpPost("mark-chapter-until-as-read")]
|
||||||
public async Task<ActionResult<bool>> MarkChaptersUntilAsRead(int seriesId, float chapterNumber)
|
public async Task<ActionResult<bool>> MarkChaptersUntilAsRead(int seriesId, float chapterNumber)
|
||||||
{
|
{
|
||||||
@ -497,7 +531,7 @@ namespace API.Controllers
|
|||||||
user.Bookmarks = user.Bookmarks.Where(bmk => bmk.SeriesId != dto.SeriesId).ToList();
|
user.Bookmarks = user.Bookmarks.Where(bmk => bmk.SeriesId != dto.SeriesId).ToList();
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -626,5 +660,34 @@ namespace API.Controllers
|
|||||||
return await _readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, userId);
|
return await _readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For the current user, returns an estimate on how long it would take to finish reading the series.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>For Epubs, this does not check words inside a chapter due to overhead so may not work in all cases.</remarks>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("time-left")]
|
||||||
|
public async Task<ActionResult<HourEstimateRangeDto>> GetEstimateToCompletion(int seriesId)
|
||||||
|
{
|
||||||
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||||
|
|
||||||
|
// Get all sum of all chapters with progress that is complete then subtract from series. Multiply by modifiers
|
||||||
|
var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(seriesId, userId);
|
||||||
|
if (series.Format == MangaFormat.Epub)
|
||||||
|
{
|
||||||
|
var chapters =
|
||||||
|
await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(progress.Select(p => p.ChapterId).ToList());
|
||||||
|
// Word count
|
||||||
|
var progressCount = chapters.Sum(c => c.WordCount);
|
||||||
|
var wordsLeft = series.WordCount - progressCount;
|
||||||
|
return _readerService.GetTimeEstimate(wordsLeft, 0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressPageCount = progress.Sum(p => p.PagesRead);
|
||||||
|
var pagesLeft = series.Pages - progressPageCount;
|
||||||
|
return _readerService.GetTimeEstimate(0, pagesLeft, false);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,8 +72,9 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
|
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
|
||||||
|
return Ok(items);
|
||||||
|
|
||||||
return Ok(await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList()));
|
//return Ok(await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(userId, items.ToList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -463,7 +464,7 @@ namespace API.Controllers
|
|||||||
|
|
||||||
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
||||||
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds))
|
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds))
|
||||||
.OrderBy(c => float.Parse(c.Volume.Name))
|
.OrderBy(c => Parser.Parser.MinNumberFromRange(c.Volume.Name))
|
||||||
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
|
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
|
||||||
|
|
||||||
var index = lastOrder + 1;
|
var index = lastOrder + 1;
|
||||||
|
@ -19,9 +19,10 @@ public class RecommendedController : BaseApiController
|
|||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Quick Reads are series that are less than 2K pages in total.
|
/// Quick Reads are series that should be readable in less than 10 in total and are not Ongoing in release.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryId">Library to restrict series to</param>
|
/// <param name="libraryId">Library to restrict series to</param>
|
||||||
|
/// <param name="userParams">Pagination</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("quick-reads")]
|
[HttpGet("quick-reads")]
|
||||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickReads(int libraryId, [FromQuery] UserParams userParams)
|
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickReads(int libraryId, [FromQuery] UserParams userParams)
|
||||||
@ -35,10 +36,29 @@ public class RecommendedController : BaseApiController
|
|||||||
return Ok(series);
|
return Ok(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quick Catchup Reads are series that should be readable in less than 10 in total and are Ongoing in release.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryId">Library to restrict series to</param>
|
||||||
|
/// <param name="userParams"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("quick-catchup-reads")]
|
||||||
|
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams userParams)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
|
||||||
|
userParams ??= new UserParams();
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetQuickCatchupReads(user.Id, libraryId, userParams);
|
||||||
|
|
||||||
|
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||||
|
return Ok(series);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Highly Rated based on other users ratings. Will pull series with ratings > 4.0, weighted by count of other users.
|
/// Highly Rated based on other users ratings. Will pull series with ratings > 4.0, weighted by count of other users.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryId">Library to restrict series to</param>
|
/// <param name="libraryId">Library to restrict series to</param>
|
||||||
|
/// <param name="userParams">Pagination</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("highly-rated")]
|
[HttpGet("highly-rated")]
|
||||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams)
|
public async Task<ActionResult<PagedList<SeriesDto>>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams)
|
||||||
@ -56,6 +76,8 @@ public class RecommendedController : BaseApiController
|
|||||||
/// Chooses a random genre and shows series that are in that without reading progress
|
/// Chooses a random genre and shows series that are in that without reading progress
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryId">Library to restrict series to</param>
|
/// <param name="libraryId">Library to restrict series to</param>
|
||||||
|
/// <param name="genreId">Genre Id</param>
|
||||||
|
/// <param name="userParams">Pagination</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("more-in")]
|
[HttpGet("more-in")]
|
||||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams userParams)
|
public async Task<ActionResult<PagedList<SeriesDto>>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams userParams)
|
||||||
@ -74,6 +96,7 @@ public class RecommendedController : BaseApiController
|
|||||||
/// Series that are fully read by the user in no particular order
|
/// Series that are fully read by the user in no particular order
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryId">Library to restrict series to</param>
|
/// <param name="libraryId">Library to restrict series to</param>
|
||||||
|
/// <param name="userParams">Pagination</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("rediscover")]
|
[HttpGet("rediscover")]
|
||||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetRediscover(int libraryId, [FromQuery] UserParams userParams)
|
public async Task<ActionResult<PagedList<SeriesDto>>> GetRediscover(int libraryId, [FromQuery] UserParams userParams)
|
||||||
|
@ -269,6 +269,14 @@ namespace API.Controllers
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
[HttpPost("analyze")]
|
||||||
|
public ActionResult AnalyzeSeries(RefreshSeriesDto refreshSeriesDto)
|
||||||
|
{
|
||||||
|
_taskScheduler.AnalyzeFilesForSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, true);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("metadata")]
|
[HttpGet("metadata")]
|
||||||
public async Task<ActionResult<SeriesMetadataDto>> GetSeriesMetadata(int seriesId)
|
public async Task<ActionResult<SeriesMetadataDto>> GetSeriesMetadata(int seriesId)
|
||||||
{
|
{
|
||||||
@ -386,6 +394,8 @@ namespace API.Controllers
|
|||||||
return Ok(await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId));
|
return Ok(await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Authorize(Policy="RequireAdminRole")]
|
[Authorize(Policy="RequireAdminRole")]
|
||||||
[HttpPost("update-related")]
|
[HttpPost("update-related")]
|
||||||
public async Task<ActionResult> UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
|
public async Task<ActionResult> UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs.Jobs;
|
||||||
using API.DTOs.Stats;
|
using API.DTOs.Stats;
|
||||||
using API.DTOs.Update;
|
using API.DTOs.Update;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
|
using Hangfire.Storage;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TaskScheduler = System.Threading.Tasks.TaskScheduler;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
@ -29,10 +34,11 @@ namespace API.Controllers
|
|||||||
private readonly IStatsService _statsService;
|
private readonly IStatsService _statsService;
|
||||||
private readonly ICleanupService _cleanupService;
|
private readonly ICleanupService _cleanupService;
|
||||||
private readonly IEmailService _emailService;
|
private readonly IEmailService _emailService;
|
||||||
|
private readonly IBookmarkService _bookmarkService;
|
||||||
|
|
||||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
|
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
|
||||||
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
||||||
ICleanupService cleanupService, IEmailService emailService)
|
ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService)
|
||||||
{
|
{
|
||||||
_applicationLifetime = applicationLifetime;
|
_applicationLifetime = applicationLifetime;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -43,6 +49,7 @@ namespace API.Controllers
|
|||||||
_statsService = statsService;
|
_statsService = statsService;
|
||||||
_cleanupService = cleanupService;
|
_cleanupService = cleanupService;
|
||||||
_emailService = emailService;
|
_emailService = emailService;
|
||||||
|
_bookmarkService = bookmarkService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -76,11 +83,10 @@ namespace API.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost("backup-db")]
|
[HttpPost("backup-db")]
|
||||||
public async Task<ActionResult> BackupDatabase()
|
public ActionResult BackupDatabase()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("{UserName} is backing up database of server from admin dashboard", User.GetUsername());
|
_logger.LogInformation("{UserName} is backing up database of server from admin dashboard", User.GetUsername());
|
||||||
await _backupService.BackupDatabase();
|
RecurringJob.Trigger("backup");
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +100,17 @@ namespace API.Controllers
|
|||||||
return Ok(await _statsService.GetServerInfo());
|
return Ok(await _statsService.GetServerInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggers the scheduling of the convert bookmarks job. Only one job will run at a time.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("convert-bookmarks")]
|
||||||
|
public ActionResult ScheduleConvertBookmarks()
|
||||||
|
{
|
||||||
|
BackgroundJob.Enqueue(() => _bookmarkService.ConvertAllBookmarkToWebP());
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("logs")]
|
[HttpGet("logs")]
|
||||||
public async Task<ActionResult> GetLogs()
|
public async Task<ActionResult> GetLogs()
|
||||||
{
|
{
|
||||||
@ -101,7 +118,7 @@ namespace API.Controllers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(files, "logs");
|
var (fileBytes, zipPath) = await _archiveService.CreateZipForDownload(files, "logs");
|
||||||
return File(fileBytes, "application/zip", Path.GetFileName(zipPath));
|
return File(fileBytes, "application/zip", Path.GetFileName(zipPath), true);
|
||||||
}
|
}
|
||||||
catch (KavitaException ex)
|
catch (KavitaException ex)
|
||||||
{
|
{
|
||||||
@ -134,5 +151,24 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
return await _emailService.CheckIfAccessible(Request.Host.ToString());
|
return await _emailService.CheckIfAccessible(Request.Host.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("jobs")]
|
||||||
|
public ActionResult<IEnumerable<JobDto>> GetJobs()
|
||||||
|
{
|
||||||
|
var recurringJobs = Hangfire.JobStorage.Current.GetConnection().GetRecurringJobs().Select(
|
||||||
|
dto =>
|
||||||
|
new JobDto() {
|
||||||
|
Id = dto.Id,
|
||||||
|
Title = dto.Id.Replace('-', ' '),
|
||||||
|
Cron = dto.Cron,
|
||||||
|
CreatedAt = dto.CreatedAt,
|
||||||
|
LastExecution = dto.LastExecution,
|
||||||
|
});
|
||||||
|
|
||||||
|
// For now, let's just do something simple
|
||||||
|
//var enqueuedJobs = JobStorage.Current.GetMonitoringApi().EnqueuedJobs("default", 0, int.MaxValue);
|
||||||
|
return Ok(recurringJobs);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,9 +54,6 @@ namespace API.Controllers
|
|||||||
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
public async Task<ActionResult<ServerSettingDto>> GetSettings()
|
||||||
{
|
{
|
||||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
// TODO: Is this needed as it gets updated in the DB on startup
|
|
||||||
settingsDto.Port = Configuration.Port;
|
|
||||||
settingsDto.LoggingLevel = Configuration.LogLevel;
|
|
||||||
return Ok(settingsDto);
|
return Ok(settingsDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,6 +166,13 @@ namespace API.Controllers
|
|||||||
_unitOfWork.SettingsRepository.Update(setting);
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setting.Key == ServerSettingKey.ConvertBookmarkToWebP && updateSettingsDto.ConvertBookmarkToWebP + string.Empty != setting.Value)
|
||||||
|
{
|
||||||
|
setting.Value = updateSettingsDto.ConvertBookmarkToWebP + string.Empty;
|
||||||
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
|
if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
|
||||||
{
|
{
|
||||||
// Validate new directory can be used
|
// Validate new directory can be used
|
||||||
@ -199,6 +203,22 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setting.Key == ServerSettingKey.EnableSwaggerUi && updateSettingsDto.EnableSwaggerUi + string.Empty != setting.Value)
|
||||||
|
{
|
||||||
|
setting.Value = updateSettingsDto.EnableSwaggerUi + string.Empty;
|
||||||
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setting.Key == ServerSettingKey.TotalBackups && updateSettingsDto.TotalBackups + string.Empty != setting.Value)
|
||||||
|
{
|
||||||
|
if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1)
|
||||||
|
{
|
||||||
|
return BadRequest("Total Backups must be between 1 and 30");
|
||||||
|
}
|
||||||
|
setting.Value = updateSettingsDto.TotalBackups + string.Empty;
|
||||||
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
|
}
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
|
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
|
||||||
{
|
{
|
||||||
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
|
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
|
||||||
|
130
API/Controllers/TachiyomiController.cs
Normal file
130
API/Controllers/TachiyomiController.cs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Comparators;
|
||||||
|
using API.Data;
|
||||||
|
using API.Data.Repositories;
|
||||||
|
using API.DTOs;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Extensions;
|
||||||
|
using API.Services;
|
||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All APIs are for Tachiyomi extension and app. They have hacks for our implementation and should not be used for any
|
||||||
|
/// other purposes.
|
||||||
|
/// </summary>
|
||||||
|
public class TachiyomiController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IReaderService _readerService;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
public TachiyomiController(IUnitOfWork unitOfWork, IReaderService readerService, IMapper mapper)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_readerService = readerService;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given the series Id, this should return the latest chapter that has been fully read.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="seriesId"></param>
|
||||||
|
/// <returns>ChapterDTO of latest chapter. Only Chapter number is used by consuming app. All other fields may be missing.</returns>
|
||||||
|
[HttpGet("latest-chapter")]
|
||||||
|
public async Task<ActionResult<ChapterDto>> GetLatestChapter(int seriesId)
|
||||||
|
{
|
||||||
|
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
|
|
||||||
|
var currentChapter = await _readerService.GetContinuePoint(seriesId, userId);
|
||||||
|
|
||||||
|
var prevChapterId =
|
||||||
|
await _readerService.GetPrevChapterIdAsync(seriesId, currentChapter.VolumeId, currentChapter.Id, userId);
|
||||||
|
|
||||||
|
// If prevChapterId is -1, this means either nothing is read or everything is read.
|
||||||
|
if (prevChapterId == -1)
|
||||||
|
{
|
||||||
|
var userWithProgress = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.Progress);
|
||||||
|
var userHasProgress =
|
||||||
|
userWithProgress.Progresses.Any(x => x.SeriesId == seriesId);
|
||||||
|
|
||||||
|
// If the user doesn't have progress, then return null, which the extension will catch as 204 (no content) and report nothing as read
|
||||||
|
if (!userHasProgress) return null;
|
||||||
|
|
||||||
|
// Else return the max chapter to Tachiyomi so it can consider everything read
|
||||||
|
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(seriesId)).ToImmutableList();
|
||||||
|
var looseLeafChapterVolume = volumes.FirstOrDefault(v => v.Number == 0);
|
||||||
|
if (looseLeafChapterVolume == null)
|
||||||
|
{
|
||||||
|
var volumeChapter = _mapper.Map<ChapterDto>(volumes.Last().Chapters.OrderBy(c => float.Parse(c.Number), new ChapterSortComparerZeroFirst()).Last());
|
||||||
|
return Ok(new ChapterDto()
|
||||||
|
{
|
||||||
|
Number = $"{int.Parse(volumeChapter.Number) / 100f}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastChapter = looseLeafChapterVolume.Chapters.OrderBy(c => float.Parse(c.Number), new ChapterSortComparer()).Last();
|
||||||
|
return Ok(_mapper.Map<ChapterDto>(lastChapter));
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is progress, we now need to figure out the highest volume or chapter and return that.
|
||||||
|
var prevChapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId);
|
||||||
|
var volumeWithProgress = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId);
|
||||||
|
if (volumeWithProgress.Number != 0)
|
||||||
|
{
|
||||||
|
// The progress is on a volume, encode it as a fake chapterDTO
|
||||||
|
return Ok(new ChapterDto()
|
||||||
|
{
|
||||||
|
Number = $"{volumeWithProgress.Number / 100f}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress is just on a chapter, return as is
|
||||||
|
return Ok(prevChapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks every chapter that is sorted below the passed number as Read. This will not mark any specials as read.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This is built for Tachiyomi and is not expected to be called by any other place</remarks>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("mark-chapter-until-as-read")]
|
||||||
|
public async Task<ActionResult<bool>> MarkChaptersUntilAsRead(int seriesId, float chapterNumber)
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||||
|
user.Progresses ??= new List<AppUserProgress>();
|
||||||
|
|
||||||
|
switch (chapterNumber)
|
||||||
|
{
|
||||||
|
// When Tachiyomi sync's progress, if there is no current progress in Tachiyomi, 0.0f is sent.
|
||||||
|
// Due to the encoding for volumes, this marks all chapters in volume 0 (loose chapters) as read.
|
||||||
|
// Hence we catch and return early, so we ignore the request.
|
||||||
|
case 0.0f:
|
||||||
|
return true;
|
||||||
|
case < 1.0f:
|
||||||
|
{
|
||||||
|
// This is a hack to track volume number. We need to map it back by x100
|
||||||
|
var volumeNumber = int.Parse($"{chapterNumber * 100f}");
|
||||||
|
await _readerService.MarkVolumesUntilAsRead(user, seriesId, volumeNumber);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
await _readerService.MarkChaptersUntilAsRead(user, seriesId, chapterNumber);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
|
if (!_unitOfWork.HasChanges()) return Ok(true);
|
||||||
|
if (await _unitOfWork.CommitAsync()) return Ok(true);
|
||||||
|
|
||||||
|
await _unitOfWork.RollbackAsync();
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ using API.Data.Repositories;
|
|||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
|
using API.SignalR;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -17,11 +18,13 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IEventHub _eventHub;
|
||||||
|
|
||||||
public UsersController(IUnitOfWork unitOfWork, IMapper mapper)
|
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
|
_eventHub = eventHub;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
@ -69,7 +72,9 @@ namespace API.Controllers
|
|||||||
[HttpPost("update-preferences")]
|
[HttpPost("update-preferences")]
|
||||||
public async Task<ActionResult<UserPreferencesDto>> UpdatePreferences(UserPreferencesDto preferencesDto)
|
public async Task<ActionResult<UserPreferencesDto>> UpdatePreferences(UserPreferencesDto preferencesDto)
|
||||||
{
|
{
|
||||||
var existingPreferences = await _unitOfWork.UserRepository.GetPreferencesAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(),
|
||||||
|
AppUserIncludes.UserPreferences);
|
||||||
|
var existingPreferences = user.UserPreferences;
|
||||||
|
|
||||||
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
|
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
|
||||||
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
|
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
|
||||||
@ -87,17 +92,18 @@ namespace API.Controllers
|
|||||||
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
||||||
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
||||||
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
|
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
|
||||||
existingPreferences.PageLayoutMode = preferencesDto.BookReaderLayoutMode;
|
existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode;
|
||||||
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
|
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
|
||||||
|
existingPreferences.GlobalPageLayoutMode = preferencesDto.GlobalPageLayoutMode;
|
||||||
|
existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries;
|
||||||
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
|
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
|
||||||
|
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
||||||
// TODO: Remove this code - this overrides layout mode to be single until the mode is released
|
|
||||||
existingPreferences.LayoutMode = LayoutMode.Single;
|
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(existingPreferences);
|
_unitOfWork.UserRepository.Update(existingPreferences);
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
|
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
|
||||||
return Ok(preferencesDto);
|
return Ok(preferencesDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using API.DTOs.Metadata;
|
using API.DTOs.Metadata;
|
||||||
|
using API.DTOs.Reader;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Interfaces;
|
||||||
|
|
||||||
namespace API.DTOs
|
namespace API.DTOs
|
||||||
{
|
{
|
||||||
@ -8,7 +11,7 @@ namespace API.DTOs
|
|||||||
/// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying
|
/// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying
|
||||||
/// file (abstracted from type).
|
/// file (abstracted from type).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ChapterDto
|
public class ChapterDto : IHasReadTimeEstimate
|
||||||
{
|
{
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -61,5 +64,30 @@ namespace API.DTOs
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Metadata field</remarks>
|
/// <remarks>Metadata field</remarks>
|
||||||
public string TitleName { get; set; }
|
public string TitleName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Summary of the Chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This is not set normally, only for Series Detail</remarks>
|
||||||
|
public string Summary { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// Age Rating for the issue/chapter
|
||||||
|
/// </summary>
|
||||||
|
public AgeRating AgeRating { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// Total words in a Chapter (books only)
|
||||||
|
/// </summary>
|
||||||
|
public long WordCount { get; set; } = 0L;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formatted Volume title ie) Volume 2.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Only available when fetched from Series Detail API</remarks>
|
||||||
|
public string VolumeTitle { get; set; } = string.Empty;
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate.MinHoursToRead"/>
|
||||||
|
public int MinHoursToRead { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate.MaxHoursToRead"/>
|
||||||
|
public int MaxHoursToRead { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
|
||||||
|
public int AvgHoursToRead { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,24 @@
|
|||||||
|
|
||||||
public enum SortField
|
public enum SortField
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sort Name of Series
|
||||||
|
/// </summary>
|
||||||
SortName = 1,
|
SortName = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// Date entity was created/imported into Kavita
|
||||||
|
/// </summary>
|
||||||
CreatedDate = 2,
|
CreatedDate = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// Date entity was last modified (tag update, etc)
|
||||||
|
/// </summary>
|
||||||
LastModifiedDate = 3,
|
LastModifiedDate = 3,
|
||||||
LastChapterAdded = 4
|
/// <summary>
|
||||||
|
/// Date series had a chapter added to it
|
||||||
|
/// </summary>
|
||||||
|
LastChapterAdded = 4,
|
||||||
|
/// <summary>
|
||||||
|
/// Time it takes to read. Uses Average.
|
||||||
|
/// </summary>
|
||||||
|
TimeToRead = 5
|
||||||
}
|
}
|
||||||
|
24
API/DTOs/Jobs/JobDto.cs
Normal file
24
API/DTOs/Jobs/JobDto.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace API.DTOs.Jobs;
|
||||||
|
|
||||||
|
public class JobDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Job Id
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Human Readable title for the Job
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// When the job was created
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? CreatedAt { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Last time the job was run
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? LastExecution { get; set; }
|
||||||
|
public string Cron { get; set; }
|
||||||
|
}
|
20
API/DTOs/JumpBar/JumpKeyDto.cs
Normal file
20
API/DTOs/JumpBar/JumpKeyDto.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
namespace API.DTOs.JumpBar;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an individual button in a Jump Bar
|
||||||
|
/// </summary>
|
||||||
|
public class JumpKeyDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Number of items in this Key
|
||||||
|
/// </summary>
|
||||||
|
public int Size { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Code to use in URL (url encoded)
|
||||||
|
/// </summary>
|
||||||
|
public string Key { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// What is visible to user
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; }
|
||||||
|
}
|
@ -47,6 +47,10 @@ namespace API.DTOs.Metadata
|
|||||||
/// Total number of issues for the series
|
/// Total number of issues for the series
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int TotalCount { get; set; }
|
public int TotalCount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Number of Words for this chapter. Only applies to Epub
|
||||||
|
/// </summary>
|
||||||
|
public long WordCount { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
API/DTOs/Reader/HourEstimateRangeDto.cs
Normal file
20
API/DTOs/Reader/HourEstimateRangeDto.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
namespace API.DTOs.Reader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A range of time to read a selection (series, chapter, etc)
|
||||||
|
/// </summary>
|
||||||
|
public record HourEstimateRangeDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Min hours to read the selection
|
||||||
|
/// </summary>
|
||||||
|
public int MinHours { get; init; } = 1;
|
||||||
|
/// <summary>
|
||||||
|
/// Max hours to read the selection
|
||||||
|
/// </summary>
|
||||||
|
public int MaxHours { get; init; } = 1;
|
||||||
|
/// <summary>
|
||||||
|
/// Estimated average hours to read the selection
|
||||||
|
/// </summary>
|
||||||
|
public int AvgHours { get; init; } = 1;
|
||||||
|
}
|
@ -10,5 +10,9 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Promoted { get; set; }
|
public bool Promoted { get; set; }
|
||||||
public bool CoverImageLocked { get; set; }
|
public bool CoverImageLocked { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// This is used to tell the UI if it should request a Cover Image or not. If null or empty, it has not been set.
|
||||||
|
/// </summary>
|
||||||
|
public string CoverImage { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace API.DTOs;
|
namespace API.DTOs.SeriesDetail;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is a special DTO for a UI page in Kavita. This performs sorting and grouping and returns exactly what UI requires for layout.
|
/// This is a special DTO for a UI page in Kavita. This performs sorting and grouping and returns exactly what UI requires for layout.
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Interfaces;
|
||||||
|
|
||||||
namespace API.DTOs
|
namespace API.DTOs
|
||||||
{
|
{
|
||||||
public class SeriesDto
|
public class SeriesDto : IHasReadTimeEstimate
|
||||||
{
|
{
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
public string Name { get; init; }
|
public string Name { get; init; }
|
||||||
@ -40,8 +41,18 @@ namespace API.DTOs
|
|||||||
public bool NameLocked { get; set; }
|
public bool NameLocked { get; set; }
|
||||||
public bool SortNameLocked { get; set; }
|
public bool SortNameLocked { get; set; }
|
||||||
public bool LocalizedNameLocked { get; set; }
|
public bool LocalizedNameLocked { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of words for the series. Only applies to epubs.
|
||||||
|
/// </summary>
|
||||||
|
public long WordCount { get; set; }
|
||||||
|
|
||||||
public int LibraryId { get; set; }
|
public int LibraryId { get; set; }
|
||||||
public string LibraryName { get; set; }
|
public string LibraryName { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate.MinHoursToRead"/>
|
||||||
|
public int MinHoursToRead { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate.MaxHoursToRead"/>
|
||||||
|
public int MaxHoursToRead { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
|
||||||
|
public int AvgHoursToRead { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using API.Services;
|
using System.Collections.Generic;
|
||||||
|
using API.Services;
|
||||||
|
|
||||||
namespace API.DTOs.Settings
|
namespace API.DTOs.Settings
|
||||||
{
|
{
|
||||||
@ -38,5 +39,17 @@ namespace API.DTOs.Settings
|
|||||||
/// <remarks>If null or empty string, will default back to default install setting aka <see cref="EmailService.DefaultApiUrl"/></remarks>
|
/// <remarks>If null or empty string, will default back to default install setting aka <see cref="EmailService.DefaultApiUrl"/></remarks>
|
||||||
public string EmailServiceUrl { get; set; }
|
public string EmailServiceUrl { get; set; }
|
||||||
public string InstallVersion { get; set; }
|
public string InstallVersion { get; set; }
|
||||||
|
|
||||||
|
public bool ConvertBookmarkToWebP { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// If the Swagger UI Should be exposed. Does not require authentication, but does require a JWT.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableSwaggerUi { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The amount of Backups before cleanup
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Value should be between 1 and 30</remarks>
|
||||||
|
public int TotalBackups { get; set; } = 30;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,49 +2,122 @@
|
|||||||
|
|
||||||
namespace API.DTOs.Stats
|
namespace API.DTOs.Stats
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents information about a Kavita Installation
|
||||||
|
/// </summary>
|
||||||
public class ServerInfoDto
|
public class ServerInfoDto
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unique Id that represents a unique install
|
||||||
|
/// </summary>
|
||||||
public string InstallId { get; set; }
|
public string InstallId { get; set; }
|
||||||
public string Os { get; set; }
|
public string Os { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// If the Kavita install is using Docker
|
||||||
|
/// </summary>
|
||||||
public bool IsDocker { get; set; }
|
public bool IsDocker { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Version of .NET instance is running
|
||||||
|
/// </summary>
|
||||||
public string DotnetVersion { get; set; }
|
public string DotnetVersion { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Version of Kavita
|
||||||
|
/// </summary>
|
||||||
public string KavitaVersion { get; set; }
|
public string KavitaVersion { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Number of Cores on the instance
|
||||||
|
/// </summary>
|
||||||
public int NumOfCores { get; set; }
|
public int NumOfCores { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The number of libraries on the instance
|
||||||
|
/// </summary>
|
||||||
public int NumberOfLibraries { get; set; }
|
public int NumberOfLibraries { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Does any user have bookmarks
|
||||||
|
/// </summary>
|
||||||
public bool HasBookmarks { get; set; }
|
public bool HasBookmarks { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The site theme the install is using
|
/// The site theme the install is using
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public string ActiveSiteTheme { get; set; }
|
public string ActiveSiteTheme { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The reading mode the main user has as a preference
|
/// The reading mode the main user has as a preference
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public ReaderMode MangaReaderMode { get; set; }
|
public ReaderMode MangaReaderMode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of users on the install
|
/// Number of users on the install
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public int NumberOfUsers { get; set; }
|
public int NumberOfUsers { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of collections on the install
|
/// Number of collections on the install
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public int NumberOfCollections { get; set; }
|
public int NumberOfCollections { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of reading lists on the install (Sum of all users)
|
/// Number of reading lists on the install (Sum of all users)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public int NumberOfReadingLists { get; set; }
|
public int NumberOfReadingLists { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is OPDS enabled
|
/// Is OPDS enabled
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public bool OPDSEnabled { get; set; }
|
public bool OPDSEnabled { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total number of files in the instance
|
/// Total number of files in the instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.2</remarks>
|
||||||
public int TotalFiles { get; set; }
|
public int TotalFiles { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of Genres in the instance
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int TotalGenres { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of People in the instance
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int TotalPeople { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Is this instance storing bookmarks as WebP
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public bool StoreBookmarksAsWebP { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Number of users on this instance using Card Layout
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int UsersOnCardLayout { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Number of users on this instance using List Layout
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int UsersOnListLayout { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Max number of Series for any library on the instance
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int MaxSeriesInALibrary { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Max number of Volumes for any library on the instance
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int MaxVolumesInASeries { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Max number of Chapters for any library on the instance
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public int MaxChaptersInASeries { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Does this instance have relationships setup between series
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Introduced in v0.5.4</remarks>
|
||||||
|
public bool UsingSeriesRelationships { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
API/DTOs/System/DirectoryDto.cs
Normal file
13
API/DTOs/System/DirectoryDto.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace API.DTOs.System;
|
||||||
|
|
||||||
|
public class DirectoryDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the directory
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Full Directory Path
|
||||||
|
/// </summary>
|
||||||
|
public string FullPath { get; set; }
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
|
||||||
namespace API.DTOs
|
namespace API.DTOs
|
||||||
{
|
{
|
||||||
@ -6,6 +7,7 @@ namespace API.DTOs
|
|||||||
{
|
{
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
public string Name { get; init; }
|
public string Name { get; init; }
|
||||||
|
public LibraryType Type { get; set; }
|
||||||
public IEnumerable<string> Folders { get; init; }
|
public IEnumerable<string> Folders { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Url of the file to download from (can be null)
|
/// Base Url encoding of the file to upload from (can be null)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using API.DTOs.Theme;
|
using API.DTOs.Theme;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Enums.UserPreferences;
|
||||||
|
|
||||||
namespace API.DTOs
|
namespace API.DTOs
|
||||||
{
|
{
|
||||||
@ -82,5 +83,15 @@ namespace API.DTOs
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Defaults to false</remarks>
|
/// <remarks>Defaults to false</remarks>
|
||||||
public bool BookReaderImmersiveMode { get; set; } = false;
|
public bool BookReaderImmersiveMode { get; set; } = false;
|
||||||
|
/// <summary>
|
||||||
|
/// Global Site Option: If the UI should layout items as Cards or List items
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Defaults to Cards</remarks>
|
||||||
|
public PageLayoutMode GlobalPageLayoutMode { get; set; } = PageLayoutMode.Cards;
|
||||||
|
/// <summary>
|
||||||
|
/// UI Site Global Setting: If unread summaries should be blurred until expanded or unless user has read it already
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Defaults to false</remarks>
|
||||||
|
public bool BlurUnreadSummaries { get; set; } = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using API.Entities.Interfaces;
|
||||||
|
|
||||||
namespace API.DTOs
|
namespace API.DTOs
|
||||||
{
|
{
|
||||||
public class VolumeDto
|
public class VolumeDto : IHasReadTimeEstimate
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public int Number { get; set; }
|
public int Number { get; set; }
|
||||||
@ -15,5 +16,11 @@ namespace API.DTOs
|
|||||||
public DateTime Created { get; set; }
|
public DateTime Created { get; set; }
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
public ICollection<ChapterDto> Chapters { get; set; }
|
public ICollection<ChapterDto> Chapters { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate.MinHoursToRead"/>
|
||||||
|
public int MinHoursToRead { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate.MaxHoursToRead"/>
|
||||||
|
public int MaxHoursToRead { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
|
||||||
|
public int AvgHoursToRead { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.Enums.UserPreferences;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
@ -78,10 +79,14 @@ namespace API.Data
|
|||||||
builder.Entity<AppUserPreferences>()
|
builder.Entity<AppUserPreferences>()
|
||||||
.Property(b => b.BackgroundColor)
|
.Property(b => b.BackgroundColor)
|
||||||
.HasDefaultValue("#000000");
|
.HasDefaultValue("#000000");
|
||||||
|
|
||||||
|
builder.Entity<AppUserPreferences>()
|
||||||
|
.Property(b => b.GlobalPageLayoutMode)
|
||||||
|
.HasDefaultValue(PageLayoutMode.Cards);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void OnEntityTracked(object sender, EntityTrackedEventArgs e)
|
private static void OnEntityTracked(object sender, EntityTrackedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity)
|
if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity)
|
||||||
{
|
{
|
||||||
@ -91,7 +96,7 @@ namespace API.Data
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
|
private static void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity)
|
if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity)
|
||||||
entity.LastModified = DateTime.Now;
|
entity.LastModified = DateTime.Now;
|
||||||
|
@ -62,6 +62,11 @@ namespace API.Data.Metadata
|
|||||||
/// Represents the sort order for the title
|
/// Represents the sort order for the title
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string TitleSort { get; set; } = string.Empty;
|
public string TitleSort { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// This comes from ComicInfo and is free form text. We use this to validate against a set of tags and mark a file as
|
||||||
|
/// special.
|
||||||
|
/// </summary>
|
||||||
|
public string Format { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The translator, can be comma separated. This is part of ComicInfo.xml draft v2.1
|
/// The translator, can be comma separated. This is part of ComicInfo.xml draft v2.1
|
||||||
|
@ -19,6 +19,9 @@ public static class MigrateBookmarks
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Bookmark directory is configurable. This will always use the default bookmark directory.</remarks>
|
/// <remarks>Bookmark directory is configurable. This will always use the default bookmark directory.</remarks>
|
||||||
/// <param name="directoryService"></param>
|
/// <param name="directoryService"></param>
|
||||||
|
/// <param name="unitOfWork"></param>
|
||||||
|
/// <param name="logger"></param>
|
||||||
|
/// <param name="cacheService"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static async Task Migrate(IDirectoryService directoryService, IUnitOfWork unitOfWork,
|
public static async Task Migrate(IDirectoryService directoryService, IUnitOfWork unitOfWork,
|
||||||
ILogger<Program> logger, ICacheService cacheService)
|
ILogger<Program> logger, ICacheService cacheService)
|
||||||
|
@ -148,7 +148,7 @@ namespace API.Data
|
|||||||
var volumes = await context.Volume.Include(v => v.Chapters).ToListAsync();
|
var volumes = await context.Volume.Include(v => v.Chapters).ToListAsync();
|
||||||
foreach (var volume in volumes)
|
foreach (var volume in volumes)
|
||||||
{
|
{
|
||||||
var firstChapter = volume.Chapters.OrderBy(x => double.Parse(x.Number), ChapterSortComparerForInChapterSorting).FirstOrDefault();
|
var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerForInChapterSorting);
|
||||||
if (firstChapter == null) continue;
|
if (firstChapter == null) continue;
|
||||||
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
|
||||||
$"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png")))
|
$"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png")))
|
||||||
|
1532
API/Data/Migrations/20220524172543_WordCount.Designer.cs
generated
Normal file
1532
API/Data/Migrations/20220524172543_WordCount.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
API/Data/Migrations/20220524172543_WordCount.cs
Normal file
37
API/Data/Migrations/20220524172543_WordCount.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class WordCount : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "WordCount",
|
||||||
|
table: "Series",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0L);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "WordCount",
|
||||||
|
table: "Chapter",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WordCount",
|
||||||
|
table: "Series");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WordCount",
|
||||||
|
table: "Chapter");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1562
API/Data/Migrations/20220610153822_TimeEstimateInDB.Designer.cs
generated
Normal file
1562
API/Data/Migrations/20220610153822_TimeEstimateInDB.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
125
API/Data/Migrations/20220610153822_TimeEstimateInDB.cs
Normal file
125
API/Data/Migrations/20220610153822_TimeEstimateInDB.cs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class TimeEstimateInDB : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "AvgHoursToRead",
|
||||||
|
table: "Volume",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "MaxHoursToRead",
|
||||||
|
table: "Volume",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "MinHoursToRead",
|
||||||
|
table: "Volume",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "WordCount",
|
||||||
|
table: "Volume",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0L);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "AvgHoursToRead",
|
||||||
|
table: "Series",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "MaxHoursToRead",
|
||||||
|
table: "Series",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "MinHoursToRead",
|
||||||
|
table: "Series",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "AvgHoursToRead",
|
||||||
|
table: "Chapter",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "MaxHoursToRead",
|
||||||
|
table: "Chapter",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "MinHoursToRead",
|
||||||
|
table: "Chapter",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AvgHoursToRead",
|
||||||
|
table: "Volume");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "MaxHoursToRead",
|
||||||
|
table: "Volume");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "MinHoursToRead",
|
||||||
|
table: "Volume");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WordCount",
|
||||||
|
table: "Volume");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AvgHoursToRead",
|
||||||
|
table: "Series");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "MaxHoursToRead",
|
||||||
|
table: "Series");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "MinHoursToRead",
|
||||||
|
table: "Series");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AvgHoursToRead",
|
||||||
|
table: "Chapter");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "MaxHoursToRead",
|
||||||
|
table: "Chapter");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "MinHoursToRead",
|
||||||
|
table: "Chapter");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1562
API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs
generated
Normal file
1562
API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class RenamedBookReaderLayoutMode : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "PageLayoutMode",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
newName: "BookReaderLayoutMode");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "BookReaderLayoutMode",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
newName: "PageLayoutMode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1567
API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs
generated
Normal file
1567
API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.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 GlobalPageLayoutModeUserSetting : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "GlobalPageLayoutMode",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GlobalPageLayoutMode",
|
||||||
|
table: "AppUserPreferences");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1570
API/Data/Migrations/20220615190640_LastFileAnalysis.Designer.cs
generated
Normal file
1570
API/Data/Migrations/20220615190640_LastFileAnalysis.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
API/Data/Migrations/20220615190640_LastFileAnalysis.cs
Normal file
27
API/Data/Migrations/20220615190640_LastFileAnalysis.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class LastFileAnalysis : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "LastFileAnalysis",
|
||||||
|
table: "MangaFile",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LastFileAnalysis",
|
||||||
|
table: "MangaFile");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1573
API/Data/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs
generated
Normal file
1573
API/Data/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
API/Data/Migrations/20220625215526_BlurUnreadSummaries.cs
Normal file
26
API/Data/Migrations/20220625215526_BlurUnreadSummaries.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class BlurUnreadSummaries : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "BlurUnreadSummaries",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BlurUnreadSummaries",
|
||||||
|
table: "AppUserPreferences");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.4");
|
modelBuilder.HasAnnotation("ProductVersion", "6.0.6");
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
{
|
{
|
||||||
@ -170,6 +170,9 @@ namespace API.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasDefaultValue("#000000");
|
.HasDefaultValue("#000000");
|
||||||
|
|
||||||
|
b.Property<bool>("BlurUnreadSummaries")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("BookReaderFontFamily")
|
b.Property<string>("BookReaderFontFamily")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -179,6 +182,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<bool>("BookReaderImmersiveMode")
|
b.Property<bool>("BookReaderImmersiveMode")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookReaderLayoutMode")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("BookReaderLineSpacing")
|
b.Property<int>("BookReaderLineSpacing")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
@ -196,10 +202,12 @@ namespace API.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasDefaultValue("Dark");
|
.HasDefaultValue("Dark");
|
||||||
|
|
||||||
b.Property<int>("LayoutMode")
|
b.Property<int>("GlobalPageLayoutMode")
|
||||||
.HasColumnType("INTEGER");
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0);
|
||||||
|
|
||||||
b.Property<int>("PageLayoutMode")
|
b.Property<int>("LayoutMode")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("PageSplitOption")
|
b.Property<int>("PageSplitOption")
|
||||||
@ -320,6 +328,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<int>("AgeRating")
|
b.Property<int>("AgeRating")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AvgHoursToRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("Count")
|
b.Property<int>("Count")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
@ -341,6 +352,12 @@ namespace API.Data.Migrations
|
|||||||
b.Property<DateTime>("LastModified")
|
b.Property<DateTime>("LastModified")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("MaxHoursToRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("MinHoursToRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("Number")
|
b.Property<string>("Number")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -368,6 +385,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<int>("VolumeId")
|
b.Property<int>("VolumeId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long>("WordCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("VolumeId");
|
b.HasIndex("VolumeId");
|
||||||
@ -499,6 +519,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<int>("Format")
|
b.Property<int>("Format")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastFileAnalysis")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<DateTime>("LastModified")
|
b.Property<DateTime>("LastModified")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -729,6 +752,9 @@ namespace API.Data.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AvgHoursToRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("CoverImage")
|
b.Property<string>("CoverImage")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -756,6 +782,12 @@ namespace API.Data.Migrations
|
|||||||
b.Property<bool>("LocalizedNameLocked")
|
b.Property<bool>("LocalizedNameLocked")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("MaxHoursToRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("MinHoursToRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -777,6 +809,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<bool>("SortNameLocked")
|
b.Property<bool>("SortNameLocked")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long>("WordCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("LibraryId");
|
b.HasIndex("LibraryId");
|
||||||
@ -862,6 +897,9 @@ namespace API.Data.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AvgHoursToRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("CoverImage")
|
b.Property<string>("CoverImage")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -871,6 +909,12 @@ namespace API.Data.Migrations
|
|||||||
b.Property<DateTime>("LastModified")
|
b.Property<DateTime>("LastModified")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("MaxHoursToRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("MinHoursToRead")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -883,6 +927,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<int>("SeriesId")
|
b.Property<int>("SeriesId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long>("WordCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("SeriesId");
|
b.HasIndex("SeriesId");
|
||||||
|
@ -18,6 +18,7 @@ public interface IGenreRepository
|
|||||||
Task<IList<GenreTagDto>> GetAllGenreDtosAsync();
|
Task<IList<GenreTagDto>> GetAllGenreDtosAsync();
|
||||||
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
|
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
|
||||||
Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds);
|
Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds);
|
||||||
|
Task<int> GetCountAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GenreRepository : IGenreRepository
|
public class GenreRepository : IGenreRepository
|
||||||
@ -72,6 +73,11 @@ public class GenreRepository : IGenreRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetCountAsync()
|
||||||
|
{
|
||||||
|
return await _context.Genre.CountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IList<Genre>> GetAllGenresAsync()
|
public async Task<IList<Genre>> GetAllGenresAsync()
|
||||||
{
|
{
|
||||||
return await _context.Genre.ToListAsync();
|
return await _context.Genre.ToListAsync();
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.Filtering;
|
||||||
|
using API.DTOs.JumpBar;
|
||||||
|
using API.DTOs.Metadata;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using AutoMapper.QueryableExtensions;
|
using AutoMapper.QueryableExtensions;
|
||||||
|
using Kavita.Common.Extensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Data.Repositories;
|
namespace API.Data.Repositories;
|
||||||
@ -38,6 +43,10 @@ public interface ILibraryRepository
|
|||||||
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
|
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
|
||||||
Task<IEnumerable<Library>> GetLibraryForIdsAsync(IList<int> libraryIds);
|
Task<IEnumerable<Library>> GetLibraryForIdsAsync(IList<int> libraryIds);
|
||||||
Task<int> GetTotalFiles();
|
Task<int> GetTotalFiles();
|
||||||
|
IEnumerable<JumpKeyDto> GetJumpBarAsync(int libraryId);
|
||||||
|
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
|
||||||
|
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
|
||||||
|
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LibraryRepository : ILibraryRepository
|
public class LibraryRepository : ILibraryRepository
|
||||||
@ -123,6 +132,37 @@ public class LibraryRepository : ILibraryRepository
|
|||||||
return await _context.MangaFile.CountAsync();
|
return await _context.MangaFile.CountAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnumerable<JumpKeyDto> GetJumpBarAsync(int libraryId)
|
||||||
|
{
|
||||||
|
var seriesSortCharacters = _context.Series.Where(s => s.LibraryId == libraryId)
|
||||||
|
.Select(s => s.SortName.ToUpper())
|
||||||
|
.OrderBy(s => s)
|
||||||
|
.AsEnumerable()
|
||||||
|
.Select(s => s[0]);
|
||||||
|
|
||||||
|
// Map the title to the number of entities
|
||||||
|
var firstCharacterMap = new Dictionary<char, int>();
|
||||||
|
foreach (var sortChar in seriesSortCharacters)
|
||||||
|
{
|
||||||
|
var c = sortChar;
|
||||||
|
var isAlpha = char.IsLetter(sortChar);
|
||||||
|
if (!isAlpha) c = '#';
|
||||||
|
if (!firstCharacterMap.ContainsKey(c))
|
||||||
|
{
|
||||||
|
firstCharacterMap[c] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstCharacterMap[c] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstCharacterMap.Keys.Select(k => new JumpKeyDto()
|
||||||
|
{
|
||||||
|
Key = k + string.Empty,
|
||||||
|
Size = firstCharacterMap[k],
|
||||||
|
Title = k + string.Empty
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<LibraryDto>> GetLibraryDtosAsync()
|
public async Task<IEnumerable<LibraryDto>> GetLibraryDtosAsync()
|
||||||
{
|
{
|
||||||
return await _context.Library
|
return await _context.Library
|
||||||
@ -224,4 +264,54 @@ public class LibraryRepository : ILibraryRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds)
|
||||||
|
{
|
||||||
|
return await _context.Series
|
||||||
|
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||||
|
.Select(s => s.Metadata.AgeRating)
|
||||||
|
.Distinct()
|
||||||
|
.Select(s => new AgeRatingDto()
|
||||||
|
{
|
||||||
|
Value = s,
|
||||||
|
Title = s.ToDescription()
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds)
|
||||||
|
{
|
||||||
|
var ret = await _context.Series
|
||||||
|
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||||
|
.Select(s => s.Metadata.Language)
|
||||||
|
.AsNoTracking()
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return ret
|
||||||
|
.Where(s => !string.IsNullOrEmpty(s))
|
||||||
|
.Select(s => new LanguageDto()
|
||||||
|
{
|
||||||
|
Title = CultureInfo.GetCultureInfo(s).DisplayName,
|
||||||
|
IsoCode = s
|
||||||
|
})
|
||||||
|
.OrderBy(s => s.Title)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
27
API/Data/Repositories/MangaFileRepository.cs
Normal file
27
API/Data/Repositories/MangaFileRepository.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using API.Entities;
|
||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace API.Data.Repositories;
|
||||||
|
|
||||||
|
public interface IMangaFileRepository
|
||||||
|
{
|
||||||
|
void Update(MangaFile file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MangaFileRepository : IMangaFileRepository
|
||||||
|
{
|
||||||
|
private readonly DataContext _context;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
public MangaFileRepository(DataContext context, IMapper mapper)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(MangaFile file)
|
||||||
|
{
|
||||||
|
_context.Entry(file).State = EntityState.Modified;
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ public interface IPersonRepository
|
|||||||
Task<IList<Person>> GetAllPeople();
|
Task<IList<Person>> GetAllPeople();
|
||||||
Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false);
|
Task RemoveAllPeopleNoLongerAssociated(bool removeExternal = false);
|
||||||
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds);
|
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds);
|
||||||
|
Task<int> GetCountAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PersonRepository : IPersonRepository
|
public class PersonRepository : IPersonRepository
|
||||||
@ -72,6 +73,11 @@ public class PersonRepository : IPersonRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetCountAsync()
|
||||||
|
{
|
||||||
|
return await _context.Person.CountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<IList<Person>> GetAllPeople()
|
public async Task<IList<Person>> GetAllPeople()
|
||||||
{
|
{
|
||||||
|
@ -17,6 +17,7 @@ using API.Entities.Enums;
|
|||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
|
using API.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using AutoMapper.QueryableExtensions;
|
using AutoMapper.QueryableExtensions;
|
||||||
@ -67,7 +68,8 @@ public interface ISeriesRepository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryId"></param>
|
/// <param name="libraryId"></param>
|
||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <param name="userParams"></param>
|
/// <param name="userParams">Pagination info</param>
|
||||||
|
/// <param name="filter">Filtering/Sorting to apply</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter);
|
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -106,13 +108,12 @@ public interface ISeriesRepository
|
|||||||
Task<Series> GetFullSeriesForSeriesIdAsync(int seriesId);
|
Task<Series> GetFullSeriesForSeriesIdAsync(int seriesId);
|
||||||
Task<Chunk> GetChunkInfo(int libraryId = 0);
|
Task<Chunk> GetChunkInfo(int libraryId = 0);
|
||||||
Task<IList<SeriesMetadata>> GetSeriesMetadataForIdsAsync(IEnumerable<int> seriesIds);
|
Task<IList<SeriesMetadata>> GetSeriesMetadataForIdsAsync(IEnumerable<int> seriesIds);
|
||||||
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<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
|
||||||
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
||||||
Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind);
|
Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind);
|
||||||
Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams);
|
Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams);
|
||||||
|
Task<PagedList<SeriesDto>> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams);
|
||||||
Task<PagedList<SeriesDto>> GetHighlyRated(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>> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams);
|
||||||
Task<PagedList<SeriesDto>> GetRediscover(int userId, int libraryId, UserParams userParams);
|
Task<PagedList<SeriesDto>> GetRediscover(int userId, int libraryId, UserParams userParams);
|
||||||
@ -752,6 +753,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
SortField.CreatedDate => query.OrderBy(s => s.Created),
|
SortField.CreatedDate => query.OrderBy(s => s.Created),
|
||||||
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
|
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
|
||||||
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
|
||||||
|
SortField.TimeToRead => query.OrderBy(s => s.AvgHoursToRead),
|
||||||
_ => query
|
_ => query
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -763,6 +765,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
|
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
|
||||||
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
|
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
|
||||||
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
|
||||||
|
SortField.TimeToRead => query.OrderByDescending(s => s.AvgHoursToRead),
|
||||||
_ => query
|
_ => query
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -920,54 +923,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds)
|
|
||||||
{
|
|
||||||
return await _context.Series
|
|
||||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
|
||||||
.Select(s => s.Metadata.AgeRating)
|
|
||||||
.Distinct()
|
|
||||||
.Select(s => new AgeRatingDto()
|
|
||||||
{
|
|
||||||
Value = s,
|
|
||||||
Title = s.ToDescription()
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds)
|
|
||||||
{
|
|
||||||
var ret = await _context.Series
|
|
||||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
|
||||||
.Select(s => s.Metadata.Language)
|
|
||||||
.AsNoTracking()
|
|
||||||
.Distinct()
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return ret
|
|
||||||
.Where(s => !string.IsNullOrEmpty(s))
|
|
||||||
.Select(s => new LanguageDto()
|
|
||||||
{
|
|
||||||
Title = CultureInfo.GetCultureInfo(s).DisplayName,
|
|
||||||
IsoCode = s
|
|
||||||
})
|
|
||||||
.OrderBy(s => s.Title)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -976,6 +932,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
/// <remarks>This provides 2 levels of pagination. Fetching the individual chapters only looks at 3000. Then when performing grouping
|
/// <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>
|
/// in memory, we stop after 30 series. </remarks>
|
||||||
/// <param name="userId">Used to ensure user has access to libraries</param>
|
/// <param name="userId">Used to ensure user has access to libraries</param>
|
||||||
|
/// <param name="pageSize">How many entities to return</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30)
|
public async Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30)
|
||||||
{
|
{
|
||||||
@ -1131,8 +1088,11 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
|
|
||||||
|
|
||||||
var query = _context.Series
|
var query = _context.Series
|
||||||
.Where(s => s.Pages < 2000 && !distinctSeriesIdsWithProgress.Contains(s.Id) &&
|
.Where(s => (
|
||||||
usersSeriesIds.Contains(s.Id))
|
(s.Pages / ReaderService.AvgPagesPerMinute / 60 < 10 && s.Format != MangaFormat.Epub)
|
||||||
|
|| (s.WordCount * ReaderService.AvgWordsPerHour < 10 && s.Format == MangaFormat.Epub))
|
||||||
|
&& !distinctSeriesIdsWithProgress.Contains(s.Id) &&
|
||||||
|
usersSeriesIds.Contains(s.Id))
|
||||||
.Where(s => s.Metadata.PublicationStatus != PublicationStatus.OnGoing)
|
.Where(s => s.Metadata.PublicationStatus != PublicationStatus.OnGoing)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
|
||||||
@ -1141,6 +1101,30 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<PagedList<SeriesDto>> GetQuickCatchupReads(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 / ReaderService.AvgPagesPerMinute / 60 < 10 && s.Format != MangaFormat.Epub)
|
||||||
|
|| (s.WordCount * ReaderService.AvgWordsPerHour < 10 && s.Format == MangaFormat.Epub))
|
||||||
|
&& !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>
|
/// <summary>
|
||||||
/// Returns all library ids for a user
|
/// Returns all library ids for a user
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1205,7 +1189,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 3000)
|
private async Task<IEnumerable<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId)
|
||||||
{
|
{
|
||||||
var libraries = await _context.AppUser
|
var libraries = await _context.AppUser
|
||||||
.Where(u => u.Id == userId)
|
.Where(u => u.Id == userId)
|
||||||
|
@ -5,6 +5,7 @@ using API.DTOs.Settings;
|
|||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using AutoMapper.QueryableExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Data.Repositories;
|
namespace API.Data.Repositories;
|
||||||
|
@ -56,6 +56,8 @@ public interface IUserRepository
|
|||||||
Task<IEnumerable<AppUser>> GetAllUsers();
|
Task<IEnumerable<AppUser>> GetAllUsers();
|
||||||
|
|
||||||
Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByThemeAsync(int themeId);
|
Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByThemeAsync(int themeId);
|
||||||
|
Task<bool> HasAccessToLibrary(int libraryId, int userId);
|
||||||
|
Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserRepository : IUserRepository
|
public class UserRepository : IUserRepository
|
||||||
@ -238,6 +240,19 @@ public class UserRepository : IUserRepository
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HasAccessToLibrary(int libraryId, int userId)
|
||||||
|
{
|
||||||
|
return await _context.Library
|
||||||
|
.Include(l => l.AppUsers)
|
||||||
|
.AnyAsync(library => library.AppUsers.Any(user => user.Id == userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags)
|
||||||
|
{
|
||||||
|
var query = AddIncludesToQuery(_context.Users.AsQueryable(), includeFlags);
|
||||||
|
return await query.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||||
{
|
{
|
||||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||||
|
@ -100,6 +100,9 @@ namespace API.Data
|
|||||||
new() {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
|
new() {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
|
||||||
new() {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
|
new() {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
|
||||||
new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
|
new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
|
||||||
|
new() {Key = ServerSettingKey.ConvertBookmarkToWebP, Value = "false"},
|
||||||
|
new() {Key = ServerSettingKey.EnableSwaggerUi, Value = "false"},
|
||||||
|
new() {Key = ServerSettingKey.TotalBackups, Value = "30"},
|
||||||
}.ToArray());
|
}.ToArray());
|
||||||
|
|
||||||
foreach (var defaultSetting in DefaultSettings)
|
foreach (var defaultSetting in DefaultSettings)
|
||||||
|
@ -22,6 +22,7 @@ public interface IUnitOfWork
|
|||||||
IGenreRepository GenreRepository { get; }
|
IGenreRepository GenreRepository { get; }
|
||||||
ITagRepository TagRepository { get; }
|
ITagRepository TagRepository { get; }
|
||||||
ISiteThemeRepository SiteThemeRepository { get; }
|
ISiteThemeRepository SiteThemeRepository { get; }
|
||||||
|
IMangaFileRepository MangaFileRepository { get; }
|
||||||
bool Commit();
|
bool Commit();
|
||||||
Task<bool> CommitAsync();
|
Task<bool> CommitAsync();
|
||||||
bool HasChanges();
|
bool HasChanges();
|
||||||
@ -58,6 +59,7 @@ public class UnitOfWork : IUnitOfWork
|
|||||||
public IGenreRepository GenreRepository => new GenreRepository(_context, _mapper);
|
public IGenreRepository GenreRepository => new GenreRepository(_context, _mapper);
|
||||||
public ITagRepository TagRepository => new TagRepository(_context, _mapper);
|
public ITagRepository TagRepository => new TagRepository(_context, _mapper);
|
||||||
public ISiteThemeRepository SiteThemeRepository => new SiteThemeRepository(_context, _mapper);
|
public ISiteThemeRepository SiteThemeRepository => new SiteThemeRepository(_context, _mapper);
|
||||||
|
public IMangaFileRepository MangaFileRepository => new MangaFileRepository(_context, _mapper);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Commits changes to the DB. Completes the open transaction.
|
/// Commits changes to the DB. Completes the open transaction.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Enums.UserPreferences;
|
||||||
|
|
||||||
namespace API.Entities
|
namespace API.Entities
|
||||||
{
|
{
|
||||||
@ -81,13 +82,22 @@ namespace API.Entities
|
|||||||
/// 2 column is fit to height, 2 columns
|
/// 2 column is fit to height, 2 columns
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Defaults to Default</remarks>
|
/// <remarks>Defaults to Default</remarks>
|
||||||
public BookPageLayoutMode PageLayoutMode { get; set; } = BookPageLayoutMode.Default;
|
public BookPageLayoutMode BookReaderLayoutMode { get; set; } = BookPageLayoutMode.Default;
|
||||||
/// <summary>
|
/// <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.
|
/// 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>
|
/// </summary>
|
||||||
/// <remarks>Defaults to false</remarks>
|
/// <remarks>Defaults to false</remarks>
|
||||||
public bool BookReaderImmersiveMode { get; set; } = false;
|
public bool BookReaderImmersiveMode { get; set; } = false;
|
||||||
|
/// <summary>
|
||||||
|
/// Global Site Option: If the UI should layout items as Cards or List items
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Defaults to Cards</remarks>
|
||||||
|
public PageLayoutMode GlobalPageLayoutMode { get; set; } = PageLayoutMode.Cards;
|
||||||
|
/// <summary>
|
||||||
|
/// UI Site Global Setting: If unread summaries should be blurred until expanded or unless user has read it already
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Defaults to false</remarks>
|
||||||
|
public bool BlurUnreadSummaries { get; set; } = false;
|
||||||
|
|
||||||
public AppUser AppUser { get; set; }
|
public AppUser AppUser { get; set; }
|
||||||
public int AppUserId { get; set; }
|
public int AppUserId { get; set; }
|
||||||
|
@ -3,10 +3,11 @@ using System.Collections.Generic;
|
|||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
using API.Parser;
|
using API.Parser;
|
||||||
|
using API.Services;
|
||||||
|
|
||||||
namespace API.Entities
|
namespace API.Entities
|
||||||
{
|
{
|
||||||
public class Chapter : IEntityDate
|
public class Chapter : IEntityDate, IHasReadTimeEstimate
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -24,7 +25,7 @@ namespace API.Entities
|
|||||||
public DateTime Created { get; set; }
|
public DateTime Created { get; set; }
|
||||||
public DateTime LastModified { get; set; }
|
public DateTime LastModified { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Absolute path to the (managed) image file
|
/// Relative path to the (managed) image file representing the cover image
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
||||||
public string CoverImage { get; set; }
|
public string CoverImage { get; set; }
|
||||||
@ -72,6 +73,18 @@ namespace API.Entities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Count { get; set; } = 0;
|
public int Count { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total Word count of all chapters in this chapter.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Word Count is only available from EPUB files</remarks>
|
||||||
|
public long WordCount { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate"/>
|
||||||
|
public int MinHoursToRead { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate"/>
|
||||||
|
public int MaxHoursToRead { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate"/>
|
||||||
|
public int AvgHoursToRead { get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All people attached at a Chapter level. Usually Comics will have different people per issue.
|
/// All people attached at a Chapter level. Usually Comics will have different people per issue.
|
||||||
|
@ -7,10 +7,6 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Other = 1,
|
Other = 1,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Artist
|
|
||||||
/// </summary>
|
|
||||||
//Artist = 2,
|
|
||||||
/// <summary>
|
|
||||||
/// Author or Writer
|
/// Author or Writer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Writer = 3,
|
Writer = 3,
|
||||||
|
@ -76,5 +76,20 @@ namespace API.Entities.Enums
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Description("CustomEmailService")]
|
[Description("CustomEmailService")]
|
||||||
EmailServiceUrl = 13,
|
EmailServiceUrl = 13,
|
||||||
|
/// <summary>
|
||||||
|
/// If Kavita should save bookmarks as WebP images
|
||||||
|
/// </summary>
|
||||||
|
[Description("ConvertBookmarkToWebP")]
|
||||||
|
ConvertBookmarkToWebP = 14,
|
||||||
|
/// <summary>
|
||||||
|
/// If the Swagger UI Should be exposed. Does not require authentication, but does require a JWT.
|
||||||
|
/// </summary>
|
||||||
|
[Description("EnableSwaggerUi")]
|
||||||
|
EnableSwaggerUi = 15,
|
||||||
|
/// <summary>
|
||||||
|
/// Total Number of Backups to maintain before cleaning. Default 30, min 1.
|
||||||
|
/// </summary>
|
||||||
|
[Description("TotalBackups")]
|
||||||
|
TotalBackups = 16,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
API/Entities/Enums/UserPreferences/PageLayoutMode.cs
Normal file
11
API/Entities/Enums/UserPreferences/PageLayoutMode.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace API.Entities.Enums.UserPreferences;
|
||||||
|
|
||||||
|
public enum PageLayoutMode
|
||||||
|
{
|
||||||
|
[Description("Cards")]
|
||||||
|
Cards = 0,
|
||||||
|
[Description("List")]
|
||||||
|
List = 1
|
||||||
|
}
|
25
API/Entities/Interfaces/IHasReadTimeEstimate.cs
Normal file
25
API/Entities/Interfaces/IHasReadTimeEstimate.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using API.Services;
|
||||||
|
|
||||||
|
namespace API.Entities.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entity has read time estimate properties to estimate time to read
|
||||||
|
/// </summary>
|
||||||
|
public interface IHasReadTimeEstimate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Min hours to read the chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Uses a fixed number to calculate from <see cref="ReaderService"/></remarks>
|
||||||
|
public int MinHoursToRead { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Max hours to read the chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Uses a fixed number to calculate from <see cref="ReaderService"/></remarks>
|
||||||
|
public int MaxHoursToRead { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Average hours to read the chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Uses a fixed number to calculate from <see cref="ReaderService"/></remarks>
|
||||||
|
public int AvgHoursToRead { get; set; }
|
||||||
|
}
|
@ -25,6 +25,10 @@ namespace API.Entities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>This gets updated anytime the file is scanned</remarks>
|
/// <remarks>This gets updated anytime the file is scanned</remarks>
|
||||||
public DateTime LastModified { get; set; }
|
public DateTime LastModified { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Last time file analysis ran on this file
|
||||||
|
/// </summary>
|
||||||
|
public DateTime LastFileAnalysis { get; set; }
|
||||||
|
|
||||||
|
|
||||||
// Relationship Mapping
|
// Relationship Mapping
|
||||||
|
@ -6,7 +6,7 @@ using API.Entities.Metadata;
|
|||||||
|
|
||||||
namespace API.Entities;
|
namespace API.Entities;
|
||||||
|
|
||||||
public class Series : IEntityDate
|
public class Series : IEntityDate, IHasReadTimeEstimate
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -65,6 +65,16 @@ public class Series : IEntityDate
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime LastChapterAdded { get; set; }
|
public DateTime LastChapterAdded { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total Word count of all chapters in this chapter.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Word Count is only available from EPUB files</remarks>
|
||||||
|
public long WordCount { get; set; }
|
||||||
|
|
||||||
|
public int MinHoursToRead { get; set; }
|
||||||
|
public int MaxHoursToRead { get; set; }
|
||||||
|
public int AvgHoursToRead { get; set; }
|
||||||
|
|
||||||
public SeriesMetadata Metadata { get; set; }
|
public SeriesMetadata Metadata { get; set; }
|
||||||
|
|
||||||
public ICollection<AppUserRating> Ratings { get; set; } = new List<AppUserRating>();
|
public ICollection<AppUserRating> Ratings { get; set; } = new List<AppUserRating>();
|
||||||
@ -82,5 +92,4 @@ public class Series : IEntityDate
|
|||||||
public List<Volume> Volumes { get; set; }
|
public List<Volume> Volumes { get; set; }
|
||||||
public Library Library { get; set; }
|
public Library Library { get; set; }
|
||||||
public int LibraryId { get; set; }
|
public int LibraryId { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Entities
|
namespace API.Entities
|
||||||
{
|
{
|
||||||
public class Volume : IEntityDate
|
public class Volume : IEntityDate, IHasReadTimeEstimate
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -25,12 +24,23 @@ namespace API.Entities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
/// <remarks>The file is managed internally to Kavita's APPDIR</remarks>
|
||||||
public string CoverImage { get; set; }
|
public string CoverImage { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Total pages of all chapters in this volume
|
||||||
|
/// </summary>
|
||||||
public int Pages { get; set; }
|
public int Pages { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Total Word count of all chapters in this volume.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Word Count is only available from EPUB files</remarks>
|
||||||
|
public long WordCount { get; set; }
|
||||||
|
public int MinHoursToRead { get; set; }
|
||||||
|
public int MaxHoursToRead { get; set; }
|
||||||
|
public int AvgHoursToRead { get; set; }
|
||||||
|
|
||||||
|
|
||||||
// Relationships
|
// Relationships
|
||||||
public Series Series { get; set; }
|
public Series Series { get; set; }
|
||||||
public int SeriesId { get; set; }
|
public int SeriesId { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using API.Data;
|
|||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
|
using API.Services.Tasks.Metadata;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using API.SignalR.Presence;
|
using API.SignalR.Presence;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
@ -20,15 +21,18 @@ namespace API.Extensions
|
|||||||
public static void AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env)
|
public static void AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env)
|
||||||
{
|
{
|
||||||
services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
|
services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
|
||||||
services.AddScoped<IStatsService, StatsService>();
|
|
||||||
services.AddScoped<ITaskScheduler, TaskScheduler>();
|
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||||
services.AddScoped<IDirectoryService, DirectoryService>();
|
services.AddScoped<IDirectoryService, DirectoryService>();
|
||||||
services.AddScoped<ITokenService, TokenService>();
|
services.AddScoped<ITokenService, TokenService>();
|
||||||
|
services.AddScoped<IFileSystem, FileSystem>();
|
||||||
|
services.AddScoped<IFileService, FileService>();
|
||||||
|
services.AddScoped<ICacheHelper, CacheHelper>();
|
||||||
|
|
||||||
|
services.AddScoped<IStatsService, StatsService>();
|
||||||
|
services.AddScoped<ITaskScheduler, TaskScheduler>();
|
||||||
services.AddScoped<ICacheService, CacheService>();
|
services.AddScoped<ICacheService, CacheService>();
|
||||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
|
||||||
services.AddScoped<IScannerService, ScannerService>();
|
|
||||||
services.AddScoped<IArchiveService, ArchiveService>();
|
services.AddScoped<IArchiveService, ArchiveService>();
|
||||||
services.AddScoped<IMetadataService, MetadataService>();
|
|
||||||
services.AddScoped<IBackupService, BackupService>();
|
services.AddScoped<IBackupService, BackupService>();
|
||||||
services.AddScoped<ICleanupService, CleanupService>();
|
services.AddScoped<ICleanupService, CleanupService>();
|
||||||
services.AddScoped<IBookService, BookService>();
|
services.AddScoped<IBookService, BookService>();
|
||||||
@ -43,10 +47,11 @@ namespace API.Extensions
|
|||||||
services.AddScoped<IThemeService, ThemeService>();
|
services.AddScoped<IThemeService, ThemeService>();
|
||||||
services.AddScoped<ISeriesService, SeriesService>();
|
services.AddScoped<ISeriesService, SeriesService>();
|
||||||
|
|
||||||
|
services.AddScoped<IScannerService, ScannerService>();
|
||||||
|
services.AddScoped<IMetadataService, MetadataService>();
|
||||||
|
services.AddScoped<IWordCountAnalyzerService, WordCountAnalyzerService>();
|
||||||
|
|
||||||
|
|
||||||
services.AddScoped<IFileSystem, FileSystem>();
|
|
||||||
services.AddScoped<IFileService, FileService>();
|
|
||||||
services.AddScoped<ICacheHelper, CacheHelper>();
|
|
||||||
|
|
||||||
services.AddScoped<IPresenceTracker, PresenceTracker>();
|
services.AddScoped<IPresenceTracker, PresenceTracker>();
|
||||||
services.AddScoped<IEventHub, EventHub>();
|
services.AddScoped<IEventHub, EventHub>();
|
||||||
@ -57,7 +62,7 @@ namespace API.Extensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void AddSqLite(this IServiceCollection services, IConfiguration config,
|
private static void AddSqLite(this IServiceCollection services, IConfiguration config,
|
||||||
IWebHostEnvironment env)
|
IHostEnvironment env)
|
||||||
{
|
{
|
||||||
services.AddDbContext<DataContext>(options =>
|
services.AddDbContext<DataContext>(options =>
|
||||||
{
|
{
|
||||||
|
@ -113,7 +113,7 @@ namespace API.Helpers
|
|||||||
opt.MapFrom(src => src.BookThemeName))
|
opt.MapFrom(src => src.BookThemeName))
|
||||||
.ForMember(dest => dest.BookReaderLayoutMode,
|
.ForMember(dest => dest.BookReaderLayoutMode,
|
||||||
opt =>
|
opt =>
|
||||||
opt.MapFrom(src => src.PageLayoutMode));
|
opt.MapFrom(src => src.BookReaderLayoutMode));
|
||||||
|
|
||||||
|
|
||||||
CreateMap<AppUserBookmark, BookmarkDto>();
|
CreateMap<AppUserBookmark, BookmarkDto>();
|
||||||
@ -138,7 +138,8 @@ namespace API.Helpers
|
|||||||
|
|
||||||
CreateMap<RegisterDto, AppUser>();
|
CreateMap<RegisterDto, AppUser>();
|
||||||
|
|
||||||
|
CreateMap<IList<ServerSetting>, ServerSettingDto>()
|
||||||
|
.ConvertUsing<ServerSettingConverter>();
|
||||||
|
|
||||||
CreateMap<IEnumerable<ServerSetting>, ServerSettingDto>()
|
CreateMap<IEnumerable<ServerSetting>, ServerSettingDto>()
|
||||||
.ConvertUsing<ServerSettingConverter>();
|
.ConvertUsing<ServerSettingConverter>();
|
||||||
|
@ -14,6 +14,7 @@ public interface ICacheHelper
|
|||||||
bool CoverImageExists(string path);
|
bool CoverImageExists(string path);
|
||||||
|
|
||||||
bool HasFileNotChangedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile firstFile);
|
bool HasFileNotChangedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile firstFile);
|
||||||
|
bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile firstFile);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ public class CacheHelper : ICacheHelper
|
|||||||
/// <remarks>If a cover image is locked but the underlying file has been deleted, this will allow regenerating. </remarks>
|
/// <remarks>If a cover image is locked but the underlying file has been deleted, this will allow regenerating. </remarks>
|
||||||
/// <param name="coverPath">This should just be the filename, no path information</param>
|
/// <param name="coverPath">This should just be the filename, no path information</param>
|
||||||
/// <param name="firstFile"></param>
|
/// <param name="firstFile"></param>
|
||||||
|
/// <param name="chapterCreated">When the chapter was created (Not Used)</param>
|
||||||
/// <param name="forceUpdate">If the user has told us to force the refresh</param>
|
/// <param name="forceUpdate">If the user has told us to force the refresh</param>
|
||||||
/// <param name="isCoverLocked">If cover has been locked by user. This will force false</param>
|
/// <param name="isCoverLocked">If cover has been locked by user. This will force false</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@ -61,6 +63,25 @@ public class CacheHelper : ICacheHelper
|
|||||||
|| _fileService.HasFileBeenModifiedSince(firstFile.FilePath, firstFile.LastModified)));
|
|| _fileService.HasFileBeenModifiedSince(firstFile.FilePath, firstFile.LastModified)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Has the file been modified since last scan or is user forcing an update
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lastScan"></param>
|
||||||
|
/// <param name="forceUpdate"></param>
|
||||||
|
/// <param name="firstFile"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile firstFile)
|
||||||
|
{
|
||||||
|
if (firstFile == null) return false;
|
||||||
|
if (forceUpdate) return true;
|
||||||
|
return _fileService.HasFileBeenModifiedSince(firstFile.FilePath, lastScan)
|
||||||
|
|| _fileService.HasFileBeenModifiedSince(firstFile.FilePath, firstFile.LastModified);
|
||||||
|
// return firstFile != null &&
|
||||||
|
// (!forceUpdate &&
|
||||||
|
// !(_fileService.HasFileBeenModifiedSince(firstFile.FilePath, lastScan)
|
||||||
|
// || _fileService.HasFileBeenModifiedSince(firstFile.FilePath, firstFile.LastModified)));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if a given coverImage path exists
|
/// Determines if a given coverImage path exists
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -48,6 +48,15 @@ namespace API.Helpers.Converters
|
|||||||
case ServerSettingKey.InstallVersion:
|
case ServerSettingKey.InstallVersion:
|
||||||
destination.InstallVersion = row.Value;
|
destination.InstallVersion = row.Value;
|
||||||
break;
|
break;
|
||||||
|
case ServerSettingKey.ConvertBookmarkToWebP:
|
||||||
|
destination.ConvertBookmarkToWebP = bool.Parse(row.Value);
|
||||||
|
break;
|
||||||
|
case ServerSettingKey.EnableSwaggerUi:
|
||||||
|
destination.EnableSwaggerUi = bool.Parse(row.Value);
|
||||||
|
break;
|
||||||
|
case ServerSettingKey.TotalBackups:
|
||||||
|
destination.TotalBackups = int.Parse(row.Value);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,14 +2,17 @@
|
|||||||
{
|
{
|
||||||
public class UserParams
|
public class UserParams
|
||||||
{
|
{
|
||||||
private const int MaxPageSize = 50;
|
private const int MaxPageSize = int.MaxValue;
|
||||||
public int PageNumber { get; set; } = 1;
|
public int PageNumber { get; init; } = 1;
|
||||||
private int _pageSize = 30;
|
private readonly int _pageSize = MaxPageSize;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If set to 0, will set as MaxInt
|
||||||
|
/// </summary>
|
||||||
public int PageSize
|
public int PageSize
|
||||||
{
|
{
|
||||||
get => _pageSize;
|
get => _pageSize;
|
||||||
set => _pageSize = (value > MaxPageSize) ? MaxPageSize : value;
|
init => _pageSize = (value == 0) ? MaxPageSize : value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ public class DefaultParser
|
|||||||
if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.DefaultVolume && !string.IsNullOrEmpty(isSpecial))
|
if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.DefaultVolume && !string.IsNullOrEmpty(isSpecial))
|
||||||
{
|
{
|
||||||
ret.IsSpecial = true;
|
ret.IsSpecial = true;
|
||||||
ParseFromFallbackFolders(filePath, rootPath, type, ref ret);
|
ParseFromFallbackFolders(filePath, rootPath, type, ref ret); // NOTE: This can cause some complications, we should try to be a bit less aggressive to fallback to folder
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are a special with marker, we need to ensure we use the correct series name. we can do this by falling back to Folder name
|
// If we are a special with marker, we need to ensure we use the correct series name. we can do this by falling back to Folder name
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@ -101,6 +102,34 @@ namespace API.Parser
|
|||||||
new Regex(
|
new Regex(
|
||||||
@"(vol_)(?<Volume>\d+(\.\d)?)",
|
@"(vol_)(?<Volume>\d+(\.\d)?)",
|
||||||
MatchOptions, RegexTimeout),
|
MatchOptions, RegexTimeout),
|
||||||
|
// Chinese Volume: 第n卷 -> Volume n, 第n册 -> Volume n, 幽游白书完全版 第03卷 天下 or 阿衰online 第1册
|
||||||
|
new Regex(
|
||||||
|
@"第(?<Volume>\d+)(卷|册)",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
|
// Chinese Volume: 卷n -> Volume n, 册n -> Volume n
|
||||||
|
new Regex(
|
||||||
|
@"(卷|册)(?<Volume>\d+)",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
|
// Korean Volume: 제n권 -> Volume n, n권 -> Volume n, 63권#200.zip -> Volume 63 (no chapter, #200 is just files inside)
|
||||||
|
new Regex(
|
||||||
|
@"제?(?<Volume>\d+)권",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
|
// Korean Season: 시즌n -> Season n,
|
||||||
|
new Regex(
|
||||||
|
@"시즌(?<Volume>\d+\-?\d+)",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
|
// Korean Season: 시즌n -> Season n, n시즌 -> season n
|
||||||
|
new Regex(
|
||||||
|
@"(?<Volume>\d+(\-|~)?\d+?)시즌",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
|
// Korean Season: 시즌n -> Season n, n시즌 -> season n
|
||||||
|
new Regex(
|
||||||
|
@"시즌(?<Volume>\d+(\-|~)?\d+?)",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
|
// Japanese Volume: n巻 -> Volume n
|
||||||
|
new Regex(
|
||||||
|
@"(?<Volume>\d+(?:(\-)\d+)?)巻",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly Regex[] MangaSeriesRegex = new[]
|
private static readonly Regex[] MangaSeriesRegex = new[]
|
||||||
@ -331,6 +360,22 @@ namespace API.Parser
|
|||||||
new Regex(
|
new Regex(
|
||||||
@"^(?<Series>.+?)(?:\s|_)(v|vol|tome|t)\.?(\s|_)?(?<Volume>\d+)",
|
@"^(?<Series>.+?)(?:\s|_)(v|vol|tome|t)\.?(\s|_)?(?<Volume>\d+)",
|
||||||
MatchOptions, RegexTimeout),
|
MatchOptions, RegexTimeout),
|
||||||
|
// Chinese Volume: 第n卷 -> Volume n, 第n册 -> Volume n, 幽游白书完全版 第03卷 天下 or 阿衰online 第1册
|
||||||
|
new Regex(
|
||||||
|
@"第(?<Volume>\d+)(卷|册)",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
|
// Chinese Volume: 卷n -> Volume n, 册n -> Volume n
|
||||||
|
new Regex(
|
||||||
|
@"(卷|册)(?<Volume>\d+)",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
|
// Korean Volume: 제n권 -> Volume n, n권 -> Volume n, 63권#200.zip
|
||||||
|
new Regex(
|
||||||
|
@"제?(?<Volume>\d+)권",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
|
// Japanese Volume: n巻 -> Volume n
|
||||||
|
new Regex(
|
||||||
|
@"(?<Volume>\d+(?:(\-)\d+)?)巻",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly Regex[] ComicChapterRegex = new[]
|
private static readonly Regex[] ComicChapterRegex = new[]
|
||||||
@ -389,11 +434,7 @@ namespace API.Parser
|
|||||||
new Regex(
|
new Regex(
|
||||||
@"^(?<Series>.+?)-(chapter-)?(?<Chapter>\d+)",
|
@"^(?<Series>.+?)-(chapter-)?(?<Chapter>\d+)",
|
||||||
MatchOptions, RegexTimeout),
|
MatchOptions, RegexTimeout),
|
||||||
// Cyberpunk 2077 - Your Voice 01
|
|
||||||
// new Regex(
|
|
||||||
// @"^(?<Series>.+?\s?-\s?(?:.+?))(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)$",
|
|
||||||
// MatchOptions,
|
|
||||||
// RegexTimeout),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly Regex[] ReleaseGroupRegex = new[]
|
private static readonly Regex[] ReleaseGroupRegex = new[]
|
||||||
@ -448,7 +489,18 @@ namespace API.Parser
|
|||||||
new Regex(
|
new Regex(
|
||||||
@"(?<Volume>((vol|volume|v))?(\s|_)?\.?\d+)(\s|_)(Chp|Chapter)\.?(\s|_)?(?<Chapter>\d+)",
|
@"(?<Volume>((vol|volume|v))?(\s|_)?\.?\d+)(\s|_)(Chp|Chapter)\.?(\s|_)?(?<Chapter>\d+)",
|
||||||
MatchOptions, RegexTimeout),
|
MatchOptions, RegexTimeout),
|
||||||
|
// Chinese Chapter: 第n话 -> Chapter n, 【TFO汉化&Petit汉化】迷你偶像漫画第25话
|
||||||
|
new Regex(
|
||||||
|
@"第(?<Chapter>\d+)话",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
|
// Korean Chapter: 제n화 -> Chapter n, 가디언즈 오브 갤럭시 죽음의 보석.E0008.7화#44
|
||||||
|
new Regex(
|
||||||
|
@"제?(?<Chapter>\d+\.?\d+)(화|장)",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
|
// Korean Chapter: 第10話 -> Chapter n, [ハレム]ナナとカオル ~高校生のSMごっこ~ 第1話
|
||||||
|
new Regex(
|
||||||
|
@"第?(?<Chapter>\d+(?:.\d+|-\d+)?)話",
|
||||||
|
MatchOptions, RegexTimeout),
|
||||||
};
|
};
|
||||||
private static readonly Regex[] MangaEditionRegex = {
|
private static readonly Regex[] MangaEditionRegex = {
|
||||||
// Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz
|
// Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz
|
||||||
@ -512,6 +564,13 @@ namespace API.Parser
|
|||||||
MatchOptions, RegexTimeout
|
MatchOptions, RegexTimeout
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private static readonly ImmutableArray<string> FormatTagSpecialKeywords = ImmutableArray.Create(
|
||||||
|
"Special", "Reference", "Director's Cut", "Box Set", "Box-Set", "Annual", "Anthology", "Epilogue",
|
||||||
|
"One Shot", "One-Shot", "Prologue", "TPB", "Trade Paper Back", "Omnibus", "Compendium", "Absolute", "Graphic Novel",
|
||||||
|
"GN", "FCBD");
|
||||||
|
|
||||||
|
private static readonly char[] LeadingZeroesTrimChars = new[] { '0' };
|
||||||
|
|
||||||
public static MangaFormat ParseFormat(string filePath)
|
public static MangaFormat ParseFormat(string filePath)
|
||||||
{
|
{
|
||||||
if (IsArchive(filePath)) return MangaFormat.Archive;
|
if (IsArchive(filePath)) return MangaFormat.Archive;
|
||||||
@ -526,15 +585,13 @@ namespace API.Parser
|
|||||||
foreach (var regex in MangaEditionRegex)
|
foreach (var regex in MangaEditionRegex)
|
||||||
{
|
{
|
||||||
var matches = regex.Matches(filePath);
|
var matches = regex.Matches(filePath);
|
||||||
foreach (Match match in matches)
|
foreach (var group in matches.Select(match => match.Groups["Edition"])
|
||||||
|
.Where(group => group.Success && group != Match.Empty))
|
||||||
{
|
{
|
||||||
if (match.Groups["Edition"].Success && match.Groups["Edition"].Value != string.Empty)
|
return group.Value
|
||||||
{
|
.Replace("{", "").Replace("}", "")
|
||||||
var edition = match.Groups["Edition"].Value.Replace("{", "").Replace("}", "")
|
.Replace("[", "").Replace("]", "")
|
||||||
.Replace("[", "").Replace("]", "").Replace("(", "").Replace(")", "");
|
.Replace("(", "").Replace(")", "");
|
||||||
|
|
||||||
return edition;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -549,15 +606,8 @@ namespace API.Parser
|
|||||||
public static bool HasSpecialMarker(string filePath)
|
public static bool HasSpecialMarker(string filePath)
|
||||||
{
|
{
|
||||||
var matches = SpecialMarkerRegex.Matches(filePath);
|
var matches = SpecialMarkerRegex.Matches(filePath);
|
||||||
foreach (Match match in matches)
|
return matches.Select(match => match.Groups["Special"])
|
||||||
{
|
.Any(group => group.Success && group != Match.Empty);
|
||||||
if (match.Groups["Special"].Success && match.Groups["Special"].Value != string.Empty)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ParseMangaSpecial(string filePath)
|
public static string ParseMangaSpecial(string filePath)
|
||||||
@ -565,12 +615,10 @@ namespace API.Parser
|
|||||||
foreach (var regex in MangaSpecialRegex)
|
foreach (var regex in MangaSpecialRegex)
|
||||||
{
|
{
|
||||||
var matches = regex.Matches(filePath);
|
var matches = regex.Matches(filePath);
|
||||||
foreach (Match match in matches)
|
foreach (var group in matches.Select(match => match.Groups["Special"])
|
||||||
|
.Where(group => group.Success && group != Match.Empty))
|
||||||
{
|
{
|
||||||
if (match.Groups["Special"].Success && match.Groups["Special"].Value != string.Empty)
|
return group.Value;
|
||||||
{
|
|
||||||
return match.Groups["Special"].Value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -582,12 +630,10 @@ namespace API.Parser
|
|||||||
foreach (var regex in ComicSpecialRegex)
|
foreach (var regex in ComicSpecialRegex)
|
||||||
{
|
{
|
||||||
var matches = regex.Matches(filePath);
|
var matches = regex.Matches(filePath);
|
||||||
foreach (Match match in matches)
|
foreach (var group in matches.Select(match => match.Groups["Special"])
|
||||||
|
.Where(group => group.Success && group != Match.Empty))
|
||||||
{
|
{
|
||||||
if (match.Groups["Special"].Success && match.Groups["Special"].Value != string.Empty)
|
return group.Value;
|
||||||
{
|
|
||||||
return match.Groups["Special"].Value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -599,12 +645,10 @@ namespace API.Parser
|
|||||||
foreach (var regex in MangaSeriesRegex)
|
foreach (var regex in MangaSeriesRegex)
|
||||||
{
|
{
|
||||||
var matches = regex.Matches(filename);
|
var matches = regex.Matches(filename);
|
||||||
foreach (Match match in matches)
|
foreach (var group in matches.Select(match => match.Groups["Series"])
|
||||||
|
.Where(group => group.Success && group != Match.Empty))
|
||||||
{
|
{
|
||||||
if (match.Groups["Series"].Success && match.Groups["Series"].Value != string.Empty)
|
return CleanTitle(group.Value);
|
||||||
{
|
|
||||||
return CleanTitle(match.Groups["Series"].Value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -615,12 +659,10 @@ namespace API.Parser
|
|||||||
foreach (var regex in ComicSeriesRegex)
|
foreach (var regex in ComicSeriesRegex)
|
||||||
{
|
{
|
||||||
var matches = regex.Matches(filename);
|
var matches = regex.Matches(filename);
|
||||||
foreach (Match match in matches)
|
foreach (var group in matches.Select(match => match.Groups["Series"])
|
||||||
|
.Where(group => group.Success && group != Match.Empty))
|
||||||
{
|
{
|
||||||
if (match.Groups["Series"].Success && match.Groups["Series"].Value != string.Empty)
|
return CleanTitle(group.Value, true);
|
||||||
{
|
|
||||||
return CleanTitle(match.Groups["Series"].Value, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -650,12 +692,12 @@ namespace API.Parser
|
|||||||
foreach (var regex in ComicVolumeRegex)
|
foreach (var regex in ComicVolumeRegex)
|
||||||
{
|
{
|
||||||
var matches = regex.Matches(filename);
|
var matches = regex.Matches(filename);
|
||||||
foreach (Match match in matches)
|
foreach (var group in matches.Select(match => match.Groups))
|
||||||
{
|
{
|
||||||
if (!match.Groups["Volume"].Success || match.Groups["Volume"] == Match.Empty) continue;
|
if (!group["Volume"].Success || group["Volume"] == Match.Empty) continue;
|
||||||
|
|
||||||
var value = match.Groups["Volume"].Value;
|
var value = group["Volume"].Value;
|
||||||
var hasPart = match.Groups["Part"].Success;
|
var hasPart = group["Part"].Success;
|
||||||
return FormatValue(value, hasPart);
|
return FormatValue(value, hasPart);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -761,12 +803,9 @@ namespace API.Parser
|
|||||||
foreach (var regex in MangaSpecialRegex)
|
foreach (var regex in MangaSpecialRegex)
|
||||||
{
|
{
|
||||||
var matches = regex.Matches(title);
|
var matches = regex.Matches(title);
|
||||||
foreach (Match match in matches)
|
foreach (var match in matches.Where(m => m.Success))
|
||||||
{
|
{
|
||||||
if (match.Success)
|
title = title.Replace(match.Value, string.Empty).Trim();
|
||||||
{
|
|
||||||
title = title.Replace(match.Value, string.Empty).Trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -778,12 +817,9 @@ namespace API.Parser
|
|||||||
foreach (var regex in EuropeanComicRegex)
|
foreach (var regex in EuropeanComicRegex)
|
||||||
{
|
{
|
||||||
var matches = regex.Matches(title);
|
var matches = regex.Matches(title);
|
||||||
foreach (Match match in matches)
|
foreach (var match in matches.Where(m => m.Success))
|
||||||
{
|
{
|
||||||
if (match.Success)
|
title = title.Replace(match.Value, string.Empty).Trim();
|
||||||
{
|
|
||||||
title = title.Replace(match.Value, string.Empty).Trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -795,12 +831,9 @@ namespace API.Parser
|
|||||||
foreach (var regex in ComicSpecialRegex)
|
foreach (var regex in ComicSpecialRegex)
|
||||||
{
|
{
|
||||||
var matches = regex.Matches(title);
|
var matches = regex.Matches(title);
|
||||||
foreach (Match match in matches)
|
foreach (var match in matches.Where(m => m.Success))
|
||||||
{
|
{
|
||||||
if (match.Success)
|
title = title.Replace(match.Value, string.Empty).Trim();
|
||||||
{
|
|
||||||
title = title.Replace(match.Value, string.Empty).Trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -858,12 +891,9 @@ namespace API.Parser
|
|||||||
foreach (var regex in ReleaseGroupRegex)
|
foreach (var regex in ReleaseGroupRegex)
|
||||||
{
|
{
|
||||||
var matches = regex.Matches(title);
|
var matches = regex.Matches(title);
|
||||||
foreach (Match match in matches)
|
foreach (var match in matches.Where(m => m.Success))
|
||||||
{
|
{
|
||||||
if (match.Success)
|
title = title.Replace(match.Value, string.Empty);
|
||||||
{
|
|
||||||
title = title.Replace(match.Value, string.Empty);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -898,8 +928,8 @@ namespace API.Parser
|
|||||||
|
|
||||||
public static string RemoveLeadingZeroes(string title)
|
public static string RemoveLeadingZeroes(string title)
|
||||||
{
|
{
|
||||||
var ret = title.TrimStart(new[] { '0' });
|
var ret = title.TrimStart(LeadingZeroesTrimChars);
|
||||||
return ret == string.Empty ? "0" : ret;
|
return string.IsNullOrEmpty(ret) ? "0" : ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsArchive(string filePath)
|
public static bool IsArchive(string filePath)
|
||||||
@ -1034,5 +1064,15 @@ namespace API.Parser
|
|||||||
{
|
{
|
||||||
return path.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
return path.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks against a set of strings to validate if a ComicInfo.Format should receive special treatment
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="comicInfoFormat"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool HasComicInfoSpecial(string comicInfoFormat)
|
||||||
|
{
|
||||||
|
return FormatTagSpecialKeywords.Contains(comicInfoFormat);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -249,7 +249,7 @@ namespace API.Services
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given an archive stream, will assess whether directory needs to be flattened so that the extracted archive files are directly
|
/// Given an archive stream, will assess whether directory needs to be flattened so that the extracted archive files are directly
|
||||||
/// under extract path and not nested in subfolders. See <see cref="DirectoryInfoExtensions"/> Flatten method.
|
/// under extract path and not nested in subfolders. See <see cref="DirectoryService"/> Flatten method.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="archive">An opened archive stream</param>
|
/// <param name="archive">An opened archive stream</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@ -412,7 +412,6 @@ namespace API.Services
|
|||||||
|
|
||||||
private void ExtractArchiveEntries(ZipArchive archive, string extractPath)
|
private void ExtractArchiveEntries(ZipArchive archive, string extractPath)
|
||||||
{
|
{
|
||||||
// TODO: In cases where we try to extract, but there are InvalidPathChars, we need to inform the user (throw exception, let middleware inform user)
|
|
||||||
var needsFlattening = ArchiveNeedsFlattening(archive);
|
var needsFlattening = ArchiveNeedsFlattening(archive);
|
||||||
if (!archive.HasFiles() && !needsFlattening) return;
|
if (!archive.HasFiles() && !needsFlattening) return;
|
||||||
|
|
||||||
@ -476,7 +475,8 @@ namespace API.Services
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(e, "[ExtractArchive] There was a problem extracting {ArchivePath} to {ExtractPath}",archivePath, extractPath);
|
_logger.LogWarning(e, "[ExtractArchive] There was a problem extracting {ArchivePath} to {ExtractPath}",archivePath, extractPath);
|
||||||
return;
|
throw new KavitaException(
|
||||||
|
$"There was an error when extracting {archivePath}. Check the file exists, has read permissions or the server OS can support all path characters.");
|
||||||
}
|
}
|
||||||
_logger.LogDebug("Extracted archive to {ExtractPath} in {ElapsedMilliseconds} milliseconds", extractPath, sw.ElapsedMilliseconds);
|
_logger.LogDebug("Extracted archive to {ExtractPath} in {ElapsedMilliseconds} milliseconds", extractPath, sw.ElapsedMilliseconds);
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ namespace API.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fileFilePath"></param>
|
/// <param name="fileFilePath"></param>
|
||||||
/// <param name="targetDirectory">Where the files will be extracted to. If doesn't exist, will be created.</param>
|
/// <param name="targetDirectory">Where the files will be extracted to. If doesn't exist, will be created.</param>
|
||||||
|
[Obsolete("This method of reading is no longer supported. Please use native pdf reader")]
|
||||||
void ExtractPdfImages(string fileFilePath, string targetDirectory);
|
void ExtractPdfImages(string fileFilePath, string targetDirectory);
|
||||||
|
|
||||||
Task<string> ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary<string, int> mappings, int page);
|
Task<string> ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary<string, int> mappings, int page);
|
||||||
@ -246,12 +247,16 @@ namespace API.Services
|
|||||||
private static void ScopeImages(HtmlDocument doc, EpubBookRef book, string apiBase)
|
private static void ScopeImages(HtmlDocument doc, EpubBookRef book, string apiBase)
|
||||||
{
|
{
|
||||||
var images = doc.DocumentNode.SelectNodes("//img")
|
var images = doc.DocumentNode.SelectNodes("//img")
|
||||||
?? doc.DocumentNode.SelectNodes("//image");
|
?? doc.DocumentNode.SelectNodes("//image") ?? doc.DocumentNode.SelectNodes("//svg");
|
||||||
|
|
||||||
if (images == null) return;
|
if (images == null) return;
|
||||||
|
|
||||||
|
|
||||||
|
var parent = images.First().ParentNode;
|
||||||
|
|
||||||
foreach (var image in images)
|
foreach (var image in images)
|
||||||
{
|
{
|
||||||
|
|
||||||
string key = null;
|
string key = null;
|
||||||
if (image.Attributes["src"] != null)
|
if (image.Attributes["src"] != null)
|
||||||
{
|
{
|
||||||
@ -269,6 +274,7 @@ namespace API.Services
|
|||||||
image.Attributes.Add(key, $"{apiBase}" + imageFile);
|
image.Attributes.Add(key, $"{apiBase}" + imageFile);
|
||||||
|
|
||||||
// Add a custom class that the reader uses to ensure images stay within reader
|
// Add a custom class that the reader uses to ensure images stay within reader
|
||||||
|
parent.AddClass("kavita-scale-width-container");
|
||||||
image.AddClass("kavita-scale-width");
|
image.AddClass("kavita-scale-width");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -579,8 +585,7 @@ namespace API.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(series) && !string.IsNullOrEmpty(seriesIndex) &&
|
if (!string.IsNullOrEmpty(series) && !string.IsNullOrEmpty(seriesIndex))
|
||||||
(!string.IsNullOrEmpty(specialName) || groupPosition.Equals("series") || groupPosition.Equals("set")))
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(specialName))
|
if (string.IsNullOrEmpty(specialName))
|
||||||
{
|
{
|
||||||
@ -600,7 +605,7 @@ namespace API.Services
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Don't set titleSort if the book belongs to a group
|
// Don't set titleSort if the book belongs to a group
|
||||||
if (!string.IsNullOrEmpty(titleSort) && string.IsNullOrEmpty(seriesIndex))
|
if (!string.IsNullOrEmpty(titleSort) && string.IsNullOrEmpty(seriesIndex) && (groupPosition.Equals("series") || groupPosition.Equals("set")))
|
||||||
{
|
{
|
||||||
info.SeriesSort = titleSort;
|
info.SeriesSort = titleSort;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ using API.DTOs.Reader;
|
|||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
|
using Hangfire;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Services;
|
namespace API.Services;
|
||||||
@ -18,6 +19,8 @@ public interface IBookmarkService
|
|||||||
Task<bool> BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark);
|
Task<bool> BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark);
|
||||||
Task<bool> RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto);
|
Task<bool> RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto);
|
||||||
Task<IEnumerable<string>> GetBookmarkFilesById(IEnumerable<int> bookmarkIds);
|
Task<IEnumerable<string>> GetBookmarkFilesById(IEnumerable<int> bookmarkIds);
|
||||||
|
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||||
|
Task ConvertAllBookmarkToWebP();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,12 +29,17 @@ public class BookmarkService : IBookmarkService
|
|||||||
private readonly ILogger<BookmarkService> _logger;
|
private readonly ILogger<BookmarkService> _logger;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
|
private readonly IImageService _imageService;
|
||||||
|
private readonly IEventHub _eventHub;
|
||||||
|
|
||||||
public BookmarkService(ILogger<BookmarkService> logger, IUnitOfWork unitOfWork, IDirectoryService directoryService)
|
public BookmarkService(ILogger<BookmarkService> logger, IUnitOfWork unitOfWork,
|
||||||
|
IDirectoryService directoryService, IImageService imageService, IEventHub eventHub)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
|
_imageService = imageService;
|
||||||
|
_eventHub = eventHub;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -87,18 +95,28 @@ public class BookmarkService : IBookmarkService
|
|||||||
var targetFolderStem = BookmarkStem(userWithBookmarks.Id, bookmarkDto.SeriesId, bookmarkDto.ChapterId);
|
var targetFolderStem = BookmarkStem(userWithBookmarks.Id, bookmarkDto.SeriesId, bookmarkDto.ChapterId);
|
||||||
var targetFilepath = Path.Join(bookmarkDirectory, targetFolderStem);
|
var targetFilepath = Path.Join(bookmarkDirectory, targetFolderStem);
|
||||||
|
|
||||||
userWithBookmarks.Bookmarks ??= new List<AppUserBookmark>();
|
var bookmark = new AppUserBookmark()
|
||||||
userWithBookmarks.Bookmarks.Add(new AppUserBookmark()
|
|
||||||
{
|
{
|
||||||
Page = bookmarkDto.Page,
|
Page = bookmarkDto.Page,
|
||||||
VolumeId = bookmarkDto.VolumeId,
|
VolumeId = bookmarkDto.VolumeId,
|
||||||
SeriesId = bookmarkDto.SeriesId,
|
SeriesId = bookmarkDto.SeriesId,
|
||||||
ChapterId = bookmarkDto.ChapterId,
|
ChapterId = bookmarkDto.ChapterId,
|
||||||
FileName = Path.Join(targetFolderStem, fileInfo.Name)
|
FileName = Path.Join(targetFolderStem, fileInfo.Name)
|
||||||
});
|
};
|
||||||
|
|
||||||
_directoryService.CopyFileToDirectory(imageToBookmark, targetFilepath);
|
_directoryService.CopyFileToDirectory(imageToBookmark, targetFilepath);
|
||||||
|
userWithBookmarks.Bookmarks ??= new List<AppUserBookmark>();
|
||||||
|
userWithBookmarks.Bookmarks.Add(bookmark);
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(userWithBookmarks);
|
_unitOfWork.UserRepository.Update(userWithBookmarks);
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
var convertToWebP = bool.Parse((await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.ConvertBookmarkToWebP)).Value);
|
||||||
|
if (convertToWebP)
|
||||||
|
{
|
||||||
|
// Enqueue a task to convert the bookmark to webP
|
||||||
|
BackgroundJob.Enqueue(() => ConvertBookmarkToWebP(bookmark.Id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -153,6 +171,94 @@ public class BookmarkService : IBookmarkService
|
|||||||
b.FileName)));
|
b.FileName)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is a long-running job that will convert all bookmarks into WebP. Do not invoke anyway except via Hangfire.
|
||||||
|
/// </summary>
|
||||||
|
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||||
|
public async Task ConvertAllBookmarkToWebP()
|
||||||
|
{
|
||||||
|
var bookmarkDirectory =
|
||||||
|
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||||
|
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
|
MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started));
|
||||||
|
var bookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync())
|
||||||
|
.Where(b => !b.FileName.EndsWith(".webp")).ToList();
|
||||||
|
|
||||||
|
var count = 1F;
|
||||||
|
foreach (var bookmark in bookmarks)
|
||||||
|
{
|
||||||
|
await SaveBookmarkAsWebP(bookmarkDirectory, bookmark);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
|
MessageFactory.ConvertBookmarksProgressEvent(count / bookmarks.Count, ProgressEventType.Started));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
|
MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended));
|
||||||
|
|
||||||
|
_logger.LogInformation("[BookmarkService] Converted bookmarks to WebP");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is a job that runs after a bookmark is saved
|
||||||
|
/// </summary>
|
||||||
|
public async Task ConvertBookmarkToWebP(int bookmarkId)
|
||||||
|
{
|
||||||
|
var bookmarkDirectory =
|
||||||
|
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||||
|
var convertBookmarkToWebP =
|
||||||
|
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertBookmarkToWebP;
|
||||||
|
|
||||||
|
if (!convertBookmarkToWebP) return;
|
||||||
|
|
||||||
|
// Validate the bookmark still exists
|
||||||
|
var bookmark = await _unitOfWork.UserRepository.GetBookmarkAsync(bookmarkId);
|
||||||
|
if (bookmark == null) return;
|
||||||
|
|
||||||
|
await SaveBookmarkAsWebP(bookmarkDirectory, bookmark);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts bookmark file, deletes original, marks bookmark as dirty. Does not commit.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bookmarkDirectory"></param>
|
||||||
|
/// <param name="bookmark"></param>
|
||||||
|
private async Task SaveBookmarkAsWebP(string bookmarkDirectory, AppUserBookmark bookmark)
|
||||||
|
{
|
||||||
|
var fullSourcePath = _directoryService.FileSystem.Path.Join(bookmarkDirectory, bookmark.FileName);
|
||||||
|
var fullTargetDirectory = fullSourcePath.Replace(new FileInfo(bookmark.FileName).Name, string.Empty);
|
||||||
|
var targetFolderStem = BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId);
|
||||||
|
|
||||||
|
_logger.LogDebug("Converting {Source} bookmark into WebP at {Target}", fullSourcePath, fullTargetDirectory);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Convert target file to webp then delete original target file and update bookmark
|
||||||
|
|
||||||
|
var originalFile = bookmark.FileName;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var targetFile = await _imageService.ConvertToWebP(fullSourcePath, fullTargetDirectory);
|
||||||
|
var targetName = new FileInfo(targetFile).Name;
|
||||||
|
bookmark.FileName = Path.Join(targetFolderStem, targetName);
|
||||||
|
_directoryService.DeleteFiles(new[] {fullSourcePath});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Could not convert file {FilePath}", bookmark.FileName);
|
||||||
|
bookmark.FileName = originalFile;
|
||||||
|
}
|
||||||
|
_unitOfWork.UserRepository.Update(bookmark);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Could not convert bookmark to WebP");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string BookmarkStem(int userId, int seriesId, int chapterId)
|
private static string BookmarkStem(int userId, int seriesId, int chapterId)
|
||||||
{
|
{
|
||||||
return Path.Join($"{userId}", $"{seriesId}", $"{chapterId}");
|
return Path.Join($"{userId}", $"{seriesId}", $"{chapterId}");
|
||||||
|
@ -29,10 +29,10 @@ namespace API.Services
|
|||||||
void CleanupBookmarks(IEnumerable<int> seriesIds);
|
void CleanupBookmarks(IEnumerable<int> seriesIds);
|
||||||
string GetCachedPagePath(Chapter chapter, int page);
|
string GetCachedPagePath(Chapter chapter, int page);
|
||||||
string GetCachedBookmarkPagePath(int seriesId, int page);
|
string GetCachedBookmarkPagePath(int seriesId, int page);
|
||||||
string GetCachedEpubFile(int chapterId, Chapter chapter);
|
string GetCachedFile(Chapter chapter);
|
||||||
public void ExtractChapterFiles(string extractPath, IReadOnlyList<MangaFile> files);
|
public void ExtractChapterFiles(string extractPath, IReadOnlyList<MangaFile> files);
|
||||||
Task<int> CacheBookmarkForSeries(int userId, int seriesId);
|
Task<int> CacheBookmarkForSeries(int userId, int seriesId);
|
||||||
void CleanupBookmarkCache(int bookmarkDtoSeriesId);
|
void CleanupBookmarkCache(int seriesId);
|
||||||
}
|
}
|
||||||
public class CacheService : ICacheService
|
public class CacheService : ICacheService
|
||||||
{
|
{
|
||||||
@ -73,14 +73,13 @@ namespace API.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the full path to the cached epub file. If the file does not exist, will fallback to the original.
|
/// Returns the full path to the cached file. If the file does not exist, will fallback to the original.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="chapterId"></param>
|
|
||||||
/// <param name="chapter"></param>
|
/// <param name="chapter"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public string GetCachedEpubFile(int chapterId, Chapter chapter)
|
public string GetCachedFile(Chapter chapter)
|
||||||
{
|
{
|
||||||
var extractPath = GetCachePath(chapterId);
|
var extractPath = GetCachePath(chapter.Id);
|
||||||
var path = Path.Join(extractPath, _directoryService.FileSystem.Path.GetFileName(chapter.Files.First().FilePath));
|
var path = Path.Join(extractPath, _directoryService.FileSystem.Path.GetFileName(chapter.Files.First().FilePath));
|
||||||
if (!(_directoryService.FileSystem.FileInfo.FromFileName(path).Exists))
|
if (!(_directoryService.FileSystem.FileInfo.FromFileName(path).Exists))
|
||||||
{
|
{
|
||||||
@ -89,6 +88,7 @@ namespace API.Services
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Caches the files for the given chapter to CacheDirectory
|
/// Caches the files for the given chapter to CacheDirectory
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -136,25 +136,25 @@ namespace API.Services
|
|||||||
extraPath = file.Id + string.Empty;
|
extraPath = file.Id + string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.Format == MangaFormat.Archive)
|
switch (file.Format)
|
||||||
{
|
{
|
||||||
_readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format);
|
case MangaFormat.Archive:
|
||||||
}
|
_readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format);
|
||||||
else if (file.Format == MangaFormat.Pdf)
|
break;
|
||||||
{
|
case MangaFormat.Epub:
|
||||||
_readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format);
|
case MangaFormat.Pdf:
|
||||||
}
|
|
||||||
else if (file.Format == MangaFormat.Epub)
|
|
||||||
{
|
|
||||||
removeNonImages = false;
|
|
||||||
if (!_directoryService.FileSystem.File.Exists(files[0].FilePath))
|
|
||||||
{
|
{
|
||||||
_logger.LogError("{Archive} does not exist on disk", files[0].FilePath);
|
removeNonImages = false;
|
||||||
throw new KavitaException($"{files[0].FilePath} does not exist on disk");
|
if (!_directoryService.FileSystem.File.Exists(files[0].FilePath))
|
||||||
}
|
{
|
||||||
|
_logger.LogError("{File} does not exist on disk", files[0].FilePath);
|
||||||
|
throw new KavitaException($"{files[0].FilePath} does not exist on disk");
|
||||||
|
}
|
||||||
|
|
||||||
_directoryService.ExistOrCreate(extractPath);
|
_directoryService.ExistOrCreate(extractPath);
|
||||||
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ using System.IO.Abstractions;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs.System;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@ -29,7 +31,7 @@ namespace API.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="rootPath">Absolute path of directory to scan.</param>
|
/// <param name="rootPath">Absolute path of directory to scan.</param>
|
||||||
/// <returns>List of folder names</returns>
|
/// <returns>List of folder names</returns>
|
||||||
IEnumerable<string> ListDirectory(string rootPath);
|
IEnumerable<DirectoryDto> ListDirectory(string rootPath);
|
||||||
Task<byte[]> ReadFileAsync(string path);
|
Task<byte[]> ReadFileAsync(string path);
|
||||||
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "");
|
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "");
|
||||||
bool Exists(string directory);
|
bool Exists(string directory);
|
||||||
@ -434,14 +436,18 @@ namespace API.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="rootPath"></param>
|
/// <param name="rootPath"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public IEnumerable<string> ListDirectory(string rootPath)
|
public IEnumerable<DirectoryDto> ListDirectory(string rootPath)
|
||||||
{
|
{
|
||||||
if (!FileSystem.Directory.Exists(rootPath)) return ImmutableList<string>.Empty;
|
if (!FileSystem.Directory.Exists(rootPath)) return ImmutableList<DirectoryDto>.Empty;
|
||||||
|
|
||||||
var di = FileSystem.DirectoryInfo.FromDirectoryName(rootPath);
|
var di = FileSystem.DirectoryInfo.FromDirectoryName(rootPath);
|
||||||
var dirs = di.GetDirectories()
|
var dirs = di.GetDirectories()
|
||||||
.Where(dir => !(dir.Attributes.HasFlag(FileAttributes.Hidden) || dir.Attributes.HasFlag(FileAttributes.System)))
|
.Where(dir => !(dir.Attributes.HasFlag(FileAttributes.Hidden) || dir.Attributes.HasFlag(FileAttributes.System)))
|
||||||
.Select(d => d.Name).ToImmutableList();
|
.Select(d => new DirectoryDto()
|
||||||
|
{
|
||||||
|
Name = d.Name,
|
||||||
|
FullPath = d.FullName,
|
||||||
|
}).ToImmutableList();
|
||||||
|
|
||||||
return dirs;
|
return dirs;
|
||||||
}
|
}
|
||||||
@ -724,7 +730,7 @@ namespace API.Services
|
|||||||
FileSystem.Path.Join(directoryName, "test.txt"),
|
FileSystem.Path.Join(directoryName, "test.txt"),
|
||||||
string.Empty);
|
string.Empty);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
ClearAndDeleteDirectory(directoryName);
|
ClearAndDeleteDirectory(directoryName);
|
||||||
return false;
|
return false;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NetVips;
|
using SixLabors.ImageSharp;
|
||||||
|
using Image = NetVips.Image;
|
||||||
|
|
||||||
namespace API.Services;
|
namespace API.Services;
|
||||||
|
|
||||||
@ -19,6 +21,12 @@ public interface IImageService
|
|||||||
string CreateThumbnailFromBase64(string encodedImage, string fileName);
|
string CreateThumbnailFromBase64(string encodedImage, string fileName);
|
||||||
|
|
||||||
string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory);
|
string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory);
|
||||||
|
/// <summary>
|
||||||
|
/// Converts the passed image to webP and outputs it in the same directory
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">Full path to the image to convert</param>
|
||||||
|
/// <returns>File of written webp image</returns>
|
||||||
|
Task<string> ConvertToWebP(string filePath, string outputPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ImageService : IImageService
|
public class ImageService : IImageService
|
||||||
@ -42,7 +50,7 @@ public class ImageService : IImageService
|
|||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ExtractImages(string fileFilePath, string targetDirectory, int fileCount)
|
public void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1)
|
||||||
{
|
{
|
||||||
_directoryService.ExistOrCreate(targetDirectory);
|
_directoryService.ExistOrCreate(targetDirectory);
|
||||||
if (fileCount == 1)
|
if (fileCount == 1)
|
||||||
@ -95,6 +103,18 @@ public class ImageService : IImageService
|
|||||||
return filename;
|
return filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> ConvertToWebP(string filePath, string outputPath)
|
||||||
|
{
|
||||||
|
var file = _directoryService.FileSystem.FileInfo.FromFileName(filePath);
|
||||||
|
var fileName = file.Name.Replace(file.Extension, string.Empty);
|
||||||
|
var outputFile = Path.Join(outputPath, fileName + ".webp");
|
||||||
|
|
||||||
|
|
||||||
|
using var sourceImage = await SixLabors.ImageSharp.Image.LoadAsync(filePath);
|
||||||
|
await sourceImage.SaveAsWebpAsync(outputFile);
|
||||||
|
return outputFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string CreateThumbnailFromBase64(string encodedImage, string fileName)
|
public string CreateThumbnailFromBase64(string encodedImage, string fileName)
|
||||||
|
@ -12,7 +12,9 @@ using API.Entities;
|
|||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
|
using API.Services.Tasks.Metadata;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
|
using Hangfire;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@ -25,13 +27,15 @@ public interface IMetadataService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryId"></param>
|
/// <param name="libraryId"></param>
|
||||||
/// <param name="forceUpdate"></param>
|
/// <param name="forceUpdate"></param>
|
||||||
|
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||||
|
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
Task RefreshMetadata(int libraryId, bool forceUpdate = false);
|
Task RefreshMetadata(int libraryId, bool forceUpdate = false);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs a forced refresh of metadata just for a series and it's nested entities
|
/// Performs a forced refresh of metadata just for a series and it's nested entities
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryId"></param>
|
/// <param name="libraryId"></param>
|
||||||
/// <param name="seriesId"></param>
|
/// <param name="seriesId"></param>
|
||||||
Task RefreshMetadataForSeries(int libraryId, int seriesId, bool forceUpdate = false);
|
Task RefreshMetadataForSeries(int libraryId, int seriesId, bool forceUpdate = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MetadataService : IMetadataService
|
public class MetadataService : IMetadataService
|
||||||
@ -194,6 +198,8 @@ public class MetadataService : IMetadataService
|
|||||||
/// <remarks>This can be heavy on memory first run</remarks>
|
/// <remarks>This can be heavy on memory first run</remarks>
|
||||||
/// <param name="libraryId"></param>
|
/// <param name="libraryId"></param>
|
||||||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||||
|
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||||
|
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
public async Task RefreshMetadata(int libraryId, bool forceUpdate = false)
|
public async Task RefreshMetadata(int libraryId, bool forceUpdate = false)
|
||||||
{
|
{
|
||||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
||||||
@ -256,10 +262,10 @@ public class MetadataService : IMetadataService
|
|||||||
|
|
||||||
await RemoveAbandonedMetadataKeys();
|
await RemoveAbandonedMetadataKeys();
|
||||||
|
|
||||||
|
|
||||||
_logger.LogInformation("[MetadataService] Updated metadata for {SeriesNumber} series in library {LibraryName} in {ElapsedMilliseconds} milliseconds total", chunkInfo.TotalSize, library.Name, totalTime);
|
_logger.LogInformation("[MetadataService] Updated metadata for {SeriesNumber} series in library {LibraryName} in {ElapsedMilliseconds} milliseconds total", chunkInfo.TotalSize, library.Name, totalTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task RemoveAbandonedMetadataKeys()
|
private async Task RemoveAbandonedMetadataKeys()
|
||||||
{
|
{
|
||||||
await _unitOfWork.TagRepository.RemoveAllTagNoLongerAssociated();
|
await _unitOfWork.TagRepository.RemoveAllTagNoLongerAssociated();
|
||||||
|
@ -7,6 +7,7 @@ using API.Comparators;
|
|||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.DTOs.Reader;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
@ -28,6 +29,7 @@ public interface IReaderService
|
|||||||
Task<ChapterDto> GetContinuePoint(int seriesId, int userId);
|
Task<ChapterDto> GetContinuePoint(int seriesId, int userId);
|
||||||
Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber);
|
Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber);
|
||||||
Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber);
|
Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber);
|
||||||
|
HourEstimateRangeDto GetTimeEstimate(long wordCount, int pageCount, bool isEpub);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ReaderService : IReaderService
|
public class ReaderService : IReaderService
|
||||||
@ -38,6 +40,14 @@ public class ReaderService : IReaderService
|
|||||||
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||||
|
|
||||||
|
private const float MinWordsPerHour = 10260F;
|
||||||
|
private const float MaxWordsPerHour = 30000F;
|
||||||
|
public const float AvgWordsPerHour = (MaxWordsPerHour + MinWordsPerHour) / 2F;
|
||||||
|
private const float MinPagesPerMinute = 3.33F;
|
||||||
|
private const float MaxPagesPerMinute = 2.75F;
|
||||||
|
public const float AvgPagesPerMinute = (MaxPagesPerMinute + MinPagesPerMinute) / 2F;
|
||||||
|
|
||||||
|
|
||||||
public ReaderService(IUnitOfWork unitOfWork, ILogger<ReaderService> logger, IEventHub eventHub)
|
public ReaderService(IUnitOfWork unitOfWork, ILogger<ReaderService> logger, IEventHub eventHub)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
@ -160,7 +170,7 @@ public class ReaderService : IReaderService
|
|||||||
var progresses = user.Progresses.Where(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id).ToList();
|
var progresses = user.Progresses.Where(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id).ToList();
|
||||||
if (progresses.Count > 1)
|
if (progresses.Count > 1)
|
||||||
{
|
{
|
||||||
user.Progresses = new List<AppUserProgress>()
|
user.Progresses = new List<AppUserProgress>
|
||||||
{
|
{
|
||||||
user.Progresses.First()
|
user.Progresses.First()
|
||||||
};
|
};
|
||||||
@ -320,7 +330,7 @@ public class ReaderService : IReaderService
|
|||||||
{
|
{
|
||||||
var chapterVolume = volumes.FirstOrDefault();
|
var chapterVolume = volumes.FirstOrDefault();
|
||||||
if (chapterVolume?.Number != 0) return -1;
|
if (chapterVolume?.Number != 0) return -1;
|
||||||
var firstChapter = chapterVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer).FirstOrDefault();
|
var firstChapter = chapterVolume.Chapters.MinBy(x => double.Parse(x.Number), _chapterSortComparer);
|
||||||
if (firstChapter == null) return -1;
|
if (firstChapter == null) return -1;
|
||||||
return firstChapter.Id;
|
return firstChapter.Id;
|
||||||
}
|
}
|
||||||
@ -362,17 +372,16 @@ public class ReaderService : IReaderService
|
|||||||
if (volume.Number == currentVolume.Number - 1)
|
if (volume.Number == currentVolume.Number - 1)
|
||||||
{
|
{
|
||||||
if (currentVolume.Number - 1 == 0) break; // If we have walked all the way to chapter volume, then we should break so logic outside can work
|
if (currentVolume.Number - 1 == 0) break; // If we have walked all the way to chapter volume, then we should break so logic outside can work
|
||||||
var lastChapter = volume.Chapters
|
var lastChapter = volume.Chapters.MaxBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
|
||||||
.OrderBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting).LastOrDefault();
|
|
||||||
if (lastChapter == null) return -1;
|
if (lastChapter == null) return -1;
|
||||||
return lastChapter.Id;
|
return lastChapter.Id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastVolume = volumes.OrderBy(v => v.Number).LastOrDefault();
|
var lastVolume = volumes.MaxBy(v => v.Number);
|
||||||
if (currentVolume.Number == 0 && currentVolume.Number != lastVolume?.Number && lastVolume?.Chapters.Count > 1)
|
if (currentVolume.Number == 0 && currentVolume.Number != lastVolume?.Number && lastVolume?.Chapters.Count > 1)
|
||||||
{
|
{
|
||||||
var lastChapter = lastVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting).LastOrDefault();
|
var lastChapter = lastVolume.Chapters.MaxBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
|
||||||
if (lastChapter == null) return -1;
|
if (lastChapter == null) return -1;
|
||||||
return lastChapter.Id;
|
return lastChapter.Id;
|
||||||
}
|
}
|
||||||
@ -396,7 +405,7 @@ public class ReaderService : IReaderService
|
|||||||
if (progress.Count == 0)
|
if (progress.Count == 0)
|
||||||
{
|
{
|
||||||
// I think i need a way to sort volumes last
|
// I think i need a way to sort volumes last
|
||||||
return volumes.OrderBy(v => double.Parse(v.Number + ""), _chapterSortComparer).First().Chapters
|
return volumes.OrderBy(v => double.Parse(v.Number + string.Empty), _chapterSortComparer).First().Chapters
|
||||||
.OrderBy(c => float.Parse(c.Number)).First();
|
.OrderBy(c => float.Parse(c.Number)).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,7 +479,7 @@ public class ReaderService : IReaderService
|
|||||||
/// <param name="chapterNumber"></param>
|
/// <param name="chapterNumber"></param>
|
||||||
public async Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber)
|
public async Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber)
|
||||||
{
|
{
|
||||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List<int>() { seriesId }, true);
|
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List<int> { seriesId }, true);
|
||||||
foreach (var volume in volumes.OrderBy(v => v.Number))
|
foreach (var volume in volumes.OrderBy(v => v.Number))
|
||||||
{
|
{
|
||||||
var chapters = volume.Chapters
|
var chapters = volume.Chapters
|
||||||
@ -482,10 +491,53 @@ public class ReaderService : IReaderService
|
|||||||
|
|
||||||
public async Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber)
|
public async Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber)
|
||||||
{
|
{
|
||||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List<int>() { seriesId }, true);
|
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List<int> { seriesId }, true);
|
||||||
foreach (var volume in volumes.OrderBy(v => v.Number).Where(v => v.Number <= volumeNumber && v.Number > 0))
|
foreach (var volume in volumes.OrderBy(v => v.Number).Where(v => v.Number <= volumeNumber && v.Number > 0))
|
||||||
{
|
{
|
||||||
MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HourEstimateRangeDto GetTimeEstimate(long wordCount, int pageCount, bool isEpub)
|
||||||
|
{
|
||||||
|
if (isEpub)
|
||||||
|
{
|
||||||
|
var minHours = Math.Max((int) Math.Round((wordCount / MinWordsPerHour)), 0);
|
||||||
|
var maxHours = Math.Max((int) Math.Round((wordCount / MaxWordsPerHour)), 0);
|
||||||
|
if (maxHours < minHours)
|
||||||
|
{
|
||||||
|
return new HourEstimateRangeDto
|
||||||
|
{
|
||||||
|
MinHours = maxHours,
|
||||||
|
MaxHours = minHours,
|
||||||
|
AvgHours = (int) Math.Round((wordCount / AvgWordsPerHour))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return new HourEstimateRangeDto
|
||||||
|
{
|
||||||
|
MinHours = minHours,
|
||||||
|
MaxHours = maxHours,
|
||||||
|
AvgHours = (int) Math.Round((wordCount / AvgWordsPerHour))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var minHoursPages = Math.Max((int) Math.Round((pageCount / MinPagesPerMinute / 60F)), 0);
|
||||||
|
var maxHoursPages = Math.Max((int) Math.Round((pageCount / MaxPagesPerMinute / 60F)), 0);
|
||||||
|
if (maxHoursPages < minHoursPages)
|
||||||
|
{
|
||||||
|
return new HourEstimateRangeDto
|
||||||
|
{
|
||||||
|
MinHours = maxHoursPages,
|
||||||
|
MaxHours = minHoursPages,
|
||||||
|
AvgHours = (int) Math.Round((pageCount / AvgPagesPerMinute / 60F))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HourEstimateRangeDto
|
||||||
|
{
|
||||||
|
MinHours = minHoursPages,
|
||||||
|
MaxHours = maxHoursPages,
|
||||||
|
AvgHours = (int) Math.Round((pageCount / AvgPagesPerMinute / 60F))
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,15 +110,13 @@ public class ReadingItemService : IReadingItemService
|
|||||||
{
|
{
|
||||||
switch (format)
|
switch (format)
|
||||||
{
|
{
|
||||||
case MangaFormat.Pdf:
|
|
||||||
_bookService.ExtractPdfImages(fileFilePath, targetDirectory);
|
|
||||||
break;
|
|
||||||
case MangaFormat.Archive:
|
case MangaFormat.Archive:
|
||||||
_archiveService.ExtractArchive(fileFilePath, targetDirectory);
|
_archiveService.ExtractArchive(fileFilePath, targetDirectory);
|
||||||
break;
|
break;
|
||||||
case MangaFormat.Image:
|
case MangaFormat.Image:
|
||||||
_imageService.ExtractImages(fileFilePath, targetDirectory, imageCount);
|
_imageService.ExtractImages(fileFilePath, targetDirectory, imageCount);
|
||||||
break;
|
break;
|
||||||
|
case MangaFormat.Pdf:
|
||||||
case MangaFormat.Unknown:
|
case MangaFormat.Unknown:
|
||||||
case MangaFormat.Epub:
|
case MangaFormat.Epub:
|
||||||
break;
|
break;
|
||||||
|
@ -8,9 +8,10 @@ using API.Data;
|
|||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.DTOs.CollectionTags;
|
using API.DTOs.CollectionTags;
|
||||||
using API.DTOs.Metadata;
|
using API.DTOs.Metadata;
|
||||||
|
using API.DTOs.Reader;
|
||||||
|
using API.DTOs.SeriesDetail;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -98,7 +99,7 @@ public class SeriesService : ISeriesService
|
|||||||
series.Metadata.SummaryLocked = true;
|
series.Metadata.SummaryLocked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata.Language)
|
if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata?.Language)
|
||||||
{
|
{
|
||||||
series.Metadata.Language = updateSeriesMetadataDto.SeriesMetadata?.Language;
|
series.Metadata.Language = updateSeriesMetadataDto.SeriesMetadata?.Language;
|
||||||
series.Metadata.LanguageLocked = true;
|
series.Metadata.LanguageLocked = true;
|
||||||
@ -112,7 +113,7 @@ public class SeriesService : ISeriesService
|
|||||||
});
|
});
|
||||||
|
|
||||||
series.Metadata.Genres ??= new List<Genre>();
|
series.Metadata.Genres ??= new List<Genre>();
|
||||||
UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata.Genres, series, allGenres, (genre) =>
|
UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, (genre) =>
|
||||||
{
|
{
|
||||||
series.Metadata.Genres.Add(genre);
|
series.Metadata.Genres.Add(genre);
|
||||||
}, () => series.Metadata.GenresLocked = true);
|
}, () => series.Metadata.GenresLocked = true);
|
||||||
@ -458,7 +459,6 @@ public class SeriesService : ISeriesService
|
|||||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
|
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
|
||||||
.OrderBy(v => Parser.Parser.MinNumberFromRange(v.Name))
|
.OrderBy(v => Parser.Parser.MinNumberFromRange(v.Name))
|
||||||
.ToList();
|
.ToList();
|
||||||
var chapters = volumes.SelectMany(v => v.Chapters).ToList();
|
|
||||||
|
|
||||||
// For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number.
|
// For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number.
|
||||||
var processedVolumes = new List<VolumeDto>();
|
var processedVolumes = new List<VolumeDto>();
|
||||||
@ -479,8 +479,15 @@ public class SeriesService : ISeriesService
|
|||||||
processedVolumes.ForEach(v => v.Name = $"Volume {v.Name}");
|
processedVolumes.ForEach(v => v.Name = $"Volume {v.Name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var specials = new List<ChapterDto>();
|
var specials = new List<ChapterDto>();
|
||||||
|
var chapters = volumes.SelectMany(v => v.Chapters.Select(c =>
|
||||||
|
{
|
||||||
|
if (v.Number == 0) return c;
|
||||||
|
c.VolumeTitle = v.Name;
|
||||||
|
return c;
|
||||||
|
})).ToList();
|
||||||
|
|
||||||
|
|
||||||
foreach (var chapter in chapters)
|
foreach (var chapter in chapters)
|
||||||
{
|
{
|
||||||
chapter.Title = FormatChapterTitle(chapter, libraryType);
|
chapter.Title = FormatChapterTitle(chapter, libraryType);
|
||||||
@ -490,7 +497,6 @@ public class SeriesService : ISeriesService
|
|||||||
specials.Add(chapter);
|
specials.Add(chapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Don't show chapter 0 (aka single volume chapters) in the Chapters tab or books that are just single numbers (they show as volumes)
|
// Don't show chapter 0 (aka single volume chapters) in the Chapters tab or books that are just single numbers (they show as volumes)
|
||||||
IEnumerable<ChapterDto> retChapters;
|
IEnumerable<ChapterDto> retChapters;
|
||||||
if (libraryType == LibraryType.Book)
|
if (libraryType == LibraryType.Book)
|
||||||
@ -503,29 +509,28 @@ public class SeriesService : ISeriesService
|
|||||||
.OrderBy(c => float.Parse(c.Number), new ChapterSortComparer());
|
.OrderBy(c => float.Parse(c.Number), new ChapterSortComparer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var storylineChapters = volumes
|
||||||
|
.Where(v => v.Number == 0)
|
||||||
|
.SelectMany(v => v.Chapters.Where(c => !c.IsSpecial))
|
||||||
|
.OrderBy(c => float.Parse(c.Number), new ChapterSortComparer());
|
||||||
|
|
||||||
return new SeriesDetailDto()
|
return new SeriesDetailDto()
|
||||||
{
|
{
|
||||||
Specials = specials,
|
Specials = specials,
|
||||||
Chapters = retChapters,
|
Chapters = retChapters,
|
||||||
Volumes = processedVolumes,
|
Volumes = processedVolumes,
|
||||||
StorylineChapters = volumes
|
StorylineChapters = storylineChapters
|
||||||
.Where(v => v.Number == 0)
|
|
||||||
.SelectMany(v => v.Chapters.Where(c => !c.IsSpecial))
|
|
||||||
.OrderBy(c => float.Parse(c.Number), new ChapterSortComparer())
|
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Should we show the given chapter on the UI. We only show non-specials and non-zero chapters.
|
/// Should we show the given chapter on the UI. We only show non-specials and non-zero chapters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="c"></param>
|
/// <param name="chapter"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private static bool ShouldIncludeChapter(ChapterDto c)
|
private static bool ShouldIncludeChapter(ChapterDto chapter)
|
||||||
{
|
{
|
||||||
return !c.IsSpecial && !c.Number.Equals(Parser.Parser.DefaultChapter);
|
return !chapter.IsSpecial && !chapter.Number.Equals(Parser.Parser.DefaultChapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType)
|
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType)
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Helpers.Converters;
|
using API.Helpers.Converters;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
|
using API.Services.Tasks.Metadata;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
|
using Hangfire.Storage;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Services;
|
namespace API.Services;
|
||||||
@ -20,9 +23,13 @@ public interface ITaskScheduler
|
|||||||
void RefreshMetadata(int libraryId, bool forceUpdate = true);
|
void RefreshMetadata(int libraryId, bool forceUpdate = true);
|
||||||
void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false);
|
void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false);
|
||||||
void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false);
|
void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false);
|
||||||
|
void AnalyzeFilesForSeries(int libraryId, int seriesId, bool forceUpdate = false);
|
||||||
|
void AnalyzeFilesForLibrary(int libraryId, bool forceUpdate = false);
|
||||||
void CancelStatsTasks();
|
void CancelStatsTasks();
|
||||||
Task RunStatCollection();
|
Task RunStatCollection();
|
||||||
void ScanSiteThemes();
|
void ScanSiteThemes();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
public class TaskScheduler : ITaskScheduler
|
public class TaskScheduler : ITaskScheduler
|
||||||
{
|
{
|
||||||
@ -37,6 +44,7 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
private readonly IStatsService _statsService;
|
private readonly IStatsService _statsService;
|
||||||
private readonly IVersionUpdaterService _versionUpdaterService;
|
private readonly IVersionUpdaterService _versionUpdaterService;
|
||||||
private readonly IThemeService _themeService;
|
private readonly IThemeService _themeService;
|
||||||
|
private readonly IWordCountAnalyzerService _wordCountAnalyzerService;
|
||||||
|
|
||||||
public static BackgroundJobServer Client => new BackgroundJobServer();
|
public static BackgroundJobServer Client => new BackgroundJobServer();
|
||||||
private static readonly Random Rnd = new Random();
|
private static readonly Random Rnd = new Random();
|
||||||
@ -45,7 +53,7 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService,
|
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService,
|
||||||
IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService,
|
IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService,
|
||||||
ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService,
|
ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService,
|
||||||
IThemeService themeService)
|
IThemeService themeService, IWordCountAnalyzerService wordCountAnalyzerService)
|
||||||
{
|
{
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -57,6 +65,7 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
_statsService = statsService;
|
_statsService = statsService;
|
||||||
_versionUpdaterService = versionUpdaterService;
|
_versionUpdaterService = versionUpdaterService;
|
||||||
_themeService = themeService;
|
_themeService = themeService;
|
||||||
|
_wordCountAnalyzerService = wordCountAnalyzerService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ScheduleTasks()
|
public async Task ScheduleTasks()
|
||||||
@ -107,6 +116,11 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
RecurringJob.AddOrUpdate("report-stats", () => _statsService.Send(), Cron.Daily(Rnd.Next(0, 22)), TimeZoneInfo.Local);
|
RecurringJob.AddOrUpdate("report-stats", () => _statsService.Send(), Cron.Daily(Rnd.Next(0, 22)), TimeZoneInfo.Local);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AnalyzeFilesForLibrary(int libraryId, bool forceUpdate = false)
|
||||||
|
{
|
||||||
|
BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanLibrary(libraryId, forceUpdate));
|
||||||
|
}
|
||||||
|
|
||||||
public void CancelStatsTasks()
|
public void CancelStatsTasks()
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Cancelling/Removing StatsTasks");
|
_logger.LogDebug("Cancelling/Removing StatsTasks");
|
||||||
@ -166,7 +180,7 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, forceUpdate));
|
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, forceUpdate));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = true)
|
public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Enqueuing series metadata refresh for: {SeriesId}", seriesId);
|
_logger.LogInformation("Enqueuing series metadata refresh for: {SeriesId}", seriesId);
|
||||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, forceUpdate));
|
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, forceUpdate));
|
||||||
@ -178,6 +192,12 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
BackgroundJob.Enqueue(() => _scannerService.ScanSeries(libraryId, seriesId, CancellationToken.None));
|
BackgroundJob.Enqueue(() => _scannerService.ScanSeries(libraryId, seriesId, CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AnalyzeFilesForSeries(int libraryId, int seriesId, bool forceUpdate = false)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Enqueuing analyze files scan for: {SeriesId}", seriesId);
|
||||||
|
BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(libraryId, seriesId, forceUpdate));
|
||||||
|
}
|
||||||
|
|
||||||
public void BackupDatabase()
|
public void BackupDatabase()
|
||||||
{
|
{
|
||||||
BackgroundJob.Enqueue(() => _backupService.BackupDatabase());
|
BackgroundJob.Enqueue(() => _backupService.BackupDatabase());
|
||||||
|
@ -45,11 +45,6 @@ public class BackupService : IBackupService
|
|||||||
_config = config;
|
_config = config;
|
||||||
_eventHub = eventHub;
|
_eventHub = eventHub;
|
||||||
|
|
||||||
// var maxRollingFiles = config.GetMaxRollingFiles();
|
|
||||||
// var loggingSection = config.GetLoggingFileName();
|
|
||||||
// var files = GetLogFiles(maxRollingFiles, loggingSection);
|
|
||||||
|
|
||||||
|
|
||||||
_backupFiles = new List<string>()
|
_backupFiles = new List<string>()
|
||||||
{
|
{
|
||||||
"appsettings.json",
|
"appsettings.json",
|
||||||
@ -59,11 +54,6 @@ public class BackupService : IBackupService
|
|||||||
"kavita.db-shm", // This wont always be there
|
"kavita.db-shm", // This wont always be there
|
||||||
"kavita.db-wal" // This wont always be there
|
"kavita.db-wal" // This wont always be there
|
||||||
};
|
};
|
||||||
|
|
||||||
// foreach (var file in files.Select(f => (_directoryService.FileSystem.FileInfo.FromFileName(f)).Name))
|
|
||||||
// {
|
|
||||||
// _backupFiles.Add(file);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> GetLogFiles(int maxRollingFiles, string logFileName)
|
public IEnumerable<string> GetLogFiles(int maxRollingFiles, string logFileName)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
@ -19,7 +20,6 @@ namespace API.Services.Tasks
|
|||||||
Task DeleteChapterCoverImages();
|
Task DeleteChapterCoverImages();
|
||||||
Task DeleteTagCoverImages();
|
Task DeleteTagCoverImages();
|
||||||
Task CleanupBackups();
|
Task CleanupBackups();
|
||||||
Task CleanupBookmarks();
|
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cleans up after operations on reoccurring basis
|
/// Cleans up after operations on reoccurring basis
|
||||||
@ -65,7 +65,6 @@ namespace API.Services.Tasks
|
|||||||
await SendProgress(0.7F, "Cleaning deleted cover images");
|
await SendProgress(0.7F, "Cleaning deleted cover images");
|
||||||
await DeleteTagCoverImages();
|
await DeleteTagCoverImages();
|
||||||
await DeleteReadingListCoverImages();
|
await DeleteReadingListCoverImages();
|
||||||
await SendProgress(0.8F, "Cleaning deleted cover images");
|
|
||||||
await SendProgress(1F, "Cleanup finished");
|
await SendProgress(1F, "Cleanup finished");
|
||||||
_logger.LogInformation("Cleanup finished");
|
_logger.LogInformation("Cleanup finished");
|
||||||
}
|
}
|
||||||
@ -148,11 +147,11 @@ namespace API.Services.Tasks
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes Database backups older than 30 days. If all backups are older than 30 days, the latest is kept.
|
/// Removes Database backups older than configured total backups. If all backups are older than total backups days, only the latest is kept.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task CleanupBackups()
|
public async Task CleanupBackups()
|
||||||
{
|
{
|
||||||
const int dayThreshold = 30;
|
var dayThreshold = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).TotalBackups;
|
||||||
_logger.LogInformation("Beginning cleanup of Database backups at {Time}", DateTime.Now);
|
_logger.LogInformation("Beginning cleanup of Database backups at {Time}", DateTime.Now);
|
||||||
var backupDirectory =
|
var backupDirectory =
|
||||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value;
|
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value;
|
||||||
@ -176,39 +175,5 @@ namespace API.Services.Tasks
|
|||||||
}
|
}
|
||||||
_logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now);
|
_logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes all files in the BookmarkDirectory that don't currently have bookmarks in the Database
|
|
||||||
/// </summary>
|
|
||||||
public Task CleanupBookmarks()
|
|
||||||
{
|
|
||||||
// TODO: This is disabled for now while we test and validate a new method of deleting bookmarks
|
|
||||||
return Task.CompletedTask;
|
|
||||||
// Search all files in bookmarks/ except bookmark files and delete those
|
|
||||||
// var bookmarkDirectory =
|
|
||||||
// (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
|
||||||
// var allBookmarkFiles = _directoryService.GetFiles(bookmarkDirectory, searchOption: SearchOption.AllDirectories).Select(Parser.Parser.NormalizePath);
|
|
||||||
// var bookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync())
|
|
||||||
// .Select(b => Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(bookmarkDirectory,
|
|
||||||
// b.FileName)));
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// var filesToDelete = allBookmarkFiles.AsEnumerable().Except(bookmarks).ToList();
|
|
||||||
// _logger.LogDebug("[Bookmarks] Bookmark cleanup wants to delete {Count} files", filesToDelete.Count);
|
|
||||||
//
|
|
||||||
// if (filesToDelete.Count == 0) return;
|
|
||||||
//
|
|
||||||
// _directoryService.DeleteFiles(filesToDelete);
|
|
||||||
//
|
|
||||||
// // Clear all empty directories
|
|
||||||
// foreach (var directory in _directoryService.FileSystem.Directory.GetDirectories(bookmarkDirectory, "", SearchOption.AllDirectories))
|
|
||||||
// {
|
|
||||||
// if (_directoryService.FileSystem.Directory.GetFiles(directory, "", SearchOption.AllDirectories).Length == 0 &&
|
|
||||||
// _directoryService.FileSystem.Directory.GetDirectories(directory).Length == 0)
|
|
||||||
// {
|
|
||||||
// _directoryService.FileSystem.Directory.Delete(directory, false);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
248
API/Services/Tasks/Metadata/WordCountAnalyzerService.cs
Normal file
248
API/Services/Tasks/Metadata/WordCountAnalyzerService.cs
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.Data.Repositories;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Helpers;
|
||||||
|
using API.SignalR;
|
||||||
|
using Hangfire;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using VersOne.Epub;
|
||||||
|
|
||||||
|
namespace API.Services.Tasks.Metadata;
|
||||||
|
|
||||||
|
public interface IWordCountAnalyzerService
|
||||||
|
{
|
||||||
|
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||||
|
[AutomaticRetry(Attempts = 2, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
|
Task ScanLibrary(int libraryId, bool forceUpdate = false);
|
||||||
|
Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This service is a metadata task that generates information around time to read
|
||||||
|
/// </summary>
|
||||||
|
public class WordCountAnalyzerService : IWordCountAnalyzerService
|
||||||
|
{
|
||||||
|
private readonly ILogger<WordCountAnalyzerService> _logger;
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IEventHub _eventHub;
|
||||||
|
private readonly ICacheHelper _cacheHelper;
|
||||||
|
private readonly IReaderService _readerService;
|
||||||
|
|
||||||
|
public WordCountAnalyzerService(ILogger<WordCountAnalyzerService> logger, IUnitOfWork unitOfWork, IEventHub eventHub,
|
||||||
|
ICacheHelper cacheHelper, IReaderService readerService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_eventHub = eventHub;
|
||||||
|
_cacheHelper = cacheHelper;
|
||||||
|
_readerService = readerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||||
|
[AutomaticRetry(Attempts = 2, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
|
public async Task ScanLibrary(int libraryId, bool forceUpdate = false)
|
||||||
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
||||||
|
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
|
MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 0F, ProgressEventType.Started, string.Empty));
|
||||||
|
|
||||||
|
var chunkInfo = await _unitOfWork.SeriesRepository.GetChunkInfo(library.Id);
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
_logger.LogInformation("[MetadataService] Refreshing Library {LibraryName}. Total Items: {TotalSize}. Total Chunks: {TotalChunks} with {ChunkSize} size", library.Name, chunkInfo.TotalSize, chunkInfo.TotalChunks, chunkInfo.ChunkSize);
|
||||||
|
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
|
MessageFactory.WordCountAnalyzerProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}"));
|
||||||
|
|
||||||
|
for (var chunk = 1; chunk <= chunkInfo.TotalChunks; chunk++)
|
||||||
|
{
|
||||||
|
if (chunkInfo.TotalChunks == 0) continue;
|
||||||
|
stopwatch.Restart();
|
||||||
|
|
||||||
|
_logger.LogInformation("[MetadataService] Processing chunk {ChunkNumber} / {TotalChunks} with size {ChunkSize}. Series ({SeriesStart} - {SeriesEnd}",
|
||||||
|
chunk, chunkInfo.TotalChunks, chunkInfo.ChunkSize, chunk * chunkInfo.ChunkSize, (chunk + 1) * chunkInfo.ChunkSize);
|
||||||
|
|
||||||
|
var nonLibrarySeries = await _unitOfWork.SeriesRepository.GetFullSeriesForLibraryIdAsync(library.Id,
|
||||||
|
new UserParams()
|
||||||
|
{
|
||||||
|
PageNumber = chunk,
|
||||||
|
PageSize = chunkInfo.ChunkSize
|
||||||
|
});
|
||||||
|
_logger.LogDebug("[MetadataService] Fetched {SeriesCount} series for refresh", nonLibrarySeries.Count);
|
||||||
|
|
||||||
|
var seriesIndex = 0;
|
||||||
|
foreach (var series in nonLibrarySeries)
|
||||||
|
{
|
||||||
|
var index = chunk * seriesIndex;
|
||||||
|
var progress = Math.Max(0F, Math.Min(1F, index * 1F / chunkInfo.TotalSize));
|
||||||
|
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
|
MessageFactory.WordCountAnalyzerProgressEvent(library.Id, progress, ProgressEventType.Updated, series.Name));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ProcessSeries(series, forceUpdate, false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[MetadataService] There was an exception during metadata refresh for {SeriesName}", series.Name);
|
||||||
|
}
|
||||||
|
seriesIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_unitOfWork.HasChanges())
|
||||||
|
{
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"[MetadataService] Processed {SeriesStart} - {SeriesEnd} out of {TotalSeries} series in {ElapsedScanTime} milliseconds for {LibraryName}",
|
||||||
|
chunk * chunkInfo.ChunkSize, (chunk * chunkInfo.ChunkSize) + nonLibrarySeries.Count, chunkInfo.TotalSize, stopwatch.ElapsedMilliseconds, library.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
|
MessageFactory.WordCountAnalyzerProgressEvent(library.Id, 1F, ProgressEventType.Ended, $"Complete"));
|
||||||
|
|
||||||
|
|
||||||
|
_logger.LogInformation("[WordCountAnalyzerService] Updated metadata for {LibraryName} in {ElapsedMilliseconds} milliseconds", library.Name, sw.ElapsedMilliseconds);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true)
|
||||||
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId);
|
||||||
|
if (series == null)
|
||||||
|
{
|
||||||
|
_logger.LogError("[WordCountAnalyzerService] Series {SeriesId} was not found on Library {LibraryId}", seriesId, libraryId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
|
MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 0F, ProgressEventType.Started, series.Name));
|
||||||
|
|
||||||
|
await ProcessSeries(series, forceUpdate);
|
||||||
|
|
||||||
|
if (_unitOfWork.HasChanges())
|
||||||
|
{
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
|
MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 1F, ProgressEventType.Ended, series.Name));
|
||||||
|
|
||||||
|
_logger.LogInformation("[WordCountAnalyzerService] Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessSeries(Series series, bool forceUpdate = false, bool useFileName = true)
|
||||||
|
{
|
||||||
|
var isEpub = series.Format == MangaFormat.Epub;
|
||||||
|
var existingWordCount = series.WordCount;
|
||||||
|
series.WordCount = 0;
|
||||||
|
foreach (var volume in series.Volumes)
|
||||||
|
{
|
||||||
|
volume.WordCount = 0;
|
||||||
|
foreach (var chapter in volume.Chapters)
|
||||||
|
{
|
||||||
|
// This compares if it's changed since a file scan only
|
||||||
|
var firstFile = chapter.Files.FirstOrDefault();
|
||||||
|
if (firstFile == null) return;
|
||||||
|
if (!_cacheHelper.HasFileChangedSinceLastScan(firstFile.LastFileAnalysis, forceUpdate,
|
||||||
|
firstFile))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (series.Format == MangaFormat.Epub)
|
||||||
|
{
|
||||||
|
long sum = 0;
|
||||||
|
var fileCounter = 1;
|
||||||
|
foreach (var file in chapter.Files)
|
||||||
|
{
|
||||||
|
var filePath = file.FilePath;
|
||||||
|
var pageCounter = 1;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var book = await EpubReader.OpenBookAsync(filePath, BookService.BookReaderOptions);
|
||||||
|
|
||||||
|
var totalPages = book.Content.Html.Values;
|
||||||
|
foreach (var bookPage in totalPages)
|
||||||
|
{
|
||||||
|
var progress = Math.Max(0F,
|
||||||
|
Math.Min(1F, (fileCounter * pageCounter) * 1F / (chapter.Files.Count * totalPages.Count)));
|
||||||
|
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
|
MessageFactory.WordCountAnalyzerProgressEvent(series.LibraryId, progress,
|
||||||
|
ProgressEventType.Updated, useFileName ? filePath : series.Name));
|
||||||
|
sum += await GetWordCountFromHtml(bookPage);
|
||||||
|
pageCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileCounter++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "There was an error reading an epub file for word count, series skipped");
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.Error,
|
||||||
|
MessageFactory.ErrorEvent("There was an issue counting words on an epub",
|
||||||
|
$"{series.Name} - {file}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.LastFileAnalysis = DateTime.Now;
|
||||||
|
_unitOfWork.MangaFileRepository.Update(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter.WordCount = sum;
|
||||||
|
series.WordCount += sum;
|
||||||
|
volume.WordCount += sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
var est = _readerService.GetTimeEstimate(chapter.WordCount, chapter.Pages, isEpub);
|
||||||
|
chapter.MinHoursToRead = est.MinHours;
|
||||||
|
chapter.MaxHoursToRead = est.MaxHours;
|
||||||
|
chapter.AvgHoursToRead = est.AvgHours;
|
||||||
|
_unitOfWork.ChapterRepository.Update(chapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
var volumeEst = _readerService.GetTimeEstimate(volume.WordCount, volume.Pages, isEpub);
|
||||||
|
volume.MinHoursToRead = volumeEst.MinHours;
|
||||||
|
volume.MaxHoursToRead = volumeEst.MaxHours;
|
||||||
|
volume.AvgHoursToRead = volumeEst.AvgHours;
|
||||||
|
_unitOfWork.VolumeRepository.Update(volume);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (series.WordCount == 0 && series.WordCount != 0) series.WordCount = existingWordCount; // Restore original word count if the file hasn't changed
|
||||||
|
var seriesEstimate = _readerService.GetTimeEstimate(series.WordCount, series.Pages, isEpub);
|
||||||
|
series.MinHoursToRead = seriesEstimate.MinHours;
|
||||||
|
series.MaxHoursToRead = seriesEstimate.MaxHours;
|
||||||
|
series.AvgHoursToRead = seriesEstimate.AvgHours;
|
||||||
|
_unitOfWork.SeriesRepository.Update(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static async Task<int> GetWordCountFromHtml(EpubContentFileRef bookFile)
|
||||||
|
{
|
||||||
|
var doc = new HtmlDocument();
|
||||||
|
doc.LoadHtml(await bookFile.ReadContentAsTextAsync());
|
||||||
|
|
||||||
|
var textNodes = doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]");
|
||||||
|
if (textNodes == null) return 0;
|
||||||
|
|
||||||
|
return textNodes
|
||||||
|
.Select(node => node.InnerText.Split(' ', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Where(s => char.IsLetter(s[0])))
|
||||||
|
.Select(words => words.Count())
|
||||||
|
.Where(wordCount => wordCount > 0)
|
||||||
|
.Sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
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