v0.7.7 - Localization! (#2199)

* Report Media Issues (#1964)

* Started working on a report problems implementation.

* Started code

* Added logging to book and archive service.

* Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point.

* Added basic implementation for media errors.

* MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan.

* Fixed unit tests

* Basic code in place to view and clear. Just UI Cleanup needed.

* Slight css upgrade

* Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working.

* Fixed unit tests

* Fixed unit tests for real

* Bump versions by dotnet-bump-version.

* Expanded Metadata for EPUBs (#1965)

* Fixed a bug breaking ability to save server settings

* Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why

* Refactored the code to clean it up

* Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq.

* ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81

* Double include author for epub parsing and let the People code handle removing duplicates.

* Bump versions by dotnet-bump-version.

* Nothing changed, this is just to retrigger a stable build. (#1967) (#1968)

* Adding paper book reader theme (#1976)

* Adding paper book reader theme

# Added
- Added: Paper book reader theme

* Fixing some leftover styles

* adding book emulation to 2column layout for paper style

* Adding migrations

* removing migration and compressing image

* Reverting DataContextModelSnapshot

* checking out datacontextmodelsnapshot file

* Bump versions by dotnet-bump-version.

* Web Links (#1983)

* Updated dependencies

* Updated the default key to be 256 bits to meet security requirements.

* Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes.

* Implemented ability to see links and click on them for an individual chapter.

* Hooked up the ability to set Series web links.

* Render out the web link

* Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default.

* Added Robbie's nice error weblink fallbacks.

* Bump versions by dotnet-bump-version.

* Updated Docker entrypoint (#1984)

* Bump versions by dotnet-bump-version.

* ISBN Support (#1985)

* Fixed a bug where weblinks would always show

* Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell.

* Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10.

* Fixed Favicon not working on anything but windows

* Implemented ISBN support into Kavita

* Don't round so much when transforming bytes

* Bump versions by dotnet-bump-version.

* AVIF Support & Much More! (#1992)

* Expand the list of potential favicon icons to grab.

* Added a url mapping functionality to use alternative urls for fetching icons

* Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes.

* Started refactoring code so that webp queries use encoding format instead.

* More refactoring to remove hardcoded webp references.

* Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys.

* Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot.

* Make favicon encode setting aware

* Cleaned up favicon conversion

* Updated format counter to now just use Extension from MangaFile now that it's been out a while.

* Tweaked jumpbar code to reduce a lookup to hashmap.

* Added AVIF (8-bit only) support.

* In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed.

* You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend.

* Forgot a file

* Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont.

* Fixed Refresh token using wrong Claim to look up the user.

* Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated.

* Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures.

* Bump versions by dotnet-bump-version.

* More Fixes (#1993)

* Strip just isbn: from epub isbns and log when it's back (books)

* Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita.

* Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords.

* Show the media issue count before you open accordion

* Added a inpage filter for Media issues

* Cleanup styles

* Fixed up some code in epub isbn parsing when it's null

* Encode filenames when downloading so that non english characters can be passed properly to UI.

* Added support to parse ComicInfo's with Empty Tags.

* Reset development settings.

* Tweaked the code in generating reading lists to avoid extra work when not needed.

* Fix comicvine's favicon

* Fixed up a unit test

* Tweaked the favicon code to ignore icons that have query parameters

* More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already.

* Added a note for later

* Fixed stats server url

* Added more debugging

* Fixed unit tests

* Bump versions by dotnet-bump-version.

* More Fixes from Recent PRs (#1995)

* Added extra debugging for logout issue

* Fixed the null issue with ISBN

* Allow web links to be cleared out

* More logging on refresh token

* More key fallback when building Table of Contents

* Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced.

* Updated dependencies

* Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well.

* Bump versions by dotnet-bump-version.

* Fixed a bug with config (#1996)

* Bump versions by dotnet-bump-version.

* Changed IsDocker check (#1998)

* Refactored IsDocker to be completely static and changed to use an environment variable instead.

* Removed file from another branch

* Bump versions by dotnet-bump-version.

* Migrated up to VersOne 3.3 with epub 3.3 support. (#1999)

This enables collection and reading list support from epubs.

* Bump versions by dotnet-bump-version.

* More Bugfixes (EPUB Mainly) (#2004)

* Fixed an issue with downloading where spaces turned into plus signs.

* If the refresh token is invalid, but the auth token still has life in it, don't invalidate.

* Fixed docker users unable to save settings

* Show a default error icon until favicon loads

* Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2.

* Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml)

* Added another hack to massage key to page lookups when rewriting anchors.

* Cleaned up debugging notes

* Bump versions by dotnet-bump-version.

* More Polish  (#2005)

* Implemented sort title extraction from epub 3 files.

* Added link to wiki for media errors

* Fixed the hack to reduce JWT refresh token expiration

* Fixed up a case where favicon downloading wasn't correcting links that started with // correctly.

Added a fallback for sites that just don't pngs available.

* Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community.

* Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler.

* Bump versions by dotnet-bump-version.

* Angular 16 (#2007)

* Removed adv, which isn't needed.

* Updated zone

* Updated to angular 16

* Updated to angular 16 (partially)

* Updated to angular 16

* Package update for Angular 16 (and other dependencies) is complete.

* Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed()

* Updated all inputs that have ! to be required and deleted all unit tests.

* Corrected how takeUntilDestroyed() is supposed to be implemented.

* Bump versions by dotnet-bump-version.

* Pipeline adjustment for Angular 16 (#2008)

* Bump versions by dotnet-bump-version.

* Try a different build (#2009)

* Bump versions by dotnet-bump-version.

* Continue Reading Bugfix (#2010)

* Fixed an edge case where continue point wasn't considering any chapters that had progress.

Continue point is now slightly faster and uses less memory.

* Added a unit test for a user's case. Still not reproducible

* Bump versions by dotnet-bump-version.

* Ensure chapters are sorted when getting continue point (#2011)

Fixes new behaviour in #1625

* Bump versions by dotnet-bump-version.

* Strip more forms of comments from CSS before parsing/inlining. (#2014)

Handle if ExCSS throws an exception during inlining and attempt to fallback to scoping css instead of inlining.

I still cannot update past ExCSS v4.1.0 else NPEs for common css will be thrown.

* Bump versions by dotnet-bump-version.

* Misc Changes (#2015)

* Updated ng-bootstrap

* Fixed an issue where jumpbar would be disabled when it shouldn't have been.

* When there are duplicate files that make up a volume, show the count on series detail.

* Added basic ISBN searching which will return a chapter back.

* Bump versions by dotnet-bump-version.

* Fixed count for cards (#2016)

* Bump versions by dotnet-bump-version.

* Last Release before Release Testing (#2017)

* Attempting to invalidate JWT on login (when locked out), but can't figure a way to get a JWT, since we don't store them.

Just committing as I'm going to remove the middleware, this is not worth the performance and complexity.

* Removed some security stuff that didn't line up.

* Dropping Token Expiration down to 2 days to test during release testing.

* Bump versions by dotnet-bump-version.

* Removed old migrations for Kavita startup. Only migrations from v0.7.2 onwards are present. (#2019)

* Bump versions by dotnet-bump-version.

* Fixed up jumpbar not properly disabling/enabling (#2022)

* Bump versions by dotnet-bump-version.

* Fix StoryArc & StoryArcNumber mismatch (#2018)

* Ensure StoryArc and StoryArcNumber are max length

* Trim StoryArc to remove excess spaces.

* Replaced with cleaner approach.

* Update with majora2007 recommendations

* Bump versions by dotnet-bump-version.

* Last fixes before release (#2027)

* Disable login button when a login is in-progress. This will help prevent spamming when internet is slow.

* Fixed a bug where an empty space could cause an error when creating a library.

* Apply Split Options throughout the codebase to add extra safe-guard on empty spaces and ensure trimming.

* Bump versions by dotnet-bump-version.

* Added NoContent responses when APIs don't find entities (#2028)

* Bump versions by dotnet-bump-version.

* Few More Fixes (#2032)

* Fixed spreads stretching on PC

* Fixed a bug where reading list dates couldn't be cleared out.

* Reading list page refreshes after updating info in the modal

* Fixed an issue where create library wouldn't take into account advanced settings.

* Fixed an issue where selection of the first chapter of a series to pull series-level metadata could fail in cases where you had Volume 2 and Chapter 1, Volume 2 would be selected.

* Bump versions by dotnet-bump-version.

* Fixed a bug where scan series wouldn't trigger word count analysis nor cover generation. (#2035)

* Bump versions by dotnet-bump-version.

* Okay this should be the last (#2037)

* Fixed improper date visualization for reading list detail page.

* Correct not-read badge position (#2034)

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* Bump versions by dotnet-bump-version.

* Fixed a bug where reading list month wasn't rendering correctly (#2039)

* Bump versions by dotnet-bump-version.

* Version bump (#2040)

* Bump versions by dotnet-bump-version.

* Bugfixes for a hotfix (#2052)

* Nothing changed, this is just to retrigger a stable build. (#1967)

* v0.7.3 - The Quality of Life Update  (#2036)

* Version bump

* Okay this should be the last (#2037)

* Fixed improper date visualization for reading list detail page.

* Correct not-read badge position (#2034)

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* Bump versions by dotnet-bump-version.

* Merged develop in

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* v0.7.3 - The Quality of Life Update (#2041)

* Report Media Issues (#1964)

* Started working on a report problems implementation.

* Started code

* Added logging to book and archive service.

* Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point.

* Added basic implementation for media errors.

* MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan.

* Fixed unit tests

* Basic code in place to view and clear. Just UI Cleanup needed.

* Slight css upgrade

* Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working.

* Fixed unit tests

* Fixed unit tests for real

* Bump versions by dotnet-bump-version.

* Expanded Metadata for EPUBs (#1965)

* Fixed a bug breaking ability to save server settings

* Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why

* Refactored the code to clean it up

* Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq.

* ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81

* Double include author for epub parsing and let the People code handle removing duplicates.

* Bump versions by dotnet-bump-version.

* Nothing changed, this is just to retrigger a stable build. (#1967) (#1968)

* Adding paper book reader theme (#1976)

* Adding paper book reader theme

# Added
- Added: Paper book reader theme

* Fixing some leftover styles

* adding book emulation to 2column layout for paper style

* Adding migrations

* removing migration and compressing image

* Reverting DataContextModelSnapshot

* checking out datacontextmodelsnapshot file

* Bump versions by dotnet-bump-version.

* Web Links (#1983)

* Updated dependencies

* Updated the default key to be 256 bits to meet security requirements.

* Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes.

* Implemented ability to see links and click on them for an individual chapter.

* Hooked up the ability to set Series web links.

* Render out the web link

* Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default.

* Added Robbie's nice error weblink fallbacks.

* Bump versions by dotnet-bump-version.

* Updated Docker entrypoint (#1984)

* Bump versions by dotnet-bump-version.

* ISBN Support (#1985)

* Fixed a bug where weblinks would always show

* Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell.

* Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10.

* Fixed Favicon not working on anything but windows

* Implemented ISBN support into Kavita

* Don't round so much when transforming bytes

* Bump versions by dotnet-bump-version.

* AVIF Support & Much More! (#1992)

* Expand the list of potential favicon icons to grab.

* Added a url mapping functionality to use alternative urls for fetching icons

* Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes.

* Started refactoring code so that webp queries use encoding format instead.

* More refactoring to remove hardcoded webp references.

* Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys.

* Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot.

* Make favicon encode setting aware

* Cleaned up favicon conversion

* Updated format counter to now just use Extension from MangaFile now that it's been out a while.

* Tweaked jumpbar code to reduce a lookup to hashmap.

* Added AVIF (8-bit only) support.

* In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed.

* You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend.

* Forgot a file

* Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont.

* Fixed Refresh token using wrong Claim to look up the user.

* Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated.

* Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures.

* Bump versions by dotnet-bump-version.

* More Fixes (#1993)

* Strip just isbn: from epub isbns and log when it's back (books)

* Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita.

* Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords.

* Show the media issue count before you open accordion

* Added a inpage filter for Media issues

* Cleanup styles

* Fixed up some code in epub isbn parsing when it's null

* Encode filenames when downloading so that non english characters can be passed properly to UI.

* Added support to parse ComicInfo's with Empty Tags.

* Reset development settings.

* Tweaked the code in generating reading lists to avoid extra work when not needed.

* Fix comicvine's favicon

* Fixed up a unit test

* Tweaked the favicon code to ignore icons that have query parameters

* More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already.

* Added a note for later

* Fixed stats server url

* Added more debugging

* Fixed unit tests

* Bump versions by dotnet-bump-version.

* More Fixes from Recent PRs (#1995)

* Added extra debugging for logout issue

* Fixed the null issue with ISBN

* Allow web links to be cleared out

* More logging on refresh token

* More key fallback when building Table of Contents

* Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced.

* Updated dependencies

* Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well.

* Bump versions by dotnet-bump-version.

* Fixed a bug with config (#1996)

* Bump versions by dotnet-bump-version.

* Changed IsDocker check (#1998)

* Refactored IsDocker to be completely static and changed to use an environment variable instead.

* Removed file from another branch

* Bump versions by dotnet-bump-version.

* Migrated up to VersOne 3.3 with epub 3.3 support. (#1999)

This enables collection and reading list support from epubs.

* Bump versions by dotnet-bump-version.

* More Bugfixes (EPUB Mainly) (#2004)

* Fixed an issue with downloading where spaces turned into plus signs.

* If the refresh token is invalid, but the auth token still has life in it, don't invalidate.

* Fixed docker users unable to save settings

* Show a default error icon until favicon loads

* Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2.

* Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml)

* Added another hack to massage key to page lookups when rewriting anchors.

* Cleaned up debugging notes

* Bump versions by dotnet-bump-version.

* More Polish  (#2005)

* Implemented sort title extraction from epub 3 files.

* Added link to wiki for media errors

* Fixed the hack to reduce JWT refresh token expiration

* Fixed up a case where favicon downloading wasn't correcting links that started with // correctly.

Added a fallback for sites that just don't pngs available.

* Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community.

* Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler.

* Bump versions by dotnet-bump-version.

* Angular 16 (#2007)

* Removed adv, which isn't needed.

* Updated zone

* Updated to angular 16

* Updated to angular 16 (partially)

* Updated to angular 16

* Package update for Angular 16 (and other dependencies) is complete.

* Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed()

* Updated all inputs that have ! to be required and deleted all unit tests.

* Corrected how takeUntilDestroyed() is supposed to be implemented.

* Bump versions by dotnet-bump-version.

* Pipeline adjustment for Angular 16 (#2008)

* Bump versions by dotnet-bump-version.

* Try a different build (#2009)

* Bump versions by dotnet-bump-version.

* Continue Reading Bugfix (#2010)

* Fixed an edge case where continue point wasn't considering any chapters that had progress.

Continue point is now slightly faster and uses less memory.

* Added a unit test for a user's case. Still not reproducible

* Bump versions by dotnet-bump-version.

* Ensure chapters are sorted when getting continue point (#2011)

Fixes new behaviour in #1625

* Bump versions by dotnet-bump-version.

* Strip more forms of comments from CSS before parsing/inlining. (#2014)

Handle if ExCSS throws an exception during inlining and attempt to fallback to scoping css instead of inlining.

I still cannot update past ExCSS v4.1.0 else NPEs for common css will be thrown.

* Bump versions by dotnet-bump-version.

* Misc Changes (#2015)

* Updated ng-bootstrap

* Fixed an issue where jumpbar would be disabled when it shouldn't have been.

* When there are duplicate files that make up a volume, show the count on series detail.

* Added basic ISBN searching which will return a chapter back.

* Bump versions by dotnet-bump-version.

* Fixed count for cards (#2016)

* Bump versions by dotnet-bump-version.

* Last Release before Release Testing (#2017)

* Attempting to invalidate JWT on login (when locked out), but can't figure a way to get a JWT, since we don't store them.

Just committing as I'm going to remove the middleware, this is not worth the performance and complexity.

* Removed some security stuff that didn't line up.

* Dropping Token Expiration down to 2 days to test during release testing.

* Bump versions by dotnet-bump-version.

* Removed old migrations for Kavita startup. Only migrations from v0.7.2 onwards are present. (#2019)

* Bump versions by dotnet-bump-version.

* Fixed up jumpbar not properly disabling/enabling (#2022)

* Bump versions by dotnet-bump-version.

* Fix StoryArc & StoryArcNumber mismatch (#2018)

* Ensure StoryArc and StoryArcNumber are max length

* Trim StoryArc to remove excess spaces.

* Replaced with cleaner approach.

* Update with majora2007 recommendations

* Bump versions by dotnet-bump-version.

* Last fixes before release (#2027)

* Disable login button when a login is in-progress. This will help prevent spamming when internet is slow.

* Fixed a bug where an empty space could cause an error when creating a library.

* Apply Split Options throughout the codebase to add extra safe-guard on empty spaces and ensure trimming.

* Bump versions by dotnet-bump-version.

* Added NoContent responses when APIs don't find entities (#2028)

* Bump versions by dotnet-bump-version.

* Few More Fixes (#2032)

* Fixed spreads stretching on PC

* Fixed a bug where reading list dates couldn't be cleared out.

* Reading list page refreshes after updating info in the modal

* Fixed an issue where create library wouldn't take into account advanced settings.

* Fixed an issue where selection of the first chapter of a series to pull series-level metadata could fail in cases where you had Volume 2 and Chapter 1, Volume 2 would be selected.

* Bump versions by dotnet-bump-version.

* Fixed a bug where scan series wouldn't trigger word count analysis nor cover generation. (#2035)

* Bump versions by dotnet-bump-version.

* Okay this should be the last (#2037)

* Fixed improper date visualization for reading list detail page.

* Correct not-read badge position (#2034)

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* Bump versions by dotnet-bump-version.

* Fixed a bug where reading list month wasn't rendering correctly (#2039)

* Bump versions by dotnet-bump-version.

* Version bump (#2040)

* Bump versions by dotnet-bump-version.

* Fixed bug in CI pipeline for main

---------

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
Co-authored-by: Chris Plaatjes <kizaing@gmail.com>
Co-authored-by: pssandhu <pssandhu@users.noreply.github.com>
Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com>
Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* Reverted a scaling issue for fit to width

* Fixed an issue where creating a new library wouldn't persist advanced options due to a conflict with default value.

When deleting a library, give the library name in the prompt.

* Fixed kbd tags in epubs with paper theme having a style conflict.

* Fixed an edge case where the incorrect first cover could be chosen in some strange grouping situations.

* Manually sort directories as some OSes don't return them in a natural sort order.

* Fixed an issue where autocompleting when adding a directory could throw an error when you're typing.

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
Co-authored-by: Chris Plaatjes <kizaing@gmail.com>
Co-authored-by: pssandhu <pssandhu@users.noreply.github.com>
Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com>

* Bump versions by dotnet-bump-version.

* [skipci] No User facing Changes (#2054)

* Setup canary GA

* Fixed bad repo

* Aligned GA (#2059)

* v0.7.4 - Kavita+ Launch (#2117)

* Initial Canary Push (#2055)

* Added AniList Token

* Implemented the ability to set your AniList token. License check is not in place.

* Added a check that validates AniList token is still valid. As I build out more support, I will add more checks.

* Refactored the code to validate the license before allowing UI control to be edited.

* Started license server stuff, but may need to change approach.

Hooked up ability to scrobble rating events to KavitaPlus API.

* Hooked in the ability to sync Mark Series as Read/Unread

* Fixed up unit tests and only scrobble when a full chapter is read naturally.

* Fixed up the Scrobbling service

* Tweak one of the queries

* Started an idea for Scrobble History, might rework into generic TaskHistory.

* AniList Token now has a validation check.

* Implemented a mechanism such that events are persisted to the database, processed every X hours to the API layer, then deleted from the database.

* Hooked in code for want to read so we only send what's important. Will migrate these to bulk calls to lessen strain on API server.

* Added some todos. Need to take a break.

* Hooked up the ability to backfill scrobble events after turning it on.

* Started on integrating license key into the server and ability to turn off scrobbling at the library level. Added sync history table for scrobbling and other API based information.

* Started writing to sync table

* Refactored the migrations to flatten them.

Started working a basic license add flow and added in some of the cache. Lots to do.

* Ensure that when we backfill scrobble events, we respect if a library has scrobbling turned on or not.

* Hooked up the ability to send when the series was started to be read

* Refactored the UI to streamline and group KavitaPlus Account Forms.

* Aligning with API

* Fixed bad merge

* Fixed up inputting a user license.

* Hooked up a cron task that validates licenses every 4 hours and on startup.

* Reworked how the update license code works so that we always update the cache and we handle removing license from user.

* Cleaned up some UI code

* UserDto now has if there is a valid license or not. It's not exposed though as there is no need to expose the license key ever.

* Fixed a strange encoding issue with extra ".

Started working on having the UI aware of the license information.

Refactored all code to properly pass the correct license to the API layer.

* There is a circular dependency in the code.

Fixed some theme code which wasn't checking the right variable.

Reworked the JWT interceptor to be better at handling async code.

Lots of misc code changes, DI circular issue is still present.

* Fixed the DI issue and moved all things that need bootstrapping to app.component.

* Hooked up the ability to not have a donation button show up if the server default user/admin has a valid KavitaPlus license.

* Refactored how we extract out ids from weblinks

* Ensure if API fails, we don't delete the record.

* Refactored how rate checks occur for scrobbling processing.

* Lots of testing and ensuring rate limit doesn't get destroyed.

* Ensure the media item is valid for that user's providers set.

* Refactored the loop code into one method to keep things much cleaner

* Lots of code to get the scrobbling streamlined and foolproof. Unknown series are now reported on the UI.

* Prevent duplicates for scrobble errors.

* Ensure we are sending the correct type to the Scrobble Provider

* Ensure we send the date of the scrobble event for upstream to use.

* Replaced the dedicated run backfilling of scrobble events to just trigger when setting the anilist token for the first time.

Streamlined a lot of the code for adding your license to ensure user understands how it works.

* Fixed a bug where scan series wasn't triggering word count or cover generation.

* Started the plumbing for recommendations

* Merge conflicts

* Recommendation plumbing is nearly complete.

* Setup response caching and general cleanup

* Fixed UI not showing the recommendation tab

* Switched to prod url

* Fixed broken unit tests due to Hangfire not being setup for unit tests

* Fixed branch selection (#2056)

* Damn you GA (#2058)

* Bump versions by dotnet-bump-version.

* Fixed GA not pulling the right branch and removed unneeded building from veresion job (#2060)

* Bump versions by dotnet-bump-version.

* Canary Second (#2071)

* Just started

* Started building the user review card. Fixed Recommendations not having user progress on them.

* Fixed a bug where scrobbling ratings wasn't working.

* Added a temp ability to trigger scrobbling processing for testing.

* Cleaned up the design of review card. Added a temp way to trigger scrobbling.

* Fixed clear scrobbling errors and refactored so reviews now load from DB and is streamlined.

* Refactored so edit review is now a single module component and editable from the series detail page.

* Removed SyncHistory table as it's no longer needed. Refactored read events to properly update to the latest progress information. Refactored to a new way of clearing events, so that user's can see their scrobble history.

* Fixed a bug where Anilist token wouldn't show as set due to some state issue

* Added the ability to see your own scrobble events

* Avoid a potential collision with recommendations.

* Fixed an issue where when checking for a license on UI, it wouldn't force the check (in case server was down on first check).

* External reviews are implemented.

* Fixed unit tests

* Bump versions by dotnet-bump-version.

* Made the api url dynamic based on dev more or not. (#2072)

* Bump versions by dotnet-bump-version.

* Canary Build 3 (#2079)

* Updated reviews to have tagline support to match how Anilist has them.

Cleaned up the KavitaPlus documentation and added a feature list.

Review cards look much better.

* Fixed up a NPE in scrobble event creation

* Removed the ability to have images leak in the read more review card.

Review's now show the user if they are a local user, else External.

* Added caching to the reviews and recommendations that come from an external source. Max of 50MB will be used across whole instance. Entries are cached for 1 hour.

* Reviews are looking much better

* Added the ability for users to share their series reviews with other users on the server via a new opt-in mechanism.

Fixed up some cache busting mechanism for reviews.

* More review polish to align with better matching

* Added the extra information for Recommendation matching.

* Preview of the review is much cleaner now and the full body is styled better.

* More anilist specific syntax

* Fixed bad regex

* Added the ability to bust cache.

Spoilers are now implemented for reviews. Introduces:
--review-spoiler-bg-color
--review-spoiler-text-color

* Bump versions by dotnet-bump-version.

* Canary Build 4 (#2086)

* Updated Kavita Plus feature list. Added a hover-over to the progress bars in the app to know exact percentage of reading for a chapter or series.

* Added a button to go to external review. Changed how enums show in the documentation so you can see their string value too.

Limited reviews to top 10 with proper ordering. Drastically cleaned up how we handle preview summary generation

* Cleaned up the margin below review section

* Fixed an issue where a processed scrobble event would get updated instead of a new event created.

* By default, there is now a prompt on series review to add your own, which fills up the space nicely.

Added the backend for Series Holds.

* Scrobble History is now ordered by recent -> latest. Some minor cleanup in other files.

* Added a simple way to see and toggle scrobble service from the series.

* Fixed a bug where updating the user's last active time wasn't writing to database and causing a logout event.

* Tweaked the registration email wording to be more clear for email field.

* Improved OPDS Url generation and included using host name if defined.

* Fixed the issues with choosing the correct series cover image. Added many unit tests to cover the edge cases.

* Small cleanup

* Fixed an issue where urls with , in them would break weblinks.

* Fixed a bug where we weren't trying a png before we hit fallback for favicon parsing.

* Ensure scrobbling tab isn't active without a license.

Changed how updating user last active worked to supress more concurrency issues.

* Fixed an issue where duplicate series could appear on newly added during a scan.

* Bump versions by dotnet-bump-version.

* Fixed a bad dto (#2087)

* Bump versions by dotnet-bump-version.

* Canary Build 4 (#2089)

* New server-based auth is in place with the ability to register the instance.

* Refactored to single install bound licensing.

* Made the Kavita+ tab gold.

* Change the JWTs to last 10 days. This is a self-hosted software and the usage doesn't need the level of 2 days expiration

* Bump versions by dotnet-bump-version.

* Canary Build 4 (#2090)

* By default, a new library will only have scrobbling on if it's of type book or manga given current scrobble providers.

* Started building out external reviews.

* Added the ability to re-enter your license information.

* Fixed side nav not extending enough

* Fixed a bug with info cards

* Integrated rating support, fixed review cards without a tagline, and misc fixes.

* Streamlined where ratings are located on series detail page.

* Aligned with other series lookups

* Bump versions by dotnet-bump-version.

* Canary Build 6 (#2092)

* Cleaned up some messaging

* Fixed up series detail

* Cleanup

* Bump versions by dotnet-bump-version.

* Canary Build 6 (#2093)

* Fixed scrobble token not being visible by default.

* Added a loader for external reviews

* Added the ability to edit series details (weblinks) from Scrobble Issues page.

* Slightly lessened the focus on buttons

* Fixed review cards so whenever you click your own review, it will open the edit modal.

* Need for speed - Updated Kavita log to be much smaller and replaced all code ones with a 32x version.

* Optimized a ton of our images to be much smaller and faster to load.

* Added more MIME types for response compression

* Edit Series modal name field should be readonly as it is directly mapped to file metadata or filename parsed. It shouldn't be changeable via the UI.

* Removed the ability to update the Series name via Kavita UI/API as it is no longer editable.

* Moved Image component to be standalone

* Moved ReadMore component to be standalone

* Moved PersonBadge component to be standalone

* Moved IconAndTitle component to be standalone

* Fixed some bugs with standalone.

* Hooked in the ability to scrobble series reviews.

* Refactored everything to use HashUtil token rather than InstallId.

* Swapped over to a generated machine token and fixed an issue where after registering, the license would not say valid.

* Added the missing migration for review scrobble events.

* Clean up some wording around busting cache.

* Fixed a bug where chapters within a volume could be unordered in the UI info screen.

* Refactored to prepare for external series rendering on series detail.

* Implemented external recs

* Bump versions by dotnet-bump-version.

* Canary Build 7 (#2097)

* Aligned ExtractId to extract a long, since MAL id can be just that.

* Fixed external series card not clicking correctly.

Fixed a bug when extracting a Mal link.

Fixed cancel button on license component.

* Renamed user-license to license component given new direction for licensing.

* Implemented card layout for recommendations

* Moved more components over to be standalone and removed pipes module. This is going to take some time for sure.

* Removed Cards and SharedCardsSideNav and SideNav over to standalone. This has been shaken out.

* Cleaned up a bunch of extra space on reading list detail page.

* Fixed rating popover not having a black triangle.

* When checking license, show a loading indicator for validity icon.

* Cache size can now be changed by admins if they want to give more memory for better browsing.

* Added LastReadTime

* Cleanup the scrobbling control text for Library Settings.

* Fixed yet another edge case for getting series cover image where first volume is higher than 1 and the rest is just loose leaf chapters.

* Changed OPDS Content Type to be application/atom+xml to align better with the spec.

* Fixed unit tests

* Bump versions by dotnet-bump-version.

* Canary Build 7 (#2098)

* Fixed the percentage readout on card item progress bar

* Ensure scrobble control is always visible

* Review card could show person icon in tablet viewport.

* Changed how the ServerToken for node locking works as docker was giving different results each time.

* After we update series metadata, bust cache

* License componet cleanup on the styles

* Moved license to admin module and removed feature modal as wiki is much easier to maintain.

* Bump versions by dotnet-bump-version.

* Canary Build 8 (#2100)

* Fixed a very slight amount of the active nav tag bleeding outside the border radius

* Switched how we count words in epub to handle languages that don't have spaces.

* Updated dependencies and fixed a series cover image on list item view for recs.

* Fixed a bug where external recs werent showing summary of the series.

* Rewrote the rec loop to be cleaner

* Added the ability to see series summary on series detail page on list view.

Changed Scrobble Event page to show in server time and not utc.

* Added tons of output to identify why unraid generates a new fingerprint each time.

* Refactored scrobble event table to have filtering and pagination support.

Fixed a few bad template issues and fixed loading scrobbling tab on refresh of page.

* Aligned a few apis to use a default pagination rather than a higher level one.

* Undo OPDS change as Chunky/Panels break.

* Moved the holds code around

* Don't show an empty review for the user, it eats up uneeded space and is ugly.

* Cleaned up the review code

* Fixed a bug with arrow on sortable table header.

* More scrobbling debug information to ensure events are being processed correctly.

* Applied a ton of code cleanup build warnings

* Enhanced rec matching by prioritizing matching on weblinks before falling back to name matching.

* Fixed the calculation of word count for epubs.

* Bump versions by dotnet-bump-version.

* Canary Build 9 (#2104)

* Added another unit test

* Changed how we create cover images to force the aspect ratio, which allows for Kavita to do some extra work later down the line. Prevents skewing from comic sources.

* Code cleanup

* Updated signatures to explicitly indicate they return a physical file.

* Refactored the GA to be a bit more streamlined.

* Fixed up how after cover conversion, how we refresh volume and series image links.

* Undid the PhysicalFileResult stuff.

* Fixed an issue in the epub reader where html tags within an anchor could break the navigation code for inner-links.

* Fixed a bug in GetContinueChapter where a special could appear ahead of a loose leaf chapter.

* Optimized aspect ratios for custom library images to avoid shift layout.

Moved the series detail page down a bit to be inline with first row of actionables.

* Finally fixed the media conversion issue where volumes and series wouldn't get their file links updated.

* Added some new layout for license to allow a user to buy a sub after their last sub expired.

* Added more metrics for fingerprinting to test on docker.

* Tried to fix a bug with getnextchapter looping incorrectly, but unable to solve.

* Cleanup some UI stuff to reduce bad calls.

* Suppress annoying issues with reaching K+ when it's down (only affects local builds)

* Fixed an edge case bug for picking the correct cover image for a series.

* Fixed a bug where typeahead x wouldn't clear out the input field.

* Renamed Clear -> Reset for metadata filter to be more informative of its function.

* Don't allow duplicates for reading list characters.

* Fixed a bug where when calculating recently updated, series with the same name but different libraries could get grouped.

* Fixed an issue with fit to height where there could still be a small amount of scroll due to a timing issue with the image loading.

* Don't show a loading if the user doesn't have a license for external ratings

* Fixed bad stat url

* Fixed up licensing to make it so you have to email me to get a sub renewed.

* Updated deps

* When scrobbling reading events, recalculate the highest chapter/volume during processing.

* Code cleanup

* Disabled some old test code that is likely not needed as it breaks a lot on netvips updates

* Bump versions by dotnet-bump-version.

* Canary Build 10 (#2105)

* Aligned fingerprint to be unique

* Updated email button to have a template

* Fixed inability to progress to next chapter when last page is a spread and user is using split rendering.

* Attempted fix at the column reader cutting off parts of the words. Can't fully reproduce, but added a bit of padding to help.

* Aligned AniList icon to match that of weblinks.

* Bump versions by dotnet-bump-version.

* Canary Build 11 (#2108)

* Fixed an issue with continuous reader in manga reader.

* Aligned KavitaPlus->Kavita+

* Updated the readme

* Adjusted first time registration messaging.

* Fixed a bug where having just one type of weblink could cause a bad recommendation lookup

* Removed manual invocation of scrobbling as testing is over for that feature.

* Fixed a bad observerable for downloading logs from browser.

* Don't get reviews/recs for comic libraries. Override user selection for scrobbling on Comics since there are no places to scrobble to.

* Added a migration so all existing comic libraries will have scrobbling turned off.

* Don't allow the UI to toggle scrobbling on a library with no providers.

* Refactored the code to not throw generic 500 toasts on the UI. Added the ability to clear your license on Kavita side.

* Converted reader settings to new accordion format.

* Converted user preferences to new accordion format.

* I couldn't convert CBL Reading modal to new accordion directives due to some weird bug.

* Migrated the whole application to standalone components. This fixes the download progress bar not showing up.

* Hooked up the ability to have reading list generate random items. Removed the old code as it's no longer needed.

* Added random covers for collection's as well.

* Added a speed up to not regenerate merged covers if we've already created them.

* Fixed an issue where tooltips weren't styled correctly after updating a library. Migrated Library access modal to OnPush.

* Fixed broken table styling. Fixed grid breakpoint css variables not using the ones from variables due to a missing import.

* Misc fixes around tables and some api doc cleanup

* Fixed a bug where when switching from webtoon back to a non-webtoon reading mode, if the browser size isn't large enough for double, the reader wouldn't go to single mode.

* When combining external recs, normalize names to filter out differences, like capitalization.

* Finally get to update ExCSS to the latest version! This adds much more css properties for epubs.

* Ensure rejected reviews are saved as errors

* A crap ton of code cleanup

* Cleaned up some equality code in GenreHelper.cs

* Fixed up the table styling after the bootstrap update changed it.

* Bump versions by dotnet-bump-version.

* Canary Build 12 (#2111)

* Aligned GA (#2059)

* Fixed the code around merging images to resize them. This will only look correct if this release's cover generation runs.

* Misc code cleanup

* Fixed an issue with epub column layout cutting off text

* Collection detail page will now default sort by sort name.

* Explicitly lazy load library icon images.

* Make sure the full error message can be passed to the license component/user.

* Use WhereIf in some places

* Changed the hash util code for unraid again

* Fixed up an issue with split render mode where last page wouldn't move into the next chapter.

* Bump versions by dotnet-bump-version.

* Don't ask me how, but i think I fixed the epub cutoff issue (#2112)

* Bump versions by dotnet-bump-version.

* Canary 14 (#2113)

* Switched how we build the unraid fingerprint.

* Fixed a bit of space below the image on fit to height

* Removed some bad code

* Bump versions by dotnet-bump-version.

* Canary Build 15 (#2114)

* When performing a scan series, force a recount of words/pages to ensure read time gets updated.

* Fixed broken download logs button (develop)

* Sped up the query for getting libraries and added caching for that api, which is helpful for users with larger library counts.

* Fixed an issue in directory picker where if you had two folders with the same name, the 2nd to last wouldn't be clickable.

* Added more destroy ref stuff.

* Switched the buy/manage links over to be environment specific.

* Bump versions by dotnet-bump-version.

* Canary Build 16 (#2115)

* Added the promo code for K+ and version bump.

* Don't show see more if there isn't more to see on series detail.

* Bump versions by dotnet-bump-version.

* Last Build (#2116)

* Merge

* Close the view after removing a license key from server.

* Bump versions by dotnet-bump-version.

* Reset version to v0.7.4 for merge.

* Bump versions by dotnet-bump-version.

* Cleanup from the Release (#2127)

* Added an FAQ link on the Kavita+ tab.

* Don't query Kavita+ for ratings on comic libraries as there is no upstream provider yet.

* Jumpbar keys are a little hard to click

* Fixed an issue where libraries that don't allow scrobbling could be scrobbled when generating past history with read events.

* Made the min/max release year on metadata filter number and removed the spin arrows for styling.

* Fixed disable tabs color contrast due to bootstrap undocumented change.

* Refactored whole codebase to unify caching mechanism. Upped the default cache memory amount to 75 to account for the extra data load. Still LRU.

Fixed an issue where Cache key was using Port instead.

Refactored all the Configuration code to use strongly typed deserialization.

* Fixed an issue where get latest progress would throw an exception if there was no progress due to LINQ and MAX query.

* Fixed a bug where Send to Device wasn't present on Series cards.

* Hooked up the ability to change the cache size for Kavita via the UI.

* Bump versions by dotnet-bump-version.

* Overall Ratings (#2129)

* Corrected tooltip for Cache

* Ensure we sync the DB to what's in appsettings.json for Cache key.

* Change the fingerprinting method for Windows installs exclusively to avoid churn due to how security updates are handled.

* Hooked up the ability to see where reviews are from via an icon on the review card, rather than having to click or know that MAL has "external Review" as title.

* Updated FAQ for Kavita+ to link directly to the FAQ

* Added the ability for all ratings on a series to be shown to the user.

Added favorite count on AL and MAL

* Cleaned up so the check for Kavita+ license doesn't seem like it's running when no license is registered.

* Tweaked the test instance buy link to test new product.

* Bump versions by dotnet-bump-version.

* Remove From On Deck (#2131)

* Allow admins to customize the amount of progress time or last item added time for on deck calculation

* Implemented the ability to remove series from on deck. They will be removed until the user reads a new chapter.

Quite a few db lookup reduction calls for reading based stuff, like continue point, bookmarks, etc.

* Bump versions by dotnet-bump-version.

* Preparation for Release (#2135)

* Don't allow Comic libraries to do any scrobbling as there aren't any Comic scrobbling providers yet.

* Fixed a bug where if you have multiple libraries pointing the same folder (for whatever reason), the Scan Folder api could be rejected.

* Handle if publication from an epub is empty to avoid a bad parse error

* Cleaned up some hardcoded default strings.

* Fixed up some defaulting code for the cache size.

* Changed how moving something back to on deck works after it's been removed. Now any progress will trigger it, as epubs don't have chapters.

* Ignore .caltrash, which is a Calibre managed folder, when scanning.

* Added the ability to see Volume Last Read Date (or individual chapter) in details drawer. Hover over the clock for the full timestamp.

* Bump versions by dotnet-bump-version.

* Forgot 2 files in last PR (#2136)

* Don't allow Comic libraries to do any scrobbling as there aren't any Comic scrobbling providers yet.

* Fixed a bug where if you have multiple libraries pointing the same folder (for whatever reason), the Scan Folder api could be rejected.

* Handle if publication from an epub is empty to avoid a bad parse error

* Cleaned up some hardcoded default strings.

* Fixed up some defaulting code for the cache size.

* Changed how moving something back to on deck works after it's been removed. Now any progress will trigger it, as epubs don't have chapters.

* Ignore .caltrash, which is a Calibre managed folder, when scanning.

* Added the ability to see Volume Last Read Date (or individual chapter) in details drawer. Hover over the clock for the full timestamp.

* Somehow some files got left off the commit

* Bump versions by dotnet-bump-version.

* Changed the fingerprinting code for Kavita+. Optimized System tab to be way faster. (#2140)

* Bump versions by dotnet-bump-version.

* Version bump (#2141)

* Bump versions by dotnet-bump-version.

* Personal Table of Contents (#2148)

* Fixed a bad default setting for token key

* Changed the payment link to support Google Pay

* Fixed duplicate events occurring on newly added series from a scan.

Fixed the version update code from not firing and made it check every 4-6 hours (random per user per restart)

* Check for new releases on startup as well.

Added Personal Table of Contents (called Bookmarks on epub and pdf reader). The idea is that sometimes you want to bookmark certain parts of pages to get back to quickly later. This mechanism will allow you to do that without having to edit the underlying ToC.

* Added a button to update modal to show how to update for those unaware.

* Darkened the link text within tables to be more visible.

* Update link for how to update now is dynamic for docker users

* Refactored to send proper star/end dates for scrobble read events for upcoming changes in the API.

Added GoogleBooks Rating UI code if I go forward with API changes.

* When Scrobbling, send when the first and last progress for the series was.

Added OpenLibrary icon for upcoming enhancements for Kavita+.

Changed the Update checker to execute at start.

* Fixed backups not saving favicons in the correct place

* Refactored the layout code for Personal ToC

* More bugfixes around toc

* Box alignment

* Fixed up closing the overlay when bookmark mode is active

* Fixed up closing the overlay when bookmark mode is active

---------

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>

* Bump versions by dotnet-bump-version.

* Add files via upload (#2149)

* Bump versions by dotnet-bump-version.

* Misc Fixes (#2155)

* Fixed default token key not being long enough and Kavita auto-generating

* When scheduling nightly backup job, make it run at 2am to ensure everything else has ran.

* Made the overlay system work better on mobile. In order to do this, had to implement my own copy button.

* Tweaked the code to ensure we clear the selection doing anything and clicking off the overlay clears more reliably.

* Cleaned up the overlay code

* Added the ability to view the series that a rating is representing. Requires Kavita+ deployment.

* When calculating overall average rating of server, if only review is yours, don't include it.

When calculating overall average rating of server, scale to percentage (* 20) to match all other rating scales.

* Fixed side nav on mobile without donate link not fully covering the height of the screen

* Only trigger the task conversion warning on Media screen if you've touched the appropriate control.

* Fixed a bug where bookmark directory wasn't able to be changed.

* Fixed a bug where see More wouldn't show if there were just characters due to missing that check.

* Fixed a typo in documentation

* If a chapter has a range 1-6 and is fully read, when calculating highest chapter for Scrobbling, use the 6.

* Bump versions by dotnet-bump-version.

* Epub Reading Overlay Re-Design (#2156)

* Removed DeviceId

* Dependency updates part 1

* Dependency updates part 2

* Dependency updates part 3

* Dependency updates part 4

* Dependency updates done. Updated all backend and UI ones.

* Refactored the book line overlay to sit at the top of the reader. It looks much better and will work a lot better for future work.

* Removed an event that was causing series detail to load extra data when it didn't need to after editing series metadata.

* Removed one more load request on series detail after updating edit series modal.

* Bump versions by dotnet-bump-version.

* Removed manual Migrate Series Relations migration from v0.7 release (5 releases ago). (#2158)

Don't try to backup the DB if it doesn't exist. This will stop errors in log on first start.

* Bump versions by dotnet-bump-version.

* Rating Overhaul (#2159)

* Switched Ratings to a float system. Allow rating something as 0%. Allow half step ratings. Added new css variable: --rating-star-color. By default, N/A will show for series that have no ratings. N/A ratings are not included in overall rating calculations.

* Show extended entity properties on desktop for list view cards.

* Refactored the code for series metadata detail to use a re-usable component to reduce the copy/paste for the Genres tags like sections.

* List Item will show extended properties about a chapter/volume, like weblinks on Desktop viewports.

* Refactored even further so all of series detail uses the same component code. Tweaked the spacing on the series detail area.

List items will now show Characters and Tags which are helpful for more Hentai related content.

* Fixed a bug with removing something from "OnDeckRemoval" table when something was read.

* Bump versions by dotnet-bump-version.

* Few fixes from last PR (#2160)

* Added a migration for existing ratings

* Fixed duplicating web links and changed so it has the see more functionality.

* One more unit test

* Bump versions by dotnet-bump-version.

* v0.7.6 - Personal Table of Contents + Rating Overhaul (#2169)

* Nothing changed, this is just to retrigger a stable build. (#1967)

* v0.7.3 - The Quality of Life Update  (#2036)

* Version bump

* Okay this should be the last (#2037)

* Fixed improper date visualization for reading list detail page.

* Correct not-read badge position (#2034)

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* Bump versions by dotnet-bump-version.

* Merged develop in

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* v0.7.3 - The Quality of Life Update (#2041)

* Report Media Issues (#1964)

* Started working on a report problems implementation.

* Started code

* Added logging to book and archive service.

* Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point.

* Added basic implementation for media errors.

* MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan.

* Fixed unit tests

* Basic code in place to view and clear. Just UI Cleanup needed.

* Slight css upgrade

* Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working.

* Fixed unit tests

* Fixed unit tests for real

* Bump versions by dotnet-bump-version.

* Expanded Metadata for EPUBs (#1965)

* Fixed a bug breaking ability to save server settings

* Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why

* Refactored the code to clean it up

* Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq.

* ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81

* Double include author for epub parsing and let the People code handle removing duplicates.

* Bump versions by dotnet-bump-version.

* Nothing changed, this is just to retrigger a stable build. (#1967) (#1968)

* Adding paper book reader theme (#1976)

* Adding paper book reader theme

# Added
- Added: Paper book reader theme

* Fixing some leftover styles

* adding book emulation to 2column layout for paper style

* Adding migrations

* removing migration and compressing image

* Reverting DataContextModelSnapshot

* checking out datacontextmodelsnapshot file

* Bump versions by dotnet-bump-version.

* Web Links (#1983)

* Updated dependencies

* Updated the default key to be 256 bits to meet security requirements.

* Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes.

* Implemented ability to see links and click on them for an individual chapter.

* Hooked up the ability to set Series web links.

* Render out the web link

* Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default.

* Added Robbie's nice error weblink fallbacks.

* Bump versions by dotnet-bump-version.

* Updated Docker entrypoint (#1984)

* Bump versions by dotnet-bump-version.

* ISBN Support (#1985)

* Fixed a bug where weblinks would always show

* Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell.

* Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10.

* Fixed Favicon not working on anything but windows

* Implemented ISBN support into Kavita

* Don't round so much when transforming bytes

* Bump versions by dotnet-bump-version.

* AVIF Support & Much More! (#1992)

* Expand the list of potential favicon icons to grab.

* Added a url mapping functionality to use alternative urls for fetching icons

* Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes.

* Started refactoring code so that webp queries use encoding format instead.

* More refactoring to remove hardcoded webp references.

* Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys.

* Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot.

* Make favicon encode setting aware

* Cleaned up favicon conversion

* Updated format counter to now just use Extension from MangaFile now that it's been out a while.

* Tweaked jumpbar code to reduce a lookup to hashmap.

* Added AVIF (8-bit only) support.

* In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed.

* You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend.

* Forgot a file

* Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont.

* Fixed Refresh token using wrong Claim to look up the user.

* Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated.

* Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures.

* Bump versions by dotnet-bump-version.

* More Fixes (#1993)

* Strip just isbn: from epub isbns and log when it's back (books)

* Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita.

* Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords.

* Show the media issue count before you open accordion

* Added a inpage filter for Media issues

* Cleanup styles

* Fixed up some code in epub isbn parsing when it's null

* Encode filenames when downloading so that non english characters can be passed properly to UI.

* Added support to parse ComicInfo's with Empty Tags.

* Reset development settings.

* Tweaked the code in generating reading lists to avoid extra work when not needed.

* Fix comicvine's favicon

* Fixed up a unit test

* Tweaked the favicon code to ignore icons that have query parameters

* More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already.

* Added a note for later

* Fixed stats server url

* Added more debugging

* Fixed unit tests

* Bump versions by dotnet-bump-version.

* More Fixes from Recent PRs (#1995)

* Added extra debugging for logout issue

* Fixed the null issue with ISBN

* Allow web links to be cleared out

* More logging on refresh token

* More key fallback when building Table of Contents

* Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced.

* Updated dependencies

* Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well.

* Bump versions by dotnet-bump-version.

* Fixed a bug with config (#1996)

* Bump versions by dotnet-bump-version.

* Changed IsDocker check (#1998)

* Refactored IsDocker to be completely static and changed to use an environment variable instead.

* Removed file from another branch

* Bump versions by dotnet-bump-version.

* Migrated up to VersOne 3.3 with epub 3.3 support. (#1999)

This enables collection and reading list support from epubs.

* Bump versions by dotnet-bump-version.

* More Bugfixes (EPUB Mainly) (#2004)

* Fixed an issue with downloading where spaces turned into plus signs.

* If the refresh token is invalid, but the auth token still has life in it, don't invalidate.

* Fixed docker users unable to save settings

* Show a default error icon until favicon loads

* Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2.

* Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml)

* Added another hack to massage key to page lookups when rewriting anchors.

* Cleaned up debugging notes

* Bump versions by dotnet-bump-version.

* More Polish  (#2005)

* Implemented sort title extraction from epub 3 files.

* Added link to wiki for media errors

* Fixed the hack to reduce JWT refresh token expiration

* Fixed up a case where favicon downloading wasn't correcting links that started with // correctly.

Added a fallback for sites that just don't pngs available.

* Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community.

* Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler.

* Bump versions by dotnet-bump-version.

* Angular 16 (#2007)

* Removed adv, which isn't needed.

* Updated zone

* Updated to angular 16

* Updated to angular 16 (partially)

* Updated to angular 16

* Package update for Angular 16 (and other dependencies) is complete.

* Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed()

* Updated all inputs that have ! to be required and deleted all unit tests.

* Corrected how takeUntilDestroyed() is supposed to be implemented.

* Bump versions by dotnet-bump-version.

* Pipeline adjustment for Angular 16 (#2008)

* Bump versions by dotnet-bump-version.

* Try a different build (#2009)

* Bump versions by dotnet-bump-version.

* Continue Reading Bugfix (#2010)

* Fixed an edge case where continue point wasn't considering any chapters that had progress.

Continue point is now slightly faster and uses less memory.

* Added a unit test for a user's case. Still not reproducible

* Bump versions by dotnet-bump-version.

* Ensure chapters are sorted when getting continue point (#2011)

Fixes new behaviour in #1625

* Bump versions by dotnet-bump-version.

* Strip more forms of comments from CSS before parsing/inlining. (#2014)

Handle if ExCSS throws an exception during inlining and attempt to fallback to scoping css instead of inlining.

I still cannot update past ExCSS v4.1.0 else NPEs for common css will be thrown.

* Bump versions by dotnet-bump-version.

* Misc Changes (#2015)

* Updated ng-bootstrap

* Fixed an issue where jumpbar would be disabled when it shouldn't have been.

* When there are duplicate files that make up a volume, show the count on series detail.

* Added basic ISBN searching which will return a chapter back.

* Bump versions by dotnet-bump-version.

* Fixed count for cards (#2016)

* Bump versions by dotnet-bump-version.

* Last Release before Release Testing (#2017)

* Attempting to invalidate JWT on login (when locked out), but can't figure a way to get a JWT, since we don't store them.

Just committing as I'm going to remove the middleware, this is not worth the performance and complexity.

* Removed some security stuff that didn't line up.

* Dropping Token Expiration down to 2 days to test during release testing.

* Bump versions by dotnet-bump-version.

* Removed old migrations for Kavita startup. Only migrations from v0.7.2 onwards are present. (#2019)

* Bump versions by dotnet-bump-version.

* Fixed up jumpbar not properly disabling/enabling (#2022)

* Bump versions by dotnet-bump-version.

* Fix StoryArc & StoryArcNumber mismatch (#2018)

* Ensure StoryArc and StoryArcNumber are max length

* Trim StoryArc to remove excess spaces.

* Replaced with cleaner approach.

* Update with majora2007 recommendations

* Bump versions by dotnet-bump-version.

* Last fixes before release (#2027)

* Disable login button when a login is in-progress. This will help prevent spamming when internet is slow.

* Fixed a bug where an empty space could cause an error when creating a library.

* Apply Split Options throughout the codebase to add extra safe-guard on empty spaces and ensure trimming.

* Bump versions by dotnet-bump-version.

* Added NoContent responses when APIs don't find entities (#2028)

* Bump versions by dotnet-bump-version.

* Few More Fixes (#2032)

* Fixed spreads stretching on PC

* Fixed a bug where reading list dates couldn't be cleared out.

* Reading list page refreshes after updating info in the modal

* Fixed an issue where create library wouldn't take into account advanced settings.

* Fixed an issue where selection of the first chapter of a series to pull series-level metadata could fail in cases where you had Volume 2 and Chapter 1, Volume 2 would be selected.

* Bump versions by dotnet-bump-version.

* Fixed a bug where scan series wouldn't trigger word count analysis nor cover generation. (#2035)

* Bump versions by dotnet-bump-version.

* Okay this should be the last (#2037)

* Fixed improper date visualization for reading list detail page.

* Correct not-read badge position (#2034)

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* Bump versions by dotnet-bump-version.

* Fixed a bug where reading list month wasn't rendering correctly (#2039)

* Bump versions by dotnet-bump-version.

* Version bump (#2040)

* Bump versions by dotnet-bump-version.

* Fixed bug in CI pipeline for main

---------

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
Co-authored-by: Chris Plaatjes <kizaing@gmail.com>
Co-authored-by: pssandhu <pssandhu@users.noreply.github.com>
Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com>
Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* v0.7.3.1 Hotfix (#2053)

* Report Media Issues (#1964)

* Started working on a report problems implementation.

* Started code

* Added logging to book and archive service.

* Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point.

* Added basic implementation for media errors.

* MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan.

* Fixed unit tests

* Basic code in place to view and clear. Just UI Cleanup needed.

* Slight css upgrade

* Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working.

* Fixed unit tests

* Fixed unit tests for real

* Bump versions by dotnet-bump-version.

* Expanded Metadata for EPUBs (#1965)

* Fixed a bug breaking ability to save server settings

* Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why

* Refactored the code to clean it up

* Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq.

* ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81

* Double include author for epub parsing and let the People code handle removing duplicates.

* Bump versions by dotnet-bump-version.

* Nothing changed, this is just to retrigger a stable build. (#1967) (#1968)

* Adding paper book reader theme (#1976)

* Adding paper book reader theme

# Added
- Added: Paper book reader theme

* Fixing some leftover styles

* adding book emulation to 2column layout for paper style

* Adding migrations

* removing migration and compressing image

* Reverting DataContextModelSnapshot

* checking out datacontextmodelsnapshot file

* Bump versions by dotnet-bump-version.

* Web Links (#1983)

* Updated dependencies

* Updated the default key to be 256 bits to meet security requirements.

* Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes.

* Implemented ability to see links and click on them for an individual chapter.

* Hooked up the ability to set Series web links.

* Render out the web link

* Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default.

* Added Robbie's nice error weblink fallbacks.

* Bump versions by dotnet-bump-version.

* Updated Docker entrypoint (#1984)

* Bump versions by dotnet-bump-version.

* ISBN Support (#1985)

* Fixed a bug where weblinks would always show

* Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell.

* Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10.

* Fixed Favicon not working on anything but windows

* Implemented ISBN support into Kavita

* Don't round so much when transforming bytes

* Bump versions by dotnet-bump-version.

* AVIF Support & Much More! (#1992)

* Expand the list of potential favicon icons to grab.

* Added a url mapping functionality to use alternative urls for fetching icons

* Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes.

* Started refactoring code so that webp queries use encoding format instead.

* More refactoring to remove hardcoded webp references.

* Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys.

* Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot.

* Make favicon encode setting aware

* Cleaned up favicon conversion

* Updated format counter to now just use Extension from MangaFile now that it's been out a while.

* Tweaked jumpbar code to reduce a lookup to hashmap.

* Added AVIF (8-bit only) support.

* In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed.

* You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend.

* Forgot a file

* Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont.

* Fixed Refresh token using wrong Claim to look up the user.

* Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated.

* Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures.

* Bump versions by dotnet-bump-version.

* More Fixes (#1993)

* Strip just isbn: from epub isbns and log when it's back (books)

* Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita.

* Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords.

* Show the media issue count before you open accordion

* Added a inpage filter for Media issues

* Cleanup styles

* Fixed up some code in epub isbn parsing when it's null

* Encode filenames when downloading so that non english characters can be passed properly to UI.

* Added support to parse ComicInfo's with Empty Tags.

* Reset development settings.

* Tweaked the code in generating reading lists to avoid extra work when not needed.

* Fix comicvine's favicon

* Fixed up a unit test

* Tweaked the favicon code to ignore icons that have query parameters

* More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already.

* Added a note for later

* Fixed stats server url

* Added more debugging

* Fixed unit tests

* Bump versions by dotnet-bump-version.

* More Fixes from Recent PRs (#1995)

* Added extra debugging for logout issue

* Fixed the null issue with ISBN

* Allow web links to be cleared out

* More logging on refresh token

* More key fallback when building Table of Contents

* Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced.

* Updated dependencies

* Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well.

* Bump versions by dotnet-bump-version.

* Fixed a bug with config (#1996)

* Bump versions by dotnet-bump-version.

* Changed IsDocker check (#1998)

* Refactored IsDocker to be completely static and changed to use an environment variable instead.

* Removed file from another branch

* Bump versions by dotnet-bump-version.

* Migrated up to VersOne 3.3 with epub 3.3 support. (#1999)

This enables collection and reading list support from epubs.

* Bump versions by dotnet-bump-version.

* More Bugfixes (EPUB Mainly) (#2004)

* Fixed an issue with downloading where spaces turned into plus signs.

* If the refresh token is invalid, but the auth token still has life in it, don't invalidate.

* Fixed docker users unable to save settings

* Show a default error icon until favicon loads

* Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2.

* Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml)

* Added another hack to massage key to page lookups when rewriting anchors.

* Cleaned up debugging notes

* Bump versions by dotnet-bump-version.

* More Polish  (#2005)

* Implemented sort title extraction from epub 3 files.

* Added link to wiki for media errors

* Fixed the hack to reduce JWT refresh token expiration

* Fixed up a case where favicon downloading wasn't correcting links that started with // correctly.

Added a fallback for sites that just don't pngs available.

* Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community.

* Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler.

* Bump versions by dotnet-bump-version.

* Angular 16 (#2007)

* Removed adv, which isn't needed.

* Updated zone

* Updated to angular 16

* Updated to angular 16 (partially)

* Updated to angular 16

* Package update for Angular 16 (and other dependencies) is complete.

* Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed()

* Updated all inputs that have ! to be required and deleted all unit tests.

* Corrected how takeUntilDestroyed() is supposed to be implemented.

* Bump versions by dotnet-bump-version.

* Pipeline adjustment for Angular 16 (#2008)

* Bump versions by dotnet-bump-version.

* Try a different build (#2009)

* Bump versions by dotnet-bump-version.

* Continue Reading Bugfix (#2010)

* Fixed an edge case where continue point wasn't considering any chapters that had progress.

Continue point is now slightly faster and uses less memory.

* Added a unit test for a user's case. Still not reproducible

* Bump versions by dotnet-bump-version.

* Ensure chapters are sorted when getting continue point (#2011)

Fixes new behaviour in #1625

* Bump versions by dotnet-bump-version.

* Strip more forms of comments from CSS before parsing/inlining. (#2014)

Handle if ExCSS throws an exception during inlining and attempt to fallback to scoping css instead of inlining.

I still cannot update past ExCSS v4.1.0 else NPEs for common css will be thrown.

* Bump versions by dotnet-bump-version.

* Misc Changes (#2015)

* Updated ng-bootstrap

* Fixed an issue where jumpbar would be disabled when it shouldn't have been.

* When there are duplicate files that make up a volume, show the count on series detail.

* Added basic ISBN searching which will return a chapter back.

* Bump versions by dotnet-bump-version.

* Fixed count for cards (#2016)

* Bump versions by dotnet-bump-version.

* Last Release before Release Testing (#2017)

* Attempting to invalidate JWT on login (when locked out), but can't figure a way to get a JWT, since we don't store them.

Just committing as I'm going to remove the middleware, this is not worth the performance and complexity.

* Removed some security stuff that didn't line up.

* Dropping Token Expiration down to 2 days to test during release testing.

* Bump versions by dotnet-bump-version.

* Removed old migrations for Kavita startup. Only migrations from v0.7.2 onwards are present. (#2019)

* Bump versions by dotnet-bump-version.

* Fixed up jumpbar not properly disabling/enabling (#2022)

* Bump versions by dotnet-bump-version.

* Fix StoryArc & StoryArcNumber mismatch (#2018)

* Ensure StoryArc and StoryArcNumber are max length

* Trim StoryArc to remove excess spaces.

* Replaced with cleaner approach.

* Update with majora2007 recommendations

* Bump versions by dotnet-bump-version.

* Last fixes before release (#2027)

* Disable login button when a login is in-progress. This will help prevent spamming when internet is slow.

* Fixed a bug where an empty space could cause an error when creating a library.

* Apply Split Options throughout the codebase to add extra safe-guard on empty spaces and ensure trimming.

* Bump versions by dotnet-bump-version.

* Added NoContent responses when APIs don't find entities (#2028)

* Bump versions by dotnet-bump-version.

* Few More Fixes (#2032)

* Fixed spreads stretching on PC

* Fixed a bug where reading list dates couldn't be cleared out.

* Reading list page refreshes after updating info in the modal

* Fixed an issue where create library wouldn't take into account advanced settings.

* Fixed an issue where selection of the first chapter of a series to pull series-level metadata could fail in cases where you had Volume 2 and Chapter 1, Volume 2 would be selected.

* Bump versions by dotnet-bump-version.

* Fixed a bug where scan series wouldn't trigger word count analysis nor cover generation. (#2035)

* Bump versions by dotnet-bump-version.

* Okay this should be the last (#2037)

* Fixed improper date visualization for reading list detail page.

* Correct not-read badge position (#2034)

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* Bump versions by dotnet-bump-version.

* Fixed a bug where reading list month wasn't rendering correctly (#2039)

* Bump versions by dotnet-bump-version.

* Version bump (#2040)

* Bump versions by dotnet-bump-version.

* Bugfixes for a hotfix (#2052)

* Nothing changed, this is just to retrigger a stable build. (#1967)

* v0.7.3 - The Quality of Life Update  (#2036)

* Version bump

* Okay this should be the last (#2037)

* Fixed improper date visualization for reading list detail page.

* Correct not-read badge position (#2034)

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* Bump versions by dotnet-bump-version.

* Merged develop in

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* v0.7.3 - The Quality of Life Update (#2041)

* Report Media Issues (#1964)

* Started working on a report problems implementation.

* Started code

* Added logging to book and archive service.

* Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point.

* Added basic implementation for media errors.

* MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan.

* Fixed unit tests

* Basic code in place to view and clear. Just UI Cleanup needed.

* Slight css upgrade

* Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working.

* Fixed unit tests

* Fixed unit tests for real

* Bump versions by dotnet-bump-version.

* Expanded Metadata for EPUBs (#1965)

* Fixed a bug breaking ability to save server settings

* Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why

* Refactored the code to clean it up

* Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq.

* ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81

* Double include author for epub parsing and let the People code handle removing duplicates.

* Bump versions by dotnet-bump-version.

* Nothing changed, this is just to retrigger a stable build. (#1967) (#1968)

* Adding paper book reader theme (#1976)

* Adding paper book reader theme

# Added
- Added: Paper book reader theme

* Fixing some leftover styles

* adding book emulation to 2column layout for paper style

* Adding migrations

* removing migration and compressing image

* Reverting DataContextModelSnapshot

* checking out datacontextmodelsnapshot file

* Bump versions by dotnet-bump-version.

* Web Links (#1983)

* Updated dependencies

* Updated the default key to be 256 bits to meet security requirements.

* Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes.

* Implemented ability to see links and click on them for an individual chapter.

* Hooked up the ability to set Series web links.

* Render out the web link

* Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default.

* Added Robbie's nice error weblink fallbacks.

* Bump versions by dotnet-bump-version.

* Updated Docker entrypoint (#1984)

* Bump versions by dotnet-bump-version.

* ISBN Support (#1985)

* Fixed a bug where weblinks would always show

* Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell.

* Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10.

* Fixed Favicon not working on anything but windows

* Implemented ISBN support into Kavita

* Don't round so much when transforming bytes

* Bump versions by dotnet-bump-version.

* AVIF Support & Much More! (#1992)

* Expand the list of potential favicon icons to grab.

* Added a url mapping functionality to use alternative urls for fetching icons

* Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes.

* Started refactoring code so that webp queries use encoding format instead.

* More refactoring to remove hardcoded webp references.

* Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys.

* Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot.

* Make favicon encode setting aware

* Cleaned up favicon conversion

* Updated format counter to now just use Extension from MangaFile now that it's been out a while.

* Tweaked jumpbar code to reduce a lookup to hashmap.

* Added AVIF (8-bit only) support.

* In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed.

* You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend.

* Forgot a file

* Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont.

* Fixed Refresh token using wrong Claim to look up the user.

* Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated.

* Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures.

* Bump versions by dotnet-bump-version.

* More Fixes (#1993)

* Strip just isbn: from epub isbns and log when it's back (books)

* Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita.

* Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords.

* Show the media issue count before you open accordion

* Added a inpage filter for Media issues

* Cleanup styles

* Fixed up some code in epub isbn parsing when it's null

* Encode filenames when downloading so that non english characters can be passed properly to UI.

* Added support to parse ComicInfo's with Empty Tags.

* Reset development settings.

* Tweaked the code in generating reading lists to avoid extra work when not needed.

* Fix comicvine's favicon

* Fixed up a unit test

* Tweaked the favicon code to ignore icons that have query parameters

* More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already.

* Added a note for later

* Fixed stats server url

* Added more debugging

* Fixed unit tests

* Bump versions by dotnet-bump-version.

* More Fixes from Recent PRs (#1995)

* Added extra debugging for logout issue

* Fixed the null issue with ISBN

* Allow web links to be cleared out

* More logging on refresh token

* More key fallback when building Table of Contents

* Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced.

* Updated dependencies

* Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well.

* Bump versions by dotnet-bump-version.

* Fixed a bug with config (#1996)

* Bump versions by dotnet-bump-version.

* Changed IsDocker check (#1998)

* Refactored IsDocker to be completely static and changed to use an environment variable instead.

* Removed file from another branch

* Bump versions by dotnet-bump-version.

* Migrated up to VersOne 3.3 with epub 3.3 support. (#1999)

This enables collection and reading list support from epubs.

* Bump versions by dotnet-bump-version.

* More Bugfixes (EPUB Mainly) (#2004)

* Fixed an issue with downloading where spaces turned into plus signs.

* If the refresh token is invalid, but the auth token still has life in it, don't invalidate.

* Fixed docker users unable to save settings

* Show a default error icon until favicon loads

* Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2.

* Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml)

* Added another hack to massage key to page lookups when rewriting anchors.

* Cleaned up debugging notes

* Bump versions by dotnet-bump-version.

* More Polish  (#2005)

* Implemented sort title extraction from epub 3 files.

* Added link to wiki for media errors

* Fixed the hack to reduce JWT refresh token expiration

* Fixed up a case where favicon downloading wasn't correcting links that started with // correctly.

Added a fallback for sites that just don't pngs available.

* Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community.

* Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler.

* Bump versions by dotnet-bump-version.

* Angular 16 (#2007)

* Removed adv, which isn't needed.

* Updated zone

* Updated to angular 16

* Updated to angular 16 (partially)

* Updated to angular 16

* Package update for Angular 16 (and other dependencies) is complete.

* Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed()

* Updated all inputs that have ! to be required and deleted all unit tests.

* Corrected how takeUntilDestroyed() is supposed to be implemented.

* Bump versions by dotnet-bump-version.

* Pipeline adjustment for Angular 16 (#2008)

* Bump versions by dotnet-bump-version.

* Try a different build (#2009)

* Bump versions by dotnet-bump-version.

* Continue Reading Bugfix (#2010)

* Fixed an edge case where continue point wasn't considering any chapters that had progress.

Continue point is now slightly faster and uses less memory.

* Added a unit test for a user's case. Still not reproducible

* Bump versions by dotnet-bump-version.

* Ensure chapters are sorted when getting continue point (#2011)

Fixes new behaviour in #1625

* Bump versions by dotnet-bump-version.

* Strip more forms of comments from CSS before parsing/inlining. (#2014)

Handle if ExCSS throws an exception during inlining and attempt to fallback to scoping css instead of inlining.

I still cannot update past ExCSS v4.1.0 else NPEs for common css will be thrown.

* Bump versions by dotnet-bump-version.

* Misc Changes (#2015)

* Updated ng-bootstrap

* Fixed an issue where jumpbar would be disabled when it shouldn't have been.

* When there are duplicate files that make up a volume, show the count on series detail.

* Added basic ISBN searching which will return a chapter back.

* Bump versions by dotnet-bump-version.

* Fixed count for cards (#2016)

* Bump versions by dotnet-bump-version.

* Last Release before Release Testing (#2017)

* Attempting to invalidate JWT on login (when locked out), but can't figure a way to get a JWT, since we don't store them.

Just committing as I'm going to remove the middleware, this is not worth the performance and complexity.

* Removed some security stuff that didn't line up.

* Dropping Token Expiration down to 2 days to test during release testing.

* Bump versions by dotnet-bump-version.

* Removed old migrations for Kavita startup. Only migrations from v0.7.2 onwards are present. (#2019)

* Bump versions by dotnet-bump-version.

* Fixed up jumpbar not properly disabling/enabling (#2022)

* Bump versions by dotnet-bump-version.

* Fix StoryArc & StoryArcNumber mismatch (#2018)

* Ensure StoryArc and StoryArcNumber are max length

* Trim StoryArc to remove excess spaces.

* Replaced with cleaner approach.

* Update with majora2007 recommendations

* Bump versions by dotnet-bump-version.

* Last fixes before release (#2027)

* Disable login button when a login is in-progress. This will help prevent spamming when internet is slow.

* Fixed a bug where an empty space could cause an error when creating a library.

* Apply Split Options throughout the codebase to add extra safe-guard on empty spaces and ensure trimming.

* Bump versions by dotnet-bump-version.

* Added NoContent responses when APIs don't find entities (#2028)

* Bump versions by dotnet-bump-version.

* Few More Fixes (#2032)

* Fixed spreads stretching on PC

* Fixed a bug where reading list dates couldn't be cleared out.

* Reading list page refreshes after updating info in the modal

* Fixed an issue where create library wouldn't take into account advanced settings.

* Fixed an issue where selection of the first chapter of a series to pull series-level metadata could fail in cases where you had Volume 2 and Chapter 1, Volume 2 would be selected.

* Bump versions by dotnet-bump-version.

* Fixed a bug where scan series wouldn't trigger word count analysis nor cover generation. (#2035)

* Bump versions by dotnet-bump-version.

* Okay this should be the last (#2037)

* Fixed improper date visualization for reading list detail page.

* Correct not-read badge position (#2034)

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* Bump versions by dotnet-bump-version.

* Fixed a bug where reading list month wasn't rendering correctly (#2039)

* Bump versions by dotnet-bump-version.

* Version bump (#2040)

* Bump versions by dotnet-bump-version.

* Fixed bug in CI pipeline for main

---------

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
Co-authored-by: Chris Plaatjes <kizaing@gmail.com>
Co-authored-by: pssandhu <pssandhu@users.noreply.github.com>
Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com>
Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* Reverted a scaling issue for fit to width

* Fixed an issue where creating a new library wouldn't persist advanced options due to a conflict with default value.

When deleting a library, give the library name in the prompt.

* Fixed kbd tags in epubs with paper theme having a style conflict.

* Fixed an edge case where the incorrect first cover could be chosen in some strange grouping situations.

* Manually sort directories as some OSes don't return them in a natural sort order.

* Fixed an issue where autocompleting when adding a directory could throw an error when you're typing.

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
Co-authored-by: Chris Plaatjes <kizaing@gmail.com>
Co-authored-by: pssandhu <pssandhu@users.noreply.github.com>
Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com>

* Bump versions by dotnet-bump-version.

* Version Bump

---------

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
Co-authored-by: Chris Plaatjes <kizaing@gmail.com>
Co-authored-by: pssandhu <pssandhu@users.noreply.github.com>
Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com>
Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* v0.7.4 - Kavita+ Launch (#2118)

* Report Media Issues (#1964)

* Started working on a report problems implementation.

* Started code

* Added logging to book and archive service.

* Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point.

* Added basic implementation for media errors.

* MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan.

* Fixed unit tests

* Basic code in place to view and clear. Just UI Cleanup needed.

* Slight css upgrade

* Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working.

* Fixed unit tests

* Fixed unit tests for real

* Bump versions by dotnet-bump-version.

* Expanded Metadata for EPUBs (#1965)

* Fixed a bug breaking ability to save server settings

* Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why

* Refactored the code to clean it up

* Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq.

* ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81

* Double include author for epub parsing and let the People code handle removing duplicates.

* Bump versions by dotnet-bump-version.

* Nothing changed, this is just to retrigger a stable build. (#1967) (#1968)

* Adding paper book reader theme (#1976)

* Adding paper book reader theme

# Added
- Added: Paper book reader theme

* Fixing some leftover styles

* adding book emulation to 2column layout for paper style

* Adding migrations

* removing migration and compressing image

* Reverting DataContextModelSnapshot

* checking out datacontextmodelsnapshot file

* Bump versions by dotnet-bump-version.

* Web Links (#1983)

* Updated dependencies

* Updated the default key to be 256 bits to meet security requirements.

* Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes.

* Implemented ability to see links and click on them for an individual chapter.

* Hooked up the ability to set Series web links.

* Render out the web link

* Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default.

* Added Robbie's nice error weblink fallbacks.

* Bump versions by dotnet-bump-version.

* Updated Docker entrypoint (#1984)

* Bump versions by dotnet-bump-version.

* ISBN Support (#1985)

* Fixed a bug where weblinks would always show

* Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell.

* Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10.

* Fixed Favicon not working on anything but windows

* Implemented ISBN support into Kavita

* Don't round so much when transforming bytes

* Bump versions by dotnet-bump-version.

* AVIF Support & Much More! (#1992)

* Expand the list of potential favicon icons to grab.

* Added a url mapping functionality to use alternative urls for fetching icons

* Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes.

* Started refactoring code so that webp queries use encoding format instead.

* More refactoring to remove hardcoded webp references.

* Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys.

* Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot.

* Make favicon encode setting aware

* Cleaned up favicon conversion

* Updated format counter to now just use Extension from MangaFile now that it's been out a while.

* Tweaked jumpbar code to reduce a lookup to hashmap.

* Added AVIF (8-bit only) support.

* In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed.

* You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend.

* Forgot a file

* Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont.

* Fixed Refresh token using wrong Claim to look up the user.

* Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated.

* Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures.

* Bump versions by dotnet-bump-version.

* More Fixes (#1993)

* Strip just isbn: from epub isbns and log when it's back (books)

* Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita.

* Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords.

* Show the media issue count before you open accordion

* Added a inpage filter for Media issues

* Cleanup styles

* Fixed up some code in epub isbn parsing when it's null

* Encode filenames when downloading so that non english characters can be passed properly to UI.

* Added support to parse ComicInfo's with Empty Tags.

* Reset development settings.

* Tweaked the code in generating reading lists to avoid extra work when not needed.

* Fix comicvine's favicon

* Fixed up a unit test

* Tweaked the favicon code to ignore icons that have query parameters

* More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already.

* Added a note for later

* Fixed stats server url

* Added more debugging

* Fixed unit tests

* Bump versions by dotnet-bump-version.

* More Fixes from Recent PRs (#1995)

* Added extra debugging for logout issue

* Fixed the null issue with ISBN

* Allow web links to be cleared out

* More logging on refresh token

* More key fallback when building Table of Contents

* Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced.

* Updated dependencies

* Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well.

* Bump versions by dotnet-bump-version.

* Fixed a bug with config (#1996)

* Bump versions by dotnet-bump-version.

* Changed IsDocker check (#1998)

* Refactored IsDocker to be completely static and changed to use an environment variable instead.

* Removed file from another branch

* Bump versions by dotnet-bump-version.

* Migrated up to VersOne 3.3 with epub 3.3 support. (#1999)

This enables collection and reading list support from epubs.

* Bump versions by dotnet-bump-version.

* More Bugfixes (EPUB Mainly) (#2004)

* Fixed an issue with downloading where spaces turned into plus signs.

* If the refresh token is invalid, but the auth token still has life in it, don't invalidate.

* Fixed docker users unable to save settings

* Show a default error icon until favicon loads

* Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2.

* Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml)

* Added another hack to massage key to page lookups when rewriting anchors.

* Cleaned up debugging notes

* Bump versions by dotnet-bump-version.

* More Polish  (#2005)

* Implemented sort title extraction from epub 3 files.

* Added link to wiki for media errors

* Fixed the hack to reduce JWT refresh token expiration

* Fixed up a case where favicon downloading wasn't correcting links that started with // correctly.

Added a fallback for sites that just don't pngs available.

* Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community.

* Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler.

* Bump versions by dotnet-bump-version.

* Angular 16 (#2007)

* Removed adv, which isn't needed.

* Updated zone

* Updated to angular 16

* Updated to angular 16 (partially)

* Updated to angular 16

* Package update for Angular 16 (and other dependencies) is complete.

* Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed()

* Updated all inputs that have ! to be required and deleted all unit tests.

* Corrected how takeUntilDestroyed() is supposed to be implemented.

* Bump versions by dotnet-bump-version.

* Pipeline adjustment for Angular 16 (#2008)

* Bump versions by dotnet-bump-version.

* Try a different build (#2009)

* Bump versions by dotnet-bump-version.

* Continue Reading Bugfix (#2010)

* Fixed an edge case where continue point wasn't considering any chapters that had progress.

Continue point is now slightly faster and uses less memory.

* Added a unit test for a user's case. Still not reproducible

* Bump versions by dotnet-bump-version.

* Ensure chapters are sorted when getting continue point (#2011)

Fixes new behaviour in #1625

* Bump versions by dotnet-bump-version.

* Strip more forms of comments from CSS before parsing/inlining. (#2014)

Handle if ExCSS throws an exception during inlining and attempt to fallback to scoping css instead of inlining.

I still cannot update past ExCSS v4.1.0 else NPEs for common css will be thrown.

* Bump versions by dotnet-bump-version.

* Misc Changes (#2015)

* Updated ng-bootstrap

* Fixed an issue where jumpbar would be disabled when it shouldn't have been.

* When there are duplicate files that make up a volume, show the count on series detail.

* Added basic ISBN searching which will return a chapter back.

* Bump versions by dotnet-bump-version.

* Fixed count for cards (#2016)

* Bump versions by dotnet-bump-version.

* Last Release before Release Testing (#2017)

* Attempting to invalidate JWT on login (when locked out), but can't figure a way to get a JWT, since we don't store them.

Just committing as I'm going to remove the middleware, this is not worth the performance and complexity.

* Removed some security stuff that didn't line up.

* Dropping Token Expiration down to 2 days to test during release testing.

* Bump versions by dotnet-bump-version.

* Removed old migrations for Kavita startup. Only migrations from v0.7.2 onwards are present. (#2019)

* Bump versions by dotnet-bump-version.

* Fixed up jumpbar not properly disabling/enabling (#2022)

* Bump versions by dotnet-bump-version.

* Fix StoryArc & StoryArcNumber mismatch (#2018)

* Ensure StoryArc and StoryArcNumber are max length

* Trim StoryArc to remove excess spaces.

* Replaced with cleaner approach.

* Update with majora2007 recommendations

* Bump versions by dotnet-bump-version.

* Last fixes before release (#2027)

* Disable login button when a login is in-progress. This will help prevent spamming when internet is slow.

* Fixed a bug where an empty space could cause an error when creating a library.

* Apply Split Options throughout the codebase to add extra safe-guard on empty spaces and ensure trimming.

* Bump versions by dotnet-bump-version.

* Added NoContent responses when APIs don't find entities (#2028)

* Bump versions by dotnet-bump-version.

* Few More Fixes (#2032)

* Fixed spreads stretching on PC

* Fixed a bug where reading list dates couldn't be cleared out.

* Reading list page refreshes after updating info in the modal

* Fixed an issue where create library wouldn't take into account advanced settings.

* Fixed an issue where selection of the first chapter of a series to pull series-level metadata could fail in cases where you had Volume 2 and Chapter 1, Volume 2 would be selected.

* Bump versions by dotnet-bump-version.

* Fixed a bug where scan series wouldn't trigger word count analysis nor cover generation. (#2035)

* Bump versions by dotnet-bump-version.

* Okay this should be the last (#2037)

* Fixed improper date visualization for reading list detail page.

* Correct not-read badge position (#2034)

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* Bump versions by dotnet-bump-version.

* Fixed a bug where reading list month wasn't rendering correctly (#2039)

* Bump versions by dotnet-bump-version.

* Version bump (#2040)

* Bump versions by dotnet-bump-version.

* Bugfixes for a hotfix (#2052)

* Nothing changed, this is just to retrigger a stable build. (#1967)

* v0.7.3 - The Quality of Life Update  (#2036)

* Version bump

* Okay this should be the last (#2037)

* Fixed improper date visualization for reading list detail page.

* Correct not-read badge position (#2034)

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* Bump versions by dotnet-bump-version.

* Merged develop in

---------

Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>

* v0.7.3 - The Quality of Life Update (#2041)

* Report Media Issues (#1964)

* Started working on a report problems implementation.

* Started code

* Added logging to book and archive service.

* Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point.

* Added basic implementation for media errors.

* MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan.

* Fixed unit tests

* Basic code in place to view and clear. Just UI Cleanup needed.

* Slight css upgrade

* Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working.

* Fixed unit tests

* Fixed unit tests for real

* Bump versions by dotnet-bump-version.

* Expanded Metadata for EPUBs (#1965)

* Fixed a bug breaking ability to save server settings

* Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why

* Refactored the code to clean it up

* Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq.

* ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81

* Double include author for epub parsing and let the People code handle removing duplicates.

* Bump versions by dotnet-bump-version.

* Nothing changed, this is just to retrigger a stable build. (#1967) (#1968)

* Adding paper book reader theme (#1976)

* Adding paper book reader theme

# Added
- Added: Paper book reader theme

* Fixing some leftover styles

* adding book emulation to 2column layout for paper style

* Adding migrations

* removing migration and compressing image

* Reverting DataContextModelSnapshot

* checking out datacontextmodelsnapshot file

* Bump versions by dotnet-bump-version.

* Web Links (#1983)

* Updated dependencies

* Updated the default key to be 256 bits to meet security requirements.

* Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes.

* Implemented ability to see links and click on them for an individual chapter.

* Hooked up the ability to set Series web links.

* Render out the web link

* Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default.

* Added Robbie's nice error weblink fallbacks.

* Bump versions by dotnet-bump-version.

* Updated Docker entrypoint (#1984)

* Bump versions by dotnet-bump-version.

* ISBN Support (#1985)

* Fixed a bug where weblinks would always show

* Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell.

* Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10.

* Fixed Favicon not working on anything but windows

* Implemented ISBN support into Kavita

* Don't round so much when transforming bytes

* Bump versions by dotnet-bump-version.

* AVIF Support & Much More! (#1992)

* Expand the list of potential favicon icons to grab.

* Added a url mapping functionality to use alternative urls for fetching icons

* Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes.

* Started refactoring code so that webp queries use encoding format instead.

* More refactoring to remove hardcoded webp references.

* Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys.

* Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot.

* Make favicon encode setting aware

* Cleaned up favicon conversion

* Updated format counter to now just use Extension from MangaFile now that it's been out a while.

* Tweaked jumpbar code to reduce a lookup to hashmap.

* Added AVIF (8-bit only) support.

* In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed.

* You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend.

* Forgot a file

* Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont.

* Fixed Refresh token using wrong Claim to look up the user.

* Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated.

* Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures.

* Bump versions by dotnet-bump-version.

* More Fixes (#1993)

* Strip just isbn: from epub isbns and log when it's back (books)

* Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita.

* Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords.

* Show the media issue count before you open accordion

* Added a inpage filter for Media issues

* Cleanup styles

* Fixed up some code in epub isbn parsing when it's null

* Encode filenames when downloading so that non english characters can be passed properly to UI.

* Added support to parse ComicInfo's with Empty Tags.

* Reset development settings.

* Tweaked the code in generating reading lists to avoid extra work when not needed.

* Fix comicvine's favicon

* Fixed up a unit test

* Tweaked the favicon code to ignore icons that have query parameters

* More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already.

* Added a note for later

* Fixed stats server url

* Added more debugging

* Fixed unit tests

* Bump versions by dotnet-bump-version.

* More Fixes from Recent PRs (#1995)

* Added extra debugging for logout issue

* Fixed the null issue with ISBN

* Allow web links to be cleared out

* More logging on refresh token

* More key fallback when building Table of Contents

* Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced.

* Updated dependencies

* Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well.

* Bump versions by dotnet-bump-version.

* Fixed a bug with config (#1996)

* Bump versions by dotnet-bump-version.

* Changed IsDocker check (#1998)

* Refactored IsDocker to be completely static and changed to use an environment variable instead.

* Removed file from another branch

* Bump versions by dotnet-bump-version.

* Migrated up to VersOne 3.3 with epub 3.3 support. (#1999)

This enables collection and reading list support from epubs.

* Bump versions by dotnet-bump-version.

* More Bugfixes (EPUB Mainly) (#2004)

* Fixed an issue with downloading where spaces turned into plus signs.

* If the refresh token is invalid, but the auth token still has life in it, don't invalidate.

* Fixed docker users unable to save settings

* Show a default error icon until favicon loads

* Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2.

* Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml)

* Added another hack to massage key to page lookups when rewriting anchors.

* Cleaned up debugging notes

* Bump versions by dotnet-bump-version.

* More Polish  (#2005)

* Implemented sort title extraction from epub 3 files.

* Added link to wiki for media errors

* Fixed the hack to reduce JWT refresh token expiration

* Fixed up a case where favicon downloading wasn't correcting links that started with // correctly.

Added a fallback for sites that just don't pngs available.

* Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community.

* Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler.

* Bump versions by dotnet-bump-version.

* Angular 16 (#2007)

* Removed adv, which isn't needed.

* Updated zone

* Updated to angular 16

* Updated to angular 16 (partially)

* Updated to angular 16

* Package update for Angular 16 (and other dependencies) is complete.

* Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed()

* Updated all inputs that have ! to be required and deleted all unit tests.

* Corrected how takeUntilDestroyed() is supposed to be implemented.

* Bump versions by dotnet-bump-version.

* Pipeline adjustment for Angular 16 (#2008)

* Bump versions by dotnet-bump-version.

* Try a different build (#2009)

* Bump versions by dotnet-bump-version.

* Continue Reading Bugfix (#2010)

* Fixed an edge case where continue point wasn't considering any chapters that had progress.

Continue point is now slightly faster and uses less memory.

* Added a unit test for a user's case. Still not reproducible

* Bump versions by dotnet-bump-version.

* Ensure chapters are sorted when getting continue point (#2011)

Fixes new behaviour in #1625

* Bump versions by dotnet-bump-version.

* Strip more forms of comments from CSS b…

* Bump versions by dotnet-bump-version.

* Localization - First Pass (#2174)

* Started designing the backend localization service

* Worked in Transloco for initial PoC

* Worked in Transloco for initial PoC

* Translated the login screen

* translated dashboard screen

* Started work on the backend

* Fixed a logic bug

* translated edit-user screen

* Hooked up the backend for having a locale property.

* Hooked up the ability to view the available locales and switch to them.

* Made the localization service languages be derived from what's in langs/ directory.

* Fixed up localization switching

* Switched when we check for a license on UI bootstrap

* Tweaked some code

* Fixed the bug where dashboard wasn't loading and made it so language switching is working.

* Fixed a bug on dashboard with languagePath

* Converted user-scrobble-history.component.html

* Converted spoiler.component.html

* Converted review-series-modal.component.html

* Converted review-card-modal.component.html

* Updated the readme

* Translated using Weblate (English)

Currently translated at 100.0% (54 of 54 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/en/

* Converted review-card.component.html

* Deleted dead component

* Converted want-to-read.component.html

* Added translation using Weblate (Korean)

* Translated using Weblate (Spanish)

Currently translated at 40.7% (22 of 54 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Korean)

Currently translated at 62.9% (34 of 54 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/

* Converted user-preferences.component.html

* Translated using Weblate (Korean)

Currently translated at 92.5% (50 of 54 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/

* Converted user-holds.component.html

* Converted theme-manager.component.html

* Converted restriction-selector.component.html

* Converted manage-devices.component.html

* Converted edit-device.component.html

* Converted change-password.component.html

* Converted change-email.component.html

* Converted change-age-restriction.component.html

* Converted api-key.component.html

* Converted anilist-key.component.html

* Converted typeahead.component.html

* Converted user-stats-info-cards.component.html

* Converted user-stats.component.html

* Converted top-readers.component.html

* Converted some pipes and ensure translation is loaded before the app.

* Finished all but one pipe for localization

* Converted directory-picker.component.html

* Converted library-access-modal.component.html

* Converted a few components

* Converted a few components

* Converted a few components

* Converted a few components

* Converted a few components

* Merged weblate in

* ... -> … update

* Updated the readme

* Updateded all fonts to be woff2

* Cleaned up some strings to increase re-use

* Removed an old flow (that doesn't exist in backend any longer) from when we introduced emails on Kavita.

* Converted Series detail

* Lots more converted

* Lots more converted & hooked up the ability to flatten during prod build the language files.

* Lots more converted

* Lots more converted & fixed a bunch of broken pipes due to inject()

* Lots more converted

* Lots more converted

* Lots more converted & fixed some bad keys

* Lots more converted

* Fixed some bugs with admin dasbhoard nested tabs not rendering on first load due to not using onpush change detection

* Fixed up some localization errors and fixed forgot password error when the user doesn't have change password permission

* Fixed a stupid build issue again

* Started adding errors for interceptor and backend.

* Finished off manga-reader

* More translations

* Few fixes

* Fixed a bug where character tag badges weren't showing the name on chapter info

* All components are translated

* All toasts are translated

* All confirm/alerts are translated

* Trying something new for the backend

* Migrated the localization strings for the backend into a new file.

* Updated the localization service to be able to do backend localization with fallback to english.

* Cleaned up some external reviews code to reduce looping

* Localized AccountController.cs

* 60% done with controllers

* All controllers are done

* All KavitaExceptions are covered

* Some shakeout fixes

* Prep for initial merge

* Everything is done except options and basic shakeout proves response times are good. Unit tests are broken.

* Fixed up the unit tests

* All unit tests are now working

* Removed some quantifier

* I'm not sure I can support localization for some Volume/Chapter/Book strings within the codebase.

---------

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
Co-authored-by: majora2007 <kavitareader@gmail.com>
Co-authored-by: expertjun <jtrobin@naver.com>
Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>

* Bump versions by dotnet-bump-version.

* Added translation using Weblate (French)

* Translated using Weblate (French)

Currently translated at 5.6% (9 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/fr/

* Bump versions by dotnet-bump-version.

* Added translation using Weblate (Dutch)

* Bump versions by dotnet-bump-version.

* Translated using Weblate (Dutch)

Currently translated at 20.8% (33 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Translated using Weblate (Spanish)

Currently translated at 1.4% (20 of 1371 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 60.1% (95 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Translated using Weblate (Dutch)

Currently translated at 60.1% (95 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Added translation using Weblate (Dutch)

* Localization - Part 2 (#2178)

* Changed language codes in the UI to be a list of all codes we will ever support.

* Converted actionables

* Fixed the GetLocales not using Intersect, but Union.

* Fixed some localization strings in backend when user doesn't exist.

Removed AllowAnonymous from reset-password, since it is a protected API

* Fixed all instances of anonymous APIs where Claim wouldn't work

* Keyed preference options and mixed misc localization issues

* Translations update from Hosted Weblate (#2177)

* Bump versions by dotnet-bump-version.

* Added translation using Weblate (Dutch)

* Bump versions by dotnet-bump-version.

* Translated using Weblate (Dutch)

Currently translated at 20.8% (33 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Translated using Weblate (Spanish)

Currently translated at 1.4% (20 of 1371 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 60.1% (95 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Translated using Weblate (Dutch)

Currently translated at 60.1% (95 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Added translation using Weblate (Dutch)

---------

Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Javier Barbero <javier.agustin.barbero@gmail.com>
Co-authored-by: Stijn <stijn.biemans@gmail.com>

---------

Co-authored-by: Weblate (bot) <hosted@weblate.org>
Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Javier Barbero <javier.agustin.barbero@gmail.com>
Co-authored-by: Stijn <stijn.biemans@gmail.com>

* Translated using Weblate (Spanish)

Currently translated at 1.6% (22 of 1371 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 94.9% (150 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Translated using Weblate (Dutch)

Currently translated at 1.6% (22 of 1371 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Bump versions by dotnet-bump-version.

* Bump versions by dotnet-bump-version.

* Translated using Weblate (French)

Currently translated at 8.2% (13 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/fr/

* Translated using Weblate (French)

Currently translated at 13.2% (21 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/fr/

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Undetermined)

* Added translation using Weblate (Thai)

* Added translation using Weblate (Chinese (Simplified))

* Added translation using Weblate (Chinese (Simplified))

* Added translation using Weblate (Thai)

* Translated using Weblate (Spanish)

Currently translated at 1.5% (22 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 98.1% (155 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Translated using Weblate (Dutch)

Currently translated at 98.1% (155 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Translated using Weblate (Dutch)

Currently translated at 8.1% (115 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Dutch)

Currently translated at 8.1% (115 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Thai)

Currently translated at 7.5% (12 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/th/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 5.0% (72 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 8.2% (13 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 5.6% (80 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/zh_Hans/

* Added translation using Weblate (Portuguese)

* Translated using Weblate (Dutch)

Currently translated at 11.4% (162 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Dutch)

Currently translated at 11.4% (162 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 12.0% (19 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/zh_Hans/

* Added translation using Weblate (Italian)

* Translated using Weblate (Dutch)

Currently translated at 12.6% (179 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Dutch)

Currently translated at 12.6% (179 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Thai)

Currently translated at 2.3% (33 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Thai)

Currently translated at 2.3% (33 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Portuguese)

Currently translated at 0.6% (1 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/pt/

* Translated using Weblate (Italian)

Currently translated at 40.5% (64 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/it/

* Translated using Weblate (French)

Currently translated at 15.8% (25 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/fr/

* Translated using Weblate (Thai)

Currently translated at 4.8% (69 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Portuguese)

Currently translated at 89.8% (142 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/pt/

* Translated using Weblate (Italian)

Currently translated at 67.7% (107 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/it/

* Translated using Weblate (Spanish)

Currently translated at 5.1% (73 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 12.8% (182 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Spanish)

Currently translated at 12.8% (182 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translations update from Hosted Weblate (#2179)

* Translated using Weblate (French)

Currently translated at 8.2% (13 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/fr/

* Translated using Weblate (French)

Currently translated at 13.2% (21 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/fr/

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Undetermined)

* Added translation using Weblate (Thai)

* Added translation using Weblate (Chinese (Simplified))

* Added translation using Weblate (Chinese (Simplified))

* Added translation using Weblate (Thai)

* Translated using Weblate (Spanish)

Currently translated at 1.5% (22 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 98.1% (155 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Translated using Weblate (Dutch)

Currently translated at 98.1% (155 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Translated using Weblate (Dutch)

Currently translated at 8.1% (115 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Dutch)

Currently translated at 8.1% (115 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Thai)

Currently translated at 7.5% (12 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/th/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 5.0% (72 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 8.2% (13 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 5.6% (80 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/zh_Hans/

* Added translation using Weblate (Portuguese)

* Translated using Weblate (Dutch)

Currently translated at 11.4% (162 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Dutch)

Currently translated at 11.4% (162 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 12.0% (19 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/zh_Hans/

* Added translation using Weblate (Italian)

* Translated using Weblate (Dutch)

Currently translated at 12.6% (179 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Dutch)

Currently translated at 12.6% (179 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Thai)

Currently translated at 2.3% (33 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Thai)

Currently translated at 2.3% (33 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Portuguese)

Currently translated at 0.6% (1 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/pt/

* Translated using Weblate (Italian)

Currently translated at 40.5% (64 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/it/

* Translated using Weblate (French)

Currently translated at 15.8% (25 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/fr/

* Translated using Weblate (Thai)

Currently translated at 4.8% (69 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Portuguese)

Currently translated at 89.8% (142 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/pt/

* Translated using Weblate (Italian)

Currently translated at 67.7% (107 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/it/

* Translated using Weblate (Spanish)

Currently translated at 5.1% (73 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 12.8% (182 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Spanish)

Currently translated at 12.8% (182 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

---------

Co-authored-by: Francois Wilhelmy <ice_mouton@hotmail.com>
Co-authored-by: 周書丞 <tmrsm_chan@hotmail.com>
Co-authored-by: 书签 <shuqian.emu@gmail.com>
Co-authored-by: AlienHack <the4got10@windowslive.com>
Co-authored-by: NeneNeko <lennon.rin@gmail.com>
Co-authored-by: Toto Saurio <totosaurio3279@gmail.com>
Co-authored-by: Stijn <stijn.biemans@gmail.com>
Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: stan xu <fatexsd@gmail.com>
Co-authored-by: Tomas Battistini <tomas.battistini@gmail.com>
Co-authored-by: majora2007 <kavitareader@gmail.com>
Co-authored-by: zeedif <carlos_antonio-rl@hotmail.com>

* Bump versions by dotnet-bump-version.

* Epub Weblinks + Localization Changes (#2180)

* Fixed a bug where scan series wasn't ignoring optimizations for chapter metadata updates.

Implemented pulling weblinks from epubs.

* Fixed localization issue

* Translations update from Hosted Weblate (#2179)

* Translated using Weblate (French)

Currently translated at 8.2% (13 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/fr/

* Translated using Weblate (French)

Currently translated at 13.2% (21 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/fr/

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Undetermined)

* Added translation using Weblate (Thai)

* Added translation using Weblate (Chinese (Simplified))

* Added translation using Weblate (Chinese (Simplified))

* Added translation using Weblate (Thai)

* Translated using Weblate (Spanish)

Currently translated at 1.5% (22 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 98.1% (155 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Translated using Weblate (Dutch)

Currently translated at 98.1% (155 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/nl/

* Translated using Weblate (Dutch)

Currently translated at 8.1% (115 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Dutch)

Currently translated at 8.1% (115 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Thai)

Currently translated at 7.5% (12 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/th/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 5.0% (72 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 8.2% (13 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 5.6% (80 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/zh_Hans/

* Added translation using Weblate (Portuguese)

* Translated using Weblate (Dutch)

Currently translated at 11.4% (162 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Dutch)

Currently translated at 11.4% (162 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 12.0% (19 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/zh_Hans/

* Added translation using Weblate (Italian)

* Translated using Weblate (Dutch)

Currently translated at 12.6% (179 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Dutch)

Currently translated at 12.6% (179 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Thai)

Currently translated at 2.3% (33 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Thai)

Currently translated at 2.3% (33 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Portuguese)

Currently translated at 0.6% (1 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/pt/

* Translated using Weblate (Italian)

Currently translated at 40.5% (64 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/it/

* Translated using Weblate (French)

Currently translated at 15.8% (25 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/fr/

* Translated using Weblate (Thai)

Currently translated at 4.8% (69 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Portuguese)

Currently translated at 89.8% (142 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/pt/

* Translated using Weblate (Italian)

Currently translated at 67.7% (107 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/it/

* Translated using Weblate (Spanish)

Currently translated at 5.1% (73 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 12.8% (182 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Spanish)

Currently translated at 12.8% (182 of 1416 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

---------

Co-authored-by: Francois Wilhelmy <ice_mouton@hotmail.com>
Co-authored-by: 周書丞 <tmrsm_chan@hotmail.com>
Co-authored-by: 书签 <shuqian.emu@gmail.com>
Co-authored-by: AlienHack <the4got10@windowslive.com>
Co-authored-by: NeneNeko <lennon.rin@gmail.com>
Co-authored-by: Toto Saurio <totosaurio3279@gmail.com>
Co-authored-by: Stijn <stijn.biemans@gmail.com>
Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: stan xu <fatexsd@gmail.com>
Co-authored-by: Tomas Battistini <tomas.battistini@gmail.com>
Co-authored-by: majora2007 <kavitareader@gmail.com>
Co-authored-by: zeedif <carlos_antonio-rl@hotmail.com>

* Removed a blank API localization file

---------

Co-authored-by: Weblate (bot) <hosted@weblate.org>
Co-authored-by: Francois Wilhelmy <ice_mouton@hotmail.com>
Co-authored-by: 周書丞 <tmrsm_chan@hotmail.com>
Co-authored-by: 书签 <shuqian.emu@gmail.com>
Co-authored-by: AlienHack <the4got10@windowslive.com>
Co-authored-by: NeneNeko <lennon.rin@gmail.com>
Co-authored-by: Toto Saurio <totosaurio3279@gmail.com>
Co-authored-by: Stijn <stijn.biemans@gmail.com>
Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: stan xu <fatexsd@gmail.com>
Co-authored-by: Tomas Battistini <tomas.battistini@gmail.com>
Co-authored-by: majora2007 <kavitareader@gmail.com>
Co-authored-by: zeedif <carlos_antonio-rl@hotmail.com>

* Bump versions by dotnet-bump-version.

* Localized Dates (#2182)

* Removed 4 properties from SiteThemeDto which weren't supposed to be there.

* Removed another set of date fields not used on DTOs

* Hangfire jobs will now grab a utc date and render that date in user's local timezone.

* Scrobble errors are now localized dates.

Added simplified chinese language code

* Fixed a bunch of newlines in the translation files

* Localized compact number and fixed some missing localizations

* Fixed remove from on deck key issue

* Scrobble events is now localized

* Scrobble events is now localized

* Removed some duplicate fields from chapter

* Bump versions by dotnet-bump-version.

* Fixed leftover commas in json (#2183)

* Bump versions by dotnet-bump-version.

* Added translation using Weblate (Turkish)

* Added translation using Weblate (Malay)

* Added translation using Weblate (Portuguese)

* Added translation using Weblate (Russian)

* Translated using Weblate (Dutch)

Currently translated at 13.4% (190 of 1417 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Dutch)

Currently translated at 13.4% (190 of 1417 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Turkish)

Currently translated at 0.1% (2 of 1417 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/tr/

* Translated using Weblate (Malay)

Currently translated at 21.5% (34 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/ms/

* Translated using Weblate (Portuguese)

Currently translated at 0.6% (9 of 1417 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/pt/

* Added translation using Weblate (German)

* Translated using Weblate (Japanese)

Currently translated at 0.6% (1 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/ja/

* Translated using Weblate (German)

Currently translated at 20.8% (33 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/de/

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Spanish)

* Translated using Weblate (Spanish)

Currently translated at 22.0% (314 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (German)

Currently translated at 51.8% (82 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/de/

* Translated using Weblate (Japanese)

Currently translated at 0.4% (7 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ja/

* Translations update from Hosted Weblate (#2181)

* Added translation using Weblate (Turkish)

* Added translation using Weblate (Malay)

* Added translation using Weblate (Portuguese)

* Added translation using Weblate (Russian)

* Translated using Weblate (Dutch)

Currently translated at 13.4% (190 of 1417 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Dutch)

Currently translated at 13.4% (190 of 1417 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Turkish)

Currently translated at 0.1% (2 of 1417 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/tr/

* Translated using Weblate (Malay)

Currently translated at 21.5% (34 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/ms/

* Translated using Weblate (Portuguese)

Currently translated at 0.6% (9 of 1417 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/pt/

* Added translation using Weblate (German)

* Translated using Weblate (Japanese)

Currently translated at 0.6% (1 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/ja/

* Translated using Weblate (German)

Currently translated at 20.8% (33 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/de/

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Spanish)

* Translated using Weblate (Spanish)

Currently translated at 22.0% (314 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (German)

Currently translated at 51.8% (82 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/de/

* Translated using Weblate (Japanese)

Currently translated at 0.4% (7 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ja/

---------

Co-authored-by: xe1st <dnzkckali@gmail.com>
Co-authored-by: Safu Wan <safu@yahoo.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: Dmitry “V” Kostylev <chubits@mail.ru>
Co-authored-by: CtrlAltDefeat <ctrlaltdefeat1994@gmail.com>
Co-authored-by: Stijn <stijn.biemans@gmail.com>
Co-authored-by: Andre <andruecha32@gmail.com>
Co-authored-by: Andre Smith <andrepsmithjr@gmail.com>
Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>

* Bump versions by dotnet-bump-version.

* Added translation using Weblate (Kannada)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (German)

* Added translation using Weblate (Russian)

* Added translation using Weblate (Malay)

* Translated using Weblate (Dutch)

Currently translated at 20.8% (296 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Portuguese)

Currently translated at 5.2% (75 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/pt/

* Translated using Weblate (German)

Currently translated at 98.7% (156 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/de/

* Translated using Weblate (Japanese)

Currently translated at 1.9% (28 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ja/

* Translated using Weblate (Spanish)

Currently translated at 1.8% (3 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/es/

* Translated using Weblate (Hindi)

Currently translated at 100.0% (158 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/hi/

* Translated using Weblate (German)

Currently translated at 7.3% (105 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/de/

* Deleted translation using Weblate (Undetermined)

* Added translation using Weblate (Italian)

* Translated using Weblate (Spanish)

Currently translated at 22.2% (316 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 24.4% (348 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Japanese)

Currently translated at 1.2% (2 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/ja/

* Translated using Weblate (Thai)

Currently translated at 7.4% (106 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Italian)

Currently translated at 100.0% (158 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/it/

* Translated using Weblate (Portuguese)

Currently translated at 10.5% (150 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/pt/

* Translated using Weblate (German)

Currently translated at 99.3% (157 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/de/

* Translated using Weblate (German)

Currently translated at 13.2% (189 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/de/

* Translated using Weblate (German)

Currently translated at 13.2% (189 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/de/

* Translated using Weblate (Italian)

Currently translated at 6.6% (94 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/it/

* Bugfixes! (#2187)

* Updated readme to have progress bars on localization to help motivate users.

* Fixed a bug where downloads could trigger on lazy loaded module boundaries.

* Updated all packages to latest

* Fixed a bug where remove from on deck would show on all series cards when it shouldn't have.

* Fixed a bug where virtualized reading list page wasn't showing the correct order on the UI

* Localization fixes from shakeout

* Fixed fullscreen mode broken in nightly from localization.

* Fixed a bug where duplicate series add events could show duplicate items in library detail page.

* Translations update from Hosted Weblate (#2184)

* Added translation using Weblate (Kannada)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (German)

* Added translation using Weblate (Russian)

* Added translation using Weblate (Malay)

* Translated using Weblate (Dutch)

Currently translated at 20.8% (296 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Portuguese)

Currently translated at 5.2% (75 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/pt/

* Translated using Weblate (German)

Currently translated at 98.7% (156 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/de/

* Translated using Weblate (Japanese)

Currently translated at 1.9% (28 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ja/

* Translated using Weblate (Spanish)

Currently translated at 1.8% (3 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/es/

* Translated using Weblate (Hindi)

Currently translated at 100.0% (158 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/hi/

* Translated using Weblate (German)

Currently translated at 7.3% (105 of 1423 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/de/

* Deleted translation using Weblate (Undetermined)

* Added translation using Weblate (Italian)

---------

Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: Andre <andruecha32@gmail.com>
Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: Andre Smith <andrepsmithjr@gmail.com>
Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
Co-authored-by: majora2007 <kavitareader@gmail.com>
Co-authored-by: Tomas Battistini <tomas.battistini@gmail.com>

---------

Co-authored-by: Weblate (bot) <hosted@weblate.org>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: Andre <andruecha32@gmail.com>
Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: Andre Smith <andrepsmithjr@gmail.com>
Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
Co-authored-by: majora2007 <kavitareader@gmail.com>
Co-authored-by: Tomas Battistini <tomas.battistini@gmail.com>

* Bump versions by dotnet-bump-version.

* Bump versions by dotnet-bump-version.

* Added translation using Weblate (Turkish)

* Translated using Weblate (Thai)

Currently translated at 100.0% (158 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/th/

* Translated using Weblate (Thai)

Currently translated at 15.2% (218 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Turkish)

Currently translated at 7.7% (110 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/tr/

* Translated using Weblate (Portuguese)

Currently translated at 17.5% (250 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/pt/

* Translated using Weblate (Russian)

Currently translated at 1.2% (2 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/ru/

* Translated using Weblate (Russian)

Currently translated at 4.9% (71 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ru/

* Translated using Weblate (Italian)

Currently translated at 6.7% (96 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/it/

* Translated using Weblate (Turkish)

Currently translated at 8.8% (14 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/tr/

* Weblate Changes (#2189)

* Added translation using Weblate (Turkish)

* Translated using Weblate (Thai)

Currently translated at 100.0% (158 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/th/

* Translated using Weblate (Thai)

Currently translated at 15.2% (218 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Turkish)

Currently translated at 7.7% (110 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/tr/

* Translated using Weblate (Portuguese)

Currently translated at 17.5% (250 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/pt/

* Translated using Weblate (Russian)

Currently translated at 1.2% (2 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/ru/

* Translated using Weblate (Russian)

Currently translated at 4.9% (71 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ru/

* Translated using Weblate (Italian)

Currently translated at 6.7% (96 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/it/

* Translated using Weblate (Turkish)

Currently translated at 8.8% (14 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/tr/

---------

Co-authored-by: akoray420 <akoray420@gmail.com>
Co-authored-by: AlienHack <the4got10@windowslive.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: Blezz Rot <markus.jenya04@yandex.ru>
Co-authored-by: Tomas Battistini <tomas.battistini@gmail.com>

* A few bugfixes (#2188)

* Fixed a case where when setting up initial rates for scrobbling, Kavita would log a user without a token set had no rate.

* Migrated the whole app to use just the directive instead of whole transloco module.

* Migrated the whole app to use just the directive instead of whole transloco module. Fixed prod mode breaking localization & fixed broken minification for language files.

* Time Ago pipe will now show Never if there is a null date. Changed the wording of Last Added To -> Last Item Added for volume/series info screen.

* Fixed Tachiyomi DTOs and bumped sonar to use Java 17

* One more GA thing

* GA junk

* Bump versions by dotnet-bump-version.

* Weblate Changes (#2189)

* Added translation using Weblate (Turkish)

* Translated using Weblate (Thai)

Currently translated at 100.0% (158 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/th/

* Translated using Weblate (Thai)

Currently translated at 15.2% (218 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Turkish)

Currently translated at 7.7% (110 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/tr/

* Translated using Weblate (Portuguese)

Currently translated at 17.5% (250 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/pt/

* Translated using Weblate (Russian)

Currently translated at 1.2% (2 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/ru/

* Translated using Weblate (Russian)

Currently translated at 4.9% (71 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ru/

* Translated using Weblate (Italian)

Currently translated at 6.7% (96 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/it/

* Translated using Weblate (Turkish)

Currently translated at 8.8% (14 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/tr/

---------

Co-authored-by: akoray420 <akoray420@gmail.com>
Co-authored-by: AlienHack <the4got10@windowslive.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: Blezz Rot <markus.jenya04@yandex.ru>
Co-authored-by: Tomas Battistini <tomas.battistini@gmail.com>

---------

Co-authored-by: Weblate (bot) <hosted@weblate.org>
Co-authored-by: akoray420 <akoray420@gmail.com>
Co-authored-by: AlienHack <the4got10@windowslive.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: Blezz Rot <markus.jenya04@yandex.ru>
Co-authored-by: Tomas Battistini <tomas.battistini@gmail.com>

* Translated using Weblate (Portuguese)

Currently translated at 17.6% (252 of 1426 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/pt/

* Bump versions by dotnet-bump-version.

* Readme Change (#2190)

* Implemented the ability to login to the app by passing apiKey to the login. This is for an upcoming feature (but currently blocked by another story)

* Added a comment

* Ensure locales are sorted

* Added a new status badge that shows how many active installs we have via users that use stats.

* Bump all GA to latest versions

* Bumped dependencies

* Bumped backend notifications

* Updated ngx-pdf-reader to upcoming beta which fixes some PDFs taking time to load. PDF reader will use browser locale to load localization rather than Kavita locale for now.

* Downgraded pdf viewer as beta has lots of bugs.

* Bump versions by dotnet-bump-version.

* Weblate Changes (#2191)

* Translated using Weblate (Dutch)

Currently translated at 40.7% (582 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Dutch)

Currently translated at 40.7% (582 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 5.7% (82 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/zh_Hans/

* Translated using Weblate (Thai)

Currently translated at 23.4% (334 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/th/

* Translated using Weblate (Portuguese)

Currently translated at 95.5% (151 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/pt/

* Translated using Weblate (Portuguese)

Currently translated at 21.2% (303 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/pt/

* Translated using Weblate (German)

Currently translated at 100.0% (158 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/de/

* Translated using Weblate (German)

Currently translated at 15.9% (228 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/de/

---------

Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: nielsvdp <niels@vandenput.com>
Co-authored-by: oxygen44k <iiccpp@outlook.com>
Co-authored-by: AlienHack <the4got10@windowslive.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: Andre <andruecha32@gmail.com>

* Last Fixes before Release (#2192)

* Removed Moq from the project. Fixed a localization string

* Fixed a bug with virtualized reading lists when reordering, it wouldn't use the correct order index.

* Added some german common strings cause weblate is a PIA to use.

* Added a bug marker for something that needs another release for

* Bump versions by dotnet-bump-version.

* Weblate Changes (#2194)

* Translated using Weblate (Spanish)

Currently translated at 40.9% (585 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Spanish)

Currently translated at 40.9% (585 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 48.5% (693 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Portuguese)

Currently translated at 96.8% (153 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/pt/

* Translated using Weblate (Portuguese)

Currently translated at 24.3% (348 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/pt/

* Translated using Weblate (Spanish)

Currently translated at 3.7% (6 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/es/

* Translated using Weblate (Italian)

Currently translated at 11.2% (161 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/it/

---------

Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: Tomas Battistini <tomas.battistini@gmail.com>

* Shakeout (#2195)

* Fixed some localization issues. Fixed double slash on base url.

* Weblate Changes (#2194)

* Translated using Weblate (Spanish)

Currently translated at 40.9% (585 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Spanish)

Currently translated at 40.9% (585 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 48.5% (693 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Portuguese)

Currently translated at 96.8% (153 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/pt/

* Translated using Weblate (Portuguese)

Currently translated at 24.3% (348 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/pt/

* Translated using Weblate (Spanish)

Currently translated at 3.7% (6 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/es/

* Translated using Weblate (Italian)

Currently translated at 11.2% (161 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/it/

---------

Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: Tomas Battistini <tomas.battistini@gmail.com>

---------

Co-authored-by: Weblate (bot) <hosted@weblate.org>
Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: Tomas Battistini <tomas.battistini@gmail.com>

* Bump versions by dotnet-bump-version.

* Weblate Changes (#2196)

* Translated using Weblate (Spanish)

Currently translated at 40.9% (585 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Spanish)

Currently translated at 40.9% (585 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Dutch)

Currently translated at 48.5% (693 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/nl/

* Translated using Weblate (Portuguese)

Currently translated at 96.8% (153 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/pt/

* Translated using Weblate (Portuguese)

Currently translated at 24.3% (348 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/pt/

* Translated using Weblate (Spanish)

Currently translated at 3.7% (6 of 158 strings)

Translation: Kavita/backend
Translate-URL: https://hosted.weblate.org/projects/kavita/backend/es/

* Translated using Weblate (Italian)

Currently translated at 11.2% (161 of 1427 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/it/

---------

Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: Tomas Battistini <tomas.battistini@gmail.com>

* Version bump (#2198)

* Bump versions by dotnet-bump-version.

* Version bump

---------

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
Co-authored-by: Chris Plaatjes <kizaing@gmail.com>
Co-authored-by: pssandhu <pssandhu@users.noreply.github.com>
Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com>
Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com>
Co-authored-by: JShiesty <102483672+JShiesty-dev@users.noreply.github.com>
Co-authored-by: majora2007 <kavitareader@gmail.com>
Co-authored-by: expertjun <jtrobin@naver.com>
Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
Co-authored-by: Francois Wilhelmy <ice_mouton@hotmail.com>
Co-authored-by: weblate <hosted@weblate.org>
Co-authored-by: Hans Kalisvaart <hans.kalisvaart@gmail.com>
Co-authored-by: Javier Barbero <javier.agustin.barbero@gmail.com>
Co-authored-by: Stijn <stijn.biemans@gmail.com>
Co-authored-by: 周書丞 <tmrsm_chan@hotmail.com>
Co-authored-by: 书签 <shuqian.emu@gmail.com>
Co-authored-by: AlienHack <the4got10@windowslive.com>
Co-authored-by: NeneNeko <lennon.rin@gmail.com>
Co-authored-by: Toto Saurio <totosaurio3279@gmail.com>
Co-authored-by: Duarte Silva <smallflake@protonmail.com>
Co-authored-by: stan xu <fatexsd@gmail.com>
Co-authored-by: Tomas Battistini <tomas.battistini@gmail.com>
Co-authored-by: zeedif <carlos_antonio-rl@hotmail.com>
Co-authored-by: xe1st <dnzkckali@gmail.com>
Co-authored-by: Safu Wan <safu@yahoo.com>
Co-authored-by: Dmitry “V” Kostylev <chubits@mail.ru>
Co-authored-by: CtrlAltDefeat <ctrlaltdefeat1994@gmail.com>
Co-authored-by: Andre <andruecha32@gmail.com>
Co-authored-by: Andre Smith <andrepsmithjr@gmail.com>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: handy1928 <hendrik@hanbek.de>
Co-authored-by: akoray420 <akoray420@gmail.com>
Co-authored-by: Blezz Rot <markus.jenya04@yandex.ru>
Co-authored-by: nielsvdp <niels@vandenput.com>
Co-authored-by: oxygen44k <iiccpp@outlook.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
This commit is contained in:
Joe Milazzo 2023-08-10 10:12:19 -05:00 committed by GitHub
parent f037bf3a35
commit bdaadbecfc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
453 changed files with 32973 additions and 8606 deletions

View file

@ -29,18 +29,19 @@ jobs:
- name: Install dependencies
run: dotnet restore
- name: Set up JDK 11
uses: actions/setup-java@v1
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: 1.11
distribution: 'zulu'
java-version: '17'
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: csproj
path: Kavita.Common/Kavita.Common.csproj
- name: Cache SonarCloud packages
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ~\sonar\cache
key: ${{ runner.os }}-sonar
@ -48,7 +49,7 @@ jobs:
- name: Cache SonarCloud scanner
id: cache-sonar-scanner
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: .\.sonar\scanner
key: ${{ runner.os }}-sonar-scanner
@ -106,7 +107,7 @@ jobs:
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
steps:
- name: Find Current Pull Request
uses: jwalton/gh-find-current-pr@v1.0.2
uses: jwalton/gh-find-current-pr@v1.3.2
id: findPr
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -10,8 +10,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.6" />
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.6" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.7" />
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.7" />
<PackageReference Include="NSubstitute" Version="5.0.0" />
</ItemGroup>

View file

@ -6,12 +6,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
<PackageReference Include="NSubstitute" Version="5.0.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.29" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.29" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.51" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.51" />
<PackageReference Include="xunit" Version="2.5.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -806,7 +806,7 @@ public class ReadingListServiceTests
}
catch (Exception ex)
{
Assert.Equal("A list of this name already exists", ex.Message);
Assert.Equal("reading-list-name-exists", ex.Message);
}
Assert.Single((await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.ReadingLists))
.ReadingLists);
@ -834,7 +834,7 @@ public class ReadingListServiceTests
}
catch (Exception ex)
{
Assert.Equal("A list of this name already exists", ex.Message);
Assert.Equal("reading-list-name-exists", ex.Message);
}
}
@ -860,7 +860,7 @@ public class ReadingListServiceTests
}
catch (Exception ex)
{
Assert.Equal("A list of this name already exists", ex.Message);
Assert.Equal("reading-list-name-exists", ex.Message);
}
Assert.Single((await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.ReadingLists))
.ReadingLists);

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO.Abstractions;
using System.Linq;
using System.Threading.Tasks;
using API.Data;
@ -19,21 +20,48 @@ using API.SignalR;
using API.Tests.Helpers;
using Hangfire;
using Hangfire.InMemory;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Internal;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace API.Tests.Services;
internal class MockHostingEnvironment : IHostEnvironment {
public string ApplicationName { get => "API"; set => throw new NotImplementedException(); }
public IFileProvider ContentRootFileProvider { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public string ContentRootPath
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public string EnvironmentName { get => "Testing"; set => throw new NotImplementedException(); }
}
public class SeriesServiceTests : AbstractDbTest
{
private readonly ISeriesService _seriesService;
public SeriesServiceTests() : base()
{
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new FileSystem()
{
});
var locService = new LocalizationService(ds, new MockHostingEnvironment(),
Substitute.For<IMemoryCache>(), Substitute.For<IUnitOfWork>());
_seriesService = new SeriesService(_unitOfWork, Substitute.For<IEventHub>(),
Substitute.For<ITaskScheduler>(), Substitute.For<ILogger<SeriesService>>(),
Substitute.For<IScrobblingService>());
Substitute.For<IScrobblingService>(), locService);
}
#region Setup
@ -1194,9 +1222,19 @@ public class SeriesServiceTests : AbstractDbTest
[InlineData(LibraryType.Comic, false, "Issue")]
[InlineData(LibraryType.Comic, true, "Issue #")]
[InlineData(LibraryType.Book, false, "Book")]
public void FormatChapterNameTest(LibraryType libraryType, bool withHash, string expected )
public async Task FormatChapterNameTest(LibraryType libraryType, bool withHash, string expected )
{
Assert.Equal(expected, SeriesService.FormatChapterName(libraryType, withHash));
await ResetDb();
_context.Library.Add(new LibraryBuilder("Test LIb")
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
.WithLocale("en")
.Build())
.Build());
await _context.SaveChangesAsync();
Assert.Equal(expected, await _seriesService.FormatChapterName(1, libraryType, withHash));
}
#endregion
@ -1204,59 +1242,132 @@ public class SeriesServiceTests : AbstractDbTest
#region FormatChapterTitle
[Fact]
public void FormatChapterTitle_Manga_NonSpecial()
public async Task FormatChapterTitle_Manga_NonSpecial()
{
await ResetDb();
_context.Library.Add(new LibraryBuilder("Test LIb")
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
.WithLocale("en")
.Build())
.Build());
await _context.SaveChangesAsync();
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build();
Assert.Equal("Chapter Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Manga, false));
Assert.Equal("Chapter Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Manga, false));
}
[Fact]
public void FormatChapterTitle_Manga_Special()
public async Task FormatChapterTitle_Manga_Special()
{
await ResetDb();
_context.Library.Add(new LibraryBuilder("Test LIb")
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
.WithLocale("en")
.Build())
.Build());
await _context.SaveChangesAsync();
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build();
Assert.Equal("Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Manga, false));
Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Manga, false));
}
[Fact]
public void FormatChapterTitle_Comic_NonSpecial_WithoutHash()
public async Task FormatChapterTitle_Comic_NonSpecial_WithoutHash()
{
await ResetDb();
_context.Library.Add(new LibraryBuilder("Test LIb")
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
.WithLocale("en")
.Build())
.Build());
await _context.SaveChangesAsync();
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build();
Assert.Equal("Issue Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Comic, false));
Assert.Equal("Issue Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, false));
}
[Fact]
public void FormatChapterTitle_Comic_Special_WithoutHash()
public async Task FormatChapterTitle_Comic_Special_WithoutHash()
{
await ResetDb();
_context.Library.Add(new LibraryBuilder("Test LIb")
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
.WithLocale("en")
.Build())
.Build());
await _context.SaveChangesAsync();
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build();
Assert.Equal("Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Comic, false));
Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, false));
}
[Fact]
public void FormatChapterTitle_Comic_NonSpecial_WithHash()
public async Task FormatChapterTitle_Comic_NonSpecial_WithHash()
{
await ResetDb();
_context.Library.Add(new LibraryBuilder("Test LIb")
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
.WithLocale("en")
.Build())
.Build());
await _context.SaveChangesAsync();
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build();
Assert.Equal("Issue #Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Comic, true));
Assert.Equal("Issue #Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, true));
}
[Fact]
public void FormatChapterTitle_Comic_Special_WithHash()
public async Task FormatChapterTitle_Comic_Special_WithHash()
{
await ResetDb();
_context.Library.Add(new LibraryBuilder("Test LIb")
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
.WithLocale("en")
.Build())
.Build());
await _context.SaveChangesAsync();
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build();
Assert.Equal("Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Comic, true));
Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, true));
}
[Fact]
public void FormatChapterTitle_Book_NonSpecial()
public async Task FormatChapterTitle_Book_NonSpecial()
{
await ResetDb();
_context.Library.Add(new LibraryBuilder("Test LIb")
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
.WithLocale("en")
.Build())
.Build());
await _context.SaveChangesAsync();
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(false).Build();
Assert.Equal("Book Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Book, false));
Assert.Equal("Book Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Book, false));
}
[Fact]
public void FormatChapterTitle_Book_Special()
public async Task FormatChapterTitle_Book_Special()
{
await ResetDb();
_context.Library.Add(new LibraryBuilder("Test LIb")
.WithAppUser(new AppUserBuilder("majora2007", string.Empty)
.WithLocale("en")
.Build())
.Build());
await _context.SaveChangesAsync();
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build();
Assert.Equal("Some title", SeriesService.FormatChapterTitle(chapter, LibraryType.Book, false));
Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Book, false));
}
#endregion

View file

@ -60,23 +60,23 @@
<PackageReference Include="ExCSS" Version="4.2.1" />
<PackageReference Include="Flurl" Version="3.0.7" />
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Hangfire" Version="1.8.3" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.3" />
<PackageReference Include="Hangfire" Version="1.8.4" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.4" />
<PackageReference Include="Hangfire.InMemory" Version="0.5.1" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.4" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.50" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.51" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.10" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.9">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
@ -95,14 +95,14 @@
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.33.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.5.0.73987">
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.7.0.75501">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.8" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.32.0" />
<PackageReference Include="System.IO.Abstractions" Version="19.2.29" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.32.1" />
<PackageReference Include="System.IO.Abstractions" Version="19.2.51" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
</ItemGroup>
@ -191,6 +191,7 @@
<ItemGroup>
<Folder Include="config\themes" />
<Folder Include="I18N\**" />
</ItemGroup>
<ItemGroup>

View file

@ -14,12 +14,9 @@ using API.Entities.Enums;
using API.Errors;
using API.Extensions;
using API.Helpers.Builders;
using API.Middleware.RateLimit;
using API.Services;
using API.Services.Plus;
using API.SignalR;
using AutoMapper;
using EasyCaching.Core;
using Hangfire;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
@ -46,6 +43,7 @@ public class AccountController : BaseApiController
private readonly IAccountService _accountService;
private readonly IEmailService _emailService;
private readonly IEventHub _eventHub;
private readonly ILocalizationService _localizationService;
/// <inheritdoc />
public AccountController(UserManager<AppUser> userManager,
@ -53,7 +51,8 @@ public class AccountController : BaseApiController
ITokenService tokenService, IUnitOfWork unitOfWork,
ILogger<AccountController> logger,
IMapper mapper, IAccountService accountService,
IEmailService emailService, IEventHub eventHub)
IEmailService emailService, IEventHub eventHub,
ILocalizationService localizationService)
{
_userManager = userManager;
_signInManager = signInManager;
@ -64,6 +63,7 @@ public class AccountController : BaseApiController
_accountService = accountService;
_emailService = emailService;
_eventHub = eventHub;
_localizationService = localizationService;
}
/// <summary>
@ -71,7 +71,6 @@ public class AccountController : BaseApiController
/// </summary>
/// <param name="resetPasswordDto"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("reset-password")]
public async Task<ActionResult> UpdatePassword(ResetPasswordDto resetPasswordDto)
{
@ -82,19 +81,21 @@ public class AccountController : BaseApiController
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
if (resetPasswordDto.UserName == User.GetUsername() && !(User.IsInRole(PolicyConstants.ChangePasswordRole) || isAdmin))
return Unauthorized("You are not permitted to this operation.");
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
if (resetPasswordDto.UserName != User.GetUsername() && !isAdmin)
return Unauthorized("You are not permitted to this operation.");
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
if (string.IsNullOrEmpty(resetPasswordDto.OldPassword) && !isAdmin)
return BadRequest(new ApiException(400, "You must enter your existing password to change your account unless you're an admin"));
return BadRequest(
new ApiException(400,
await _localizationService.Translate(User.GetUserId(), "password-required")));
// If you're an admin and the username isn't yours, you don't need to validate the password
var isResettingOtherUser = (resetPasswordDto.UserName != User.GetUsername() && isAdmin);
if (!isResettingOtherUser && !await _userManager.CheckPasswordAsync(user, resetPasswordDto.OldPassword))
{
return BadRequest("Invalid Password");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-password"));
}
var errors = await _accountService.ChangeUserPassword(user, resetPasswordDto.Password);
@ -117,7 +118,7 @@ public class AccountController : BaseApiController
public async Task<ActionResult<UserDto>> RegisterFirstUser(RegisterDto registerDto)
{
var admins = await _userManager.GetUsersInRoleAsync("Admin");
if (admins.Count > 0) return BadRequest("Not allowed");
if (admins.Count > 0) return BadRequest(await _localizationService.Get("en", "denied"));
try
{
@ -135,8 +136,8 @@ public class AccountController : BaseApiController
if (!result.Succeeded) return BadRequest(result.Errors);
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
if (string.IsNullOrEmpty(token)) return BadRequest("There was an issue generating a confirmation token.");
if (!await ConfirmEmailToken(token, user)) return BadRequest($"There was an issue validating your email: {token}");
if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Get("en", "confirm-token-gen"));
if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Get("en", "validate-email", token));
var roleResult = await _userManager.AddToRoleAsync(user, PolicyConstants.AdminRole);
@ -163,7 +164,7 @@ public class AccountController : BaseApiController
await _unitOfWork.CommitAsync();
}
return BadRequest("Something went wrong when registering user");
return BadRequest(await _localizationService.Get("en", "register-user"));
}
@ -176,26 +177,40 @@ public class AccountController : BaseApiController
[HttpPost("login")]
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
{
var user = await _userManager.Users
AppUser? user;
if (!string.IsNullOrEmpty(loginDto.ApiKey))
{
user = await _userManager.Users
.Include(u => u.UserPreferences)
.SingleOrDefaultAsync(x => x.ApiKey == loginDto.ApiKey);
}
else
{
user = await _userManager.Users
.Include(u => u.UserPreferences)
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper());
}
if (user == null) return Unauthorized("Your credentials are not correct");
if (user == null) return Unauthorized(await _localizationService.Get("en", "bad-credentials"));
var roles = await _userManager.GetRolesAsync(user);
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized("Your account is disabled. Contact the server admin.");
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account"));
if (string.IsNullOrEmpty(loginDto.ApiKey))
{
var result = await _signInManager
.CheckPasswordSignInAsync(user, loginDto.Password, true);
if (result.IsLockedOut)
{
await _userManager.UpdateSecurityStampAsync(user);
return Unauthorized("You've been locked out from too many authorization attempts. Please wait 10 minutes.");
return Unauthorized(await _localizationService.Translate(user.Id, "locked-out"));
}
if (!result.Succeeded)
{
return Unauthorized(result.IsNotAllowed ? "You must confirm your email first" : "Your credentials are not correct");
return Unauthorized(await _localizationService.Translate(user.Id, result.IsNotAllowed ? "confirm-email" : "bad-credentials"));
}
}
// Update LastActive on account
@ -256,7 +271,7 @@ public class AccountController : BaseApiController
var token = await _tokenService.ValidateRefreshToken(tokenRequestDto);
if (token == null)
{
return Unauthorized(new { message = "Invalid token" });
return Unauthorized(new { message = await _localizationService.Get("en", "invalid-token") });
}
return Ok(token);
@ -295,7 +310,7 @@ public class AccountController : BaseApiController
}
await _unitOfWork.RollbackAsync();
return BadRequest("Something went wrong, unable to reset key");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "unable-to-reset-key"));
}
@ -310,26 +325,27 @@ public class AccountController : BaseApiController
public async Task<ActionResult> UpdateEmail(UpdateEmailDto? dto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (user == null) return Unauthorized("You do not have permission");
if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
if (dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password)) return BadRequest("Invalid payload");
if (dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password))
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload"));
// Validate this user's password
if (! await _userManager.CheckPasswordAsync(user, dto.Password))
{
_logger.LogCritical("A user tried to change {UserName}'s email, but password didn't validate", user.UserName);
return BadRequest("You do not have permission");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
}
// Validate no other users exist with this email
if (user.Email!.Equals(dto.Email)) return Ok("Nothing to do");
if (user.Email!.Equals(dto.Email)) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
// Check if email is used by another user
var existingUserEmail = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
if (existingUserEmail != null)
{
return BadRequest("You cannot share emails across multiple accounts");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "share-multiple-emails"));
}
// All validations complete, generate a new token and email it to the user at the new address. Confirm email link will update the email
@ -337,7 +353,7 @@ public class AccountController : BaseApiController
if (string.IsNullOrEmpty(token))
{
_logger.LogError("There was an issue generating a token for the email");
return BadRequest("There was an issue creating a confirmation email token. See logs.");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generate-token"));
}
user.EmailConfirmed = false;
@ -392,10 +408,10 @@ public class AccountController : BaseApiController
public async Task<ActionResult> UpdateAgeRestriction(UpdateAgeRestrictionDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (user == null) return Unauthorized("You do not have permission");
if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
if (!await _accountService.HasChangeRestrictionRole(user)) return BadRequest("You do not have permission");
if (!await _accountService.HasChangeRestrictionRole(user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating;
user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns;
@ -410,7 +426,7 @@ public class AccountController : BaseApiController
catch (Exception ex)
{
_logger.LogError(ex, "There was an error updating the age restriction");
return BadRequest("There was an error updating the age restriction");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "age-restriction-update"));
}
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
@ -429,17 +445,17 @@ public class AccountController : BaseApiController
{
var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (adminUser == null) return Unauthorized();
if (!await _unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized("You do not have permission");
if (!await _unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(dto.UserId);
if (user == null) return BadRequest("User does not exist");
if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-user"));
// Check if username is changing
if (!user.UserName!.Equals(dto.Username))
{
// Validate username change
var errors = await _accountService.ValidateUsername(dto.Username);
if (errors.Any()) return BadRequest("Username already taken");
if (errors.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "username-taken"));
user.UserName = dto.Username;
_unitOfWork.UserRepository.Update(user);
}
@ -504,7 +520,7 @@ public class AccountController : BaseApiController
}
await _unitOfWork.RollbackAsync();
return BadRequest("There was an exception when updating the user");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-user-update"));
}
/// <summary>
@ -520,9 +536,9 @@ public class AccountController : BaseApiController
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null) return Unauthorized();
if (user.EmailConfirmed)
return BadRequest("User is already confirmed");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-confirmed"));
if (string.IsNullOrEmpty(user.ConfirmationToken))
return BadRequest("Manual setup is unable to be completed. Please cancel and recreate the invite.");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "manual-setup-fail"));
return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl);
}
@ -539,7 +555,7 @@ public class AccountController : BaseApiController
public async Task<ActionResult<string>> InviteUser(InviteUserDto dto)
{
var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (adminUser == null) return Unauthorized("You are not permitted");
if (adminUser == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
_logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email);
@ -552,8 +568,8 @@ public class AccountController : BaseApiController
{
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
return BadRequest($"User is already registered as {invitedUser!.UserName}");
return BadRequest("User is already invited under this email and has yet to accepted invite.");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-registered", invitedUser!.UserName));
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-invited"));
}
}
@ -608,7 +624,7 @@ public class AccountController : BaseApiController
if (string.IsNullOrEmpty(token))
{
_logger.LogError("There was an issue generating a token for the email");
return BadRequest("There was an creating the invite user");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user"));
}
user.ConfirmationToken = token;
@ -650,7 +666,7 @@ public class AccountController : BaseApiController
_logger.LogError(ex, "There was an error during invite user flow, unable to send an email");
}
return BadRequest("There was an error setting up your account. Please check the logs");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user"));
}
/// <summary>
@ -667,7 +683,7 @@ public class AccountController : BaseApiController
if (user == null)
{
_logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email);
return BadRequest("Invalid email confirmation");
return BadRequest(await _localizationService.Get("en", "invalid-email-confirmation"));
}
// Validate Password and Username
@ -688,7 +704,7 @@ public class AccountController : BaseApiController
if (!await ConfirmEmailToken(dto.Token, user))
{
_logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token);
return BadRequest("Invalid email confirmation");
return BadRequest(await _localizationService.Translate(user.Id, "invalid-email-confirmation"));
}
user.UserName = dto.Username;
@ -731,13 +747,13 @@ public class AccountController : BaseApiController
if (user == null)
{
_logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email);
return BadRequest("Invalid email confirmation");
return BadRequest(await _localizationService.Get("en", "invalid-email-confirmation"));
}
if (!await ConfirmEmailToken(dto.Token, user))
{
_logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token);
return BadRequest("Invalid email confirmation");
return BadRequest(await _localizationService.Translate(user.Id, "invalid-email-confirmation"));
}
_logger.LogInformation("User is updating email from {OldEmail} to {NewEmail}", user.Email, dto.Email);
@ -745,7 +761,7 @@ public class AccountController : BaseApiController
if (!result.Succeeded)
{
_logger.LogError("Unable to update email for users: {Errors}", result.Errors.Select(e => e.Description));
return BadRequest("Unable to update email for user. Check logs");
return BadRequest(await _localizationService.Translate(user.Id, "generic-user-email-update"));
}
user.ConfirmationToken = null;
await _unitOfWork.CommitAsync();
@ -763,12 +779,12 @@ public class AccountController : BaseApiController
[HttpPost("confirm-password-reset")]
public async Task<ActionResult<string>> ConfirmForgotPassword(ConfirmPasswordResetDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
try
{
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
if (user == null)
{
return BadRequest("Invalid credentials");
return BadRequest(await _localizationService.Get("en", "bad-credentials"));
}
var result = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider,
@ -776,16 +792,16 @@ public class AccountController : BaseApiController
if (!result)
{
_logger.LogInformation("Unable to reset password, your email token is not correct: {@Dto}", dto);
return BadRequest("Invalid credentials");
return BadRequest(await _localizationService.Translate(user.Id, "bad-credentials"));
}
var errors = await _accountService.ChangeUserPassword(user, dto.Password);
return errors.Any() ? BadRequest(errors) : Ok("Password updated");
return errors.Any() ? BadRequest(errors) : Ok(await _localizationService.Translate(user.Id, "password-updated"));
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an unexpected error when confirming new password");
return BadRequest("There was an unexpected error when confirming new password");
return BadRequest(await _localizationService.Translate(user.Id, "generic-password-update"));
}
}
@ -804,15 +820,15 @@ public class AccountController : BaseApiController
if (user == null)
{
_logger.LogError("There are no users with email: {Email} but user is requesting password reset", email);
return Ok("An email will be sent to the email if it exists in our database");
return Ok(await _localizationService.Get("en", "forgot-password-generic"));
}
var roles = await _userManager.GetRolesAsync(user);
if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole))
return Unauthorized("You are not permitted to this operation.");
return Unauthorized(await _localizationService.Translate(user.Id, "permission-denied"));
if (string.IsNullOrEmpty(user.Email) || !user.EmailConfirmed)
return BadRequest("You do not have an email on account or it has not been confirmed");
return BadRequest(await _localizationService.Translate(user.Id, "confirm-email"));
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email);
@ -825,10 +841,10 @@ public class AccountController : BaseApiController
ServerConfirmationLink = emailLink,
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
});
return Ok("Email sent");
return Ok(await _localizationService.Translate(user.Id, "email-sent"));
}
return Ok("Your server is not accessible. The Link to reset your password is in the logs.");
return Ok(await _localizationService.Translate(user.Id, "not-accessible-password"));
}
[HttpGet("email-confirmed")]
@ -845,12 +861,12 @@ public class AccountController : BaseApiController
public async Task<ActionResult<UserDto>> ConfirmMigrationEmail(ConfirmMigrationEmailDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
if (user == null) return BadRequest("Invalid credentials");
if (user == null) return BadRequest(await _localizationService.Get("en", "bad-credentials"));
if (!await ConfirmEmailToken(dto.Token, user))
{
_logger.LogInformation("confirm-migration-email email token is invalid");
return BadRequest("Invalid credentials");
return BadRequest(await _localizationService.Translate(user.Id, "bad-credentials"));
}
await _unitOfWork.CommitAsync();
@ -881,12 +897,12 @@ public class AccountController : BaseApiController
public async Task<ActionResult<string>> ResendConfirmationSendEmail([FromQuery] int userId)
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null) return BadRequest("User does not exist");
if (user == null) return BadRequest(await _localizationService.Get("en", "no-user"));
if (string.IsNullOrEmpty(user.Email))
return BadRequest(
"This user needs to migrate. Have them log out and login to trigger a migration flow");
if (user.EmailConfirmed) return BadRequest("User already confirmed");
await _localizationService.Translate(user.Id, "user-migration-needed"));
if (user.EmailConfirmed) return BadRequest(await _localizationService.Translate(user.Id, "user-already-confirmed"));
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email", user.Email);
@ -907,12 +923,12 @@ public class AccountController : BaseApiController
catch (Exception ex)
{
_logger.LogError(ex, "There was an issue resending invite email");
return BadRequest("There was an issue resending invite email");
return BadRequest(await _localizationService.Translate(user.Id, "generic-invite-email"));
}
return Ok(emailLink);
}
return Ok("The server is not accessible externally");
return Ok(await _localizationService.Translate(user.Id, "not-accessible"));
}
/// <summary>
@ -926,7 +942,7 @@ public class AccountController : BaseApiController
{
// If there is an admin account already, return
var users = await _unitOfWork.UserRepository.GetAdminUsersAsync();
if (users.Any()) return BadRequest("Admin already exists");
if (users.Any()) return BadRequest(await _localizationService.Get("en", "admin-already-exists"));
// Check if there is an existing invite
var emailValidationErrors = await _accountService.ValidateEmail(dto.Email);
@ -934,27 +950,27 @@ public class AccountController : BaseApiController
{
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
return BadRequest($"User is already registered as {invitedUser!.UserName}");
return BadRequest(await _localizationService.Get("en", "user-already-registered", invitedUser!.UserName));
_logger.LogInformation("A user is attempting to login, but hasn't accepted email invite");
return BadRequest("User is already invited under this email and has yet to accepted invite.");
return BadRequest(await _localizationService.Get("en", "user-already-invited"));
}
var user = await _userManager.Users
.Include(u => u.UserPreferences)
.SingleOrDefaultAsync(x => x.NormalizedUserName == dto.Username.ToUpper());
if (user == null) return BadRequest("Invalid username");
if (user == null) return BadRequest(await _localizationService.Get("en", "invalid-username"));
var validPassword = await _signInManager.UserManager.CheckPasswordAsync(user, dto.Password);
if (!validPassword) return BadRequest("Your credentials are not correct");
if (!validPassword) return BadRequest(await _localizationService.Get("en", "bad-credentials"));
try
{
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
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(await _localizationService.Get("en", "critical-email-migration"));
_unitOfWork.UserRepository.Update(user);
await _unitOfWork.CommitAsync();
@ -968,7 +984,7 @@ public class AccountController : BaseApiController
await _unitOfWork.CommitAsync();
}
return BadRequest("There was an error setting up your account. Please check the logs");
return BadRequest(await _localizationService.Get("en", "critical-email-migration"));
}
@ -978,8 +994,6 @@ public class AccountController : BaseApiController
var result = await _userManager.ConfirmEmailAsync(user, token);
if (result.Succeeded) return true;
_logger.LogCritical("[Account] Email validation failed");
if (!result.Errors.Any()) return false;
@ -1004,7 +1018,20 @@ public class AccountController : BaseApiController
if (!string.IsNullOrEmpty(serverSettings.HostName)) origin = serverSettings.HostName;
var baseUrl = string.Empty;
if (!string.IsNullOrEmpty(serverSettings.BaseUrl) && !serverSettings.BaseUrl.Equals(Configuration.DefaultBaseUrl)) baseUrl = serverSettings.BaseUrl + "/";
if (!string.IsNullOrEmpty(serverSettings.BaseUrl) &&
!serverSettings.BaseUrl.Equals(Configuration.DefaultBaseUrl))
{
baseUrl = serverSettings.BaseUrl + "/";
if (baseUrl.EndsWith("//"))
{
baseUrl = baseUrl.Replace("//", "/");
}
if (baseUrl.StartsWith("/"))
{
baseUrl = baseUrl.Substring(1, baseUrl.Length - 1);
}
}
return Ok(origin + "/" + baseUrl + "api/opds/" + user!.ApiKey);
}

View file

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using API.Data;
using API.DTOs.Reader;
using API.Entities.Enums;
using API.Extensions;
using API.Services;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
@ -18,13 +19,16 @@ public class BookController : BaseApiController
private readonly IBookService _bookService;
private readonly IUnitOfWork _unitOfWork;
private readonly ICacheService _cacheService;
private readonly ILocalizationService _localizationService;
public BookController(IBookService bookService,
IUnitOfWork unitOfWork, ICacheService cacheService)
IUnitOfWork unitOfWork, ICacheService cacheService,
ILocalizationService localizationService)
{
_bookService = bookService;
_unitOfWork = unitOfWork;
_cacheService = cacheService;
_localizationService = localizationService;
}
/// <summary>
@ -37,7 +41,7 @@ public class BookController : BaseApiController
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
{
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
if (dto == null) return BadRequest("Chapter does not exist");
if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var bookTitle = string.Empty;
switch (dto.SeriesFormat)
{
@ -92,14 +96,14 @@ public class BookController : BaseApiController
[AllowAnonymous]
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
{
if (chapterId <= 0) return BadRequest("Chapter is not valid");
if (chapterId <= 0) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist"));
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
if (chapter == null) return BadRequest("Chapter is not valid");
if (chapter == null) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist"));
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
var key = BookService.CoalesceKeyForAnyFile(book, file);
if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return BadRequest("File was not found in book");
if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return BadRequest(await _localizationService.Get("en", "file-missing"));
var bookFile = book.Content.AllFiles.GetLocalFileRefByKey(key);
var content = await bookFile.ReadContentAsBytesAsync();
@ -118,9 +122,9 @@ public class BookController : BaseApiController
[HttpGet("{chapterId}/chapters")]
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
{
if (chapterId <= 0) return BadRequest("Chapter is not valid");
if (chapterId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
if (chapter == null) return BadRequest("Chapter is not valid");
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
try
{
@ -144,7 +148,7 @@ public class BookController : BaseApiController
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
{
var chapter = await _cacheService.Ensure(chapterId);
if (chapter == null) return BadRequest("Could not find Chapter");
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var path = _cacheService.GetCachedFile(chapter);
var baseUrl = "//" + Request.Host + Request.PathBase + "/api/";
@ -155,7 +159,7 @@ public class BookController : BaseApiController
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
}
}
}

View file

@ -20,12 +20,15 @@ public class CollectionController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly ICollectionTagService _collectionService;
private readonly ILocalizationService _localizationService;
/// <inheritdoc />
public CollectionController(IUnitOfWork unitOfWork, ICollectionTagService collectionService)
public CollectionController(IUnitOfWork unitOfWork, ICollectionTagService collectionService,
ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_collectionService = collectionService;
_localizationService = localizationService;
}
/// <summary>
@ -87,14 +90,14 @@ public class CollectionController : BaseApiController
{
try
{
if (await _collectionService.UpdateTag(updatedTag)) return Ok("Tag updated successfully");
if (await _collectionService.UpdateTag(updatedTag)) return Ok(await _localizationService.Translate(User.GetUserId(), "collection-updated-successfully"));
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
}
return BadRequest("Something went wrong, please try again");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
}
/// <summary>
@ -111,7 +114,7 @@ public class CollectionController : BaseApiController
if (await _collectionService.AddTagToSeries(tag, dto.SeriesIds)) return Ok();
return BadRequest("There was an issue updating series with collection tag");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
}
/// <summary>
@ -126,18 +129,17 @@ public class CollectionController : BaseApiController
try
{
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updateSeriesForTagDto.Tag.Id, CollectionTagIncludes.SeriesMetadata);
if (tag == null) return BadRequest("Not a valid Tag");
if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist"));
tag.SeriesMetadatas ??= new List<SeriesMetadata>();
if (await _collectionService.RemoveTagFromSeries(tag, updateSeriesForTagDto.SeriesIdsToRemove))
return Ok("Tag updated");
return Ok(await _localizationService.Translate(User.GetUserId(), "collection-updated"));
}
catch (Exception)
{
await _unitOfWork.RollbackAsync();
}
return BadRequest("Something went wrong. Please try again.");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
}
}

View file

@ -21,13 +21,16 @@ public class DeviceController : BaseApiController
private readonly IDeviceService _deviceService;
private readonly IEmailService _emailService;
private readonly IEventHub _eventHub;
private readonly ILocalizationService _localizationService;
public DeviceController(IUnitOfWork unitOfWork, IDeviceService deviceService, IEmailService emailService, IEventHub eventHub)
public DeviceController(IUnitOfWork unitOfWork, IDeviceService deviceService,
IEmailService emailService, IEventHub eventHub, ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_deviceService = deviceService;
_emailService = emailService;
_eventHub = eventHub;
_localizationService = localizationService;
}
@ -36,9 +39,19 @@ public class DeviceController : BaseApiController
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
if (user == null) return Unauthorized();
try
{
var device = await _deviceService.Create(dto, user);
if (device == null)
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-device-create"));
}
catch (KavitaException ex)
{
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
}
if (device == null) return BadRequest("There was an error when creating the device");
return Ok();
}
@ -50,7 +63,7 @@ public class DeviceController : BaseApiController
if (user == null) return Unauthorized();
var device = await _deviceService.Update(dto, user);
if (device == null) return BadRequest("There was an error when updating the device");
if (device == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-device-update"));
return Ok();
}
@ -63,12 +76,12 @@ public class DeviceController : BaseApiController
[HttpDelete]
public async Task<ActionResult> DeleteDevice(int deviceId)
{
if (deviceId <= 0) return BadRequest("Not a valid deviceId");
if (deviceId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "device-doesnt-exist"));
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
if (user == null) return Unauthorized();
if (await _deviceService.Delete(user, deviceId)) return Ok();
return BadRequest("Could not delete device");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-device-delete"));
}
[HttpGet]
@ -81,15 +94,16 @@ public class DeviceController : BaseApiController
[HttpPost("send-to")]
public async Task<ActionResult> SendToDevice(SendToDeviceDto dto)
{
if (dto.ChapterIds.Any(i => i < 0)) return BadRequest("ChapterIds must be greater than 0");
if (dto.DeviceId < 0) return BadRequest("DeviceId must be greater than 0");
if (dto.ChapterIds.Any(i => i < 0)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "ChapterIds"));
if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "DeviceId"));
if (await _emailService.IsDefaultEmailService())
return BadRequest("Send to device cannot be used with Kavita's email service. Please configure your own.");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email"));
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress,
MessageFactory.SendingToDeviceEvent($"Transferring files to your device", "started"), userId);
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
"started"), userId);
try
{
var success = await _deviceService.SendTo(dto.ChapterIds, dto.DeviceId);
@ -97,15 +111,16 @@ public class DeviceController : BaseApiController
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
}
finally
{
await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice,
MessageFactory.SendingToDeviceEvent($"Transferring files to your device", "ended"), userId);
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
"ended"), userId);
}
return BadRequest("There was an error sending the file to the device");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to"));
}
@ -113,19 +128,21 @@ public class DeviceController : BaseApiController
[HttpPost("send-series-to")]
public async Task<ActionResult> SendSeriesToDevice(SendSeriesToDeviceDto dto)
{
if (dto.SeriesId <= 0) return BadRequest("SeriesId must be greater than 0");
if (dto.DeviceId < 0) return BadRequest("DeviceId must be greater than 0");
if (dto.SeriesId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "SeriesId"));
if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "DeviceId"));
if (await _emailService.IsDefaultEmailService())
return BadRequest("Send to device cannot be used with Kavita's email service. Please configure your own.");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email"));
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, MessageFactory.SendingToDeviceEvent($"Transferring files to your device", "started"), userId);
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress,
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
"started"), userId);
var series =
await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId,
SeriesIncludes.Volumes | SeriesIncludes.Chapters);
if (series == null) return BadRequest("Series doesn't Exist");
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
var chapterIds = series.Volumes.SelectMany(v => v.Chapters.Select(c => c.Id)).ToList();
try
{
@ -134,14 +151,16 @@ public class DeviceController : BaseApiController
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
}
finally
{
await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice, MessageFactory.SendingToDeviceEvent($"Transferring files to your device", "ended"), userId);
await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice,
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
"ended"), userId);
}
return BadRequest("There was an error sending the file(s) to the device");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to"));
}

View file

@ -30,11 +30,12 @@ public class DownloadController : BaseApiController
private readonly ILogger<DownloadController> _logger;
private readonly IBookmarkService _bookmarkService;
private readonly IAccountService _accountService;
private readonly ILocalizationService _localizationService;
private const string DefaultContentType = "application/octet-stream";
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService,
IDownloadService downloadService, IEventHub eventHub, ILogger<DownloadController> logger, IBookmarkService bookmarkService,
IAccountService accountService)
IAccountService accountService, ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_archiveService = archiveService;
@ -44,6 +45,7 @@ public class DownloadController : BaseApiController
_logger = logger;
_bookmarkService = bookmarkService;
_accountService = accountService;
_localizationService = localizationService;
}
/// <summary>
@ -92,9 +94,9 @@ public class DownloadController : BaseApiController
[HttpGet("volume")]
public async Task<ActionResult> DownloadVolume(int volumeId)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId);
if (volume == null) return BadRequest("Volume doesn't exist");
if (volume == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "volume-doesnt-exist"));
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
try
@ -128,10 +130,10 @@ public class DownloadController : BaseApiController
[HttpGet("chapter")]
public async Task<ActionResult> DownloadChapter(int chapterId)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
if (chapter == null) return BadRequest("Invalid chapter");
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId);
try
@ -178,7 +180,7 @@ public class DownloadController : BaseApiController
[HttpGet("series")]
public async Task<ActionResult> DownloadSeries(int seriesId)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
if (series == null) return BadRequest("Invalid Series");
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
@ -200,8 +202,8 @@ public class DownloadController : BaseApiController
[HttpPost("bookmarks")]
public async Task<ActionResult> DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest("Bookmarks cannot be empty");
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmarks-empty"));
// We know that all bookmarks will be for one single seriesId
var userId = User.GetUserId()!;

View file

@ -22,13 +22,16 @@ public class ImageController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IDirectoryService _directoryService;
private readonly IImageService _imageService;
private readonly ILocalizationService _localizationService;
/// <inheritdoc />
public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService, IImageService imageService)
public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService,
IImageService imageService, ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_directoryService = directoryService;
_imageService = imageService;
_localizationService = localizationService;
}
/// <summary>
@ -40,9 +43,10 @@ public class ImageController : BaseApiController
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"chapterId", "apiKey"})]
public async Task<ActionResult> GetChapterCoverImage(int chapterId, string apiKey)
{
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId == 0) return BadRequest();
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
var format = _directoryService.FileSystem.Path.GetExtension(path);
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
@ -57,9 +61,10 @@ public class ImageController : BaseApiController
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"libraryId", "apiKey"})]
public async Task<ActionResult> GetLibraryCoverImage(int libraryId, string apiKey)
{
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId == 0) return BadRequest();
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
var format = _directoryService.FileSystem.Path.GetExtension(path);
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
@ -74,9 +79,10 @@ public class ImageController : BaseApiController
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"volumeId", "apiKey"})]
public async Task<ActionResult> GetVolumeCoverImage(int volumeId, string apiKey)
{
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId == 0) return BadRequest();
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
var format = _directoryService.FileSystem.Path.GetExtension(path);
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
@ -91,9 +97,10 @@ public class ImageController : BaseApiController
[HttpGet("series-cover")]
public async Task<ActionResult> GetSeriesCoverImage(int seriesId, string apiKey)
{
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId == 0) return BadRequest();
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
var format = _directoryService.FileSystem.Path.GetExtension(path);
Response.AddCacheHeader(path);
@ -110,13 +117,15 @@ public class ImageController : BaseApiController
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"collectionTagId", "apiKey"})]
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId, string apiKey)
{
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId == 0) return BadRequest();
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
{
var destFile = await GenerateCollectionCoverImage(collectionTagId);
if (string.IsNullOrEmpty(destFile)) return BadRequest("No cover image");
return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)), _directoryService.FileSystem.Path.GetFileName(destFile));
if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)),
_directoryService.FileSystem.Path.GetFileName(destFile));
}
var format = _directoryService.FileSystem.Path.GetExtension(path);
@ -132,12 +141,13 @@ public class ImageController : BaseApiController
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Images, VaryByQueryKeys = new []{"readingListId", "apiKey"})]
public async Task<ActionResult> GetReadingListCoverImage(int readingListId, string apiKey)
{
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId == 0) return BadRequest();
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
{
var destFile = await GenerateReadingListCoverImage(readingListId);
if (string.IsNullOrEmpty(destFile)) return BadRequest("No cover image");
if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(userId, "no-cover-image"));
return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)), _directoryService.FileSystem.Path.GetFileName(destFile));
}
@ -199,7 +209,7 @@ public class ImageController : BaseApiController
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId == 0) return BadRequest();
var bookmark = await _unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, userId);
if (bookmark == null) return BadRequest("Bookmark does not exist");
if (bookmark == null) return BadRequest(await _localizationService.Translate(userId, "bookmark-doesnt-exist"));
var bookmarkDirectory =
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
@ -220,7 +230,7 @@ public class ImageController : BaseApiController
{
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId == 0) return BadRequest();
if (string.IsNullOrEmpty(url)) return BadRequest("Url cannot be null");
if (string.IsNullOrEmpty(url)) return BadRequest(await _localizationService.Translate(userId, "must-be-defined", "Url"));
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
// Check if the domain exists
@ -235,7 +245,7 @@ public class ImageController : BaseApiController
}
catch (Exception)
{
return BadRequest("There was an issue fetching favicon for domain");
return BadRequest(await _localizationService.Translate(userId, "generic-favicon"));
}
}
@ -256,10 +266,11 @@ public class ImageController : BaseApiController
public async Task<ActionResult> GetCoverUploadImage(string filename, string apiKey)
{
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
if (filename.Contains("..")) return BadRequest("Invalid Filename");
if (filename.Contains("..")) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-filename"));
var path = Path.Join(_directoryService.TempDirectory, filename);
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"File does not exist");
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
return BadRequest(await _localizationService.Translate(User.GetUserId(), "file-doesnt-exist"));
var format = _directoryService.FileSystem.Path.GetExtension(path);
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));

View file

@ -36,13 +36,14 @@ public class LibraryController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IEventHub _eventHub;
private readonly ILibraryWatcher _libraryWatcher;
private readonly ILocalizationService _localizationService;
private readonly IEasyCachingProvider _libraryCacheProvider;
private const string CacheKey = "library_";
public LibraryController(IDirectoryService directoryService,
ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
IUnitOfWork unitOfWork, IEventHub eventHub, ILibraryWatcher libraryWatcher,
IEasyCachingProviderFactory cachingProviderFactory)
IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService)
{
_directoryService = directoryService;
_logger = logger;
@ -51,6 +52,7 @@ public class LibraryController : BaseApiController
_unitOfWork = unitOfWork;
_eventHub = eventHub;
_libraryWatcher = libraryWatcher;
_localizationService = localizationService;
_libraryCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.Library);
}
@ -66,7 +68,7 @@ public class LibraryController : BaseApiController
{
if (await _unitOfWork.LibraryRepository.LibraryExists(dto.Name))
{
return BadRequest("Library name already exists. Please choose a unique name to the server.");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-name-exists"));
}
var library = new LibraryBuilder(dto.Name, dto.Type)
@ -96,7 +98,7 @@ public class LibraryController : BaseApiController
}
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue. Please try again.");
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library"));
_logger.LogInformation("Created a new library: {LibraryName}", library.Name);
await _libraryWatcher.RestartWatching();
@ -160,7 +162,8 @@ public class LibraryController : BaseApiController
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");
if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, userId))
return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-library-access"));
return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId));
}
@ -175,9 +178,9 @@ public class LibraryController : BaseApiController
public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username);
if (user == null) return BadRequest("Could not validate user");
if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-doesnt-exist"));
var libraryString = string.Join(",", updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name));
var libraryString = string.Join(',', updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name));
_logger.LogInformation("Granting user {UserName} access to: {Libraries}", updateLibraryForUserDto.Username, libraryString);
var allLibraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
@ -195,7 +198,6 @@ public class LibraryController : BaseApiController
{
library.AppUsers.Add(user);
}
}
if (!_unitOfWork.HasChanges())
@ -213,7 +215,7 @@ public class LibraryController : BaseApiController
}
return BadRequest("There was a critical issue. Please try again.");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library"));
}
/// <summary>
@ -224,9 +226,9 @@ public class LibraryController : BaseApiController
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("scan")]
public ActionResult Scan(int libraryId, bool force = false)
public async Task<ActionResult> Scan(int libraryId, bool force = false)
{
if (libraryId <= 0) return BadRequest("Invalid libraryId");
if (libraryId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "libraryId"));
_taskScheduler.ScanLibrary(libraryId, force);
return Ok();
}
@ -277,7 +279,7 @@ public class LibraryController : BaseApiController
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
if (!isAdmin) return BadRequest("API key must belong to an admin");
if (dto.FolderPath.Contains("..")) return BadRequest("Invalid Path");
if (dto.FolderPath.Contains("..")) return BadRequest(await _localizationService.Translate(user.Id, "invalid-path"));
dto.FolderPath = Services.Tasks.Scanner.Parser.Parser.NormalizePath(dto.FolderPath);
@ -310,12 +312,11 @@ public class LibraryController : BaseApiController
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
{
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
return BadRequest(
"You cannot delete a library while a scan is in progress. Please wait for scan to complete or restart Kavita then try to delete");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "delete-library-while-scan"));
}
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
if (library == null) return BadRequest("Library no longer exists");
if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist"));
// Due to a bad schema that I can't figure out how to fix, we need to erase all RelatedSeries before we delete the library
// Aka SeriesRelation has an invalid foreign key
@ -354,7 +355,7 @@ public class LibraryController : BaseApiController
}
catch (Exception ex)
{
_logger.LogError(ex, "There was a critical error trying to delete the library");
_logger.LogError(ex, await _localizationService.Translate(User.GetUserId(), "generic-library"));
await _unitOfWork.RollbackAsync();
return Ok(false);
}
@ -384,11 +385,11 @@ public class LibraryController : BaseApiController
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto dto)
{
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders);
if (library == null) return BadRequest("Library doesn't exist");
if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist"));
var newName = dto.Name.Trim();
if (await _unitOfWork.LibraryRepository.LibraryExists(newName) && !library.Name.Equals(newName))
return BadRequest("Library name already exists");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-name-exists"));
var originalFolders = library.Folders.Select(x => x.Path).ToList();
@ -416,7 +417,7 @@ public class LibraryController : BaseApiController
_unitOfWork.LibraryRepository.Update(library);
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library-update"));
if (originalFolders.Count != dto.Folders.Count() || typeUpdate)
{
await _libraryWatcher.RestartWatching();

View file

@ -5,6 +5,8 @@ using API.Data;
using API.DTOs.Account;
using API.DTOs.License;
using API.Entities.Enums;
using API.Extensions;
using API.Services;
using API.Services.Plus;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
@ -18,13 +20,15 @@ public class LicenseController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<LicenseController> _logger;
private readonly ILicenseService _licenseService;
private readonly ILocalizationService _localizationService;
public LicenseController(IUnitOfWork unitOfWork, ILogger<LicenseController> logger,
ILicenseService licenseService)
ILicenseService licenseService, ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_logger = logger;
_licenseService = licenseService;
_localizationService = localizationService;
}
/// <summary>
@ -72,8 +76,15 @@ public class LicenseController : BaseApiController
[Authorize("RequireAdminRole")]
[HttpPost]
public async Task<ActionResult> UpdateLicense(UpdateLicenseDto dto)
{
try
{
await _licenseService.AddLicense(dto.License.Trim(), dto.Email.Trim());
}
catch (Exception ex)
{
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
}
return Ok();
}
}

View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using API.DTOs.Filtering;
using API.Services;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
public class LocaleController : BaseApiController
{
private readonly ILocalizationService _localizationService;
public LocaleController(ILocalizationService localizationService)
{
_localizationService = localizationService;
}
[HttpGet]
public ActionResult<IEnumerable<string>> GetAllLocales()
{
var languages = _localizationService.GetLocales().Select(c => new CultureInfo(c)).Select(c =>
new LanguageDto()
{
Title = c.DisplayName,
IsoCode = c.IetfLanguageTag
})
.Where(l => !string.IsNullOrEmpty(l.IsoCode))
.OrderBy(d => d.Title);
return Ok(languages);
}
}

View file

@ -10,6 +10,7 @@ using API.DTOs.Filtering;
using API.DTOs.Metadata;
using API.Entities.Enums;
using API.Extensions;
using API.Services;
using Kavita.Common.Extensions;
using Microsoft.AspNetCore.Mvc;
@ -19,10 +20,12 @@ namespace API.Controllers;
public class MetadataController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILocalizationService _localizationService;
public MetadataController(IUnitOfWork unitOfWork)
public MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_localizationService = localizationService;
}
/// <summary>
@ -35,7 +38,7 @@ public class MetadataController : BaseApiController
public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var ids = libraryIds?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
{
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, userId));
@ -56,7 +59,7 @@ public class MetadataController : BaseApiController
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryIds)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var ids = libraryIds?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
{
return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, userId));
@ -74,7 +77,7 @@ public class MetadataController : BaseApiController
public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var ids = libraryIds?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
{
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, userId));
@ -92,7 +95,7 @@ public class MetadataController : BaseApiController
[HttpGet("age-ratings")]
public async Task<ActionResult<IList<AgeRatingDto>>> GetAllAgeRatings(string? libraryIds)
{
var ids = libraryIds?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
{
return Ok(await _unitOfWork.LibraryRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids));
@ -115,7 +118,7 @@ public class MetadataController : BaseApiController
[HttpGet("publication-status")]
public ActionResult<IList<AgeRatingDto>> GetAllPublicationStatus(string? libraryIds)
{
var ids = libraryIds?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids is {Count: > 0})
{
return Ok(_unitOfWork.LibraryRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
@ -138,7 +141,7 @@ public class MetadataController : BaseApiController
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
public async Task<ActionResult<IList<LanguageDto>>> GetAllLanguages(string? libraryIds)
{
var ids = libraryIds?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
if (ids is {Count: > 0})
{
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
@ -168,9 +171,9 @@ public class MetadataController : BaseApiController
[HttpGet("chapter-summary")]
public async Task<ActionResult<string>> GetChapterSummary(int chapterId)
{
if (chapterId <= 0) return BadRequest("Chapter does not exist");
if (chapterId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
if (chapter == null) return BadRequest("Chapter does not exist");
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
return Ok(chapter.Summary);
}
}

View file

@ -17,7 +17,6 @@ using API.Entities.Enums;
using API.Extensions;
using API.Helpers;
using API.Services;
using EasyCaching.Core;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -37,6 +36,7 @@ public class OpdsController : BaseApiController
private readonly IReaderService _readerService;
private readonly ISeriesService _seriesService;
private readonly IAccountService _accountService;
private readonly ILocalizationService _localizationService;
private readonly XmlSerializer _xmlSerializer;
@ -71,7 +71,7 @@ public class OpdsController : BaseApiController
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
IDirectoryService directoryService, ICacheService cacheService,
IReaderService readerService, ISeriesService seriesService,
IAccountService accountService, IEasyCachingProvider provider)
IAccountService accountService, ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_downloadService = downloadService;
@ -80,6 +80,7 @@ public class OpdsController : BaseApiController
_readerService = readerService;
_seriesService = seriesService;
_accountService = accountService;
_localizationService = localizationService;
_xmlSerializer = new XmlSerializer(typeof(Feed));
_xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription));
@ -90,8 +91,9 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> Get(string apiKey)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
@ -100,10 +102,10 @@ public class OpdsController : BaseApiController
feed.Entries.Add(new FeedEntry()
{
Id = "onDeck",
Title = "On Deck",
Title = await _localizationService.Translate(userId, "on-deck"),
Content = new FeedEntryContent()
{
Text = "Browse by On Deck"
Text = await _localizationService.Translate(userId, "browse-on-deck")
},
Links = new List<FeedLink>()
{
@ -113,10 +115,10 @@ public class OpdsController : BaseApiController
feed.Entries.Add(new FeedEntry()
{
Id = "recentlyAdded",
Title = "Recently Added",
Title = await _localizationService.Translate(userId, "recently-added"),
Content = new FeedEntryContent()
{
Text = "Browse by Recently Added"
Text = await _localizationService.Translate(userId, "browse-recently-added")
},
Links = new List<FeedLink>()
{
@ -126,10 +128,10 @@ public class OpdsController : BaseApiController
feed.Entries.Add(new FeedEntry()
{
Id = "readingList",
Title = "Reading Lists",
Title = await _localizationService.Translate(userId, "reading-lists"),
Content = new FeedEntryContent()
{
Text = "Browse by Reading Lists"
Text = await _localizationService.Translate(userId, "browse-reading-lists")
},
Links = new List<FeedLink>()
{
@ -139,10 +141,10 @@ public class OpdsController : BaseApiController
feed.Entries.Add(new FeedEntry()
{
Id = "allLibraries",
Title = "All Libraries",
Title = await _localizationService.Translate(userId, "libraries"),
Content = new FeedEntryContent()
{
Text = "Browse by Libraries"
Text = await _localizationService.Translate(userId, "browse-libraries")
},
Links = new List<FeedLink>()
{
@ -152,10 +154,10 @@ public class OpdsController : BaseApiController
feed.Entries.Add(new FeedEntry()
{
Id = "allCollections",
Title = "All Collections",
Title = await _localizationService.Translate(userId, "collections"),
Content = new FeedEntryContent()
{
Text = "Browse by Collections"
Text = await _localizationService.Translate(userId, "browse-collections")
},
Links = new List<FeedLink>()
{
@ -183,12 +185,12 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetLibraries(string apiKey)
{
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
var (baseUrl, prefix) = await GetPrefix();
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var libraries = await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId);
var feed = CreateFeed("All Libraries", $"{prefix}{apiKey}/libraries", apiKey, prefix);
var feed = CreateFeed(await _localizationService.Translate(userId, "libraries"), $"{prefix}{apiKey}/libraries", apiKey, prefix);
SetFeedId(feed, "libraries");
foreach (var library in libraries)
{
@ -210,10 +212,10 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetCollections(string apiKey)
{
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
var (baseUrl, prefix) = await GetPrefix();
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null) return Unauthorized();
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
@ -222,7 +224,7 @@ public class OpdsController : BaseApiController
: (await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(userId));
var feed = CreateFeed("All Collections", $"{prefix}{apiKey}/collections", apiKey, prefix);
var feed = CreateFeed(await _localizationService.Translate(userId, "collections"), $"{prefix}{apiKey}/collections", apiKey, prefix);
SetFeedId(feed, "collections");
foreach (var tag in tags)
{
@ -248,10 +250,10 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetCollection(int collectionId, string apiKey, [FromQuery] int pageNumber = 0)
{
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
var (baseUrl, prefix) = await GetPrefix();
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null) return Unauthorized();
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
@ -292,10 +294,10 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetReadingLists(string apiKey, [FromQuery] int pageNumber = 0)
{
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
var (baseUrl, prefix) = await GetPrefix();
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId,
true, GetUserParams(pageNumber), false);
@ -333,10 +335,10 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetReadingListItems(int readingListId, string apiKey)
{
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
var (baseUrl, prefix) = await GetPrefix();
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
var userWithLists = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user!.UserName!, AppUserIncludes.ReadingListsWithItems);
@ -344,10 +346,10 @@ public class OpdsController : BaseApiController
var readingList = userWithLists.ReadingLists.SingleOrDefault(t => t.Id == readingListId);
if (readingList == null)
{
return BadRequest("Reading list does not exist or you don't have access");
return BadRequest(await _localizationService.Translate(userId, "reading-list-restricted"));
}
var feed = CreateFeed(readingList.Title + " Reading List", $"{prefix}{apiKey}/reading-list/{readingListId}", apiKey, prefix);
var feed = CreateFeed(readingList.Title + " " + await _localizationService.Translate(userId, "reading-list"), $"{prefix}{apiKey}/reading-list/{readingListId}", apiKey, prefix);
SetFeedId(feed, $"reading-list-{readingListId}");
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId)).ToList();
@ -364,16 +366,16 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetSeriesForLibrary(int libraryId, string apiKey, [FromQuery] int pageNumber = 0)
{
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
var (baseUrl, prefix) = await GetPrefix();
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var library =
(await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).SingleOrDefault(l =>
l.Id == libraryId);
if (library == null)
{
return BadRequest("User does not have access to this library");
return BadRequest(await _localizationService.Translate(userId, "no-library-access"));
}
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, GetUserParams(pageNumber), _filterDto);
@ -395,14 +397,14 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetRecentlyAdded(string apiKey, [FromQuery] int pageNumber = 1)
{
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
var (baseUrl, prefix) = await GetPrefix();
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var recentlyAdded = await _unitOfWork.SeriesRepository.GetRecentlyAdded(0, userId, GetUserParams(pageNumber), _filterDto);
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(recentlyAdded.Select(s => s.Id));
var feed = CreateFeed("Recently Added", $"{prefix}{apiKey}/recently-added", apiKey, prefix);
var feed = CreateFeed(await _localizationService.Translate(userId, "recently-added"), $"{prefix}{apiKey}/recently-added", apiKey, prefix);
SetFeedId(feed, "recently-added");
AddPagination(feed, recentlyAdded, $"{prefix}{apiKey}/recently-added");
@ -418,19 +420,19 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetOnDeck(string apiKey, [FromQuery] int pageNumber = 1)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var userId = await GetUser(apiKey);
var userParams = GetUserParams(pageNumber);
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, 0, userParams, _filterDto);
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(pagedList.Select(s => s.Id));
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
var feed = CreateFeed("On Deck", $"{prefix}{apiKey}/on-deck", apiKey, prefix);
var feed = CreateFeed(await _localizationService.Translate(userId, "on-deck"), $"{prefix}{apiKey}/on-deck", apiKey, prefix);
SetFeedId(feed, "on-deck");
AddPagination(feed, pagedList, $"{prefix}{apiKey}/on-deck");
@ -446,20 +448,20 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> SearchSeries(string apiKey, [FromQuery] string query)
{
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
var (baseUrl, prefix) = await GetPrefix();
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (string.IsNullOrEmpty(query))
{
return BadRequest("You must pass a query parameter");
return BadRequest(await _localizationService.Translate(userId, "query-required"));
}
query = query.Replace(@"%", string.Empty);
// Get libraries user has access to
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList();
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
if (!libraries.Any()) return BadRequest(await _localizationService.Translate(userId, "libraries-restricted"));
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
@ -518,13 +520,14 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetSearchDescriptor(string apiKey)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (_, prefix) = await GetPrefix();
var feed = new OpenSearchDescription()
{
ShortName = "Search",
Description = "Search for Series, Collections, or Reading Lists",
ShortName = await _localizationService.Translate(userId, "search"),
Description = await _localizationService.Translate(userId, "search-description"),
Url = new SearchLink()
{
Type = FeedLinkType.AtomAcquisition,
@ -542,13 +545,13 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetSeries(string apiKey, int seriesId)
{
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
var (baseUrl, prefix) = await GetPrefix();
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
var feed = CreateFeed(series.Name + " - Storyline", $"{prefix}{apiKey}/series/{series.Id}", apiKey, prefix);
var feed = CreateFeed(series!.Name + " - Storyline", $"{prefix}{apiKey}/series/{series.Id}", apiKey, prefix);
SetFeedId(feed, $"series-{series.Id}");
feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesId}&apiKey={apiKey}"));
@ -564,7 +567,7 @@ public class OpdsController : BaseApiController
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id);
foreach (var mangaFile in files)
{
feed.Entries.Add(await CreateChapterWithFile(seriesId, volume.Id, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volume.Id, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
}
}
@ -576,7 +579,7 @@ public class OpdsController : BaseApiController
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(storylineChapter.Id);
foreach (var mangaFile in files)
{
feed.Entries.Add(await CreateChapterWithFile(seriesId, storylineChapter.VolumeId, storylineChapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, storylineChapter.VolumeId, storylineChapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
}
}
@ -586,7 +589,7 @@ public class OpdsController : BaseApiController
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(special.Id);
foreach (var mangaFile in files)
{
feed.Entries.Add(await CreateChapterWithFile(seriesId, special.VolumeId, special.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, special.VolumeId, special.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
}
}
@ -597,26 +600,26 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetVolume(string apiKey, int seriesId, int volumeId)
{
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
var (baseUrl, prefix) = await GetPrefix();
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
var chapters =
(await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number),
_chapterSortComparer);
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s ",
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s ",
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix);
SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{SeriesService.FormatChapterName(libraryType)}s");
SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{_seriesService.FormatChapterName(userId, libraryType)}s");
foreach (var chapter in chapters)
{
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapter.Id);
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id);
foreach (var mangaFile in files)
{
feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volumeId, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
}
}
@ -627,23 +630,23 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetChapter(string apiKey, int seriesId, int volumeId, int chapterId)
{
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
var (baseUrl, prefix) = await GetPrefix();
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId);
if (chapter == null) return BadRequest("Chapter doesn't exist");
if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "chapter-doesnt-exist"));
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s",
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s",
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey, prefix);
SetFeedId(feed, $"series-{series.Id}-volume-{volumeId}-{SeriesService.FormatChapterName(libraryType)}-{chapterId}-files");
SetFeedId(feed, $"series-{series.Id}-volume-{volumeId}-{_seriesService.FormatChapterName(userId, libraryType)}-{chapterId}-files");
foreach (var mangaFile in files)
{
feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapterId, mangaFile, series, chapter, apiKey, prefix, baseUrl));
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volumeId, chapterId, mangaFile, series, chapter, apiKey, prefix, baseUrl));
}
return CreateXmlResult(SerializeXml(feed));
@ -661,8 +664,9 @@ public class OpdsController : BaseApiController
[HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}/download/{filename}")]
public async Task<ActionResult> DownloadFile(string apiKey, int seriesId, int volumeId, int chapterId, string filename)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest("OPDS is not enabled on this server");
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey));
if (!await _accountService.HasDownloadPermission(user))
{
@ -781,7 +785,7 @@ public class OpdsController : BaseApiController
};
}
private async Task<FeedEntry> CreateChapterWithFile(int seriesId, int volumeId, int chapterId, MangaFile mangaFile, SeriesDto series, ChapterDto chapter, string apiKey, string prefix, string baseUrl)
private async Task<FeedEntry> CreateChapterWithFile(int userId, int seriesId, int volumeId, int chapterId, MangaFile mangaFile, SeriesDto series, ChapterDto chapter, string apiKey, string prefix, string baseUrl)
{
var fileSize =
mangaFile.Bytes > 0 ? DirectoryService.GetHumanReadableBytes(mangaFile.Bytes) :
@ -797,7 +801,8 @@ public class OpdsController : BaseApiController
if (volume!.Chapters.Count == 1)
{
SeriesService.RenameVolumeName(volume.Chapters.First(), volume, libraryType);
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
SeriesService.RenameVolumeName(volume.Chapters.First(), volume, libraryType, volumeLabel);
if (volume.Name != "0")
{
title += $" - {volume.Name}";
@ -805,11 +810,11 @@ public class OpdsController : BaseApiController
}
else if (volume.Number != 0)
{
title = $"{series.Name} - Volume {volume.Name} - {SeriesService.FormatChapterTitle(chapter, libraryType)}";
title = $"{series.Name} - Volume {volume.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}";
}
else
{
title = $"{series.Name} - {SeriesService.FormatChapterTitle(chapter, libraryType)}";
title = $"{series.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}";
}
// Chunky requires a file at the end. Our API ignores this
@ -857,14 +862,16 @@ public class OpdsController : BaseApiController
[HttpGet("{apiKey}/image")]
public async Task<ActionResult> GetPageStreamedImage(string apiKey, [FromQuery] int libraryId, [FromQuery] int seriesId, [FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber)
{
if (pageNumber < 0) return BadRequest("Page cannot be less than 0");
var userId = await GetUser(apiKey);
if (pageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "Page"));
var chapter = await _cacheService.Ensure(chapterId);
if (chapter == null) return BadRequest("There was an issue finding image file for reading");
if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "cache-file-find"));
try
{
var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber);
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {pageNumber}");
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path))
return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", pageNumber));
var content = await _directoryService.ReadFileAsync(path);
var format = Path.GetExtension(path);
@ -895,8 +902,9 @@ public class OpdsController : BaseApiController
[ResponseCache(Duration = 60 * 60, Location = ResponseCacheLocation.Client, NoStore = false)]
public async Task<ActionResult> GetFavicon(string apiKey)
{
var userId = await GetUser(apiKey);
var files = _directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico");
if (files.Length == 0) return BadRequest("Cannot find icon");
if (files.Length == 0) return BadRequest(await _localizationService.Translate(userId, "favicon-doesnt-exist"));
var path = files[0];
var content = await _directoryService.ReadFileAsync(path);
var format = Path.GetExtension(path);
@ -919,7 +927,7 @@ public class OpdsController : BaseApiController
{
/* Do nothing */
}
throw new KavitaException("User does not exist");
throw new KavitaException(await _localizationService.Get("en", "user-doesnt-exist"));
}
private async Task<FeedLink> CreatePageStreamLink(int libraryId, int seriesId, int volumeId, int chapterId, MangaFile mangaFile, string apiKey, string prefix)

View file

@ -16,6 +16,7 @@ using API.Services;
using API.Services.Plus;
using API.SignalR;
using Hangfire;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@ -37,13 +38,15 @@ public class ReaderController : BaseApiController
private readonly IAccountService _accountService;
private readonly IEventHub _eventHub;
private readonly IScrobblingService _scrobblingService;
private readonly ILocalizationService _localizationService;
/// <inheritdoc />
public ReaderController(ICacheService cacheService,
IUnitOfWork unitOfWork, ILogger<ReaderController> logger,
IReaderService readerService, IBookmarkService bookmarkService,
IAccountService accountService, IEventHub eventHub,
IScrobblingService scrobblingService)
IScrobblingService scrobblingService,
ILocalizationService localizationService)
{
_cacheService = cacheService;
_unitOfWork = unitOfWork;
@ -53,6 +56,7 @@ public class ReaderController : BaseApiController
_accountService = accountService;
_eventHub = eventHub;
_scrobblingService = scrobblingService;
_localizationService = localizationService;
}
/// <summary>
@ -71,13 +75,13 @@ public class ReaderController : BaseApiController
// Validate the user has access to the PDF
var series = await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapter.Id,
await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()));
if (series == null) return BadRequest("Invalid Access");
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-access"));
try
{
var path = _cacheService.GetCachedFile(chapter);
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"Pdf doesn't exist when it should.");
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "pdf-doesnt-exist"));
return PhysicalFile(path, MimeTypeMap.GetMimeType(Path.GetExtension(path)), Path.GetFileName(path), true);
}
@ -103,14 +107,16 @@ public class ReaderController : BaseApiController
public async Task<ActionResult> GetImage(int chapterId, int page, string apiKey, bool extractPdf = false)
{
if (page < 0) page = 0;
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId == 0) return BadRequest();
var chapter = await _cacheService.Ensure(chapterId, extractPdf);
if (chapter == null) return NoContent();
try
{
var path = _cacheService.GetCachedPagePath(chapter.Id, page);
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}. Try refreshing to allow re-cache.");
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path))
return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page));
var format = Path.GetExtension(path);
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path), true);
@ -134,7 +140,8 @@ public class ReaderController : BaseApiController
[AllowAnonymous]
public async Task<ActionResult> GetThumbnail(int chapterId, int pageNum, string apiKey)
{
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId == 0) return BadRequest();
var chapter = await _cacheService.Ensure(chapterId, true);
if (chapter == null) return NoContent();
var images = _cacheService.GetCachedPages(chapterId);
@ -170,7 +177,7 @@ public class ReaderController : BaseApiController
try
{
var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page);
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", page));
var format = Path.GetExtension(path);
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path));
@ -217,7 +224,7 @@ public class ReaderController : BaseApiController
if (chapter == null) return NoContent();
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
if (dto == null) return BadRequest("Please perform a scan on this series or library and try again");
if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "perform-scan"));
var mangaFile = chapter.Files.First();
var info = new ChapterInfoDto()
@ -256,7 +263,8 @@ public class ReaderController : BaseApiController
}
else
{
info.Subtitle = "Volume " + info.VolumeNumber;
//info.Subtitle = await _localizationService.Translate(User.GetUserId(), "volume-num", info.VolumeNumber);
info.Subtitle = $"Volume {info.VolumeNumber}";
if (!info.ChapterNumber.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultChapter))
{
info.Subtitle += " " + ReaderService.FormatChapterName(info.LibraryType, true, true) +
@ -309,9 +317,16 @@ public class ReaderController : BaseApiController
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
if (user == null) return Unauthorized();
try
{
await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId);
}
catch (KavitaException ex)
{
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
}
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId));
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markReadDto.SeriesId, user.Id));
@ -331,7 +346,7 @@ public class ReaderController : BaseApiController
if (user == null) return Unauthorized();
await _readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId);
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId));
return Ok();
@ -357,7 +372,7 @@ public class ReaderController : BaseApiController
return Ok();
}
return BadRequest("Could not save progress");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
}
/// <summary>
@ -372,12 +387,19 @@ public class ReaderController : BaseApiController
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
if (user == null) return Unauthorized();
try
{
await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
}
catch (KavitaException ex)
{
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
}
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, markVolumeReadDto.SeriesId,
markVolumeReadDto.VolumeId, 0, chapters.Sum(c => c.Pages)));
if (!await _unitOfWork.CommitAsync()) return BadRequest("Could not save progress");
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId));
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markVolumeReadDto.SeriesId, user.Id));
@ -405,7 +427,7 @@ public class ReaderController : BaseApiController
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters.ToList());
if (!await _unitOfWork.CommitAsync()) return BadRequest("Could not save progress");
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId));
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(dto.SeriesId, user.Id));
return Ok();
@ -439,7 +461,7 @@ public class ReaderController : BaseApiController
return Ok();
}
return BadRequest("Could not save progress");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
}
/// <summary>
@ -460,7 +482,7 @@ public class ReaderController : BaseApiController
await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
}
if (!await _unitOfWork.CommitAsync()) return BadRequest("Could not save progress");
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
foreach (var sId in dto.SeriesIds)
{
@ -497,7 +519,7 @@ public class ReaderController : BaseApiController
return Ok();
}
return BadRequest("Could not save progress");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
}
/// <summary>
@ -529,7 +551,7 @@ public class ReaderController : BaseApiController
{
var userId = User.GetUserId();
if (!await _readerService.SaveReadingProgress(progressDto, userId))
return BadRequest("Could not save progress");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
return Ok(true);
@ -589,7 +611,7 @@ public class ReaderController : BaseApiController
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
if (user == null) return Unauthorized();
if (user.Bookmarks == null) return Ok("Nothing to remove");
if (user.Bookmarks == null) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
try
{
@ -616,7 +638,7 @@ public class ReaderController : BaseApiController
await _unitOfWork.RollbackAsync();
}
return BadRequest("Could not clear bookmarks");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-clear-bookmarks"));
}
/// <summary>
@ -629,7 +651,7 @@ public class ReaderController : BaseApiController
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
if (user == null) return Unauthorized();
if (user.Bookmarks == null) return Ok("Nothing to remove");
if (user.Bookmarks == null) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
try
{
@ -653,7 +675,7 @@ public class ReaderController : BaseApiController
await _unitOfWork.RollbackAsync();
}
return BadRequest("Could not clear bookmarks");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-clear-bookmarks"));
}
/// <summary>
@ -692,15 +714,16 @@ public class ReaderController : BaseApiController
if (user == null) return new UnauthorizedResult();
if (!await _accountService.HasBookmarkPermission(user))
return BadRequest("You do not have permission to bookmark");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-permission"));
var chapter = await _cacheService.Ensure(bookmarkDto.ChapterId);
if (chapter == null) return BadRequest("Could not find cached image. Reload and try again.");
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "cache-file-find"));
bookmarkDto.Page = _readerService.CapPageToChapter(chapter, bookmarkDto.Page);
var path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page);
if (!await _bookmarkService.BookmarkPage(user, bookmarkDto, path)) return BadRequest("Could not save bookmark");
if (!await _bookmarkService.BookmarkPage(user, bookmarkDto, path))
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-save"));
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
return Ok();
@ -719,10 +742,10 @@ public class ReaderController : BaseApiController
if (user.Bookmarks.IsNullOrEmpty()) return Ok();
if (!await _accountService.HasBookmarkPermission(user))
return BadRequest("You do not have permission to unbookmark");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-permission"));
if (!await _bookmarkService.RemoveBookmarkPage(user, bookmarkDto))
return BadRequest("Could not remove bookmark");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-save"));
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
return Ok();
}
@ -806,9 +829,10 @@ public class ReaderController : BaseApiController
[HttpDelete("ptoc")]
public async Task<ActionResult> DeletePersonalToc([FromQuery] int chapterId, [FromQuery] int pageNum, [FromQuery] string title)
{
if (string.IsNullOrWhiteSpace(title)) return BadRequest("Name cannot be empty");
if (pageNum < 0) return BadRequest("Must be valid page number");
var toc = await _unitOfWork.UserTableOfContentRepository.Get(User.GetUserId(), chapterId, pageNum, title);
var userId = User.GetUserId();
if (string.IsNullOrWhiteSpace(title)) return BadRequest(await _localizationService.Translate(userId, "name-required"));
if (pageNum < 0) return BadRequest(await _localizationService.Translate(userId, "valid-number"));
var toc = await _unitOfWork.UserTableOfContentRepository.Get(userId, chapterId, pageNum, title);
if (toc == null) return Ok();
_unitOfWork.UserTableOfContentRepository.Remove(toc);
await _unitOfWork.CommitAsync();
@ -825,13 +849,13 @@ public class ReaderController : BaseApiController
public async Task<ActionResult> CreatePersonalToC(CreatePersonalToCDto dto)
{
// Validate there isn't already an existing page title combo?
if (string.IsNullOrWhiteSpace(dto.Title)) return BadRequest("Name cannot be empty");
if (dto.PageNumber < 0) return BadRequest("Must be valid page number");
var userId = User.GetUserId();
if (string.IsNullOrWhiteSpace(dto.Title)) return BadRequest(await _localizationService.Translate(userId, "name-required"));
if (dto.PageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "valid-number"));
if (await _unitOfWork.UserTableOfContentRepository.IsUnique(userId, dto.ChapterId, dto.PageNumber,
dto.Title.Trim()))
{
return BadRequest("Duplicate ToC entry already exists");
return BadRequest(await _localizationService.Translate(userId, "duplicate-bookmark"));
}
_unitOfWork.UserTableOfContentRepository.Attach(new AppUserTableOfContent()

View file

@ -21,11 +21,14 @@ public class ReadingListController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly IReadingListService _readingListService;
private readonly ILocalizationService _localizationService;
public ReadingListController(IUnitOfWork unitOfWork, IReadingListService readingListService)
public ReadingListController(IUnitOfWork unitOfWork, IReadingListService readingListService,
ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_readingListService = readingListService;
_localizationService = localizationService;
}
/// <summary>
@ -99,13 +102,13 @@ public class ReadingListController : BaseApiController
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
}
if (await _readingListService.UpdateReadingListItemPosition(dto)) return Ok("Updated");
if (await _readingListService.UpdateReadingListItemPosition(dto)) return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
return BadRequest("Couldn't update position");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-position"));
}
/// <summary>
@ -119,15 +122,15 @@ public class ReadingListController : BaseApiController
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
}
if (await _readingListService.DeleteReadingListItem(dto))
{
return Ok("Updated");
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
}
return BadRequest("Couldn't delete item");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-item-delete"));
}
/// <summary>
@ -141,15 +144,15 @@ public class ReadingListController : BaseApiController
var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
}
if (await _readingListService.RemoveFullyReadItems(readingListId, user))
{
return Ok("Updated");
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
}
return BadRequest("Could not remove read items");
return BadRequest("Couldn't delete item(s)");
}
/// <summary>
@ -163,12 +166,13 @@ public class ReadingListController : BaseApiController
var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
}
if (await _readingListService.DeleteReadingList(readingListId, user)) return Ok("List was deleted");
if (await _readingListService.DeleteReadingList(readingListId, user))
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-deleted"));
return BadRequest("There was an issue deleting reading list");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-reading-list-delete"));
}
/// <summary>
@ -188,7 +192,7 @@ public class ReadingListController : BaseApiController
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
}
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(user.Id, dto.Title));
@ -203,12 +207,12 @@ public class ReadingListController : BaseApiController
public async Task<ActionResult> UpdateList(UpdateReadingListDto dto)
{
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
if (readingList == null) return BadRequest("List does not exist");
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
var user = await _readingListService.UserHasReadingListAccess(readingList.Id, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
}
try
@ -217,10 +221,10 @@ public class ReadingListController : BaseApiController
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
}
return Ok("Updated");
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
}
/// <summary>
@ -234,11 +238,11 @@ public class ReadingListController : BaseApiController
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
var chapterIdsForSeries =
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new [] {dto.SeriesId});
@ -253,7 +257,7 @@ public class ReadingListController : BaseApiController
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
}
}
catch
@ -261,7 +265,7 @@ public class ReadingListController : BaseApiController
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
}
@ -276,10 +280,10 @@ public class ReadingListController : BaseApiController
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
foreach (var chapterId in dto.ChapterIds)
@ -298,7 +302,7 @@ public class ReadingListController : BaseApiController
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
}
}
catch
@ -306,7 +310,7 @@ public class ReadingListController : BaseApiController
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
}
/// <summary>
@ -320,10 +324,10 @@ public class ReadingListController : BaseApiController
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
var ids = await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray());
@ -341,7 +345,7 @@ public class ReadingListController : BaseApiController
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
}
}
catch
@ -349,7 +353,7 @@ public class ReadingListController : BaseApiController
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
}
[HttpPost("update-by-volume")]
@ -358,10 +362,10 @@ public class ReadingListController : BaseApiController
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
var chapterIdsForVolume =
(await _unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList();
@ -377,7 +381,7 @@ public class ReadingListController : BaseApiController
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
}
}
catch
@ -385,7 +389,7 @@ public class ReadingListController : BaseApiController
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
}
[HttpPost("update-by-chapter")]
@ -394,10 +398,10 @@ public class ReadingListController : BaseApiController
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
// If there are adds, tell tracking this has been modified
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, new List<int>() { dto.ChapterId }, readingList))
@ -410,7 +414,7 @@ public class ReadingListController : BaseApiController
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
}
}
catch
@ -418,7 +422,7 @@ public class ReadingListController : BaseApiController
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
}
/// <summary>
@ -446,7 +450,7 @@ public class ReadingListController : BaseApiController
{
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
if (readingListItem == null) return BadRequest("Id does not exist");
if (readingListItem == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var index = items.IndexOf(readingListItem) + 1;
if (items.Count > index)
{
@ -467,7 +471,7 @@ public class ReadingListController : BaseApiController
{
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
if (readingListItem == null) return BadRequest("Id does not exist");
if (readingListItem == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var index = items.IndexOf(readingListItem) - 1;
if (0 <= index)
{

View file

@ -8,6 +8,7 @@ using API.DTOs;
using API.DTOs.Recommendation;
using API.Extensions;
using API.Helpers;
using API.Services;
using API.Services.Plus;
using EasyCaching.Core;
using Microsoft.AspNetCore.Mvc;
@ -21,15 +22,18 @@ public class RecommendedController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IRecommendationService _recommendationService;
private readonly ILicenseService _licenseService;
private readonly ILocalizationService _localizationService;
private readonly IEasyCachingProvider _cacheProvider;
public const string CacheKey = "recommendation_";
public RecommendedController(IUnitOfWork unitOfWork, IRecommendationService recommendationService,
ILicenseService licenseService, IEasyCachingProviderFactory cachingProviderFactory)
ILicenseService licenseService, IEasyCachingProviderFactory cachingProviderFactory,
ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_recommendationService = recommendationService;
_licenseService = licenseService;
_localizationService = localizationService;
_cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations);
}
@ -50,7 +54,7 @@ public class RecommendedController : BaseApiController
if (!await _unitOfWork.UserRepository.HasAccessToSeries(userId, seriesId))
{
return BadRequest("User does not have access to this Series");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-restricted"));
}
var cacheKey = $"{CacheKey}-{seriesId}-{userId}";

View file

@ -14,9 +14,7 @@ using AutoMapper;
using EasyCaching.Core;
using Hangfire;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace API.Controllers;
@ -65,7 +63,6 @@ public class ReviewController : BaseApiController
var cacheKey = CacheKey + seriesId;
IEnumerable<UserReviewDto> externalReviews;
var setCache = false;
var result = await _cacheProvider.GetAsync<IEnumerable<UserReviewDto>>(cacheKey);
if (result.HasValue)
@ -74,35 +71,15 @@ public class ReviewController : BaseApiController
}
else
{
externalReviews = await _reviewService.GetReviewsForSeries(userId, seriesId);
setCache = true;
}
// if (_cache.TryGetValue(cacheKey, out string cachedData))
// {
// externalReviews = JsonConvert.DeserializeObject<IEnumerable<UserReviewDto>>(cachedData);
// }
// else
// {
// externalReviews = await _reviewService.GetReviewsForSeries(userId, seriesId);
// setCache = true;
// }
// Fetch external reviews and splice them in
foreach (var r in externalReviews)
{
userRatings.Add(r);
}
if (setCache)
{
// var cacheEntryOptions = new MemoryCacheEntryOptions()
// .SetSize(userRatings.Count)
// .SetAbsoluteExpiration(TimeSpan.FromHours(10));
//_cache.Set(cacheKey, JsonConvert.SerializeObject(externalReviews), cacheEntryOptions);
externalReviews = (await _reviewService.GetReviewsForSeries(userId, seriesId)).ToList();
await _cacheProvider.SetAsync(cacheKey, externalReviews, TimeSpan.FromHours(10));
_logger.LogDebug("Caching external reviews for {Key}", cacheKey);
}
// Fetch external reviews and splice them in
userRatings.AddRange(externalReviews);
return Ok(userRatings.Take(10));
}

View file

@ -10,6 +10,7 @@ using API.Entities.Scrobble;
using API.Extensions;
using API.Helpers;
using API.Helpers.Builders;
using API.Services;
using API.Services.Plus;
using Hangfire;
using Microsoft.AspNetCore.Authorization;
@ -26,12 +27,15 @@ public class ScrobblingController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IScrobblingService _scrobblingService;
private readonly ILogger<ScrobblingController> _logger;
private readonly ILocalizationService _localizationService;
public ScrobblingController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, ILogger<ScrobblingController> logger)
public ScrobblingController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService,
ILogger<ScrobblingController> logger, ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_scrobblingService = scrobblingService;
_logger = logger;
_localizationService = localizationService;
}
[HttpGet("anilist-token")]
@ -153,7 +157,8 @@ public class ScrobblingController : BaseApiController
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ScrobbleHolds);
if (user == null) return Unauthorized();
if (user.ScrobbleHolds.Any(s => s.SeriesId == seriesId)) return Ok("Nothing to do");
if (user.ScrobbleHolds.Any(s => s.SeriesId == seriesId))
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
var seriesHold = new ScrobbleHoldBuilder().WithSeriesId(seriesId).Build();
user.ScrobbleHolds.Add(seriesHold);
@ -181,7 +186,8 @@ public class ScrobblingController : BaseApiController
{
// Handle other exceptions or log the error
_logger.LogError(ex, "An error occurred while adding the hold");
return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while adding the hold");
return StatusCode(StatusCodes.Status500InternalServerError,
await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
}
}

View file

@ -5,6 +5,7 @@ using API.Data.Repositories;
using API.DTOs;
using API.DTOs.Search;
using API.Extensions;
using API.Services;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
@ -15,10 +16,12 @@ namespace API.Controllers;
public class SearchController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILocalizationService _localizationService;
public SearchController(IUnitOfWork unitOfWork)
public SearchController(IUnitOfWork unitOfWork, ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_localizationService = localizationService;
}
/// <summary>
@ -55,7 +58,7 @@ public class SearchController : BaseApiController
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (user == null) return Unauthorized();
var libraries = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(user.Id, QueryContext.Search).ToList();
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
if (!libraries.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "libraries-restricted"));
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);

View file

@ -15,6 +15,7 @@ using API.Helpers;
using API.Services;
using API.Services.Plus;
using EasyCaching.Core;
using Kavita.Common;
using Kavita.Common.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
@ -30,6 +31,7 @@ public class SeriesController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly ISeriesService _seriesService;
private readonly ILicenseService _licenseService;
private readonly ILocalizationService _localizationService;
private readonly IEasyCachingProvider _ratingCacheProvider;
private readonly IEasyCachingProvider _reviewCacheProvider;
private readonly IEasyCachingProvider _recommendationCacheProvider;
@ -37,13 +39,14 @@ public class SeriesController : BaseApiController
public SeriesController(ILogger<SeriesController> logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork,
ISeriesService seriesService, ILicenseService licenseService,
IEasyCachingProviderFactory cachingProviderFactory)
IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService)
{
_logger = logger;
_taskScheduler = taskScheduler;
_unitOfWork = unitOfWork;
_seriesService = seriesService;
_licenseService = licenseService;
_localizationService = localizationService;
_ratingCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings);
_reviewCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews);
@ -58,7 +61,7 @@ public class SeriesController : BaseApiController
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series for library");
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
@ -101,7 +104,7 @@ public class SeriesController : BaseApiController
if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok();
return BadRequest("There was an issue deleting the series requested");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-series-delete"));
}
/// <summary>
@ -149,7 +152,8 @@ public class SeriesController : BaseApiController
public async Task<ActionResult> UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Ratings);
if (!await _seriesService.UpdateRating(user!, updateSeriesRatingDto)) return BadRequest("There was a critical error.");
if (!await _seriesService.UpdateRating(user!, updateSeriesRatingDto))
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
return Ok();
}
@ -162,8 +166,8 @@ public class SeriesController : BaseApiController
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
{
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(updateSeries.Id);
if (series == null) return BadRequest("Series does not exist");
if (series == null)
return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
series.NormalizedName = series.Name.ToNormalized();
if (!string.IsNullOrEmpty(updateSeries.SortName?.Trim()))
@ -199,7 +203,7 @@ public class SeriesController : BaseApiController
return Ok();
}
return BadRequest("There was an error with updating the series");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-series-update"));
}
/// <summary>
@ -218,7 +222,7 @@ public class SeriesController : BaseApiController
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series");
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
@ -254,7 +258,7 @@ public class SeriesController : BaseApiController
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series");
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
@ -318,7 +322,7 @@ public class SeriesController : BaseApiController
[HttpPost("scan")]
public ActionResult ScanSeries(RefreshSeriesDto refreshSeriesDto)
{
_taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
_taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, true);
return Ok();
}
@ -370,10 +374,10 @@ public class SeriesController : BaseApiController
}
}
return Ok("Successfully updated");
return Ok(await _localizationService.Translate(User.GetUserId(), "series-updated"));
}
return BadRequest("Could not update metadata");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "update-metadata-fail"));
}
/// <summary>
@ -390,7 +394,7 @@ public class SeriesController : BaseApiController
await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series for collection");
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series-collection"));
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
@ -407,7 +411,7 @@ public class SeriesController : BaseApiController
[HttpPost("series-by-ids")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto)
{
if (dto.SeriesIds == null) return BadRequest("Must pass seriesIds");
if (dto.SeriesIds == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload"));
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, userId));
}
@ -420,10 +424,11 @@ public class SeriesController : BaseApiController
/// <remarks>This is cached for an hour</remarks>
[ResponseCache(CacheProfileName = "Month", VaryByQueryKeys = new [] {"ageRating"})]
[HttpGet("age-rating")]
public ActionResult<string> GetAgeRating(int ageRating)
public async Task<ActionResult<string>> GetAgeRating(int ageRating)
{
var val = (AgeRating) ageRating;
if (val == AgeRating.NotApplicable) return "No Restriction";
if (val == AgeRating.NotApplicable)
return await _localizationService.Translate(User.GetUserId(), "age-restriction-not-applicable");
return Ok(val.ToDescription());
}
@ -439,8 +444,15 @@ public class SeriesController : BaseApiController
public async Task<ActionResult<SeriesDetailDto>> GetSeriesDetailBreakdown(int seriesId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
try
{
return await _seriesService.GetSeriesDetail(seriesId, userId);
}
catch (KavitaException ex)
{
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
}
}
@ -485,7 +497,7 @@ public class SeriesController : BaseApiController
return Ok();
}
return BadRequest("There was an issue updating relationships");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-relationship"));
}

View file

@ -41,11 +41,13 @@ public class ServerController : BaseApiController
private readonly ITaskScheduler _taskScheduler;
private readonly IUnitOfWork _unitOfWork;
private readonly IEasyCachingProviderFactory _cachingProviderFactory;
private readonly ILocalizationService _localizationService;
public ServerController(ILogger<ServerController> logger,
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
ICleanupService cleanupService, IScannerService scannerService, IAccountService accountService,
ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory)
ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory,
ILocalizationService localizationService)
{
_logger = logger;
_backupService = backupService;
@ -58,6 +60,7 @@ public class ServerController : BaseApiController
_taskScheduler = taskScheduler;
_unitOfWork = unitOfWork;
_cachingProviderFactory = cachingProviderFactory;
_localizationService = localizationService;
}
/// <summary>
@ -103,12 +106,12 @@ public class ServerController : BaseApiController
/// </summary>
/// <returns></returns>
[HttpPost("analyze-files")]
public ActionResult AnalyzeFiles()
public async Task<ActionResult> AnalyzeFiles()
{
_logger.LogInformation("{UserName} is performing file analysis from admin dashboard", User.GetUsername());
if (TaskScheduler.HasAlreadyEnqueuedTask(ScannerService.Name, "AnalyzeFiles",
Array.Empty<object>(), TaskScheduler.DefaultQueue, true))
return Ok("Job already running");
return Ok(await _localizationService.Translate(User.GetUserId(), "job-already-running"));
BackgroundJob.Enqueue(() => _scannerService.AnalyzeFiles());
return Ok();
@ -127,7 +130,7 @@ public class ServerController : BaseApiController
/// <summary>
/// Returns non-sensitive information about the current system
/// </summary>
/// <remarks>This is just for the UI and is extremly lightweight</remarks>
/// <remarks>This is just for the UI and is extremely lightweight</remarks>
/// <returns></returns>
[HttpGet("server-info-slim")]
public async Task<ActionResult<ServerInfoDto>> GetSlimVersion()
@ -146,8 +149,7 @@ public class ServerController : BaseApiController
var encoding = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
if (encoding == EncodeFormat.PNG)
{
return BadRequest(
"You cannot convert to PNG. For covers, use Refresh Covers. Bookmarks and favicons cannot be encoded back.");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "encode-as-warning"));
}
_taskScheduler.CovertAllCoversToEncoding();
@ -160,7 +162,7 @@ public class ServerController : BaseApiController
/// </summary>
/// <returns></returns>
[HttpGet("logs")]
public ActionResult GetLogs()
public async Task<ActionResult> GetLogs()
{
var files = _backupService.GetLogFiles();
try
@ -171,7 +173,7 @@ public class ServerController : BaseApiController
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
}
}
@ -220,8 +222,7 @@ public class ServerController : BaseApiController
Id = dto.Id,
Title = dto.Id.Replace('-', ' '),
Cron = dto.Cron,
CreatedAt = dto.CreatedAt,
LastExecution = dto.LastExecution,
LastExecutionUtc = dto.LastExecution.HasValue ? new DateTime(dto.LastExecution.Value.Ticks, DateTimeKind.Utc) : null
});
return Ok(recurringJobs);

View file

@ -33,9 +33,11 @@ public class SettingsController : BaseApiController
private readonly IMapper _mapper;
private readonly IEmailService _emailService;
private readonly ILibraryWatcher _libraryWatcher;
private readonly ILocalizationService _localizationService;
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler,
IDirectoryService directoryService, IMapper mapper, IEmailService emailService, ILibraryWatcher libraryWatcher)
IDirectoryService directoryService, IMapper mapper, IEmailService emailService, ILibraryWatcher libraryWatcher,
ILocalizationService localizationService)
{
_logger = logger;
_unitOfWork = unitOfWork;
@ -44,6 +46,7 @@ public class SettingsController : BaseApiController
_mapper = mapper;
_emailService = emailService;
_libraryWatcher = libraryWatcher;
_localizationService = localizationService;
}
[HttpGet("base-url")]
@ -224,7 +227,7 @@ public class SettingsController : BaseApiController
foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
{
if (!IPAddress.TryParse(ipAddress.Trim(), out _)) {
return BadRequest($"IP Address '{ipAddress}' is invalid");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "ip-address-invalid", ipAddress));
}
}
@ -279,7 +282,7 @@ public class SettingsController : BaseApiController
// Validate new directory can be used
if (!await _directoryService.CheckWriteAccess(bookmarkDirectory))
{
return BadRequest("Bookmark Directory does not have correct permissions for Kavita to use");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-dir-permissions"));
}
originalBookmarkDirectory = setting.Value;
@ -308,7 +311,7 @@ public class SettingsController : BaseApiController
{
if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1)
{
return BadRequest("Total Backups must be between 1 and 30");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-backups"));
}
setting.Value = updateSettingsDto.TotalBackups + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
@ -318,7 +321,7 @@ public class SettingsController : BaseApiController
{
if (updateSettingsDto.TotalLogs > 30 || updateSettingsDto.TotalLogs < 1)
{
return BadRequest("Total Logs must be between 1 and 30");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-logs"));
}
setting.Value = updateSettingsDto.TotalLogs + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
@ -366,7 +369,7 @@ public class SettingsController : BaseApiController
{
_logger.LogError(ex, "There was an exception when updating server settings");
await _unitOfWork.RollbackAsync();
return BadRequest("There was a critical issue. Please try again.");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
}

View file

@ -19,12 +19,15 @@ public class StatsController : BaseApiController
private readonly IStatisticService _statService;
private readonly IUnitOfWork _unitOfWork;
private readonly UserManager<AppUser> _userManager;
private readonly ILocalizationService _localizationService;
public StatsController(IStatisticService statService, IUnitOfWork unitOfWork, UserManager<AppUser> userManager)
public StatsController(IStatisticService statService, IUnitOfWork unitOfWork,
UserManager<AppUser> userManager, ILocalizationService localizationService)
{
_statService = statService;
_unitOfWork = unitOfWork;
_userManager = userManager;
_localizationService = localizationService;
}
[HttpGet("user/{userId}/read")]
@ -33,7 +36,7 @@ public class StatsController : BaseApiController
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
if (user!.Id != userId && !await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole))
return Unauthorized("You are not authorized to view another user's statistics");
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "stats-permission-denied"));
return Ok(await _statService.GetUserReadStatistics(userId, new List<int>()));
}

View file

@ -16,11 +16,14 @@ public class TachiyomiController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly ITachiyomiService _tachiyomiService;
private readonly ILocalizationService _localizationService;
public TachiyomiController(IUnitOfWork unitOfWork, ITachiyomiService tachiyomiService)
public TachiyomiController(IUnitOfWork unitOfWork, ITachiyomiService tachiyomiService,
ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_tachiyomiService = tachiyomiService;
_localizationService = localizationService;
}
/// <summary>
@ -31,7 +34,7 @@ public class TachiyomiController : BaseApiController
[HttpGet("latest-chapter")]
public async Task<ActionResult<ChapterDto>> GetLatestChapter(int seriesId)
{
if (seriesId < 1) return BadRequest("seriesId must be greater than 0");
if (seriesId < 1) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "SeriesId"));
return Ok(await _tachiyomiService.GetLatestChapter(seriesId, User.GetUserId()));
}

View file

@ -2,6 +2,7 @@
using System.Threading.Tasks;
using API.Data;
using API.DTOs.Theme;
using API.Extensions;
using API.Services;
using API.Services.Tasks;
using Kavita.Common;
@ -15,12 +16,15 @@ public class ThemeController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IThemeService _themeService;
private readonly ITaskScheduler _taskScheduler;
private readonly ILocalizationService _localizationService;
public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, ITaskScheduler taskScheduler)
public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, ITaskScheduler taskScheduler,
ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_themeService = themeService;
_taskScheduler = taskScheduler;
_localizationService = localizationService;
}
[ResponseCache(CacheProfileName = "10Minute")]
@ -42,8 +46,16 @@ public class ThemeController : BaseApiController
[Authorize("RequireAdminRole")]
[HttpPost("update-default")]
public async Task<ActionResult> UpdateDefault(UpdateDefaultThemeDto dto)
{
try
{
await _themeService.UpdateDefault(dto.ThemeId);
}
catch (KavitaException ex)
{
return BadRequest(await _localizationService.Translate(User.GetUserId(), "theme-doesnt-exist"));
}
return Ok();
}
@ -61,7 +73,7 @@ public class ThemeController : BaseApiController
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
return BadRequest(await _localizationService.Get("en", ex.Message));
}
}
}

View file

@ -25,10 +25,12 @@ public class UploadController : BaseApiController
private readonly IDirectoryService _directoryService;
private readonly IEventHub _eventHub;
private readonly IReadingListService _readingListService;
private readonly ILocalizationService _localizationService;
/// <inheritdoc />
public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger<UploadController> logger,
ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub, IReadingListService readingListService)
ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub, IReadingListService readingListService,
ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_imageService = imageService;
@ -37,6 +39,7 @@ public class UploadController : BaseApiController
_directoryService = directoryService;
_eventHub = eventHub;
_readingListService = readingListService;
_localizationService = localizationService;
}
/// <summary>
@ -57,9 +60,9 @@ public class UploadController : BaseApiController
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
return BadRequest($"Could not download file");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
if (!await _imageService.IsImage(path)) return BadRequest("Url does not return a valid image");
if (!await _imageService.IsImage(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
return $"coverupload_{dateString}.{format}";
}
@ -67,10 +70,10 @@ public class UploadController : BaseApiController
{
// Unauthorized
if (ex.StatusCode == 401)
return BadRequest("The server requires authentication to load the url externally");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
}
return BadRequest("Unable to download image, please use another url or upload by file");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
}
/// <summary>
@ -87,13 +90,13 @@ public class UploadController : BaseApiController
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
return BadRequest("You must pass a url to use");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required"));
}
try
{
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
if (series == null) return BadRequest("Invalid Series");
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}");
if (!string.IsNullOrEmpty(filePath))
@ -118,7 +121,7 @@ public class UploadController : BaseApiController
await _unitOfWork.RollbackAsync();
}
return BadRequest("Unable to save cover image to Series");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-series-save"));
}
/// <summary>
@ -135,13 +138,13 @@ public class UploadController : BaseApiController
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
return BadRequest("You must pass a url to use");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required"));
}
try
{
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id);
if (tag == null) return BadRequest("Invalid Tag id");
if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist"));
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
if (!string.IsNullOrEmpty(filePath))
@ -166,7 +169,7 @@ public class UploadController : BaseApiController
await _unitOfWork.RollbackAsync();
}
return BadRequest("Unable to save cover image to Collection Tag");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-collection-save"));
}
/// <summary>
@ -183,16 +186,16 @@ public class UploadController : BaseApiController
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
return BadRequest("You must pass a url to use");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required"));
}
if (_readingListService.UserHasReadingListAccess(uploadFileDto.Id, User.GetUsername()) == null)
return Unauthorized("You do not have access");
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "access-denied"));
try
{
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
if (readingList == null) return BadRequest("Reading list is not valid");
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
if (!string.IsNullOrEmpty(filePath))
@ -217,7 +220,7 @@ public class UploadController : BaseApiController
await _unitOfWork.RollbackAsync();
}
return BadRequest("Unable to save cover image to Reading List");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-reading-list-save"));
}
private async Task<string> CreateThumbnail(UploadFileDto uploadFileDto, string filename, int thumbnailSize = 0)
@ -247,13 +250,13 @@ public class UploadController : BaseApiController
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
return BadRequest("You must pass a url to use");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required"));
}
try
{
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
if (chapter == null) return BadRequest("Invalid Chapter");
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
if (!string.IsNullOrEmpty(filePath))
@ -286,7 +289,7 @@ public class UploadController : BaseApiController
await _unitOfWork.RollbackAsync();
}
return BadRequest("Unable to save cover image to Chapter");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-chapter-save"));
}
/// <summary>
@ -345,7 +348,7 @@ public class UploadController : BaseApiController
await _unitOfWork.RollbackAsync();
}
return BadRequest("Unable to save cover image to Library");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-library-save"));
}
/// <summary>
@ -360,7 +363,7 @@ public class UploadController : BaseApiController
try
{
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
if (chapter == null) return BadRequest("Chapter no longer exists");
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
var originalFile = chapter.CoverImage;
chapter.CoverImage = string.Empty;
chapter.CoverImageLocked = false;
@ -385,7 +388,7 @@ public class UploadController : BaseApiController
await _unitOfWork.RollbackAsync();
}
return BadRequest("Unable to resetting cover lock for Chapter");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reset-chapter-lock"));
}
}

View file

@ -5,6 +5,7 @@ using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.Extensions;
using API.Services;
using API.SignalR;
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
@ -18,12 +19,15 @@ public class UsersController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly IEventHub _eventHub;
private readonly ILocalizationService _localizationService;
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub)
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub,
ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_eventHub = eventHub;
_localizationService = localizationService;
}
[Authorize(Policy = "RequireAdminRole")]
@ -38,7 +42,7 @@ public class UsersController : BaseApiController
if (await _unitOfWork.CommitAsync()) return Ok();
return BadRequest("Could not delete the user.");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-user-delete"));
}
/// <summary>
@ -66,7 +70,7 @@ public class UsersController : BaseApiController
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
if (library == null) return BadRequest("Library does not exist");
if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist"));
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
}
@ -113,18 +117,19 @@ public class UsersController : BaseApiController
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
existingPreferences.ShareReviews = preferencesDto.ShareReviews;
if (_localizationService.GetLocales().Contains(preferencesDto.Locale))
{
existingPreferences.Locale = preferencesDto.Locale;
}
_unitOfWork.UserRepository.Update(existingPreferences);
if (await _unitOfWork.CommitAsync())
{
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-user-pref"));
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
return Ok(preferencesDto);
}
return BadRequest("There was an issue saving preferences.");
}
/// <summary>
/// Returns the preferences of the user
/// </summary>

View file

@ -7,6 +7,7 @@ using API.DTOs.Filtering;
using API.DTOs.WantToRead;
using API.Extensions;
using API.Helpers;
using API.Services;
using API.Services.Plus;
using Hangfire;
using Microsoft.AspNetCore.Mvc;
@ -21,11 +22,14 @@ public class WantToReadController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly IScrobblingService _scrobblingService;
private readonly ILocalizationService _localizationService;
public WantToReadController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService)
public WantToReadController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService,
ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_scrobblingService = scrobblingService;
_localizationService = localizationService;
}
/// <summary>
@ -85,7 +89,7 @@ public class WantToReadController : BaseApiController
return Ok();
}
return BadRequest("There was an issue updating Read List");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-reading-list-update"));
}
/// <summary>
@ -113,6 +117,6 @@ public class WantToReadController : BaseApiController
return Ok();
}
return BadRequest("There was an issue updating Read List");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-reading-list-update"));
}
}

View file

@ -4,4 +4,8 @@ public class LoginDto
{
public string Username { get; init; } = default!;
public string Password { get; set; } = default!;
/// <summary>
/// If ApiKey is passed, will ignore username/password for validation
/// </summary>
public string? ApiKey { get; set; } = default!;
}

View file

@ -9,7 +9,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
/// file (abstracted from type).
/// </summary>
public class ChapterDto : IHasReadTimeEstimate, IEntityDate
public class ChapterDto : IHasReadTimeEstimate
{
public int Id { get; init; }
/// <summary>
@ -59,11 +59,14 @@ public class ChapterDto : IHasReadTimeEstimate, IEntityDate
/// <summary>
/// When chapter was created
/// </summary>
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
public DateTime CreatedUtc { get; set; }
public DateTime LastModifiedUtc { get; set; }
/// <summary>
/// When chapter was created in local server time
/// </summary>
/// <remarks>This is required for Tachiyomi Extension</remarks>
public DateTime Created { get; set; }
/// <summary>
/// When the chapter was released.
/// </summary>
/// <remarks>Metadata field</remarks>

View file

@ -26,12 +26,4 @@ public class DeviceDto
/// Platform (ie) Windows 10
/// </summary>
public DevicePlatform Platform { get; set; }
/// <summary>
/// Last time this device was used to send a file
/// </summary>
public DateTime LastUsed { get; set; }
/// <summary>
/// Last time this device was used to send a file
/// </summary>
public DateTime LastUsedUtc { get; set; }
}

View file

@ -15,14 +15,6 @@ public class JobDto
/// <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; }
/// <summary>
/// When the job was created
/// </summary>
public DateTime? CreatedAtUtc { get; set; }
/// <summary>
/// Last time the job was run

View file

@ -20,6 +20,4 @@ public class MediaErrorDto
/// Exception message
/// </summary>
public string Details { get; set; }
public DateTime Created { get; set; }
public DateTime CreatedUtc { get; set; }
}

View file

@ -10,8 +10,8 @@ public class ScrobbleEventDto
public bool IsProcessed { get; set; }
public int? VolumeNumber { get; set; }
public int? ChapterNumber { get; set; }
public DateTime LastModified { get; set; }
public DateTime Created { get; set; }
public DateTime LastModifiedUtc { get; set; }
public DateTime CreatedUtc { get; set; }
public float? Rating { get; set; }
public ScrobbleEventType ScrobbleEventType { get; set; }

View file

@ -1,6 +1,4 @@
using System;
using API.Entities.Enums.Theme;
using API.Entities.Interfaces;
using API.Services;
namespace API.DTOs.Theme;
@ -8,7 +6,7 @@ namespace API.DTOs.Theme;
/// <summary>
/// Represents a set of css overrides the user can upload to Kavita and will load into webui
/// </summary>
public class SiteThemeDto : IEntityDate
public class SiteThemeDto
{
public int Id { get; set; }
/// <summary>
@ -32,9 +30,5 @@ public class SiteThemeDto : IEntityDate
/// Where did the theme come from
/// </summary>
public ThemeProvider Provider { get; set; }
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
public DateTime CreatedUtc { get; set; }
public DateTime LastModifiedUtc { get; set; }
public string Selector => "bg-" + Name.ToLower();
}

View file

@ -147,4 +147,9 @@ public class UserPreferencesDto
/// </summary>
[Required]
public bool ShareReviews { get; set; } = false;
/// <summary>
/// UI Site Global Setting: The language locale that should be used for the user
/// </summary>
[Required]
public string Locale { get; set; }
}

View file

@ -16,8 +16,18 @@ public class VolumeDto : IHasReadTimeEstimate
public string Name { get; set; } = default!;
public int Pages { get; set; }
public int PagesRead { get; set; }
public DateTime LastModified { get; set; }
public DateTime LastModifiedUtc { get; set; }
public DateTime CreatedUtc { get; set; }
/// <summary>
/// When chapter was created in local server time
/// </summary>
/// <remarks>This is required for Tachiyomi Extension</remarks>
public DateTime Created { get; set; }
/// <summary>
/// When chapter was last modified in local server time
/// </summary>
/// <remarks>This is required for Tachiyomi Extension</remarks>
public DateTime LastModified { get; set; }
public int SeriesId { get; set; }
public ICollection<ChapterDto> Chapters { get; set; } = new List<ChapterDto>();
/// <inheritdoc cref="IHasReadTimeEstimate.MinHoursToRead"/>

View file

@ -100,6 +100,10 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
builder.Entity<AppUserPreferences>()
.Property(b => b.BookReaderWritingStyle)
.HasDefaultValue(WritingStyle.Horizontal);
builder.Entity<AppUserPreferences>()
.Property(b => b.Locale)
.IsRequired(true)
.HasDefaultValue("en");
builder.Entity<Library>()
.Property(b => b.AllowScrobbling)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class AddLocaleOnPrefs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Locale",
table: "AppUserPreferences",
type: "TEXT",
nullable: false,
defaultValue: "en");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Locale",
table: "AppUserPreferences");
}
}
}

View file

@ -272,6 +272,12 @@ namespace API.Data.Migrations
b.Property<int>("LayoutMode")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("en");
b.Property<bool>("NoTransitions")
.HasColumnType("INTEGER");

View file

@ -28,7 +28,7 @@ public interface IAppUserProgressRepository
Task<IEnumerable<AppUserProgress>> GetUserProgressForSeriesAsync(int seriesId, int userId);
Task<IEnumerable<AppUserProgress>> GetAllProgress();
Task<DateTime> GetLatestProgress();
Task<ProgressDto> GetUserProgressDtoAsync(int chapterId, int userId);
Task<ProgressDto?> GetUserProgressDtoAsync(int chapterId, int userId);
Task<bool> AnyUserProgressForSeriesAsync(int seriesId, int userId);
Task<int> GetHighestFullyReadChapterForSeries(int seriesId, int userId);
Task<int> GetHighestFullyReadVolumeForSeries(int seriesId, int userId);
@ -143,7 +143,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository
.FirstOrDefaultAsync();
}
public async Task<ProgressDto> GetUserProgressDtoAsync(int chapterId, int userId)
public async Task<ProgressDto?> GetUserProgressDtoAsync(int chapterId, int userId)
{
return await _context.AppUserProgresses
.Where(p => p.AppUserId == userId && p.ChapterId == chapterId)

View file

@ -73,7 +73,7 @@ public interface IUserRepository
Task<IEnumerable<AppUserRating>> GetSeriesWithReviews(int userId);
Task<bool> HasHoldOnSeries(int userId, int seriesId);
Task<IList<ScrobbleHoldDto>> GetHolds(int userId);
Task<string> GetLocale(int userId);
}
public class UserRepository : IUserRepository
@ -291,6 +291,13 @@ public class UserRepository : IUserRepository
.ToListAsync();
}
public async Task<string> GetLocale(int userId)
{
return await _context.AppUserPreferences.Where(p => p.AppUserId == userId)
.Select(p => p.Locale)
.SingleAsync();
}
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
{
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);

View file

@ -127,6 +127,10 @@ public class AppUserPreferences
/// UI Site Global Setting: Should series reviews be shared with all users in the server
/// </summary>
public bool ShareReviews { get; set; } = false;
/// <summary>
/// UI Site Global Setting: The language locale that should be used for the user
/// </summary>
public string Locale { get; set; }
public AppUser AppUser { get; set; } = null!;
public int AppUserId { get; set; }

View file

@ -66,6 +66,8 @@ public static class ApplicationServiceExtensions
services.AddScoped<IPresenceTracker, PresenceTracker>();
services.AddScoped<IImageService, ImageService>();
services.AddScoped<ILocalizationService, LocalizationService>();
services.AddScoped<IScrobblingService, ScrobblingService>();
services.AddScoped<ILicenseService, LicenseService>();

View file

@ -37,4 +37,11 @@ public class AppUserBuilder : IEntityBuilder<AppUser>
_appUser.Libraries.Add(library);
return this;
}
public AppUserBuilder WithLocale(string locale)
{
_appUser.UserPreferences.Locale = locale;
return this;
}
}

160
API/I18N/de.json Normal file
View file

@ -0,0 +1,160 @@
{
"register-user": "Bei der Benutzerregistrierung ist etwas schiefgelaufen",
"validate-email": "Es gab ein Problem bei der Verifizierung Ihrer E-Mail: {0}",
"denied": "Nicht gestattet",
"permission-denied": "Sie sind zu diesem Vorgang nicht berechtigt",
"share-multiple-emails": "Sie können E-Mails nicht für mehrere Konten verwenden",
"generate-token": "Es gab ein Problem bei der Generierung eines E-Mail-Bestätigungs-Tokens. Siehe Protokoll",
"no-user": "Der Benutzer existiert nicht",
"generic-user-update": "Beim Aktualisieren des Benutzers trat eine Abweichung auf",
"user-already-registered": "Der Benutzer ist bereits als {0} registriert",
"username-taken": "Der Benutzername ist bereits vergeben",
"generic-user-email-update": "E-Mail für Benutzer kann nicht aktualisiert werden. Protokolle überprüfen.",
"generic-password-update": "Es gab einen unerwarteten Fehler beim Bestätigen des neuen Passworts",
"password-updated": "Passwort aktualisiert",
"forgot-password-generic": "Es wird eine E-Mail an die E-Mail-Adresse gesendet, wenn sie in der Datenbank vorhanden ist",
"not-accessible-password": "Es kann nicht auf Ihren Server zugegriffen werden. Der Link zum Zurücksetzen Ihres Passworts finden Sie in den Protokollen",
"disabled-account": "Ihr Konto ist deaktiviert. Kontaktieren Sie den Server-Administrator.",
"confirm-email": "Sie müssen Ihre E-Mail zuerst bestätigen",
"locked-out": "Sie wurden wegen zu vieler Autorisierungsversuche ausgesperrt. Bitte warten Sie 10 Minuten.",
"confirm-token-gen": "Es gab ein Problem bei der Generierung eines Bestätigungs-Tokens",
"invalid-password": "Ungültiges Passwort",
"password-required": "Sie müssen Ihr bestehendes Passwort eingeben, um Ihr Konto zu ändern, es sei denn, Sie sind ein Administrator",
"invalid-payload": "Ungültiger Payload",
"nothing-to-do": "Nichts zu tun",
"age-restriction-update": "Es ist ein Fehler bei der Aktualisierung der Altersbeschränkung aufgetreten",
"manual-setup-fail": "Die manuelle Einrichtung kann nicht abgeschlossen werden. Bitte brechen Sie die Einladung ab und erstellen Sie sie neu",
"user-already-confirmed": "Der Benutzer ist bereits bestätigt",
"generic-invite-user": "Es gab ein Problem beim Einladen des Benutzers. Bitte prüfen Sie die Protokolle.",
"user-already-invited": "Der Benutzer ist bereits unter dieser E-Mail eingeladen und hat die Einladung noch nicht angenommen.",
"invalid-email-confirmation": "Ungültige E-Mail Bestätigung",
"not-accessible": "Es kann von außen nicht auf Ihren Server zugegriffen werden",
"bad-credentials": "Ihre Anmeldedaten sind nicht korrekt",
"unable-to-reset-key": "Etwas ist schief gelaufen, Schlüssel kann nicht zurückgesetzt werden",
"invalid-token": "Ungültiger Token",
"email-sent": "E-Mail versendet",
"invalid-username": "Ungültiger Benutzername",
"critical-email-migration": "Es gab ein Problem bei der E-Mail-Migration. Kontaktieren Sie den Support",
"chapter-doesnt-exist": "Das Kapitel existiert nicht",
"generic-error": "Es ist ein Fehler ist aufgetreten, bitte versuchen Sie es erneut",
"device-doesnt-exist": "Das Gerät existiert nicht",
"generic-device-create": "Beim Erstellen des Geräts ist ein Fehler aufgetreten",
"send-to-kavita-email": "Das Senden an Gerät kann nicht mit dem E-Mail-Dienst von Kavita durchgeführt werden. Bitte konfigurieren Sie Ihren eigenen.",
"send-to-device-status": "Übertrage Dateien auf Ihr Gerät",
"series-doesnt-exist": "Die Serie existiert nicht",
"volume-doesnt-exist": "Das Band existiert nicht",
"no-cover-image": "Kein Coverbild",
"bookmark-doesnt-exist": "Lesezeichen ist nicht vorhanden",
"must-be-defined": "{0} muss definiert sein",
"generic-favicon": "Es gab ein Problem beim Abrufen des Favicons für die Domain",
"invalid-filename": "Ungültiger Dateiname",
"library-name-exists": "Der Name der Bibliothek existiert bereits. Bitte wählen Sie einen einzigartigen Namen für den Server.",
"no-library-access": "Der Benutzer hat keinen Zugang zu dieser Bibliothek",
"user-doesnt-exist": "Der Benutzer existiert nicht",
"library-doesnt-exist": "Die Bibliothek existiert nicht",
"invalid-access": "Unzulässiger Zugang",
"generic-clear-bookmarks": "Lesezeichen konnten nicht gelöscht werden",
"name-required": "Name darf nicht leer sein",
"valid-number": "Muss eine gültige Seitenzahl sein",
"duplicate-bookmark": "Doppelter Lesezeicheneintrag bereits vorhanden",
"user-migration-needed": "Dieser Benutzer muss migriert werden. Lassen Sie ihn sich abmelden und wieder anmelden, um einen Migrationsprozess auszulösen",
"file-missing": "Datei wurde im Buch nicht gefunden",
"generic-invite-email": "Es gab ein Problem beim erneuten Senden der Einladungsmail",
"collection-updated": "Sammlung wurde erfolgreich aktualisiert",
"admin-already-exists": "Admin existiert bereits",
"collection-doesnt-exist": "Sammlung existiert nicht",
"generic-device-update": "Beim Aktualisieren des Geräts ist ein Fehler aufgetreten",
"generic-device-delete": "Beim Löschen des Geräts ist ein Fehler aufgetreten",
"generic-send-to": "Es ist ein Fehler beim Senden der Datei(en) an das Gerät aufgetreten",
"greater-0": "{0} muss grösser als 0 sein",
"bookmarks-empty": "Lesezeichen dürfen nicht leer sein",
"bookmark-save": "Lesezeichen konnte nicht gespeichert werden",
"file-doesnt-exist": "Datei existiert nicht",
"generic-library": "Es gab ein schwerwiegendes Problem. Bitte versuchen Sie es erneut.",
"invalid-path": "Ungültiger Pfad",
"pdf-doesnt-exist": "PDF ist nicht vorhanden, obwohl es vorhanden sein sollte",
"no-image-for-page": "Kein derartiges Bild für Seite {0}. Versuchen Sie zu aktualisieren, um einen erneuten Cache zu ermöglichen.",
"perform-scan": "Bitte führen Sie eine Durchsuchung dieser Serie oder Bibliothek durch und versuchen Sie es erneut",
"bookmark-permission": "Sie haben nicht die Berechtigung, Lesezeichen zu erstellen oder zu entfernen",
"cache-file-find": "Das gecachte Bild konnte nicht gefunden werden. Neu laden und erneut versuchen.",
"reading-list-permission": "Sie haben keine Berechtigung für diese Leseliste oder die Liste existiert nicht",
"delete-library-while-scan": "Sie können eine Bibliothek nicht löschen, während ein Scanvorgang läuft. Bitte warten Sie, bis der Scanvorgang abgeschlossen ist oder starten Sie Kavita neu und versuchen Sie es dann zu löschen",
"generic-library-update": "Es gab ein schwerwiegendes Problem bei der Aktualisierung der Bibliothek.",
"generic-read-progress": "Es gab ein Problem beim Erfassen des Fortschritts",
"reading-list-updated": "Aktualisiert",
"reading-list-item-delete": "Element(e) konnte(n) nicht gelöscht werden",
"reading-list-deleted": "Leseliste wurde gelöscht",
"generic-reading-list-delete": "Es gab ein Problem beim Löschen der Leseliste",
"generic-reading-list-create": "Es gab ein Problem bei der Erstellung der Leseliste",
"generic-reading-list-update": "Es gab ein Problem bei der Aktualisierung der Leseliste",
"reading-list-doesnt-exist": "Die Leseliste existiert nicht",
"series-restricted": "Der Benutzer hat keinen Zugriff auf diese Serie",
"generic-series-update": "Beim Aktualisieren der Serie ist ein Fehler aufgetreten",
"series-updated": "Erfolgreich aktualisiert",
"update-metadata-fail": "Metadaten konnten nicht aktualisiert werden",
"job-already-running": "Aufgabe läuft bereits",
"generic-cover-series-save": "Das Coverbild konnte nicht für die Serie gespeichert werden",
"generic-cover-library-save": "Das Coverbild konnte nicht für die Bibliothek gespeichert werden",
"no-series-collection": "Es konnten keine Serien zur Sammlung hinzugefügt werden",
"generic-user-pref": "Es gab ein Problem beim Speichern von Präferenzen",
"browse-recently-added": "Zuletzt hinzugefügtes ansehen",
"search-description": "Suche nach Serien, Sammlungen oder Leselisten",
"not-authenticated": "Benutzer ist nicht authentifiziert",
"scrobble-bad-payload": "Schlechte Daten vom Scrobble Anbieter",
"theme-doesnt-exist": "Designdatei fehlt oder ist ungültig",
"epub-malformed": "Die Datei ist fehlerhaft formatiert! Kann nicht gelesen werden.",
"collection-tag-duplicate": "Eine Sammlung mit diesem Namen existiert bereits",
"send-to-permission": "Nicht-EPUB oder -PDF können nicht an Geräte gesendet werden, da sie von Kindle nicht unterstützt werden",
"progress-must-exist": "Der Fortschritt muss beim Benutzer vorhanden sein",
"device-duplicate": "Ein Gerät mit diesem Namen existiert bereits",
"device-not-created": "Dieses Gerät existiert noch nicht. Bitte zuerst erstellen",
"reading-list-name-exists": "Eine Leseliste mit diesem Namen existiert bereits",
"user-no-access-library-from-series": "Der Benutzer hat keinen Zugang zu der Bibliothek, der zu dieser Serie gehört",
"series-restricted-age-restriction": "Benutzer darf diese Serie aufgrund von Altersbeschränkungen nicht sehen",
"book-num": "Buch {0}",
"issue-num": "Fehler {0}{1}",
"chapter-num": "Kapitel {0}",
"reading-list-position": "Position konnte nicht aktualisiert werden",
"libraries-restricted": "Benutzer hat keinen Zugriff auf jegliche Bibliothek",
"no-series": "Es konnte keine Serie zur Bibliothek hinzugefügt werden",
"generic-scrobble-hold": "Beim Pausieren der Funktion ist ein Fehler aufgetreten",
"generic-series-delete": "Es gab ein Fehler beim Löschen der Serie",
"age-restriction-not-applicable": "Keine Einschränkung",
"generic-relationship": "Es gab ein Problem bei der Aktualisierung von Relationen",
"encode-as-warning": "Sie können nicht in PNG konvertieren. Für Covers verwenden Sie Covers aktualisieren. Lesezeichen und Favicons können nicht zurückkodiert werden.",
"ip-address-invalid": "IP-Adresse '{0}' ist ungültig",
"bookmark-dir-permissions": "Das Lesezeichenverzeichnis hat nicht die richtigen Rechte für die Verwendung durch Kavita",
"total-backups": "Die Gesamtzahl der Backups muss zwischen 1 und 30 liegen",
"total-logs": "Die Gesamtzahl der Protokolle muss zwischen 1 und 30 liegen",
"stats-permission-denied": "Sie sind nicht berechtigt, die Statistiken eines anderen Benutzers einzusehen",
"url-not-valid": "Url gibt kein gültiges Bild zurück oder erfordert Autorisierung",
"url-required": "Sie müssen eine Url angeben, um zu verwenden",
"generic-cover-collection-save": "Das Coverbild konnte nicht für die Sammlung gespeichert werden",
"generic-cover-reading-list-save": "Das Coverbild konnte nicht für die Leseliste gespeichert werden",
"generic-cover-chapter-save": "Das Coverbild konnte nicht für das Kapitel gespeichert werden",
"access-denied": "Sie haben keinen Zugriff",
"reset-chapter-lock": "Die Cover Sperre konnte für das Kapitel nicht zurückgesetzt werden",
"generic-user-delete": "Der Benutzer konnte nicht gelöscht werden",
"opds-disabled": "OPDS ist auf diesem Server nicht aktiviert",
"recently-added": "Zuletzt hinzugefügt",
"reading-lists": "Leselisten",
"libraries": "Alle Bibliotheken",
"collections": "Alle Sammlungen",
"browse-reading-lists": "Leselisten durchsuchen",
"browse-libraries": "Bibliotheken durchsuchen",
"browse-collections": "Sammlungen durchsuchen",
"search": "Suche",
"reading-list-restricted": "Die Leseliste existiert nicht oder Sie haben keinen Zugriff darauf",
"query-required": "Sie müssen einen Abfrageparameter angeben",
"favicon-doesnt-exist": "Favicon existiert nicht",
"unable-to-register-k+": "Die Lizenz kann aufgrund eines Fehlers nicht registriert werden. Wenden Sie sich an den Kavita+ Support",
"anilist-cred-expired": "AniList Zugangsdaten sind abgelaufen oder nicht vorhanden",
"bad-copy-files-for-download": "Dateien konnten nicht in das Temporärverzeichnis des Archivdownloads kopiert werden.",
"generic-create-temp-archive": "Es gab ein Fehler bei der Erstellung eines temporären Archivs",
"epub-html-missing": "Die entsprechende HTML-Datei für diese Seite konnte nicht gefunden werden",
"collection-tag-title-required": "Titel der Sammlung darf nicht leer sein",
"reading-list-title-required": "Leselisten Titel darf nicht leer sein",
"volume-num": "Band {0}",
"on-deck": "Weiterlesen",
"browse-on-deck": "Weiterlesen durchsuchen"
}

185
API/I18N/en.json Normal file
View file

@ -0,0 +1,185 @@
{
"confirm-email": "You must confirm your email first",
"bad-credentials": "Your credentials are not correct",
"locked-out": "You've been locked out from too many authorization attempts. Please wait 10 minutes.",
"disabled-account": "Your account is disabled. Contact the server admin.",
"register-user": "Something went wrong when registering user",
"validate-email": "There was an issue validating your email: {0}",
"confirm-token-gen": "There was an issue generating a confirmation token",
"denied": "Not allowed",
"permission-denied": "You are not permitted to this operation",
"password-required": "You must enter your existing password to change your account unless you're an admin",
"invalid-password": "Invalid Password",
"invalid-token": "Invalid token",
"unable-to-reset-key": "Something went wrong, unable to reset key",
"invalid-payload": "Invalid payload",
"nothing-to-do": "Nothing to do",
"share-multiple-emails": "You cannot share emails across multiple accounts",
"generate-token": "There was an issue generating a confirmation email token. See logs",
"age-restriction-update": "There was an error updating the age restriction",
"no-user": "User does not exist",
"username-taken": "Username already taken",
"user-already-confirmed": "User is already confirmed",
"generic-user-update": "There was an exception when updating the user",
"manual-setup-fail": "Manual setup is unable to be completed. Please cancel and recreate the invite",
"user-already-registered": "User is already registered as {0}",
"user-already-invited": "User is already invited under this email and has yet to accepted invite.",
"generic-invite-user": "There was an issue inviting the user. Please check logs.",
"invalid-email-confirmation": "Invalid email confirmation",
"generic-user-email-update": "Unable to update email for user. Check logs.",
"generic-password-update": "There was an unexpected error when confirming new password",
"password-updated": "Password Updated",
"forgot-password-generic": "An email will be sent to the email if it exists in our database",
"not-accessible-password": "Your server is not accessible. The link to reset your password is in the logs",
"not-accessible": "Your server is not accessible externally",
"email-sent": "Email sent",
"user-migration-needed": "This user needs to migrate. Have them log out and login to trigger a migration flow",
"generic-invite-email": "There was an issue resending invite email",
"admin-already-exists": "Admin already exists",
"invalid-username": "Invalid username",
"critical-email-migration": "There was an issue during email migration. Contact support",
"chapter-doesnt-exist": "Chapter does not exist",
"file-missing": "File was not found in book",
"collection-updated": "Collection updated successfully",
"generic-error": "Something went wrong, please try again",
"collection-doesnt-exist": "Collection does not exist",
"device-doesnt-exist": "Device does not exist",
"generic-device-create": "There was an error when creating the device",
"generic-device-update": "There was an error when updating the device",
"generic-device-delete": "There was an error when deleting the device",
"greater-0": "{0} must be greater than 0",
"send-to-kavita-email": "Send to device cannot be used with Kavita's email service. Please configure your own.",
"send-to-device-status": "Transferring files to your device",
"generic-send-to": "There was an error sending the file(s) to the device",
"series-doesnt-exist": "Series does not exist",
"volume-doesnt-exist": "Volume does not exist",
"bookmarks-empty": "Bookmarks cannot be empty",
"no-cover-image": "No cover image",
"bookmark-doesnt-exist": "Bookmark does not exist",
"must-be-defined": "{0} must be defined",
"generic-favicon": "There was an issue fetching favicon for domain",
"invalid-filename": "Invalid Filename",
"file-doesnt-exist": "File does not exist",
"library-name-exists": "Library name already exists. Please choose a unique name to the server.",
"generic-library": "There was a critical issue. Please try again.",
"no-library-access": "User does not have access to this library",
"user-doesnt-exist": "User does not exist",
"library-doesnt-exist": "Library does not exist",
"invalid-path": "Invalid Path",
"delete-library-while-scan": "You cannot delete a library while a scan is in progress. Please wait for scan to complete or restart Kavita then try to delete",
"generic-library-update": "There was a critical issue updating the library.",
"pdf-doesnt-exist": "PDF does not exist when it should",
"invalid-access": "Invalid Access",
"no-image-for-page": "No such image for page {0}. Try refreshing to allow re-cache.",
"perform-scan": "Please perform a scan on this series or library and try again",
"generic-read-progress": "There was an issue saving progress",
"generic-clear-bookmarks": "Could not clear bookmarks",
"bookmark-permission": "You do not have permission to bookmark/unbookmark",
"bookmark-save": "Could not save bookmark",
"cache-file-find": "Could not find cached image. Reload and try again.",
"name-required": "Name cannot be empty",
"valid-number": "Must be valid page number",
"duplicate-bookmark": "Duplicate bookmark entry already exists",
"reading-list-permission": "You do not have permissions on this reading list or the list doesn't exist",
"reading-list-position": "Couldn't update position",
"reading-list-updated": "Updated",
"reading-list-item-delete": "Couldn't delete item(s)",
"reading-list-deleted": "Reading List was deleted",
"generic-reading-list-delete": "There was an issue deleting the reading list",
"generic-reading-list-update": "There was an issue updating the reading list",
"generic-reading-list-create": "There was an issue creating the reading list",
"reading-list-doesnt-exist": "Reading list does not exist",
"series-restricted": "User does not have access to this Series",
"generic-scrobble-hold": "An error occurred while adding the hold",
"libraries-restricted": "User does not have access to any libraries",
"no-series": "Could not get series for Library",
"no-series-collection": "Could not get series for Collection",
"generic-series-delete": "There was an issue deleting the series",
"generic-series-update": "There was an error with updating the series",
"series-updated": "Successfully updated",
"update-metadata-fail": "Could not update metadata",
"age-restriction-not-applicable": "No Restriction",
"generic-relationship": "There was an issue updating relationships",
"job-already-running": "Job already running",
"encode-as-warning": "You cannot convert to PNG. For covers, use Refresh Covers. Bookmarks and favicons cannot be encoded back.",
"ip-address-invalid": "IP Address '{0}' is invalid",
"bookmark-dir-permissions": "Bookmark Directory does not have correct permissions for Kavita to use",
"total-backups": "Total Backups must be between 1 and 30",
"total-logs": "Total Logs must be between 1 and 30",
"stats-permission-denied": "You are not authorized to view another user's statistics",
"url-not-valid": "Url does not return a valid image or requires authorization",
"url-required": "You must pass a url to use",
"generic-cover-series-save": "Unable to save cover image to Series",
"generic-cover-collection-save": "Unable to save cover image to Collection",
"generic-cover-reading-list-save": "Unable to save cover image to Reading List",
"generic-cover-chapter-save": "Unable to save cover image to Chapter",
"generic-cover-library-save": "Unable to save cover image to Library",
"access-denied": "You do not have access",
"reset-chapter-lock": "Unable to resetting cover lock for Chapter",
"generic-user-delete": "Could not delete the user",
"generic-user-pref": "There was an issue saving preferences",
"opds-disabled": "OPDS is not enabled on this server",
"on-deck": "On Deck",
"browse-on-deck": "Browse On Deck",
"recently-added": "Recently Added",
"browse-recently-added": "Browse Recently Added",
"reading-lists": "Reading Lists",
"browse-reading-lists": "Browse by Reading Lists",
"libraries": "All Libraries",
"browse-libraries": "Browse by Libraries",
"collections": "All Collections",
"browse-collections": "Browse by Collections",
"reading-list-restricted": "Reading list does not exist or you don't have access",
"query-required": "You must pass a query parameter",
"search": "Search",
"search-description": "Search for Series, Collections, or Reading Lists",
"favicon-doesnt-exist": "Favicon does not exist",
"not-authenticated": "User is not authenticated",
"unable-to-register-k+": "Unable to register license due to error. Reach out to Kavita+ Support",
"anilist-cred-expired": "AniList Credentials have expired or not set",
"scrobble-bad-payload": "Bad payload from Scrobble Provider",
"theme-doesnt-exist": "Theme file missing or invalid",
"bad-copy-files-for-download": "Unable to copy files to temp directory archive download.",
"generic-create-temp-archive": "There was an issue creating temp archive",
"epub-malformed": "The file is malformed! Cannot read.",
"epub-html-missing": "Could not find the appropriate html for that page",
"collection-tag-title-required": "Collection Title cannot be empty",
"reading-list-title-required": "Reading List Title cannot be empty",
"collection-tag-duplicate": "A collection with this name already exists",
"device-duplicate": "A device with this name already exists",
"device-not-created": "This device doesn't exist yet. Please create first",
"send-to-permission": "Cannot Send non-EPUB or PDF to devices as not supported on Kindle",
"progress-must-exist": "Progress must exist on user",
"reading-list-name-exists": "A reading list of this name already exists",
"user-no-access-library-from-series": "User does not have access to the library this series belongs to",
"series-restricted-age-restriction": "User is not allowed to view this series due to age restrictions",
"volume-num": "Volume {0}",
"book-num": "Book {0}",
"issue-num": "Issue {0}{1}",
"chapter-num": "Chapter {0}"
}

8
API/I18N/es.json Normal file
View file

@ -0,0 +1,8 @@
{
"bad-credentials": "Las credenciales son incorrectas",
"confirm-email": "Debes confirmar el correo electrónico primero",
"disabled-account": "La cuenta está deshabilitada. Contacta con un administrador.",
"validate-email": "Ha habido un error al validar el correo: {0}",
"locked-out": "Se ha bloqueado el acceso debido a demasiados intentos. Por favor espera 10 minutos.",
"register-user": "Ha ocurrido un error registrando el usuario"
}

27
API/I18N/fr.json Normal file
View file

@ -0,0 +1,27 @@
{
"register-user": "Une erreur est survenue lors de l'enregistrement de l'usager",
"denied": "Interdit",
"permission-denied": "Vous n'avez pas les permissions requises pour effectuer cette opération",
"disabled-account": "Votre compte a été désactivé. Veuillez contacter un administrateur.",
"confirm-email": "Vous devez d'abord confirmer votre adresse courriel",
"locked-out": "Vous avez été bloqués suite à un nombre trop élevé de tentatives. Veuillez réessayer dans 10 minutes.",
"bad-credentials": "Vos codes d'accès sont invalides",
"validate-email": "Une erreur est survenue lors de la validation de votre courriel: {0}",
"confirm-token-gen": "Une erreur est survenue lors de la génération du code de confirmation",
"password-required": "Vous devez entrer votre mot de passe existant afin de le changer si vous n'êtes pas un administrateur",
"invalid-password": "Mot de passe invalide",
"invalid-token": "Code invalide",
"unable-to-reset-key": "Une erreur est survenue, impossible de générer la clé",
"generate-token": "Une erreur est survenue lors de la génération du code de confirmation du courriel. Voir le journal",
"nothing-to-do": "Rien à faire",
"share-multiple-emails": "Vous ne pouvez partager une adresse courriel avec un autre compte",
"age-restriction-update": "Une erreur est survenue lors de la mise-à-jour de la restriction d'âge",
"no-user": "L'usager n'existe pas",
"username-taken": "Le nom d'usager existe déjà",
"user-already-confirmed": "L'usager à déjà été confirmé",
"generic-user-update": "Une erreur est survenue lors de la confirmation de l'usager",
"user-already-registered": "L'usager à déjà été enregistré en tant que {0}",
"user-already-invited": "L'usager à déjà été invité avec ce courriel et n'a pas encore accepté l'invitation.",
"generic-invite-user": "Une erreur est survenue lors de l'invitation de l'usager. Voir le journal.",
"invalid-email-confirmation": "La confirmation de courriel est invalide"
}

160
API/I18N/hi.json Normal file
View file

@ -0,0 +1,160 @@
{
"generic-device-create": "डिवाइस बनाते समय एक त्रुटि हुई",
"validate-email": "हम आपके ईमेल की पुष्टि नहीं कर पा रहे हैं: {0}",
"confirm-token-gen": "पुष्टिकरण टोकन उत्पन्न करने में एक समस्याआ रही है",
"denied": "अनुमति नहीं है",
"permission-denied": "आपको इस ऑपरेशन की अनुमति नहीं है",
"password-required": "अपना खाता बदलने के लिए आपको अपना मौजूदा पासवर्ड दर्ज करना होगा,जब तक आप व्यवस्थापक न हों",
"invalid-password": "अवैध पासवर्ड",
"invalid-payload": "अमान्य पेलोड",
"age-restriction-update": "आयु प्रतिबंध को अद्यतन करने में त्रुटि आ रही है",
"generic-user-update": "उपयोगकर्ता को अपडेट करते समय एक एक्सेप्शन आ रहा है",
"user-already-invited": "उपयोगकर्ता को इस ईमेल के अंतर्गत पहले ही आमंत्रित किया जा चुका है और उसने अभी तक आमंत्रण स्वीकार नहीं किया है।",
"invalid-email-confirmation": "अवैध ईमेल पुष्टिकरण",
"password-updated": "पासवर्ड अपडेट किया गया",
"not-accessible": "आपका सर्वर बाह्य रूप से पहुंच योग्य नहीं है",
"email-sent": "ईमेल भेजा",
"generic-invite-email": "आमंत्रण ईमेल पुनः भेजने में एक समस्या है",
"file-missing": "पुस्तक में फ़ाइल नहीं मिली",
"generic-error": "कुछ गलत हो गया, फिर से कोशिश करें",
"device-doesnt-exist": "डिवाइस मौजूद नहीं है",
"generic-device-delete": "डिवाइस को हटाते समय एक त्रुटि हुई",
"send-to-device-status": "अपने डिवाइस पर फ़ाइलों को स्थानांतरित करना",
"volume-doesnt-exist": "वोलूम(Volume) मौजूद नहीं है",
"bookmark-doesnt-exist": "बुकमार्क मौजूद नहीं है",
"invalid-filename": "अमान्य फ़ाइल नाम",
"library-name-exists": "पुस्तकालय का नाम पहले से ही मौजूद है। कृपया सर्वर पर एक अद्वितीय नाम चुनें।।",
"no-library-access": "उपयोगकर्ता के पास इस पुस्तकालय तक पहुंच नहीं है",
"user-doesnt-exist": "उपयोगकर्ता मौजूद नहीं है",
"generic-library-update": "लाइब्रेरी(Library) को अपडेट करने में एक गंभीर समस्या है।",
"pdf-doesnt-exist": "जब यह होना चाहिए तो पीडीएफ मौजूद नहीं है",
"invalid-access": "अवैध पहुँच",
"perform-scan": "कृपया इस श्रृंखला(Series) या पुस्तकालय(Library) पर एक स्कैन करें और फिर से कोशिश करें",
"generic-clear-bookmarks": "बुकमार्क साफ़ नहीं किए जा सके",
"bookmark-permission": "आपको बुकमार्क/अनबुकमार्क करने की अनुमति नहीं है",
"cache-file-find": "कैश्ड छवि(Image) नहीं मिल सका। पुनः लोड करें और फिर से प्रयास करें।।",
"reading-list-permission": "इस सूची में आपको अनुमति नहीं है या सूची मौजूद नहीं है",
"reading-list-position": "स्थिति अपडेट नहीं की जा सका",
"reading-list-updated": "अपडेटेड",
"reading-list-deleted": "पठन सूची(Reading List) को हटा दिया गया",
"generic-reading-list-update": "पठन सूची(Reading List) को अपडेट करने में एक समस्या है",
"reading-list-doesnt-exist": "पठन सूची(Reading List) मौजूद नहीं है",
"no-series": "पुस्तकालय के लिए श्रृंखला(Series) नहीं मिल सका",
"no-series-collection": "संग्रह(Collection) के लिए श्रृंखला(Series) नहीं मिल सका",
"generic-scrobble-hold": "होल्ड जोड़ते समय एक त्रुटि उत्पन्न हुई",
"generic-series-update": "श्रृंखला(Series) को अपडेट करने में त्रुटि हुई",
"update-metadata-fail": "मेटाडाटा अद्यतन नहीं कर सका",
"total-backups": "कुल बैकअप 1 और 30 के बीच होना चाहिए",
"total-logs": "कुल लॉग 1 और 30 के बीच होना चाहिए",
"url-not-valid": "Url एक वैध छवि वापस नहीं करता है या प्राधिकरण की आवश्यकता है",
"url-required": "आपको उपयोग करने के लिए एक URL पास करना होगा",
"generic-cover-chapter-save": "कवर छवि को अध्याय में बचाने में असमर्थ",
"generic-cover-library-save": "पुस्तकालय(Library) में कवर छवि को बचाने में असमर्थ",
"access-denied": "आपके पास पहुंच नहीं है",
"browse-recently-added": "हाल ही में जोड़ा गया ब्राउज़ करें",
"browse-libraries": "पुस्तकालयों द्वारा ब्राउज़ करें",
"query-required": "आपको एक क्वेरी पैरामीटर पास करना होगा",
"search": "खोज",
"scrobble-bad-payload": "Scrobble प्रदाता से बुरा पेलोड",
"epub-malformed": "फ़ाइल विकृत है! पढ़ा नहीं जा सकता हैं।।",
"epub-html-missing": "उस पृष्ठ(Page) के लिए उपयुक्त HTML नहीं मिल सका",
"reading-list-title-required": "पठन सूची शीर्षक खाली नहीं हो सकता",
"device-duplicate": "इस नाम के साथ पहले से मौजूद एक डिवाइस",
"progress-must-exist": "उपयोगकर्ता पर प्रगति होना चाहिए",
"confirm-email": "आपको पहले अपने ईमेल की पुष्टि करनी होगी",
"reading-list-name-exists": "इस नाम की पठन सूची पहले से मौजूद है",
"series-restricted-age-restriction": "उपयोगकर्ता को आयु प्रतिबंध के कारण इस श्रृंखला को देखने की अनुमति नहीं है",
"volume-num": "वॉल्यूम {0}",
"book-num": "बुक {0}",
"issue-num": "अंक(Issue) {0}{1}",
"bad-credentials": "आपकी क्रेडेंशियल सही नहीं हैं",
"locked-out": "आपको कई प्राधिकरण प्रयासों के कारण बंद कर दिया गया है। कृपया 10 मिनट प्रतीक्षा करें।।",
"register-user": "उपयोगकर्ता पंजीकरण करते समय कुछ गलत हो गया",
"disabled-account": "आपका खाता अक्षम है। सर्वर व्यवस्थापक से संपर्क करें।।",
"invalid-token": "अमान्य टोकन",
"unable-to-reset-key": "कुछ गलत हो गया, कुंजी को रीसेट करने में असमर्थ",
"nothing-to-do": "कुछ नहीं करना",
"share-multiple-emails": "आप एकाधिक खातों में ईमेल साझा नहीं कर सकते",
"generate-token": "पुष्टिकरण ईमेल टोकन उत्पन्न करने में एक समस्याआ रही है। लॉग देखें",
"no-user": "उपयोगकर्ता मौजूद नहीं है",
"username-taken": "उपयोगकर्ता नाम पहले से ही लिया गया है",
"user-already-confirmed": "उपयोगकर्ता पहले से ही पुष्टि की है",
"manual-setup-fail": "मैनुअल सेटअप पूरा करने में असमर्थ है। कृपया निमंत्रण रद्द करें और फिर से बनाएँ",
"user-already-registered": "उपयोगकर्ता पहले से ही {0} के रूप में पंजीकृत है",
"generic-invite-user": "एक्सेप्शन आ रहा है उपयोगकर्ता को आमंत्रित करने का एक समस्या आ रहा है। कृपया लॉग की जाँच करें।।",
"generic-user-email-update": "उपयोगकर्ता के लिए ईमेल अद्यतन करने में असमर्थ। लॉग की जाँच करें।।",
"generic-password-update": "नए पासवर्ड की पुष्टि करते समय एक अप्रत्याशित त्रुटि आ रहा है",
"forgot-password-generic": "यदि यह हमारे डेटाबेस में मौजूद है तो ईमेल को भेजा जाएगा",
"user-migration-needed": "इस उपयोगकर्ता को माइग्रेट करने की जरूरत है। उन्हें लॉग आउट करें और माइग्रेशन प्रवाह को ट्रिगर करने के लिए लॉगिन करें",
"chapter-doesnt-exist": "अध्याय मौजूद नहीं है",
"series-doesnt-exist": "सीरीज(Series) मौजूद नहीं है",
"generic-device-update": "डिवाइस को अपडेट करते समय एक त्रुटि हुई",
"not-accessible-password": "आपका सर्वर पहुंच योग्य नहीं है. आपका पासवर्ड रीसेट करने का लिंक लॉग में है",
"admin-already-exists": "व्यवस्थापक पहले से ही मौजूद है",
"invalid-username": "अमान्य उपयोगकर्ता नाम",
"critical-email-migration": "ईमेल माइग्रेशन के दौरान एक समस्या थी. समर्थन से संपर्क करें",
"greater-0": "{0} 0 से अधिक होना चाहिए",
"send-to-kavita-email": "सेंड टु डिवाइस कविता की ईमेल सेवा के साथ इस्तेमाल नहीं किया जा सकता है। कृपया अपना खुद का ईमेल विन्यास करें।",
"generic-send-to": "डिवाइस पर फ़ाइल भेजने में त्रुटि हुई",
"collection-updated": "कलेक्शन सफलतापूर्वक अपडेट किया गया",
"collection-doesnt-exist": "कलेक्शन मौजूद नहीं है",
"bookmarks-empty": "बुकमार्क खाली नहीं हो सकता",
"no-cover-image": "कोई कवर नहीं",
"must-be-defined": "{0} को परिभाषित किया जाना चाहिए",
"generic-favicon": "डोमेन के लिए फ़ेविकॉन लाने में एक समस्या है",
"file-doesnt-exist": "फ़ाइल मौजूद नहीं है",
"generic-library": "एक महत्वपूर्ण समस्या है। फिर से प्रयास करें।।",
"library-doesnt-exist": "पुस्तकालय(Library) मौजूद नहीं है",
"invalid-path": "अवैध पथ",
"no-image-for-page": "पृष्ठ {0} के लिए ऐसी कोई छवि नहीं। फिर से कैश की अनुमति देने के लिए ताज़ा प्रयास करें।।",
"delete-library-while-scan": "आप पुस्तकालय को नष्ट नहीं कर सकते जबकि स्कैन प्रगति पर है।",
"duplicate-bookmark": "डुप्लिकेट बुकमार्क प्रविष्टि पहले से मौजूद है",
"reading-list-item-delete": "आइटम को नष्ट नहीं कर सकता",
"generic-read-progress": "प्रगति को सहेजने में एक समस्या है",
"bookmark-save": "बुकमार्क नहीं बचा सकता",
"valid-number": "मान्य पृष्ठ(Page) संख्या होना चाहिए",
"libraries-restricted": "उपयोगकर्ता को किसी भी पुस्तकालय(Library) तक पहुंच अधिकार नहीं है",
"name-required": "नाम खाली नहीं हो सकता है",
"generic-reading-list-delete": "पठन सूची(Reading List) को हटाने में एक समस्या है",
"generic-series-delete": "श्रृंखला(Series) को हटाने का एक समस्या है",
"generic-reading-list-create": "पठन सूची(Reading List) को बनाने में एक समस्या है",
"series-restricted": "उपयोगकर्ता के पास इस श्रृंखला(Series) तक पहुंच नहीं है",
"series-updated": "सफलतापूर्वक अपडेटेड",
"job-already-running": "पहले से ही चल रहा है",
"ip-address-invalid": "आईपी एड्रेस '{0}' अमान्य है",
"age-restriction-not-applicable": "कोई प्रतिबंध नहीं",
"generic-relationship": "रिश्तों को अपडेट करने में एक समस्या हुई",
"generic-cover-series-save": "श्रृंखला(Series) के लिए कवर छवि(Cover Image) को बचाने में असमर्थ",
"encode-as-warning": "आप पीएनजी में परिवर्तित नहीं कर सकते। कवर के लिए, रिफ्रेश कवर का उपयोग करें। बुकमार्क और favicons को वापस कोडित नहीं किया जा सकता है।।",
"chapter-num": "अध्याय {0}",
"bookmark-dir-permissions": "Bookmark डायरेक्टरी के पास Kavita के लिए सही अनुमति नहीं है",
"stats-permission-denied": "आप किसी अन्य उपयोगकर्ता के आंकड़े देखने के लिए अधिकृत नहीं हैं",
"generic-cover-collection-save": "संग्रह(Collection) के लिए कवर छवि को बचाने में असमर्थ",
"browse-reading-lists": "पठन सूचियों द्वारा ब्राउज़ करें",
"generic-cover-reading-list-save": "पठन सूचि(Reading List) में कवर छवि(Cover Image) को बचाने में असमर्थ",
"on-deck": "डेक पर",
"reset-chapter-lock": "अध्याय के लिए कवर लॉक को रीसेट करने में असमर्थ",
"opds-disabled": "इस सर्वर पर OPDS सक्षम नहीं है",
"reading-lists": "पठन सूची",
"collections": "सभी संग्रह",
"browse-collections": "संग्रह द्वारा ब्राउज़ करें",
"theme-doesnt-exist": "थीम फ़ाइल लापता या अमान्य",
"bad-copy-files-for-download": "फ़ाइलों को अस्थायी निर्देशिका संग्रह डाउनलोड करने में असमर्थ।।",
"generic-user-delete": "उपयोगकर्ता को नष्ट नहीं कर सकता",
"generic-user-pref": "प्राथमिकताएँ सहेजने में एक समस्या है",
"browse-on-deck": "डेक पर ब्राउज़ करें",
"recently-added": "हाल ही में जोड़ा गया",
"reading-list-restricted": "पठन सूची मौजूद नहीं है या आपके पास एक्सेस नहीं है",
"search-description": "श्रृंखला, संग्रह, या पठन सूची के लिए खोज",
"favicon-doesnt-exist": "Favicon मौजूद नहीं है",
"anilist-cred-expired": "AniList Credentials समाप्त हो गया है या निर्धारित नहीं है",
"collection-tag-title-required": "संग्रह(Collection) शीर्षक खाली नहीं हो सकता",
"libraries": "सभी पुस्तकालय",
"not-authenticated": "उपयोगकर्ता प्रमाणित नहीं है",
"unable-to-register-k+": "त्रुटि के कारण लाइसेंस पंजीकृत करने में असमर्थ। Kavita+ समर्थन तक पहुंचें",
"generic-create-temp-archive": "वहाँ एक समस्या अस्थायी संग्रह बनाने में",
"collection-tag-duplicate": "इस नाम के साथ संग्रह पहले से मौजूद है",
"send-to-permission": "किंडल पर समर्थित नहीं होने के रूप में उपकरणों के लिए गैर-EPUB या PDF नहीं भेजा जा सकता",
"device-not-created": "यह डिवाइस अभी तक मौजूद नहीं है। कृपया पहले बनाएं",
"user-no-access-library-from-series": "उपयोगकर्ता के पास पुस्तकालय तक पहुंच नहीं है इस श्रृंखला के अंतर्गत आता है"
}

160
API/I18N/it.json Normal file
View file

@ -0,0 +1,160 @@
{
"locked-out": "Sei stato bloccato per troppi tentativi di autenticazione. Si prega di attendere 10 minuti.",
"disabled-account": "Il tuo account è disabilitato. Contatta l'amministratore del server.",
"validate-email": "Si è verificato un problema durante la convalida della tua email: {0}",
"denied": "Non abilitato",
"permission-denied": "Non sei autorizzato a questa operazione",
"password-required": "Devi inserire la tua password esistente per modificare il tuo account, a meno che tu non sia un amministratore",
"invalid-password": "Password non valida",
"invalid-token": "Token non valido",
"unable-to-reset-key": "Qualcosa è andato storto, impossibile reimpostare la chiave",
"invalid-payload": "Payload non valido",
"nothing-to-do": "Nulla da fare",
"share-multiple-emails": "Non puoi condividere email su più account",
"generate-token": "Si è verificato un problema durante la generazione di un token di email di conferma. Vedi i log",
"age-restriction-update": "Si è verificato un errore durante l'aggiornamento del limite di età",
"no-user": "L'utente non esiste",
"username-taken": "Utente già preso",
"user-already-confirmed": "Utente già confermato",
"generic-user-update": "Si è verificata un'eccezione durante l'aggiornamento dell'utente",
"manual-setup-fail": "Impossibile completare la configurazione manuale. Annulla e ricrea l'invito",
"user-already-registered": "L'utente è già registrato come {0}",
"user-already-invited": "L'utente è già stato invitato con questa email e deve ancora accettare l'invito.",
"generic-invite-user": "Si è verificato un problema durante l'invito dell'utente. Si prega di controllare i log.",
"invalid-email-confirmation": "Email di conferma non valida",
"password-updated": "Password aggiornata",
"forgot-password-generic": "Verrà inviata un'e-mail all'e-mail se esiste nel nostro database",
"not-accessible-password": "Il tuo server non è accessibile. Il link per reimpostare la password è nei log",
"not-accessible": "Il tuo server non è accessibile dall'esterno",
"email-sent": "Email inviata",
"generic-invite-email": "Si è verificato un problema durante il reinvio dell'e-mail di invito",
"invalid-username": "nome utente non valido",
"critical-email-migration": "Si è verificato un problema durante la migrazione della posta elettronica. Contatta il supporto",
"name-required": "Il nome non può essere vuoto",
"valid-number": "Deve essere un numero di pagina valido",
"reading-list-permission": "Non disponi delle autorizzazioni per questo elenco di lettura o l'elenco non esiste",
"reading-list-position": "Impossibile aggiornare la posizione",
"reading-list-updated": "Aggiornato",
"reading-list-item-delete": "Impossibile eliminare gli elementi",
"reading-list-deleted": "L'elenco di lettura è stato eliminato",
"generic-reading-list-delete": "Si è verificato un problema durante l'eliminazione dell'elenco di lettura",
"generic-reading-list-update": "Si è verificato un problema durante l'aggiornamento dell'elenco di lettura",
"reading-list-doesnt-exist": "L'elenco di lettura non esiste",
"series-restricted": "L'utente non ha accesso a questa serie",
"libraries-restricted": "L'utente non ha accesso ad alcuna libreria",
"no-series": "Impossibile ottenere serie per Library",
"no-series-collection": "Impossibile ottenere la serie per la raccolta",
"generic-series-delete": "Si è verificato un problema durante l'eliminazione della serie",
"generic-series-update": "Si è verificato un errore durante l'aggiornamento della serie",
"series-updated": "Aggiornato con successo",
"update-metadata-fail": "Impossibile aggiornare i metadati",
"age-restriction-not-applicable": "Nessuna Restrizione",
"job-already-running": "Lavoro già in corso",
"ip-address-invalid": "Indirizzo IP '{0}' non valido",
"bookmark-dir-permissions": "La directory dei segnalibri non dispone delle autorizzazioni corrette per l'utilizzo da parte di Kavita",
"confirm-email": "Prima devi confermare la tua email",
"bad-credentials": "Le tue credenziali non sono corrette",
"register-user": "Qualcosa è andato storto nella registrazione dell'utente",
"confirm-token-gen": "Si è verificato un problema durante la generazione di un token di conferma",
"generic-user-email-update": "Impossibile aggiornare l'e-mail per l'utente. Controlla i log.",
"generic-password-update": "Si è verificato un errore imprevisto durante la conferma della nuova password",
"admin-already-exists": "Admin già esistente",
"user-migration-needed": "Questo utente deve eseguire la migrazione. Chiedi loro di disconnettersi e accedere per attivare un flusso di migrazione",
"chapter-doesnt-exist": "Il capitolo non esiste",
"duplicate-bookmark": "Esiste già una voce di segnalibro duplicata",
"generic-reading-list-create": "Si è verificato un problema durante la creazione dell'elenco di lettura",
"generic-scrobble-hold": "Si è verificato un errore durante l'aggiunta del blocco",
"generic-relationship": "Si è verificato un problema durante l'aggiornamento delle relazioni",
"encode-as-warning": "Non puoi convertire in PNG. Per le copertine, usa Aggiorna copertine. Segnalibri e favicon non possono essere codificati nuovamente.",
"file-missing": "Il file non è stato trovato nel libro",
"generic-error": "Qualcosa è andato storto, prova ancora",
"collection-doesnt-exist": "La Collezione non esiste",
"send-to-device-status": "Trasferimento di file sul tuo dispositivo",
"series-doesnt-exist": "La Serie non esiste",
"volume-doesnt-exist": "Il Volume non esiste",
"bookmarks-empty": "Il segnalibro non può essere vuoto",
"bookmark-doesnt-exist": "Il segnalibro non esiste",
"must-be-defined": "{0} deve essere definito",
"library-doesnt-exist": "La Libreria non esiste",
"search": "Cerca",
"favicon-doesnt-exist": "Favicon non esiste",
"not-authenticated": "L'utente non è autenticato",
"scrobble-bad-payload": "Payload errato dal provider Scrobble",
"theme-doesnt-exist": "File del tema mancante o non valido",
"bad-copy-files-for-download": "Impossibile copiare i file nel download dell'archivio della directory temporanea.",
"generic-create-temp-archive": "Si è verificato un problema durante la creazione dell'archivio temporaneo",
"epub-html-missing": "Impossibile trovare l'html appropriato per quella pagina",
"collection-tag-title-required": "Il titolo della raccolta non può essere vuoto",
"collection-tag-duplicate": "Esiste già una raccolta con questo nome",
"device-duplicate": "Esiste già un dispositivo con questo nome",
"progress-must-exist": "L'avanzamento deve esistere sull'utente",
"user-no-access-library-from-series": "L'utente non ha accesso alla libreria a cui appartiene questa serie",
"volume-num": "Volume {0}",
"book-num": "Libro {0}",
"issue-num": "Problema {0}{1}",
"chapter-num": "Capitolo {0}",
"epub-malformed": "Il file è corrotto! Non posso leggere.",
"collection-updated": "Collezione aggiornata con successo",
"anilist-cred-expired": "Le credenziali AniList sono scadute o non impostate",
"search-description": "Cerca serie, raccolte o elenchi di lettura",
"query-required": "Devi passare un parametro di ricerca",
"unable-to-register-k+": "Impossibile registrare la licenza a causa di un errore. Contatta l'assistenza Kavita+",
"reading-list-title-required": "Il titolo dell'elenco di lettura non può essere vuoto",
"device-not-created": "Questo dispositivo non esiste ancora. Si prega di creare prima",
"send-to-permission": "Impossibile inviare file non EPUB o PDF a dispositivi in quanto non supportati su Kindle",
"series-restricted-age-restriction": "L'utente non è autorizzato a visualizzare questa serie a causa di limiti di età",
"no-cover-image": "Nessuna immagine di copertina",
"file-doesnt-exist": "Il file non esiste",
"invalid-filename": "Nome file non valido",
"invalid-path": "Percorso non valido",
"user-doesnt-exist": "L'utente non esiste",
"reading-list-name-exists": "Esiste già un elenco di letture con questo nome",
"delete-library-while-scan": "Non è possibile eliminare una libreria mentre è in corso una scansione. Attendi il completamento della scansione o riavvia Kavita, quindi prova a eliminare",
"no-image-for-page": "Nessuna immagine simile per la pagina {0}. Prova ad aggiornare per consentire il re-cache.",
"browse-recently-added": "Sfoglia Aggiunti di recente",
"generic-cover-series-save": "Impossibile salvare l'immagine di copertina nella serie",
"reset-chapter-lock": "Impossibile reimpostare il blocco del coperchio per il capitolo",
"generic-cover-chapter-save": "Impossibile salvare l'immagine di copertina nel capitolo",
"recently-added": "Aggiunto recentemente",
"device-doesnt-exist": "Il dispositivo non esiste",
"generic-device-create": "Si è verificato un errore durante la creazione del dispositivo",
"generic-device-update": "Si è verificato un errore durante l'aggiornamento del dispositivo",
"generic-device-delete": "Si è verificato un errore durante l'eliminazione del dispositivo",
"greater-0": "{0} deve essere maggiore di 0",
"send-to-kavita-email": "Invia al dispositivo non può essere utilizzato con il servizio e-mail di Kavita. Si prega di configurare il proprio.",
"generic-send-to": "Si è verificato un errore durante l'invio dei file al dispositivo",
"generic-favicon": "Si è verificato un problema durante il recupero della favicon per il dominio",
"library-name-exists": "Il nome della libreria esiste già. Scegli un nome univoco per il server.",
"generic-library": "Si è verificato un problema critico. Per favore riprova.",
"no-library-access": "L'utente non ha accesso a questa libreria",
"generic-library-update": "Si è verificato un problema critico durante l'aggiornamento della libreria.",
"pdf-doesnt-exist": "PDF non esiste quando dovrebbe",
"invalid-access": "Accesso non valido",
"perform-scan": "Esegui una scansione su questa serie o libreria e riprova",
"generic-read-progress": "Si è verificato un problema durante il salvataggio dei progressi",
"generic-clear-bookmarks": "Impossibile cancellare i segnalibri",
"bookmark-permission": "Non sei autorizzato ad aggiungere/rimuovere i segnalibri",
"bookmark-save": "Impossibile salvare il segnalibro",
"cache-file-find": "Impossibile trovare l'immagine memorizzata nella cache. Ricarica e riprova.",
"total-backups": "I backup totali devono essere compresi tra 1 e 30",
"total-logs": "I log totali devono essere compresi tra 1 e 30",
"stats-permission-denied": "Non sei autorizzato a visualizzare le statistiche di un altro utente",
"url-not-valid": "L'URL non restituisce un'immagine valida o richiede l'autorizzazione",
"url-required": "Devi passare un URL da usare",
"generic-cover-collection-save": "Impossibile salvare l'immagine di copertina nella raccolta",
"generic-cover-reading-list-save": "Impossibile salvare l'immagine di copertina in Elenco di lettura",
"generic-cover-library-save": "Impossibile salvare l'immagine di copertina nella Libreria",
"access-denied": "Non hai accesso",
"generic-user-delete": "Impossibile eliminare l'utente",
"generic-user-pref": "Si è verificato un problema durante il salvataggio delle preferenze",
"opds-disabled": "OPDS non è abilitato su questo server",
"on-deck": "Sul Ponte",
"browse-on-deck": "Sfoglia Sul ponte",
"reading-lists": "Liste di lettura",
"browse-reading-lists": "Sfoglia Liste di lettura",
"libraries": "Tutte le Librerie",
"browse-libraries": "Sfoglia Librerie",
"collections": "Tutte le Collezioni",
"browse-collections": "Sfoglia per Collezioni",
"reading-list-restricted": "L'elenco di lettura non esiste o non hai accesso"
}

4
API/I18N/ja.json Normal file
View file

@ -0,0 +1,4 @@
{
"chapter-num": "章 {0}",
"invalid-token": "無効トークン"
}

1
API/I18N/kn.json Normal file
View file

@ -0,0 +1 @@
{}

36
API/I18N/ms.json Normal file
View file

@ -0,0 +1,36 @@
{
"forgot-password-generic": "Jika e-mel ini wujud dalam pangkalan data kami, E-mel ini akan di kirimkan",
"not-accessible-password": "Server anda tidak boleh di akses. Pautan untuk menetapkan semula kata laluan anda ada dalam log",
"not-accessible": "Server anda tidak boleh di akses secara luaran",
"email-sent": "E-mel sudah di kirim",
"generic-password-update": "Terdapat terkesilapan semasa mengesahkan kata laluan baru",
"password-updated": "Kata Laluan telah di kemas kinikan",
"invalid-password": "kata laluan tidak sah",
"invalid-token": "Token tidak sah",
"username-taken": "Nama pengguna sudah di miliki",
"user-already-registered": "Pengguna sudah berdaftar sebagai {0}",
"manual-setup-fail": "Persediaan manual tidak dapat di selesaikan. Sila batalkan dan cipta semula jemputan",
"user-already-invited": "Pengguna atas e-mel ini sudah di jemput dan jemputan masih belum di terima.",
"denied": "Tidak dibenarkan",
"unable-to-reset-key": "Kesilapan telah berlaku, tidak dapat menetapkan kunci semula",
"invalid-payload": "Muatan tidak sah",
"nothing-to-do": "Tiada apa yang perlu di lakukan",
"share-multiple-emails": "Anda tidak boleh berkongsi e-mel merentas berbilang akaun",
"generate-token": "Terdapat isu pengesahan menjana token e-mel. Lihat log",
"age-restriction-update": "Terdapat kesilapan semasa mengemas kini sekatan umur",
"no-user": "Pengguna tidak wujud",
"user-already-confirmed": "Pengguna sudah di sahkan",
"generic-user-update": "Terdapat pengecualian semasa mengemas kini pengguna",
"generic-invite-user": "Terdapat masalah jemputan pengguna. Sila semak log.",
"invalid-email-confirmation": "Pengesahan e-mel tidak sah",
"generic-user-email-update": "Pengguna e-mel ini tidak dapat di kemas kinikan. Semak log.",
"bad-credentials": "Bukti kelayakan anda tidak betul",
"locked-out": "Anda telah di sekat kerana terlalu banyak membuat percubaan kebenaran. Sila tunggu 10 minit.",
"disabled-account": "Akaun anda telah di sekat. Hubungi pentadbir server.",
"register-user": "Sesuatu telah berlaku kesilapan semasa pendaftaran pengguna",
"validate-email": "Terdapat isu pengesahkan e-mel anda: {0}",
"confirm-token-gen": "Terdapat isu menjana token pengesahan",
"permission-denied": "Anda tidak di benarkan melakukan operasi ini",
"password-required": "Anda mesti memasukkan kata laluan yang telah sedia ada untuk menukar akaun anda melainkan anda seorang pentadbir",
"confirm-email": "Pastikan email anda betul terdahulu"
}

158
API/I18N/nl.json Normal file
View file

@ -0,0 +1,158 @@
{
"password-updated": "Wachtwoord bijgewerkt",
"user-already-registered": "Gebruiker is al geregistreerd als {0}",
"generic-invite-user": "Er is een probleem opgetreden bij het uitnodigen van de gebruiker. Controleer de logboeken.",
"generate-token": "Er is een probleem opgetreden bij het genereren van een bevestigings- e-mailtoken. Zie logboek",
"generic-user-email-update": "E-mail voor gebruiker kan niet worden bijgewerkt. Controleer de logboeken.",
"generic-password-update": "Er is een onverwachte fout opgetreden bij het bevestigen van het nieuwe wachtwoord",
"locked-out": "U bent uitgesloten door te veel autorisatiepogingen. Wacht alsjeblieft 10 minuten.",
"register-user": "Er is iets misgegaan bij het registreren van de gebruiker",
"bad-credentials": "Uw inloggegevens zijn niet correct",
"disabled-account": "Uw account is uitgeschakeld. Neem contact op met de serverbeheerder.",
"validate-email": "Er is een probleem opgetreden bij het valideren van uw e-mailadres: {0}",
"confirm-token-gen": "Er is een probleem opgetreden bij het genereren van een bevestigingstoken",
"denied": "Niet toegestaan",
"invalid-password": "Ongeldig wachtwoord",
"invalid-token": "Ongeldige Token",
"unable-to-reset-key": "Er is iets misgegaan, kan de sleutel niet resetten",
"invalid-payload": "Ongeldige lading",
"nothing-to-do": "Niets te doen",
"share-multiple-emails": "U kunt geen e-mailadressen delen met meerdere accounts",
"age-restriction-update": "Er is een fout opgetreden bij het updaten van de leeftijdsbeperking",
"no-user": "Gebruiker bestaat niet",
"username-taken": "Gebruikersnaam al in gebruik",
"user-already-confirmed": "Gebruiker is al bevestigd",
"manual-setup-fail": "Handmatige aanmaak kan niet worden voltooid. Annuleer en maak de uitnodiging opnieuw",
"user-already-invited": "Gebruiker is al uitgenodigd onder dit e-mailadres en moet de uitnodiging nog accepteren.",
"invalid-email-confirmation": "Ongeldige e-mailbevestiging",
"forgot-password-generic": "Er wordt een e-mail verzonden naar het e-mailadres als deze in onze database voorkomt",
"not-accessible-password": "Uw server is niet toegankelijk. De link om je wachtwoord te resetten staat in het logboek",
"not-accessible": "Uw server is niet extern toegankelijk",
"email-sent": "Email verzonden",
"confirm-email": "U moet eerst uw e-mail bevestigen",
"permission-denied": "U heeft geen toestemming voor deze operatie",
"password-required": "U moet uw bestaande wachtwoord invoeren om uw account te wijzigen, tenzij u een beheerder bent",
"generic-reading-list-delete": "Er is een probleem opgetreden bij het verwijderen van de leeslijst",
"reading-list-deleted": "Leeslijst is verwijderd",
"reading-list-doesnt-exist": "Leeslijst bestaat niet",
"generic-relationship": "Er is een probleem opgetreden bij het updaten van relaties",
"no-series-collection": "Kan series niet ophalen van collectie",
"generic-series-delete": "Er is een probleem opgetreden bij het verwijderen van de serie",
"series-updated": "Succesvol geüpdatet",
"update-metadata-fail": "Kan metadata niet updaten",
"generic-series-update": "Er is een fout opgetreden bij het updaten van de serie",
"age-restriction-not-applicable": "Geen beperkingen",
"job-already-running": "Taak loopt al",
"greater-0": "{0} moet groter zijn dan 0",
"send-to-kavita-email": "Verzenden naar apparaat kan niet worden gebruikt met de e-mailservice van Kavita. Configureer uw eigen.",
"send-to-device-status": "Bestanden overzetten naar uw apparaat",
"generic-send-to": "Er is een fout opgetreden bij het verzenden van de bestanden naar het apparaat",
"volume-doesnt-exist": "Deel bestaat niet",
"series-doesnt-exist": "Serie bestaat niet",
"bookmarks-empty": "Bladwijzers kunnen niet leeg zijn",
"reading-list-updated": "Bijgewerkt",
"user-migration-needed": "Deze gebruiker moet migreren. Laat ze uitloggen en inloggen om de migratie op gang te brengen",
"generic-invite-email": "Er is een probleem opgetreden bij het opnieuw verzenden van de uitnodigingsmail",
"admin-already-exists": "Beheerder bestaat al",
"invalid-username": "Ongeldige gebruikersnaam",
"critical-email-migration": "Er is een probleem opgetreden tijdens de e-mailmigratie. Neem contact op met ondersteuning",
"chapter-doesnt-exist": "Hoofdstuk bestaat niet",
"file-missing": "Bestand is niet gevonden in boek",
"collection-updated": "Verzameling succesvol bijgewerkt",
"generic-error": "Er is iets mis gegaan, probeer het alstublieft nogmaals",
"collection-doesnt-exist": "Collectie bestaat niet",
"device-doesnt-exist": "Apparaat bestaat niet",
"generic-device-create": "Er is een fout opgetreden bij het maken van het apparaat",
"generic-device-update": "Er is een fout opgetreden bij het updaten van het apparaat",
"generic-device-delete": "Er is een fout opgetreden bij het verwijderen van het apparaat",
"no-cover-image": "Geen omslagafbeelding",
"bookmark-doesnt-exist": "Bladwijzer bestaat niet",
"must-be-defined": "{0} moet gedefinieerd zijn",
"generic-favicon": "Er is een probleem opgetreden bij het ophalen van de favicon voor het domein",
"invalid-filename": "Ongeldige bestandsnaam",
"file-doesnt-exist": "Bestand bestaat niet",
"library-name-exists": "Bibliotheeknaam bestaat al. Kies een unieke naam voor de server.",
"generic-library": "Er was een kritiek probleem. Probeer het opnieuw.",
"no-library-access": "Gebruiker heeft geen toegang tot deze bibliotheek",
"user-doesnt-exist": "Gebruiker bestaat niet",
"library-doesnt-exist": "Bibliotheek bestaat niet",
"invalid-path": "Ongeldig pad",
"delete-library-while-scan": "U kunt een bibliotheek niet verwijderen terwijl er een scan wordt uitgevoerd. Wacht tot de scan is voltooid of herstart Kavita en probeer het vervolgens te verwijderen",
"generic-library-update": "Er is een kritiek probleem opgetreden bij het updaten van de bibliotheek.",
"pdf-doesnt-exist": "PDF bestaat niet, terwijl dat wel zou moeten",
"invalid-access": "Ongeldige toegang",
"no-image-for-page": "Zo'n afbeelding ontbreekt voor pagina {0}. Probeer te vernieuwen om opnieuw cachen mogelijk te maken.",
"perform-scan": "Voer een scan uit op deze serie of bibliotheek en probeer het opnieuw",
"generic-read-progress": "Er is een probleem opgetreden bij het opslaan van de voortgang",
"generic-clear-bookmarks": "Kan bladwijzers niet wissen",
"bookmark-permission": "U heeft geen toestemming om een bladwijzer te maken/de bladwijzer ongedaan te maken",
"bookmark-save": "Kan bladwijzer niet opslaan",
"cache-file-find": "Kan afbeelding in cache niet vinden. Laad opnieuw en probeer het opnieuw.",
"name-required": "Naam mag niet leeg zijn",
"valid-number": "Moet een geldig paginanummer zijn",
"duplicate-bookmark": "Dubbele bladwijzervermelding bestaat al",
"reading-list-permission": "U heeft geen rechten voor deze leeslijst of de lijst bestaat niet",
"reading-list-position": "Kan positie niet updaten",
"reading-list-item-delete": "Kan item(s) niet verwijderen",
"generic-reading-list-update": "Er is een probleem opgetreden bij het updaten van de leeslijst",
"generic-reading-list-create": "Er is een probleem opgetreden bij het maken van de leeslijst",
"series-restricted": "Gebruiker heeft geen toegang tot deze serie",
"libraries-restricted": "Gebruiker heeft geen toegang tot de bibliotheken",
"no-series": "Kan series van bibliotheek niet ophalen",
"generic-user-update": "Er was een uitzondering bij het updaten van de gebruiker",
"reading-list-restricted": "Leeslijst bestaat niet of je hebt geen toegang",
"epub-html-missing": "Kan de juiste html voor die pagina niet vinden",
"unable-to-register-k+": "Licentie kan niet worden geregistreerd vanwege een fout. Neem contact op met Kavita+ ondersteuning",
"device-not-created": "Dit apparaat bestaat nog niet. Gelieve eerst te creëren",
"reading-list-name-exists": "Er bestaat al een leeslijst met deze naam",
"user-no-access-library-from-series": "Gebruiker heeft geen toegang tot de bibliotheek waartoe deze serie behoort",
"collections": "Alle collecties",
"total-backups": "Het totale aantal back-ups moet tussen 1 en 30 liggen",
"encode-as-warning": "U kunt niet converteren naar PNG. Gebruik Covers vernieuwen voor covers. Bladwijzers en favicons kunnen niet worden teruggecodeerd.",
"ip-address-invalid": "IP-adres '{0}' is ongeldig",
"bookmark-dir-permissions": "Bladwijzer folder heeft niet de juiste machtigingen voor Kavita om te gebruiken",
"total-logs": "Het totale aantal logboeken moet tussen 1 en 30 liggen",
"stats-permission-denied": "U bent niet geautoriseerd om de statistieken van een andere gebruiker te bekijken",
"url-not-valid": "URL retourneert geen geldige afbeelding of vereist autorisatie",
"url-required": "U moet een url doorgeven om te gebruiken",
"generic-cover-series-save": "Kan omslagafbeelding niet opslaan in serie",
"generic-cover-collection-save": "Kan omslagafbeelding niet opslaan in collectie",
"generic-cover-reading-list-save": "Kan omslagafbeelding niet opslaan in leeslijst",
"generic-cover-chapter-save": "Kan omslagafbeelding niet opslaan in hoofdstuk",
"generic-cover-library-save": "Kan omslagafbeelding niet opslaan in bibliotheek",
"access-denied": "U heeft geen toegang",
"reset-chapter-lock": "Kan omslagvergrendeling voor hoofdstuk niet resetten",
"generic-user-delete": "Kan de gebruiker niet verwijderen",
"generic-user-pref": "Er is een probleem opgetreden bij het opslaan van voorkeuren",
"opds-disabled": "OPDS is niet ingeschakeld op deze server",
"recently-added": "Recent toegevoegd",
"browse-recently-added": "Blader door recent toegevoegd",
"reading-lists": "Leeslijst",
"browse-reading-lists": "Blader door leeslijsten",
"libraries": "Alle bibliotheken",
"browse-libraries": "Blader door bibliotheken",
"browse-collections": "Blader door collecties",
"query-required": "U moet een vraag parameter doorgeven",
"search": "Zoeken",
"search-description": "Zoek naar series, collecties of leeslijsten",
"favicon-doesnt-exist": "Favicon bestaat niet",
"not-authenticated": "Gebruiker is niet geverifieerd",
"anilist-cred-expired": "AniList-referenties zijn verlopen of niet ingesteld",
"scrobble-bad-payload": "Slechte payload van Scrobble Provider",
"theme-doesnt-exist": "Themabestand ontbreekt of is ongeldig",
"bad-copy-files-for-download": "Kan bestanden niet kopiëren naar archiefdownload tijdelijke map.",
"generic-create-temp-archive": "Er is een probleem opgetreden bij het maken van tijdelijk archief",
"epub-malformed": "Het bestand is verkeerd opgemaakt! Kan niet lezen.",
"collection-tag-title-required": "Collectie titel mag niet leeg zijn",
"reading-list-title-required": "Titel leeslijst mag niet leeg zijn",
"collection-tag-duplicate": "Er bestaat al een verzameling met deze naam",
"device-duplicate": "Er bestaat al een apparaat met deze naam",
"send-to-permission": "Kan alleen EPUB of pdf naar apparaten verzenden, omdat andere formaten niet worden ondersteund op de Kindle",
"progress-must-exist": "Er moet voortgang zijn op de gebruiker",
"series-restricted-age-restriction": "Gebruiker mag deze serie niet bekijken vanwege leeftijdsbeperkingen",
"volume-num": "Deel {0}",
"book-num": "Boek {0}",
"issue-num": "Uitgave {0}{1}",
"chapter-num": "Hoofdstuk {0}",
"generic-scrobble-hold": "Er is een fout opgetreden bij het toevoegen van de bewaarplicht"
}

155
API/I18N/pt.json Normal file
View file

@ -0,0 +1,155 @@
{
"bad-credentials": "As credenciais não estão corretas",
"password-required": "Se não for administrador, tem de introduzir a sua palavra passe para alterar a sua conta",
"confirm-token-gen": "Ocorreu um problema a gerar um token de confirmação",
"age-restriction-update": "Ocorreu um erro ao atualizar a restrição de idade",
"manual-setup-fail": "Não foi possível completar o setup manual. Por favor cancele e recrie o convite",
"share-multiple-emails": "Não pode partilhar emails entre múltiplas contas",
"generate-token": "Ocorreu um problema a gerar um token de email de confirmação. Veja os logs",
"generic-password-update": "Ocorreu um erro inesperado ao confirmar a palavra passe",
"forgot-password-generic": "Será enviado um email para o endereço de email se existir na nossa base de dados",
"password-updated": "Palavra passe atualizada",
"not-accessible-password": "O seu servidor não está acessível. O link para repor a palavra passe está nos logs",
"email-sent": "Email enviado",
"user-migration-needed": "Este utilizador tem de ser migrado. Para isso o utilizador deve terminar e iniciar a sessão para despoletar a migração",
"user-already-invited": "Utilizador já foi convidado com este email mas ainda não aceitou o convite.",
"generic-invite-user": "Ocorreu um problema a convidar o utilizador. Por favor consulte os logs.",
"file-missing": "Ficheiro não encontrado no livro",
"device-doesnt-exist": "Dispositivo inexistente",
"generic-error": "Aconteceu algo de errado, por favor tente novamente",
"generic-device-create": "Ocorreu um erro ao criar o dispositivo",
"generic-device-update": "Ocorreu um erro ao atualizar o dispositivo",
"greater-0": "{0} tem de ser superior a 0",
"send-to-kavita-email": "Enviar para dispositivo não pode ser usado com o serviço de email do Kavita. Por favor configure o seu serviço de email.",
"bookmark-doesnt-exist": "Marcador inexistente",
"file-doesnt-exist": "Ficheiro inexistente",
"delete-library-while-scan": "Não pode eliminar a biblioteca enquanto uma análise está em curso. Por favor aguarde que a análise termine ou reinicie o Kavita e depois tente eliminar novamente",
"generic-favicon": "Ocorreu um problema a obter o favicon do domínio",
"library-name-exists": "O nome da biblioteca já existe. Por favor escolha um nome único neste servidor.",
"reading-list-updated": "Actualizada",
"generic-reading-list-create": "Ocorreu um problema ao criar a lista de leitura",
"generic-read-progress": "Ocorreu um problema a gravar o progresso",
"cache-file-find": "Não foi possível encontrar a imagem em cache. Recarregue e tente novamente.",
"generic-reading-list-delete": "Ocorreu um problema ao eliminar a lista de leitura",
"reading-list-permission": "Não tem permissões nesta lista de leitura ou a lista não existe",
"reading-list-position": "Não foi possível atualizar a posição",
"reading-list-deleted": "Lista de leitura eliminada",
"generic-reading-list-update": "Ocorreu um problema ao atualizar a lista de leitura",
"reading-list-doesnt-exist": "Lista de leitura inexistente",
"series-restricted": "Utilizador sem acesso a esta série",
"libraries-restricted": "Utilizador sem acesso a qualquer biblioteca",
"generic-series-delete": "Ocorreu um problema a eliminar a série",
"no-series": "Não foi possível obter séries para a bibilioteca",
"no-series-collection": "Não foi possível obter as séries para a Coleção",
"generic-series-update": "Ocorreu um erro ao atualizar a série",
"series-updated": "Atualizada com sucesso",
"age-restriction-not-applicable": "Sem Restrições",
"update-metadata-fail": "Não foi possível atualizar a metadata",
"generic-relationship": "Ocorreu um problema a atualizar as relações",
"job-already-running": "O job já está em curso",
"ip-address-invalid": "O endereço IP '{0}' é inválido",
"stats-permission-denied": "Não está autorizado a ver as estatísticas de outros utilizadores",
"url-not-valid": "O Url não retorna uma imagem válida ou requere autorização",
"generic-cover-collection-save": "Não foi possível guardar a imagem de capa da Coleção",
"generic-cover-reading-list-save": "Não foi possível guardar a imagem de capa da Lista de Leitura",
"generic-user-pref": "Ocorreu um problema a guardar as preferências",
"browse-recently-added": "Visualizar Adicionados Recentemente",
"libraries": "Todas as Bibliotecas",
"reading-list-restricted": "Lista de leitura inexistente ou sem acesso",
"not-authenticated": "Utilizador não está autenticado",
"unable-to-register-k+": "Não foi possível registar a licença devido a um erro. Contacte o suporte do Kavita+",
"theme-doesnt-exist": "Ficheiro de tema inválido ou inexistente",
"anilist-cred-expired": "As credenciais do AniList expiraram ou não foram definidas",
"epub-html-missing": "Não foi possível encontrar o html apropriado para essa página",
"send-to-permission": "Não é possível enviar não-EPUB ou PDF para dispositivos por não ser suportado no Kindle",
"user-no-access-library-from-series": "O utilizador não tem acesso à biblioteca a que esta série pertence",
"admin-already-exists": "Administrador já existe",
"invalid-username": "Nome de utilizador inválido",
"disabled-account": "A sua conta está desabilitada. Contacte o administrador do servidor.",
"register-user": "Aconteceu algo de errado ao registar o utilizador",
"validate-email": "Ocorreu um problema a validar o seu email: {0}",
"denied": "Não permitido",
"confirm-email": "Tem de confirmar o email primeiro",
"locked-out": "Ficou bloqueado por ter feito demasiadas tentativas de autorização. Por favor espere 10 minutos.",
"invalid-password": "Palavra passe inválida",
"invalid-token": "Token inválido",
"nothing-to-do": "Nada a fazer",
"no-user": "O utilizador não existe",
"username-taken": "Nome de utilizador já usado",
"user-already-confirmed": "Utilizador já confirmado",
"generic-user-update": "Ocorreu uma exceção ao atualizar o utilizador",
"user-already-registered": "Utilizador já está registado como {0}",
"invalid-email-confirmation": "Confirmação de email inválida",
"generic-user-email-update": "Não foi possível atualizar o email do utilizador. Verifique os logs.",
"not-accessible": "O seu servidor não está acessível externamente",
"generic-invite-email": "Ocorreu um problema ao reenviar o email com o convite",
"critical-email-migration": "Ocorreu um problema durante a migração do email. Contacte o suporte",
"chapter-doesnt-exist": "Capítulo inexistente",
"collection-updated": "Coleção atualizada com sucesso",
"collection-doesnt-exist": "Coleção inexistente",
"generic-device-delete": "Ocorreu um erro ao apagar o dispositivo",
"send-to-device-status": "A transferir ficheiros para o dispositivo",
"generic-send-to": "Ocorreu um erro a enviar o(s) ficheiro(s) para o dispositivo",
"series-doesnt-exist": "Série inexistente",
"volume-doesnt-exist": "Volume inexistente",
"bookmarks-empty": "Marcador não pode ser vazio",
"must-be-defined": "{0} tem de ser definido",
"invalid-filename": "Ficheiro inválido",
"generic-library": "Ocorreu um problema crítico. Por favor tente novamente.",
"no-library-access": "Utilizador não tem acesso a esta biblioteca",
"user-doesnt-exist": "Utilizador inexistente",
"library-doesnt-exist": "Biblioteca inexistente",
"invalid-path": "Caminho inválido",
"generic-library-update": "Ocorreu um problema crítico ao atualizar a biblioteca.",
"pdf-doesnt-exist": "PDF inexistente quando deveria existir",
"invalid-access": "Acesso inválido",
"perform-scan": "Por favor inicie uma análise nesta série ou biblioteca e tente novamente",
"generic-clear-bookmarks": "Não foi possível limpar os marcadores",
"bookmark-permission": "Não tem permissão para adicionar/remover marcadores",
"bookmark-save": "Não foi possível gravar o marcador",
"name-required": "Nome não pode ser vazio",
"valid-number": "Tem de ser um número de página válido",
"reading-list-item-delete": "Não foi possível eliminar o(s) item(s)",
"bookmark-dir-permissions": "A pasta dos marcadores não tem as permissões corretas para ser usada pelo Kavita",
"total-backups": "Os backups totais têm de estar entre 1 e 30",
"total-logs": "Os logs totais têm de estar entre 1 e 30",
"url-required": "Tem de definir um url",
"generic-cover-series-save": "Não foi possível guardar a imagem de capa da Série",
"generic-cover-chapter-save": "Não foi possível guardar a imagem de capa do Capítulo",
"generic-cover-library-save": "Não é possível guardar a imagem de capa na Biblioteca",
"access-denied": "Não tem acesso",
"generic-user-delete": "Não foi possível eliminar o utilizador",
"opds-disabled": "O OPDS não está habilitado neste servidor",
"recently-added": "Adicionado Recentemente",
"reading-lists": "Listas de Leitura",
"collections": "Todas as Coleções",
"query-required": "Tem de passar um parâmetro de query",
"search": "Pesquisa",
"search-description": "Pesquisa por Séries, Coleções, ou Listas de Leitura",
"favicon-doesnt-exist": "Favicon inexistente",
"generic-create-temp-archive": "Ocorreu um problema a criar arquivo temporário",
"epub-malformed": "O ficheiro está malformado! Não foi possível a sua leitura.",
"collection-tag-title-required": "O Título da Coleção não pode ser vazio",
"reading-list-title-required": "O Título da Lista de Leitura não pode ser vazio",
"collection-tag-duplicate": "Já existe uma coleção com este nome",
"device-duplicate": "Já existe um dispositivo com este nome",
"device-not-created": "Este dispositivo ainda não existe. Por favor crie-o primeiro",
"progress-must-exist": "Progresso tem de existir no utilizador",
"reading-list-name-exists": "Já existe uma lista de leitura com este nome",
"series-restricted-age-restriction": "O utilizador não tem permissão para ver esta série devido às restrições de idade",
"volume-num": "Volume {0}",
"book-num": "Livro {0}",
"chapter-num": "Capítulo {0}",
"unable-to-reset-key": "Aconteceu algo de errado, não foi possível fazer reset à chave",
"permission-denied": "Não tem permissão para esta operação",
"no-cover-image": "Sem imagem de capa",
"no-image-for-page": "Não existe a imagem para a página {0}. Tente refrescar para refazer a cache.",
"duplicate-bookmark": "Um registo duplicado deste marcador já existe",
"encode-as-warning": "Não pode converter para PNG. Para as capas, use a funcionalidade Refrescar Capas. Os marcadores e os favicons não podem novamente codificados.",
"reset-chapter-lock": "Não foi possível repor o bloqueio de capa para o Capítulo",
"browse-reading-lists": "Explorar por Listas de Leituras",
"browse-libraries": "Explorar por Bibliotecas",
"browse-collections": "Explorar por Coleções",
"invalid-payload": "Payload inválido",
"scrobble-bad-payload": "Payload inválido de Fornecedor de Scrobble"
}

4
API/I18N/ru.json Normal file
View file

@ -0,0 +1,4 @@
{
"confirm-email": "Сначала вы должны подтвердить свой адрес электронной почты",
"bad-credentials": "Ваши учетные данные неверны"
}

160
API/I18N/th.json Normal file
View file

@ -0,0 +1,160 @@
{
"invalid-payload": "เพย์โหลดไม่ถูกต้อง",
"share-multiple-emails": "คุณไม่สามารถใช้อีเมลเดียวกันในหลายบัญชีได้",
"age-restriction-update": "พบข้อผิดพลาด ไม่สามารถแก้ไขการจำกัดอายุได้",
"generic-user-update": "พบข้อผิดพลาด ไม่สามารถแก้ไขข้อมูลผู้ใช้ได้",
"nothing-to-do": "ไม่มีอะไรต้องดำเนินการ",
"generate-token": "พบปัญหาการสร้างโทเคนยืนยันอีเมล กรุณาตรวจสอบ บันทึกระบบ",
"no-user": "ไม่พบผู้ใช้งาน",
"username-taken": "ผู้ใช้งานมีอยู่ในระบบแล้ว",
"user-already-confirmed": "ผู้ใช้งานยืนยันอีเมลแล้ว",
"chapter-num": "บทที่ {0}",
"invalid-token": "โทเคนไม่ถูกต้อง",
"unable-to-reset-key": "มีบางอย่างผิดพลาด ไม่สามารถรีเซ็ตคีย์ได้",
"generic-invite-user": "มีปัญหาในการเชิญผู้ใช้ โปรดตรวจสอบบันทึกระบบ",
"register-user": "มีบางอย่างผิดพลาดขณะลงทะเบียนผู้ใช้งาน",
"validate-email": "มีปัญหาบางประการขณะยืนยันอีเมล: {0}",
"invalid-username": "ชื่อผู้ใช้ที่ไม่ถูกต้อง",
"generic-device-create": "เกิดข้อผิดพลาดขณะสร้างอุปกรณ์",
"encode-as-warning": "คุณไม่สามารถแปลงเป็น PNG สำหรับหน้าปก ให้ใช้ รีเฟรชหน้าปก บุ๊กมาร์กและ favicons ไม่สามารถเข้ารหัสย้อนกลับได้",
"critical-email-migration": "มีปัญหาระหว่างการย้ายข้อมูลอีเมล ติดต่อฝ่ายสนับสนุน",
"user-already-invited": "ผู้ใช้ได้รับคำเชิญในอีเมลนี้แล้ว และยังไม่ได้ตอบรับคำเชิญ",
"invalid-email-confirmation": "การยืนยันอีเมลไม่ถูกต้อง",
"not-accessible-password": "เซิร์ฟเวอร์ของคุณไม่สามารถเข้าถึงได้ ลิงก์เพื่อรีเซ็ตรหัสผ่านของคุณอยู่ในบันทึกระบบ",
"not-accessible": "เซิร์ฟเวอร์ของคุณไม่สามารถเข้าถึงได้จากภายนอก",
"admin-already-exists": "มีผู้ดูแลระบบอยู่แล้ว",
"generic-error": "เกิดข้อผิดพลาด โปรดลองอีกครั้ง",
"collection-doesnt-exist": "ไม่มีคอลเล็กชัน",
"device-doesnt-exist": "ไม่มีอุปกรณ์อยู่",
"generic-clear-bookmarks": "ไม่สามารถล้างบุ๊กมาร์ก",
"cache-file-find": "ไม่พบรูปภาพที่เก็บไว้ โหลดใหม่และลองอีกครั้ง",
"url-required": "คุณต้องส่ง url เพื่อใช้งาน",
"send-to-kavita-email": "ส่งไปยังอุปกรณ์ใช้กับบริการอีเมลของ Kavita ไม่ได้ โปรดกำหนดค่าของคุณเอง",
"favicon-doesnt-exist": "ไม่มีไอคอน Favicon",
"library-name-exists": "ชื่อไลบรารีมีอยู่แล้ว โปรดเลือกชื่อเฉพาะสำหรับเซิร์ฟเวอร์",
"library-doesnt-exist": "ไม่มีไลบรารี",
"invalid-path": "พาธไม่ถูกต้อง",
"bookmark-permission": "คุณไม่ได้รับอนุญาตให้คั่นหน้า/ยกเลิกการคั่นหน้า",
"bookmark-save": "ไม่สามารถบันทึกบุ๊กมาร์ก",
"no-series-collection": "ไม่สามารถรับซีรีส์สำหรับคอลเลกชัน",
"generic-series-delete": "มีปัญหาในการลบซีรีส์",
"job-already-running": "งานกำลังทำงานอยู่",
"bookmark-dir-permissions": "ไดเรกทอรีบุ๊กมาร์กไม่มีสิทธิ์ที่ถูกต้องสำหรับ Kavita ที่จะใช้",
"total-backups": "การสำรองข้อมูลทั้งหมดต้องอยู่ระหว่าง 1 ถึง 30",
"stats-permission-denied": "คุณไม่ได้รับอนุญาตให้ดูสถิติของผู้ใช้รายอื่น",
"generic-cover-series-save": "ไม่สามารถบันทึกภาพหน้าปกไปยังซีรี่ส์ได้",
"generic-cover-collection-save": "ไม่สามารถบันทึกภาพหน้าปกไปยังซีรี่ส์ได้",
"generic-cover-reading-list-save": "ไม่สามารถบันทึกภาพหน้าปกไปยังเรื่องรออ่านได้",
"generic-cover-chapter-save": "บันทึกภาพหน้าปกไปยังบทได้",
"reset-chapter-lock": "ไม่สามารถรีเซ็ตการล็อคปกสำหรับตอน",
"generic-user-pref": "มีปัญหาในการบันทึกการตั้งค่า",
"on-deck": "กำลังอ่าน",
"libraries": "ไลบราลีทั้งหมด",
"browse-collections": "เรียกดูตามคอลเลกชัน",
"search": "ค้นหา",
"collection-tag-duplicate": "มีคอลเล็กชันชื่อนี้อยู่แล้ว",
"opds-disabled": "OPDS ไม่ได้เปิดใช้งานบนเซิร์ฟเวอร์นี้",
"browse-on-deck": "ดูรายการกำลังอ่าน",
"browse-reading-lists": "เรียกดูตามรายการเรื่องรออ่าน",
"browse-libraries": "เรียกดูตามไลบราลี",
"collections": "คอลเลกชันทั้งหมด",
"search-description": "ค้นหาซีรีส์ คอลเลคชัน หรือรายการอ่าน",
"not-authenticated": "ผู้ใช้ไม่ได้รับการรับรองความถูกต้อง",
"unable-to-register-k+": "ไม่สามารถลงทะเบียนไลเซนได้เนื่องจากข้อผิดพลาด ติดต่อฝ่ายสนับสนุน Kavita+",
"anilist-cred-expired": "ข้อมูลรับรอง AniList หมดอายุหรือไม่ได้ตั้งค่า",
"scrobble-bad-payload": "เพย์โหลดไม่ถูกต้องจากผู้ให้บริการ Scrobble",
"device-duplicate": "มีอุปกรณ์ชื่อนี้อยู่แล้ว",
"device-not-created": "ยังไม่มีอุปกรณ์ โปรดสร้างก่อน",
"progress-must-exist": "ต้องมีความคืบหน้ากับผู้ใช้",
"bad-copy-files-for-download": "ไม่สามารถคัดลอกไฟล์ไปยังไดเรกทอรี่ชั่วคร่าวสำหรับดาวน์โหลดได้",
"generic-create-temp-archive": "มีปัญหาในการสร้างที่เก็บชั่วคราว",
"epub-malformed": "ไฟล์มีรูปแบบไม่ถูกต้อง! อ่านไม่ได้",
"reading-list-title-required": "ชื่อรายการเรื่องรออ่านต้องไม่ว่างเปล่า",
"send-to-permission": "ไม่สามารถส่งไฟล์ที่ไม่ใช่ EPUB หรือ PDF ไปยังอุปกรณ์เนื่องจาก Kindle ไม่รองรับ",
"reading-list-name-exists": "มีรายการอ่านชื่อนี้อยู่แล้ว",
"user-no-access-library-from-series": "ผู้ใช้ไม่มีสิทธิ์เข้าถึงไลบรารีของซีรี่ส์นี้",
"volume-num": "เล่มที่ {0}",
"book-num": "เล่มที่ {0}",
"series-restricted-age-restriction": "ผู้ใช้ไม่ได้รับอนุญาตให้ดูซีรีส์นี้เนื่องจากการจำกัดอายุ",
"issue-num": "ฉบับ {0}{1}",
"confirm-email": "คุณต้องยืนยันอีเมลของคุณก่อน",
"bad-credentials": "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง",
"locked-out": "ไม่สามารถเข้าสู่ระบบได้เนื่องจากเข้าสู่ระบบล้มเหลวมากเกินไป กรุณารอ 10 นาที",
"disabled-account": "บัญชีของคุณถูกระงับ กรุณาติดต่อผู้ดูแลระบบ",
"denied": "ไม่อนุญาต",
"permission-denied": "คุณไม่มีสิทธิในการดำเนินการต่อ",
"confirm-token-gen": "มีปัญหาขณะสร้างโทเคนยืนยันอีเมล",
"password-required": "คุณต้องป้อนรหัสผ่านปัจจุบันของคุณเพื่อเปลี่ยนการตั้งค่าบัญชีของคุณ เว้นแต่ว่าคุณเป็นผู้ดูแลระบบ",
"invalid-password": "รหัสผ่านไม่ถูกต้อง",
"manual-setup-fail": "ไม่สามารถตั้งค่าด้วยตนเองให้เสร็จสิ้นได้ กรุณายกเลิกและสร้างการเชื้อเชิญใหม่",
"user-already-registered": "ผู้ใช้ลงทะเบียนแล้วในชื่อ {0}",
"generic-password-update": "เกิดข้อผิดพลาดที่ไม่คาดคิดเมื่อยืนยันรหัสผ่านใหม่",
"password-updated": "อัปเดตรหัสผ่านแล้ว",
"generic-user-email-update": "ไม่สามารถอัปเดตอีเมลสำหรับผู้ใช้ ตรวจสอบบันทึกระบบ",
"forgot-password-generic": "อีเมลจะถูกส่งไปยังอีเมลนั้นหากมีอยู่ในฐานข้อมูลของเรา",
"email-sent": "ส่งอีเมลแล้ว",
"user-migration-needed": "ผู้ใช้รายนี้จำเป็นต้องย้ายข้อมูล ให้พวกเขาออกจากระบบและลงชื่อเข้าใช้ใหม่เพื่อเริ่มต้นย้ายข้อมูล",
"generic-invite-email": "มีปัญหาในการส่งอีเมลคำเชิญอีกครั้ง",
"chapter-doesnt-exist": "ไม่มีบทนี้",
"file-missing": "ไม่พบไฟล์ในหนังสือ",
"collection-updated": "อัปเดตคอลเลกชันเรียบร้อยแล้ว",
"generic-device-update": "เกิดข้อผิดพลาดขณะอัปเดตอุปกรณ์",
"generic-device-delete": "เกิดข้อผิดพลาดขณะลบอุปกรณ์",
"greater-0": "{0} ต้องมากกว่า 0",
"send-to-device-status": "การถ่ายโอนไฟล์ไปยังอุปกรณ์ของคุณ",
"generic-send-to": "มีข้อผิดพลาดในการส่งไฟล์ไปยังอุปกรณ์",
"series-doesnt-exist": "ไม่มีซีรีส์นี้",
"volume-doesnt-exist": "ไม่มีชุดหนังสือนี้",
"bookmarks-empty": "บุ๊กมาร์กต้องไม่ว่างเปล่า",
"no-cover-image": "ไม่มีภาพหน้าปก",
"bookmark-doesnt-exist": "ไม่มีบุ๊กมาร์ก",
"must-be-defined": "ต้องกำหนด {0}",
"generic-favicon": "มีปัญหาในการเรียก favicon สำหรับโดเมน",
"invalid-filename": "ชื่อไฟล์ไม่ถูกต้อง",
"file-doesnt-exist": "ไฟล์ไม่มีอยู่",
"no-library-access": "ผู้ใช้ไม่มีสิทธิ์เข้าถึงไลบรารีนี้",
"generic-library": "เกิดปัญหาร้ายแรง กรุณาลองอีกครั้ง",
"user-doesnt-exist": "ไม่มีผู้ใช้",
"delete-library-while-scan": "คุณไม่สามารถลบไลบรารีได้ในขณะที่การสแกนกำลังดำเนินการอยู่ โปรดรอให้การสแกนเสร็จสิ้นหรือรีสตาร์ท Kavita จากนั้นลองลบไลบรารีดูอีกครั้ง",
"generic-library-update": "มีปัญหาร้ายแรงในการอัปเดตไลบรารี",
"pdf-doesnt-exist": "PDF ไม่มีอยู่ทั้งๆ ที่มันควรจะมี",
"invalid-access": "การเข้าถึงไม่ถูกต้อง",
"no-image-for-page": "ไม่มีรูปภาพดังกล่าวสำหรับหน้า {0} ลองรีเฟรชเพื่อให้สามารถแคชใหม่ได้",
"perform-scan": "โปรดทำการสแกนซีรีส์หรือไลบรารีนี้แล้วลองอีกครั้ง",
"generic-read-progress": "มีปัญหาในการบันทึกความคืบหน้า",
"name-required": "ชื่อต้องไม่ว่างเปล่า",
"valid-number": "ต้องเป็นหมายเลขหน้าที่ถูกต้อง",
"duplicate-bookmark": "มีรายการบุ๊กมาร์กที่ซ้ำกันอยู่แล้ว",
"reading-list-permission": "คุณไม่มีสิทธิ์ในรายการเรื่องรออ่านนี้หรือรายการนั้นไม่มีอยู่",
"reading-list-position": "ไม่สามารถอัปเดตตำแหน่ง",
"reading-list-updated": "อัปเดต",
"reading-list-item-delete": "ไม่สามารถลบรายการ",
"reading-list-deleted": "รายการเรื่องรออ่านถูกลบ",
"generic-reading-list-delete": "มีปัญหาในการลบรายการรออ่าน",
"generic-reading-list-update": "มีปัญหาในการอัปเดตรายการเรื่องรออ่าน",
"generic-reading-list-create": "มีปัญหาในการสร้างรายการเรื่องรออ่าน",
"reading-list-doesnt-exist": "ไม่มีรายการเรื่องรออ่าน",
"series-restricted": "ผู้ใช้ไม่มีสิทธิ์เข้าถึงซีรีส์นี้",
"generic-scrobble-hold": "เกิดข้อผิดพลาดขณะเพิ่ม Hold",
"libraries-restricted": "ผู้ใช้ไม่มีสิทธิ์เข้าถึงไลบรารีใดๆ",
"no-series": "ไม่สามารถรับซีรีส์สำหรับไลบรารี",
"generic-series-update": "เกิดข้อผิดพลาดในการอัปเดตซีรีส์",
"series-updated": "อัปเดตเรียบร้อยแล้ว",
"update-metadata-fail": "ไม่สามารถอัปเดตข้อมูลเมตา",
"age-restriction-not-applicable": "ไม่มีข้อ จำกัด",
"generic-relationship": "มีปัญหาในการอัปเดตความสัมพันธ์",
"ip-address-invalid": "ที่อยู่ IP '{0}' ไม่ถูกต้อง",
"total-logs": "บันทึกทั้งหมดต้องอยู่ระหว่าง 1 ถึง 30",
"url-not-valid": "URL ไม่ส่งคืนรูปภาพที่ถูกต้องหรือต้องได้รับการอนุญาต",
"generic-cover-library-save": "ไม่สามารถบันทึกภาพหน้าปกไปยังไลบราลี",
"access-denied": "คุณไม่มีสิทธิ์เข้าถึง",
"generic-user-delete": "ไม่สามารถลบผู้ใช้",
"recently-added": "เพิ่มมาเร็ว ๆ นี้",
"browse-recently-added": "เรียกดูที่เพิ่มล่าสุด",
"reading-lists": "รายการอ่าน",
"reading-list-restricted": "ไม่มีรายการเรื่องรออ่านหรือคุณไม่มีสิทธิ์เข้าถึง",
"query-required": "คุณต้องส่งพารามิเตอร์การค้นหา",
"theme-doesnt-exist": "ไฟล์ธีมหายไปหรือไม่ถูกต้อง",
"epub-html-missing": "ไม่พบ html ที่เหมาะสมสำหรับหน้านั้น",
"collection-tag-title-required": "ชื่อคอลเลกชันต้องไม่ว่างเปล่า"
}

16
API/I18N/tr.json Normal file
View file

@ -0,0 +1,16 @@
{
"denied": "İzin verilmedi",
"permission-denied": "Bu operasyona izniniz yok",
"bad-credentials": "Kimlik bilgileriniz doğru değil",
"confirm-email": "İlk olarak E-Posta'nı onaylaman gerek",
"register-user": "Kullanıcıyı kayıt ederken bir şeyler yanlış gitti",
"disabled-account": "Hesabınız devre dışı bırakıldı. Sunucu yöneticisiyle iletişime geçin.",
"validate-email": "E-Posta'yı doğrularken bir hata oluştu: {0}",
"confirm-token-gen": "Doğrulama tokeni oluşturulurken bir sorun oluştu",
"password-required": "Yönetici değilseniz, hesabınızı değiştirmek için mevcut şifrenizi girmelisiniz",
"invalid-token": "Geçersiz token",
"unable-to-reset-key": "Bir şeyler yanlış gitti, key sıfırlanmadı",
"invalid-password": "Geçersiz Şifre",
"invalid-payload": "Geçersiz yük",
"nothing-to-do": "Yapılacak bir şey yok"
}

21
API/I18N/zh_Hans.json Normal file
View file

@ -0,0 +1,21 @@
{
"bad-credentials": "你的用户信息不匹配",
"validate-email": "验证你的邮件时出了点问题: {0}",
"confirm-token-gen": "生成认证令牌时出现问题",
"denied": "未被允许",
"password-required": "除非您是管理员,否则您必须输入现有密码才能更改帐户信息",
"invalid-token": "无效令牌",
"unable-to-reset-key": "出错了,无法重置",
"confirm-email": "你必须先确认你的邮箱",
"disabled-account": "你的账号已被关闭。联系服务器的管理员。",
"register-user": "注册用户时出现一些错误",
"locked-out": "您因多次错误登陆已被阻止。请稍等 10 分钟。",
"permission-denied": "您无权执行此操作",
"invalid-password": "无效密码",
"generate-token": "生成确认电子邮件令牌时出现问题。参见日志",
"generic-user-update": "更新用户时出现了异常",
"share-multiple-emails": "不能在多个账户间共享电子邮件",
"age-restriction-update": "年龄限制更新出错",
"no-user": "用户不存在",
"username-taken": "用户名已被使用"
}

View file

@ -301,7 +301,7 @@ public class ArchiveService : IArchiveService
if (!_directoryService.CopyFilesToDirectory(files, tempLocation))
{
throw new KavitaException("Unable to copy files to temp directory archive download.");
throw new KavitaException("bad-copy-files-for-download");
}
var zipPath = Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip");
@ -314,7 +314,7 @@ public class ArchiveService : IArchiveService
catch (AggregateException ex)
{
_logger.LogError(ex, "There was an issue creating temp archive");
throw new KavitaException("There was an issue creating temp archive");
throw new KavitaException("generic-create-temp-archive");
}
return zipPath;

View file

@ -455,9 +455,12 @@ public class BookService : IBookService
};
ComicInfo.CleanComicInfo(info);
foreach (var identifier in epubBook.Schema.Package.Metadata.Identifiers.Where(id => !string.IsNullOrEmpty(id.Scheme) && id.Scheme.Equals("ISBN")))
var weblinks = new List<string>();
foreach (var identifier in epubBook.Schema.Package.Metadata.Identifiers)
{
if (string.IsNullOrEmpty(identifier.Identifier)) continue;
if (!string.IsNullOrEmpty(identifier.Scheme) && identifier.Scheme.Equals("ISBN", StringComparison.InvariantCultureIgnoreCase))
{
var isbn = identifier.Identifier.Replace("urn:isbn:", string.Empty).Replace("isbn:", string.Empty);
if (!ArticleNumberHelper.IsValidIsbn10(isbn) && !ArticleNumberHelper.IsValidIsbn13(isbn))
{
@ -465,7 +468,19 @@ public class BookService : IBookService
continue;
}
info.Isbn = isbn;
break;
}
if ((!string.IsNullOrEmpty(identifier.Scheme) && identifier.Scheme.Equals("URL", StringComparison.InvariantCultureIgnoreCase)) ||
identifier.Identifier.StartsWith("url:"))
{
var url = identifier.Identifier.Replace("url:", string.Empty);
weblinks.Add(url.Trim());
}
}
if (weblinks.Count > 0)
{
info.Web = string.Join(',', weblinks.Distinct());
}
// Parse tags not exposed via Library
@ -1121,7 +1136,7 @@ public class BookService : IBookService
if (doc.ParseErrors.Any())
{
LogBookErrors(book, contentFileRef, doc);
throw new KavitaException("The file is malformed! Cannot read.");
throw new KavitaException("epub-malformed");
}
_logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath);
doc.DocumentNode.SelectSingleNode("/html").AppendChild(HtmlNode.CreateNode("<body></body>"));
@ -1137,7 +1152,7 @@ public class BookService : IBookService
"There was an issue reading one of the pages for", ex);
}
throw new KavitaException("Could not find the appropriate html for that page");
throw new KavitaException("epub-html-missing");
}
private static void CreateToCChapter(EpubBookRef book, EpubNavigationItemRef navigationItem, IList<BookChapterItem> nestedChapters,

View file

@ -52,12 +52,12 @@ public class CollectionTagService : ICollectionTagService
public async Task<bool> UpdateTag(CollectionTagDto dto)
{
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(dto.Id);
if (existingTag == null) throw new KavitaException("This tag does not exist");
if (existingTag == null) throw new KavitaException("collection-doesnt-exist");
var title = dto.Title.Trim();
if (string.IsNullOrEmpty(title)) throw new KavitaException("Title cannot be empty");
if (string.IsNullOrEmpty(title)) throw new KavitaException("collection-tag-title-required");
if (!title.Equals(existingTag.Title) && await TagExistsByName(dto.Title))
throw new KavitaException("A tag with this name already exists");
throw new KavitaException("collection-tag-duplicate");
existingTag.SeriesMetadatas ??= new List<SeriesMetadata>();
existingTag.Title = title;

View file

@ -42,7 +42,7 @@ public class DeviceService : IDeviceService
{
userWithDevices.Devices ??= new List<Device>();
var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Name!.Equals(dto.Name));
if (existingDevice != null) throw new KavitaException("A device with this name already exists");
if (existingDevice != null) throw new KavitaException("device-duplicate");
existingDevice = new DeviceBuilder(dto.Name)
.WithPlatform(dto.Platform)
@ -70,7 +70,7 @@ public class DeviceService : IDeviceService
try
{
var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Id == dto.Id);
if (existingDevice == null) throw new KavitaException("This device doesn't exist yet. Please create first");
if (existingDevice == null) throw new KavitaException("device-not-created");
existingDevice.Name = dto.Name;
existingDevice.Platform = dto.Platform;
@ -108,11 +108,11 @@ public class DeviceService : IDeviceService
public async Task<bool> SendTo(IReadOnlyList<int> chapterIds, int deviceId)
{
var device = await _unitOfWork.DeviceRepository.GetDeviceById(deviceId);
if (device == null) throw new KavitaException("Device doesn't exist");
if (device == null) throw new KavitaException("device-doesnt-exist");
var files = await _unitOfWork.ChapterRepository.GetFilesForChaptersAsync(chapterIds);
if (files.Any(f => f.Format is not (MangaFormat.Epub or MangaFormat.Pdf)) && device.Platform == DevicePlatform.Kindle)
throw new KavitaException("Cannot Send non Epub or Pdf to devices as not supported on Kindle");
throw new KavitaException("send-to-permission");
device.UpdateLastUsed();

View file

@ -25,6 +25,7 @@ public interface IDirectoryService
string ConfigDirectory { get; }
string SiteThemeDirectory { get; }
string FaviconDirectory { get; }
string LocalizationDirectory { get; }
/// <summary>
/// Original BookmarkDirectory. Only used for resetting directory. Use <see cref="ServerSettingKey.BackupDirectory"/> for actual path.
/// </summary>
@ -79,6 +80,7 @@ public class DirectoryService : IDirectoryService
public string BookmarkDirectory { get; }
public string SiteThemeDirectory { get; }
public string FaviconDirectory { get; }
public string LocalizationDirectory { get; }
private readonly ILogger<DirectoryService> _logger;
private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase;
@ -95,22 +97,23 @@ public class DirectoryService : IDirectoryService
{
_logger = logger;
FileSystem = fileSystem;
CoverImageDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "covers");
CacheDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "cache");
LogDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "logs");
TempDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "temp");
ConfigDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config");
BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks");
SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes");
FaviconDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "favicons");
ExistOrCreate(SiteThemeDirectory);
ExistOrCreate(ConfigDirectory);
CoverImageDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "covers");
ExistOrCreate(CoverImageDirectory);
CacheDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "cache");
ExistOrCreate(CacheDirectory);
LogDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "logs");
ExistOrCreate(LogDirectory);
TempDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "temp");
ExistOrCreate(TempDirectory);
BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks");
ExistOrCreate(BookmarkDirectory);
SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes");
ExistOrCreate(SiteThemeDirectory);
FaviconDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "favicons");
ExistOrCreate(FaviconDirectory);
LocalizationDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "I18N");
}
/// <summary>

View file

@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using API.Data;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting;
namespace API.Services;
#nullable enable
public interface ILocalizationService
{
Task<string> Get(string locale, string key, params object[] args);
Task<string> Translate(int userId, string key, params object[] args);
IEnumerable<string> GetLocales();
}
public class LocalizationService : ILocalizationService
{
private readonly IDirectoryService _directoryService;
private readonly IMemoryCache _cache;
private readonly IUnitOfWork _unitOfWork;
/// <summary>
/// The locales for the UI
/// </summary>
private readonly string _localizationDirectoryUi;
private readonly MemoryCacheEntryOptions _cacheOptions;
public LocalizationService(IDirectoryService directoryService,
IHostEnvironment environment, IMemoryCache cache, IUnitOfWork unitOfWork)
{
_directoryService = directoryService;
_cache = cache;
_unitOfWork = unitOfWork;
if (environment.IsDevelopment())
{
_localizationDirectoryUi = directoryService.FileSystem.Path.Join(
directoryService.FileSystem.Directory.GetCurrentDirectory(),
"../UI/Web/src/assets/langs");
} else if (environment.EnvironmentName.Equals("Testing", StringComparison.OrdinalIgnoreCase))
{
_localizationDirectoryUi = directoryService.FileSystem.Path.Join(
directoryService.FileSystem.Directory.GetCurrentDirectory(),
"/../../../../../UI/Web/src/assets/langs");
}
else
{
_localizationDirectoryUi = directoryService.FileSystem.Path.Join(
directoryService.FileSystem.Directory.GetCurrentDirectory(),
"wwwroot", "assets/langs");
}
_cacheOptions = new MemoryCacheEntryOptions()
.SetSize(1)
.SetAbsoluteExpiration(TimeSpan.FromMinutes(15));
}
/// <summary>
/// Loads a language, if language is blank, falls back to english
/// </summary>
/// <param name="languageCode"></param>
/// <returns></returns>
public async Task<Dictionary<string, string>?> LoadLanguage(string languageCode)
{
if (string.IsNullOrWhiteSpace(languageCode)) languageCode = "en";
var languageFile = _directoryService.FileSystem.Path.Join(_directoryService.LocalizationDirectory, languageCode + ".json");
if (!_directoryService.FileSystem.FileInfo.New(languageFile).Exists)
throw new ArgumentException($"Language {languageCode} does not exist");
var json = await _directoryService.FileSystem.File.ReadAllTextAsync(languageFile);
return JsonSerializer.Deserialize<Dictionary<string, string>>(json);
}
public async Task<string> Get(string locale, string key, params object[] args)
{
// Check if the translation for the given locale is cached
var cacheKey = $"{locale}_{key}";
if (!_cache.TryGetValue(cacheKey, out string? translatedString))
{
// Load the locale JSON file
var translationData = await LoadLanguage(locale);
// Find the translation for the given key
if (translationData != null && translationData.TryGetValue(key, out var value))
{
translatedString = value;
// Cache the translation for subsequent requests
_cache.Set(cacheKey, translatedString, _cacheOptions);
}
}
if (string.IsNullOrEmpty(translatedString))
{
if (!locale.Equals("en"))
{
return await Get("en", key, args);
}
return key;
}
// Format the translated string with arguments
if (args.Length > 0)
{
translatedString = string.Format(translatedString, args);
}
return translatedString;
}
/// <summary>
/// Returns a translated string for a given user's locale, falling back to english or the key if missing
/// </summary>
/// <param name="userId"></param>
/// <param name="key"></param>
/// <param name="args"></param>
/// <returns></returns>
public async Task<string> Translate(int userId, string key, params object[] args)
{
var userLocale = await _unitOfWork.UserRepository.GetLocale(userId);
return await Get(userLocale, key, args);
}
/// <summary>
/// Returns all available locales that exist on both the Frontend and the Backend
/// </summary>
/// <returns></returns>
public IEnumerable<string> GetLocales()
{
var uiLanguages = _directoryService
.GetFilesWithExtension(_directoryService.FileSystem.Path.GetFullPath(_localizationDirectoryUi), @"\.json")
.Select(f => _directoryService.FileSystem.Path.GetFileName(f).Replace(".json", string.Empty));
var backendLanguages = _directoryService
.GetFilesWithExtension(_directoryService.LocalizationDirectory, @"\.json")
.Select(f => _directoryService.FileSystem.Path.GetFileName(f).Replace(".json", string.Empty));
return uiLanguages.Intersect(backendLanguages).Distinct();
}
}

View file

@ -164,7 +164,7 @@ public class LicenseService : ILicenseService
var serverSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
var lic = await RegisterLicense(license, email);
if (string.IsNullOrWhiteSpace(lic))
throw new KavitaException("Unable to register license due to error. Reach out to Kavita+ Support");
throw new KavitaException("unable-to-register-k+");
serverSetting.Value = lic;
_unitOfWork.SettingsRepository.Update(serverSetting);
await _unitOfWork.CommitAsync();

View file

@ -60,6 +60,7 @@ public class ScrobblingService : IScrobblingService
private readonly IEventHub _eventHub;
private readonly ILogger<ScrobblingService> _logger;
private readonly ILicenseService _licenseService;
private readonly ILocalizationService _localizationService;
public const string AniListWeblinkWebsite = "https://anilist.co/manga/";
public const string MalWeblinkWebsite = "https://myanimelist.net/manga/";
@ -87,13 +88,15 @@ public class ScrobblingService : IScrobblingService
public ScrobblingService(IUnitOfWork unitOfWork, ITokenService tokenService,
IEventHub eventHub, ILogger<ScrobblingService> logger, ILicenseService licenseService)
IEventHub eventHub, ILogger<ScrobblingService> logger, ILicenseService licenseService,
ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_tokenService = tokenService;
_eventHub = eventHub;
_logger = logger;
_licenseService = licenseService;
_localizationService = localizationService;
FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
@ -184,11 +187,11 @@ public class ScrobblingService : IScrobblingService
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
{
throw new KavitaException("AniList Credentials have expired or not set");
throw new KavitaException(await _localizationService.Translate(userId, "unable-to-register-k+"));
}
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
if (series == null) throw new KavitaException("Series not found");
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
if (library is not {AllowScrobbling: true}) return;
if (library.Type == LibraryType.Comic) return;
@ -229,11 +232,11 @@ public class ScrobblingService : IScrobblingService
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
{
throw new KavitaException("AniList Credentials have expired or not set");
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
}
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
if (series == null) throw new KavitaException("Series not found");
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
if (library is not {AllowScrobbling: true}) return;
if (library.Type == LibraryType.Comic) return;
@ -273,11 +276,11 @@ public class ScrobblingService : IScrobblingService
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
{
throw new KavitaException("AniList Credentials have expired or not set");
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
}
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
if (series == null) throw new KavitaException("Series not found");
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId))
{
_logger.LogInformation("Series {SeriesName} is on UserId {UserId}'s hold list. Not scrobbling", series.Name, userId);
@ -338,11 +341,11 @@ public class ScrobblingService : IScrobblingService
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
{
throw new KavitaException("AniList Credentials have expired or not set");
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
}
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
if (series == null) throw new KavitaException("Series not found");
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
if (library is not {AllowScrobbling: true}) return;
if (library.Type == LibraryType.Comic) return;
@ -368,6 +371,7 @@ public class ScrobblingService : IScrobblingService
private async Task<int> GetRateLimit(string license, string aniListToken)
{
if (string.IsNullOrWhiteSpace(aniListToken)) return 0;
try
{
var response = await (Configuration.KavitaPlusApiUrl + "/api/scrobbling/rate-limit?accessToken=" + aniListToken)

View file

@ -117,7 +117,7 @@ public class ReaderService : IReaderService
{
var seenVolume = new Dictionary<int, bool>();
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
if (series == null) throw new KavitaException("Series suddenly doesn't exist, cannot mark as read");
if (series == null) throw new KavitaException("series-doesnt-exist");
foreach (var chapter in chapters)
{
var userProgress = GetUserProgressForChapter(user, chapter);
@ -202,8 +202,9 @@ public class ReaderService : IReaderService
if (user.Progresses == null)
{
throw new KavitaException("Progresses must exist on user");
throw new KavitaException("progress-must-exist");
}
try
{
userProgress =

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Serialization;
using API.Comparators;
using API.Data;
using API.Data.Repositories;
@ -17,7 +18,6 @@ using API.Services.Tasks.Scanner.Parser;
using API.SignalR;
using Kavita.Common;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
namespace API.Services;
@ -49,7 +49,7 @@ public interface IReadingListService
/// <summary>
/// Methods responsible for management of Reading Lists
/// </summary>
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, String)"/> to be called beforehand</remarks>
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, string)"/> to be called beforehand</remarks>
public class ReadingListService : IReadingListService
{
private readonly IUnitOfWork _unitOfWork;
@ -69,13 +69,13 @@ public class ReadingListService : IReadingListService
public static string FormatTitle(ReadingListItemDto item)
{
var title = string.Empty;
if (item.ChapterNumber == Tasks.Scanner.Parser.Parser.DefaultChapter && item.VolumeNumber != Tasks.Scanner.Parser.Parser.DefaultVolume) {
if (item.ChapterNumber == Parser.DefaultChapter && item.VolumeNumber != Parser.DefaultVolume) {
title = $"Volume {item.VolumeNumber}";
}
if (item.SeriesFormat == MangaFormat.Epub) {
var specialTitle = Tasks.Scanner.Parser.Parser.CleanSpecialTitle(item.ChapterNumber);
if (specialTitle == Tasks.Scanner.Parser.Parser.DefaultChapter)
var specialTitle = Parser.CleanSpecialTitle(item.ChapterNumber);
if (specialTitle == Parser.DefaultChapter)
{
if (!string.IsNullOrEmpty(item.ChapterTitleName))
{
@ -83,7 +83,7 @@ public class ReadingListService : IReadingListService
}
else
{
title = $"Volume {Tasks.Scanner.Parser.Parser.CleanSpecialTitle(item.VolumeNumber)}";
title = $"Volume {Parser.CleanSpecialTitle(item.VolumeNumber)}";
}
} else {
title = $"Volume {specialTitle}";
@ -92,12 +92,12 @@ public class ReadingListService : IReadingListService
var chapterNum = item.ChapterNumber;
if (!string.IsNullOrEmpty(chapterNum) && !JustNumbers.Match(item.ChapterNumber).Success) {
chapterNum = Tasks.Scanner.Parser.Parser.CleanSpecialTitle(item.ChapterNumber);
chapterNum = Parser.CleanSpecialTitle(item.ChapterNumber);
}
if (title != string.Empty) return title;
if (item.ChapterNumber == Tasks.Scanner.Parser.Parser.DefaultChapter &&
if (item.ChapterNumber == Parser.DefaultChapter &&
!string.IsNullOrEmpty(item.ChapterTitleName))
{
title = item.ChapterTitleName;
@ -124,13 +124,13 @@ public class ReadingListService : IReadingListService
var hasExisting = userWithReadingList.ReadingLists.Any(l => l.Title.Equals(title));
if (hasExisting)
{
throw new KavitaException("A list of this name already exists");
throw new KavitaException("reading-list-name-exists");
}
var readingList = new ReadingListBuilder(title).Build();
userWithReadingList.ReadingLists.Add(readingList);
if (!_unitOfWork.HasChanges()) throw new KavitaException("There was a problem creating list");
if (!_unitOfWork.HasChanges()) throw new KavitaException("generic-reading-list-create");
await _unitOfWork.CommitAsync();
return readingList;
}
@ -144,10 +144,10 @@ public class ReadingListService : IReadingListService
public async Task UpdateReadingList(ReadingList readingList, UpdateReadingListDto dto)
{
dto.Title = dto.Title.Trim();
if (string.IsNullOrEmpty(dto.Title)) throw new KavitaException("Title must be set");
if (string.IsNullOrEmpty(dto.Title)) throw new KavitaException("reading-list-title-required");
if (!dto.Title.Equals(readingList.Title) && await _unitOfWork.ReadingListRepository.ReadingListExists(dto.Title))
throw new KavitaException("Reading list already exists");
throw new KavitaException("reading-list-name-exists");
readingList.Summary = dto.Summary;
readingList.Title = dto.Title.Trim();
@ -192,7 +192,7 @@ public class ReadingListService : IReadingListService
/// <summary>
/// Removes all entries that are fully read from the reading list. This commits
/// </summary>
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, String)"/> to be called beforehand</remarks>
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, string)"/> to be called beforehand</remarks>
/// <param name="readingListId">Reading List Id</param>
/// <param name="user">User</param>
/// <returns></returns>
@ -404,7 +404,7 @@ public class ReadingListService : IReadingListService
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes))
.OrderBy(c => Tasks.Scanner.Parser.Parser.MinNumberFromRange(c.Volume.Name))
.OrderBy(c => Parser.MinNumberFromRange(c.Volume.Name))
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting)
.ToList();
@ -529,7 +529,7 @@ public class ReadingListService : IReadingListService
/// <param name="cblReading"></param>
public async Task<CblImportSummaryDto> ValidateCblFile(int userId, CblReadingList cblReading)
{
var importSummary = new CblImportSummaryDto()
var importSummary = new CblImportSummaryDto
{
CblName = cblReading.Name,
Success = CblImportResult.Success,
@ -542,20 +542,20 @@ public class ReadingListService : IReadingListService
if (await _unitOfWork.ReadingListRepository.ReadingListExists(cblReading.Name))
{
importSummary.Success = CblImportResult.Fail;
importSummary.Results.Add(new CblBookResult()
importSummary.Results.Add(new CblBookResult
{
Reason = CblImportReason.NameConflict,
ReadingListName = cblReading.Name
});
}
var uniqueSeries = cblReading.Books.Book.Select(b => Tasks.Scanner.Parser.Parser.Normalize(b.Series)).Distinct().ToList();
var uniqueSeries = cblReading.Books.Book.Select(b => Parser.Normalize(b.Series)).Distinct().ToList();
var userSeries =
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
if (!userSeries.Any())
{
// Report that no series exist in the reading list
importSummary.Results.Add(new CblBookResult()
importSummary.Results.Add(new CblBookResult
{
Reason = CblImportReason.AllSeriesMissing
});
@ -569,7 +569,7 @@ public class ReadingListService : IReadingListService
importSummary.Success = CblImportResult.Fail;
foreach (var conflict in conflicts)
{
importSummary.Results.Add(new CblBookResult()
importSummary.Results.Add(new CblBookResult
{
Reason = CblImportReason.SeriesCollision,
Series = conflict.Name,
@ -593,7 +593,7 @@ public class ReadingListService : IReadingListService
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.ReadingListsWithItems);
_logger.LogDebug("Importing {ReadingListName} CBL for User {UserName}", cblReading.Name, user!.UserName);
var importSummary = new CblImportSummaryDto()
var importSummary = new CblImportSummaryDto
{
CblName = cblReading.Name,
Success = CblImportResult.Success,
@ -601,13 +601,13 @@ public class ReadingListService : IReadingListService
SuccessfulInserts = new List<CblBookResult>()
};
var uniqueSeries = cblReading.Books.Book.Select(b => Tasks.Scanner.Parser.Parser.Normalize(b.Series)).Distinct().ToList();
var uniqueSeries = cblReading.Books.Book.Select(b => Parser.Normalize(b.Series)).Distinct().ToList();
var userSeries =
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
var allSeries = userSeries.ToDictionary(s => Tasks.Scanner.Parser.Parser.Normalize(s.Name));
var allSeriesLocalized = userSeries.ToDictionary(s => Tasks.Scanner.Parser.Parser.Normalize(s.LocalizedName));
var allSeries = userSeries.ToDictionary(s => Parser.Normalize(s.Name));
var allSeriesLocalized = userSeries.ToDictionary(s => Parser.Normalize(s.LocalizedName));
var readingListNameNormalized = Tasks.Scanner.Parser.Parser.Normalize(cblReading.Name);
var readingListNameNormalized = Parser.Normalize(cblReading.Name);
// Get all the user's reading lists
var allReadingLists = (user.ReadingLists).ToDictionary(s => s.NormalizedTitle);
if (!allReadingLists.TryGetValue(readingListNameNormalized, out var readingList))
@ -620,7 +620,7 @@ public class ReadingListService : IReadingListService
// Reading List exists, check if we own it
if (user.ReadingLists.All(l => l.NormalizedTitle != readingListNameNormalized))
{
importSummary.Results.Add(new CblBookResult()
importSummary.Results.Add(new CblBookResult
{
Reason = CblImportReason.NameConflict
});
@ -632,7 +632,7 @@ public class ReadingListService : IReadingListService
readingList.Items ??= new List<ReadingListItem>();
foreach (var (book, i) in cblReading.Books.Book.Select((value, i) => ( value, i )))
{
var normalizedSeries = Tasks.Scanner.Parser.Parser.Normalize(book.Series);
var normalizedSeries = Parser.Normalize(book.Series);
if (!allSeries.TryGetValue(normalizedSeries, out var bookSeries) && !allSeriesLocalized.TryGetValue(normalizedSeries, out bookSeries))
{
importSummary.Results.Add(new CblBookResult(book)
@ -644,7 +644,7 @@ public class ReadingListService : IReadingListService
}
// Prioritize lookup by Volume then Chapter, but allow fallback to just Chapter
var bookVolume = string.IsNullOrEmpty(book.Volume)
? Tasks.Scanner.Parser.Parser.DefaultVolume
? Parser.DefaultVolume
: book.Volume;
var matchingVolume = bookSeries.Volumes.Find(v => bookVolume == v.Name) ?? bookSeries.Volumes.Find(v => v.Number == 0);
if (matchingVolume == null)
@ -660,7 +660,7 @@ public class ReadingListService : IReadingListService
// We need to handle chapter 0 or empty string when it's just a volume
var bookNumber = string.IsNullOrEmpty(book.Number)
? Tasks.Scanner.Parser.Parser.DefaultChapter
? Parser.DefaultChapter
: book.Number;
var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.Number == bookNumber);
if (chapter == null)
@ -720,7 +720,7 @@ public class ReadingListService : IReadingListService
private static IList<Series> FindCblImportConflicts(IEnumerable<Series> userSeries)
{
var dict = new HashSet<string>();
return userSeries.Where(series => !dict.Add(Tasks.Scanner.Parser.Parser.Normalize(series.Name))).ToList();
return userSeries.Where(series => !dict.Add(Parser.Normalize(series.Name))).ToList();
}
private static bool IsCblEmpty(CblReadingList cblReading, CblImportSummaryDto importSummary,
@ -729,7 +729,7 @@ public class ReadingListService : IReadingListService
readingListFromCbl = new CblImportSummaryDto();
if (cblReading.Books == null || cblReading.Books.Book.Count == 0)
{
importSummary.Results.Add(new CblBookResult()
importSummary.Results.Add(new CblBookResult
{
Reason = CblImportReason.EmptyFile
});
@ -755,7 +755,7 @@ public class ReadingListService : IReadingListService
public static CblReadingList LoadCblFromPath(string path)
{
var reader = new System.Xml.Serialization.XmlSerializer(typeof(CblReadingList));
var reader = new XmlSerializer(typeof(CblReadingList));
using var file = new StreamReader(path);
var cblReadingList = (CblReadingList) reader.Deserialize(file);
file.Close();

View file

@ -30,6 +30,12 @@ public interface ISeriesService
Task<bool> DeleteMultipleSeries(IList<int> seriesIds);
Task<bool> UpdateRelatedSeries(UpdateRelatedSeriesDto dto);
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
Task<string> FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true);
Task<string> FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true);
Task<string> FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string? chapterTitle,
bool withHash);
Task<string> FormatChapterName(int userId, LibraryType libraryType, bool withHash = false);
}
public class SeriesService : ISeriesService
@ -39,15 +45,17 @@ public class SeriesService : ISeriesService
private readonly ITaskScheduler _taskScheduler;
private readonly ILogger<SeriesService> _logger;
private readonly IScrobblingService _scrobblingService;
private readonly ILocalizationService _localizationService;
public SeriesService(IUnitOfWork unitOfWork, IEventHub eventHub, ITaskScheduler taskScheduler,
ILogger<SeriesService> logger, IScrobblingService scrobblingService)
ILogger<SeriesService> logger, IScrobblingService scrobblingService, ILocalizationService localizationService)
{
_unitOfWork = unitOfWork;
_eventHub = eventHub;
_taskScheduler = taskScheduler;
_logger = logger;
_scrobblingService = scrobblingService;
_localizationService = localizationService;
}
/// <summary>
@ -382,16 +390,17 @@ public class SeriesService : ISeriesService
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
var libraryIds = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId);
if (!libraryIds.Contains(series.LibraryId))
throw new UnauthorizedAccessException("User does not have access to the library this series belongs to");
throw new UnauthorizedAccessException("user-no-access-library-from-series");
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user!.AgeRestriction != AgeRating.NotApplicable)
{
var seriesMetadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId);
if (seriesMetadata!.AgeRating > user.AgeRestriction)
throw new UnauthorizedAccessException("User is not allowed to view this series due to age restrictions");
throw new UnauthorizedAccessException("series-restricted-age-restriction");
}
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
.OrderBy(v => Tasks.Scanner.Parser.Parser.MinNumberFromRange(v.Name))
@ -401,13 +410,14 @@ public class SeriesService : ISeriesService
var processedVolumes = new List<VolumeDto>();
if (libraryType == LibraryType.Book)
{
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
foreach (var volume in volumes)
{
volume.Chapters = volume.Chapters.OrderBy(d => double.Parse(d.Number), ChapterSortComparer.Default).ToList();
var firstChapter = volume.Chapters.First();
// On Books, skip volumes that are specials, since these will be shown
if (firstChapter.IsSpecial) continue;
RenameVolumeName(firstChapter, volume, libraryType);
RenameVolumeName(firstChapter, volume, libraryType, volumeLabel);
processedVolumes.Add(volume);
}
}
@ -431,7 +441,7 @@ public class SeriesService : ISeriesService
foreach (var chapter in chapters)
{
chapter.Title = FormatChapterTitle(chapter, libraryType);
chapter.Title = await FormatChapterTitle(userId, chapter, libraryType);
if (!chapter.IsSpecial) continue;
if (!string.IsNullOrEmpty(chapter.TitleName)) chapter.Title = chapter.TitleName;
@ -481,7 +491,7 @@ public class SeriesService : ISeriesService
return !chapter.IsSpecial && !chapter.Number.Equals(Tasks.Scanner.Parser.Parser.DefaultChapter);
}
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType)
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume")
{
if (libraryType == LibraryType.Book)
{
@ -496,19 +506,19 @@ public class SeriesService : ISeriesService
{
volume.Name += $" - {firstChapter.TitleName}";
}
else
{
volume.Name += $"";
}
// else
// {
// volume.Name += $"";
// }
return;
}
volume.Name = $"Volume {volume.Name}";
volume.Name = $"{volumeLabel} {volume.Name}".Trim();
}
private static string FormatChapterTitle(bool isSpecial, LibraryType libraryType, string? chapterTitle, bool withHash)
public async Task<string> FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string? chapterTitle, bool withHash)
{
if (string.IsNullOrEmpty(chapterTitle)) throw new ArgumentException("Chapter Title cannot be null");
@ -520,32 +530,33 @@ public class SeriesService : ISeriesService
var hashSpot = withHash ? "#" : string.Empty;
return libraryType switch
{
LibraryType.Book => $"Book {chapterTitle}",
LibraryType.Comic => $"Issue {hashSpot}{chapterTitle}",
LibraryType.Manga => $"Chapter {chapterTitle}",
_ => "Chapter "
LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle),
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle),
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterTitle),
_ => await _localizationService.Translate(userId, "chapter-num", ' ')
};
}
public static string FormatChapterTitle(ChapterDto chapter, LibraryType libraryType, bool withHash = true)
public async Task<string> FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true)
{
return FormatChapterTitle(chapter.IsSpecial, libraryType, chapter.Title, withHash);
return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Title, withHash);
}
public static string FormatChapterTitle(Chapter chapter, LibraryType libraryType, bool withHash = true)
public async Task<string> FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true)
{
return FormatChapterTitle(chapter.IsSpecial, libraryType, chapter.Title, withHash);
return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Title, withHash);
}
public static string FormatChapterName(LibraryType libraryType, bool withHash = false)
public async Task<string> FormatChapterName(int userId, LibraryType libraryType, bool withHash = false)
{
return libraryType switch
var hashSpot = withHash ? "#" : string.Empty;
return (libraryType switch
{
LibraryType.Manga => "Chapter",
LibraryType.Comic => withHash ? "Issue #" : "Issue",
LibraryType.Book => "Book",
_ => "Chapter"
};
LibraryType.Book => await _localizationService.Translate(userId, "book-num", string.Empty),
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, string.Empty),
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", string.Empty),
_ => await _localizationService.Translate(userId, "chapter-num", ' ')
}).Trim();
}
/// <summary>

View file

@ -36,7 +36,7 @@ public interface IProcessSeries
void UpdateVolumes(Series series, IList<ParserInfo> parsedInfos, bool forceUpdate = false);
void UpdateChapters(Series series, Volume volume, IList<ParserInfo> parsedInfos, bool forceUpdate = false);
void AddOrUpdateFileForChapter(Chapter chapter, ParserInfo info, bool forceUpdate = false);
void UpdateChapterFromComicInfo(Chapter chapter, ComicInfo? comicInfo);
void UpdateChapterFromComicInfo(Chapter chapter, ComicInfo? comicInfo, bool forceUpdate = false);
}
/// <summary>
@ -146,6 +146,7 @@ public class ProcessSeries : IProcessSeries
_logger.LogInformation("[ScannerService] Processing series {SeriesName}", series.OriginalName);
// parsedInfos[0] is not the first volume or chapter. We need to find it using a ComicInfo check (as it uses firstParsedInfo for series sort)
// BUG: This check doesn't work for Books, as books usually have metadata on all files. (#2167)
var firstParsedInfo = parsedInfos.FirstOrDefault(p => p.ComicInfo != null, firstInfo);
UpdateVolumes(series, parsedInfos, forceUpdate);
@ -534,11 +535,11 @@ public class ProcessSeries : IProcessSeries
foreach (var chapter in volume.Chapters)
{
var firstFile = chapter.Files.MinBy(x => x.Chapter);
if (firstFile == null || _cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, firstFile)) continue;
if (firstFile == null || _cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, forceUpdate, firstFile)) continue;
try
{
var firstChapterInfo = infos.SingleOrDefault(i => i.FullFilePath.Equals(firstFile.FilePath));
UpdateChapterFromComicInfo(chapter, firstChapterInfo?.ComicInfo);
UpdateChapterFromComicInfo(chapter, firstChapterInfo?.ComicInfo, forceUpdate);
}
catch (Exception ex)
{
@ -660,12 +661,12 @@ public class ProcessSeries : IProcessSeries
}
}
public void UpdateChapterFromComicInfo(Chapter chapter, ComicInfo? comicInfo)
public void UpdateChapterFromComicInfo(Chapter chapter, ComicInfo? comicInfo, bool forceUpdate = false)
{
if (comicInfo == null) return;
var firstFile = chapter.Files.MinBy(x => x.Chapter);
if (firstFile == null ||
_cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, firstFile)) return;
_cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, forceUpdate, firstFile)) return;
_logger.LogTrace("[ScannerService] Read ComicInfo for {File}", firstFile.FilePath);

View file

@ -258,7 +258,7 @@ public class ScannerService : IScannerService
return;
}
await _processSeries.ProcessSeriesAsync(parsedFiles, library);
await _processSeries.ProcessSeriesAsync(parsedFiles, library, bypassFolderOptimizationChecks);
parsedSeries.Add(foundParsedSeries, parsedFiles);
}

View file

@ -36,14 +36,13 @@ public class ThemeService : IThemeService
/// </summary>
/// <param name="themeId"></param>
/// <returns></returns>
[AllowAnonymous]
public async Task<string> GetContent(int themeId)
{
var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId);
if (theme == null) throw new KavitaException("Theme file missing or invalid");
if (theme == null) throw new KavitaException("theme-doesnt-exist");
var themeFile = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, theme.FileName);
if (string.IsNullOrEmpty(themeFile) || !_directoryService.FileSystem.File.Exists(themeFile))
throw new KavitaException("Theme file missing or invalid");
throw new KavitaException("theme-doesnt-exist");
return await _directoryService.FileSystem.File.ReadAllTextAsync(themeFile);
}
@ -151,7 +150,7 @@ public class ThemeService : IThemeService
try
{
var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId);
if (theme == null) throw new KavitaException("Theme file missing or invalid");
if (theme == null) throw new KavitaException("theme-doesnt-exist");
foreach (var siteTheme in await _unitOfWork.SiteThemeRepository.GetThemes())
{

View file

@ -2,6 +2,7 @@
"TokenKey": "super secret unguessable key that is longer because we require it",
"Port": 5000,
"IpAddresses": "",
"BaseUrl": "/",
"Cache": 90
"BaseUrl": "/test/",
"Cache": 90,
"XFrameOrigins": "SAMEORIGIN"
}

View file

@ -4,7 +4,7 @@
<TargetFramework>net7.0</TargetFramework>
<Company>kavitareader.com</Company>
<Product>Kavita</Product>
<AssemblyVersion>0.7.6.0</AssemblyVersion>
<AssemblyVersion>0.7.7.0</AssemblyVersion>
<NeutralLanguage>en</NeutralLanguage>
<TieredPGO>true</TieredPGO>
</PropertyGroup>
@ -14,7 +14,7 @@
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.5.0.73987">
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.7.0.75501">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -7,6 +7,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=epubs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=kavitaignore/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=kavitaignores/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=langs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=MACOSX/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=noopener/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=noreferrer/@EntryIndexedValue">True</s:Boolean>

View file

@ -14,20 +14,24 @@ your reading collection with your friends and family!
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=Kareadita_Kavita&metric=security_rating)](https://sonarcloud.io/dashboard?id=Kareadita_Kavita)
[![Backers on Open Collective](https://opencollective.com/kavita/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/kavita/sponsors/badge.svg)](#sponsors)
<a href="https://hosted.weblate.org/engage/kavita/">
<img src="https://hosted.weblate.org/widgets/kavita/-/ui/svg-badge.svg" alt="Translation status" />
</a>
<img src="https://img.shields.io/endpoint?url=https://stats.kavitareader.com/api/ui/shield-badge"/>
</div>
## Goals
- [x] Serve up Manga/Webtoons/Comics (cbr, cbz, zip/rar/rar5, 7zip, raw images) and Books (epub, pdf)
- [x] First class responsive readers that work great on any device (phone, tablet, desktop)
- [x] Dark mode and customizable theming support
- [x] External metadata integration and scrobbling for read status, ratings, and reviews (available via Kavita+)
- [x] Rich Metadata support with filtering and searching
- [x] Ways to group reading material: Collections, Reading Lists, Want to Read
- [x] Ability to manage users, access, and ratings
- [x] Fully Accessible with active accessibility audits
- [x] Dedicated webtoon reading mode
- [ ] Full localization support
- [ ] And so much [more...](https://github.com/Kareadita/Kavita/projects)
## What Kavita Provides
- Serve up Manga/Webtoons/Comics (cbr, cbz, zip/rar/rar5, 7zip, raw images) and Books (epub, pdf)
- First class responsive readers that work great on any device (phone, tablet, desktop)
- Dark mode and customizable theming support
- External metadata integration and scrobbling for read status, ratings, and reviews (available via Kavita+)
- Rich Metadata support with filtering and searching
- Ways to group reading material: Collections, Reading Lists (CBL Import), Want to Read
- Ability to manage users with rich Role-based management for age restrictions, abilities within the app, etc
- Rich web readers supporting webtoon, continuous reading mode (continue without leaving the reader), virtual pages (epub), etc
- Full Localization Support
## Support
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/KavitaManga/)
@ -101,6 +105,13 @@ Thank you to [<img src="/Logo/jetbrains.svg" alt="" width="32"> JetBrains](http:
## Palace-Designs
We would like to extend a big thank you to [<img src="/Logo/hosting-sponsor.png" alt="" width="128">](https://www.palace-designs.com/) who hosts our infrastructure pro-bono.
## Localization
Thank you to [Weblate](https://hosted.weblate.org/engage/kavita/) who hosts our localization infrastructure pro-bono. If you want to see Kavita in your language, please help us localize.
<a href="https://hosted.weblate.org/engage/kavita/">
<img src="https://hosted.weblate.org/widgets/kavita/-/horizontal-blue.svg" alt="Translation status" />
</a>
## Huntr
We would like to extend a big thank you to [Huntr](https://huntr.dev/repos/kareadita/kavita) who has worked with Kavita in reporting security vulnerabilities. If you are interested in
being paid to help secure Kavita, please give them a try.

16
UI/Web/minify-json.js Normal file
View file

@ -0,0 +1,16 @@
const fs = require('fs');
const jsonminify = require('jsonminify');
const jsonFilesDir = 'dist/assets/langs'; // Adjust the path to your JSON files
const outputDir = 'dist/assets/langs'; // Directory to store minified files
fs.readdirSync(jsonFilesDir).forEach(file => {
if (file.endsWith('.json')) {
const filePath = `${jsonFilesDir}/${file}`;
const content = fs.readFileSync(filePath, 'utf8');
const minifiedContent = jsonminify(content);
const outputFile = `${outputDir}/${file}`;
fs.writeFileSync(outputFile, minifiedContent, 'utf8');
console.log(`Minified: ${file}`);
}
});

746
UI/Web/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,33 +5,39 @@
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"prod": "ng build --configuration production --aot --output-hashing=all",
"minify-langs": "node minify-json.js",
"prod": "ng build --configuration production --aot --output-hashing=all && npm run minify-langs",
"explore": "ng build --stats-json && webpack-bundle-analyzer dist/stats.json",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "^16.1.6",
"@angular/cdk": "^16.1.5",
"@angular/common": "^16.1.6",
"@angular/compiler": "^16.1.6",
"@angular/core": "^16.1.6",
"@angular/forms": "^16.1.6",
"@angular/localize": "^16.1.6",
"@angular/platform-browser": "^16.1.6",
"@angular/platform-browser-dynamic": "^16.1.6",
"@angular/router": "^16.1.6",
"@fortawesome/fontawesome-free": "^6.4.0",
"@angular/animations": "^16.1.8",
"@angular/cdk": "^16.1.7",
"@angular/common": "^16.1.8",
"@angular/compiler": "^16.1.8",
"@angular/core": "^16.1.8",
"@angular/forms": "^16.1.8",
"@angular/localize": "^16.1.8",
"@angular/platform-browser": "^16.1.8",
"@angular/platform-browser-dynamic": "^16.1.8",
"@angular/router": "^16.1.8",
"@fortawesome/fontawesome-free": "^6.4.2",
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
"@iplab/ngx-file-upload": "^16.0.1",
"@microsoft/signalr": "^7.0.9",
"@ng-bootstrap/ng-bootstrap": "^15.1.0",
"@microsoft/signalr": "^7.0.10",
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
"@ngneat/transloco": "^5.0.6",
"@ngneat/transloco-locale": "^5.1.1",
"@ngneat/transloco-persist-lang": "^5.0.0",
"@ngneat/transloco-persist-translations": "^5.0.0",
"@ngneat/transloco-preload-langs": "^5.0.0",
"@popperjs/core": "^2.11.7",
"@swimlane/ngx-charts": "^20.1.2",
"@tweenjs/tween.js": "^21.0.0",
"@types/file-saver": "^2.0.5",
"bootstrap": "^5.2.3",
"bootstrap": "^5.3.1",
"eventsource": "^2.0.2",
"file-saver": "^2.0.5",
"lazysizes": "^5.3.2",
@ -45,23 +51,24 @@
"rxjs": "^7.8.0",
"screenfull": "^6.0.2",
"swiper": "^8.4.6",
"tslib": "^2.3.0",
"tslib": "^2.6.1",
"zone.js": "^0.13.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^16.1.5",
"@angular-devkit/build-angular": "^16.1.8",
"@angular-eslint/builder": "^16.1.0",
"@angular-eslint/eslint-plugin": "^16.1.0",
"@angular-eslint/eslint-plugin-template": "^16.1.0",
"@angular-eslint/schematics": "^16.1.0",
"@angular-eslint/template-parser": "^16.1.0",
"@angular/cli": "^16.1.5",
"@angular/compiler-cli": "^16.1.6",
"@angular/cli": "^16.1.8",
"@angular/compiler-cli": "^16.1.8",
"@types/d3": "^7.4.0",
"@types/node": "^20.4.4",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"eslint": "^8.45.0",
"@types/node": "^20.4.8",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"eslint": "^8.46.0",
"jsonminify": "^0.4.2",
"karma-coverage": "~2.2.0",
"ts-node": "~10.9.1",
"typescript": "^5.1.6",

View file

@ -4,22 +4,24 @@ import { ToastrService } from 'ngx-toastr';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { AccountService } from '../_services/account.service';
import {TranslocoService} from "@ngneat/transloco";
@Injectable({
providedIn: 'root'
})
export class AdminGuard implements CanActivate {
constructor(private accountService: AccountService, private toastr: ToastrService) {}
constructor(private accountService: AccountService, private toastr: ToastrService,
private translocoService: TranslocoService) {}
canActivate(): Observable<boolean> {
// this automaticallys subs due to being router guard
// this automatically subs due to being router guard
return this.accountService.currentUser$.pipe(take(1),
map((user) => {
if (user && this.accountService.hasAdminRole(user)) {
return true;
}
this.toastr.error('You are not authorized to view this page.');
this.toastr.error(this.translocoService.translate('toasts.unauthorized-1'));
return false;
})
);

View file

@ -4,13 +4,17 @@ import { ToastrService } from 'ngx-toastr';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { AccountService } from '../_services/account.service';
import {TranslocoService} from "@ngneat/transloco";
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
public urlKey: string = 'kavita--auth-intersection-url';
constructor(private accountService: AccountService, private router: Router, private toastr: ToastrService) {}
constructor(private accountService: AccountService,
private router: Router,
private toastr: ToastrService,
private translocoService: TranslocoService) {}
canActivate(): Observable<boolean> {
return this.accountService.currentUser$.pipe(take(1),
@ -18,8 +22,10 @@ export class AuthGuard implements CanActivate {
if (user) {
return true;
}
if (this.toastr.toasts.filter(toast => toast.message === 'Unauthorized' || toast.message === 'You are not authorized to view this page.').length === 0) {
this.toastr.error('You are not authorized to view this page.');
const errorMessage = this.translocoService.translate('toasts.unauthorized-1');
const errorMessage2 = this.translocoService.translate('toasts.unauthorized-2');
if (this.toastr.toasts.filter(toast => toast.message === errorMessage2 || toast.message === errorMessage).length === 0) {
this.toastr.error(errorMessage);
}
localStorage.setItem(this.urlKey, window.location.pathname);
this.router.navigateByUrl('/login');

View file

@ -1,4 +1,4 @@
import {inject, Injectable} from '@angular/core';
import {Injectable} from '@angular/core';
import {
HttpRequest,
HttpHandler,
@ -10,10 +10,13 @@ import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { catchError } from 'rxjs/operators';
import { AccountService } from '../_services/account.service';
import {translate, TranslocoService} from "@ngneat/transloco";
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private router: Router, private toastr: ToastrService, private accountService: AccountService) {}
constructor(private router: Router, private toastr: ToastrService,
private accountService: AccountService,
private translocoService: TranslocoService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
@ -38,8 +41,9 @@ export class ErrorInterceptor implements HttpInterceptor {
break;
default:
// Don't throw multiple Something unexpected went wrong
if (this.toastr.previousToastMessage !== 'Something unexpected went wrong.') {
this.toastr.error('Something unexpected went wrong.');
let genericError = translate('errors.generic');
if (this.toastr.previousToastMessage !== 'Something unexpected went wrong.' && this.toastr.previousToastMessage !== genericError) {
this.toast(genericError);
}
break;
}
@ -81,36 +85,36 @@ export class ErrorInterceptor implements HttpInterceptor {
console.error('error:', error);
if (error.statusText === 'Bad Request') {
if (error.error instanceof Blob) {
this.toastr.error('There was an issue downloading this file or you do not have permissions', error.status);
this.toast('errors.download', error.status);
return;
}
this.toastr.error(error.error, error.status + ' Error');
this.toast(error.error, this.translocoService.translate('errors.error-code', {num: error.status}));
} else {
this.toastr.error(error.statusText === 'OK' ? error.error : error.statusText, error.status + ' Error');
this.toast(error.statusText === 'OK' ? error.error : error.statusText, this.translocoService.translate('errors.error-code', {num: error.status}));
}
}
}
private handleNotFound(error: any) {
this.toastr.error('That url does not exist.');
this.toast('errors.not-found');
}
private handleServerException(error: any) {
const err = error.error;
if (err.hasOwnProperty('message') && err.message.trim() !== '') {
if (err.message != 'User is not authenticated') {
if (err.message != 'User is not authenticated' && error.message !== 'errors.user-not-auth') {
console.error('500 error: ', error);
}
this.toastr.error(err.message);
this.toast(err.message);
} else if (error.hasOwnProperty('message') && error.message.trim() !== '') {
if (error.message != 'User is not authenticated') {
if (error.message !== 'User is not authenticated' && error.message !== 'errors.user-not-auth') {
console.error('500 error: ', error);
}
// This just throws duplicate errors for no reason
//this.toastr.error(error.message);
//this.toast(error.message);
}
else {
this.toastr.error('There was an unknown critical error.');
this.toast('errors.unknown-crit');
console.error('500 error:', error);
}
}
@ -125,4 +129,14 @@ export class ErrorInterceptor implements HttpInterceptor {
// if statement is due to http/2 spec issue: https://github.com/angular/angular/issues/23334
this.accountService.logout();
}
// Assume the title is already translated
private toast(message: string, title?: string) {
if (message.startsWith('errors.')) {
this.toastr.error(this.translocoService.translate(message), title);
} else {
this.toastr.error(message, title);
}
}
}

View file

@ -1,4 +1,4 @@
import {inject, Injectable} from '@angular/core';
import {Injectable} from '@angular/core';
import {
HttpRequest,
HttpHandler,

View file

@ -1,7 +1,5 @@
import { HourEstimateRange } from './series-detail/hour-estimate-range';
import { MangaFile } from './manga-file';
import { AgeRating } from './metadata/age-rating';
import { AgeRatingDto } from './metadata/age-rating-dto';
/**
* Chapter table object. This does not have metadata on it, use ChapterMetadata which is the same Chapter but with those fields.
@ -21,7 +19,7 @@ export interface Chapter {
pagesRead: number; // Attached for the given user when requesting from API
isSpecial: boolean;
title: string;
created: string;
createdUtc: string;
/**
* Actual name of the Chapter if populated in underlying metadata
*/

View file

@ -6,4 +6,5 @@ export interface Device {
platform: DevicePlatform;
emailAddress: string;
lastUsed: string;
lastUsedUtc: string;
}

View file

@ -2,6 +2,5 @@ export interface Job {
id: string;
title: string;
cron: string;
createdAt: string;
lastExecution: string;
lastExecutionUtc: string;
}

View file

@ -23,4 +23,8 @@
* Inner HTML
*/
content: string;
/**
* Key for translation
*/
translationKey: string;
}

View file

@ -42,13 +42,14 @@ export interface Preferences {
noTransitions: boolean;
collapseSeriesRelationships: boolean;
shareReviews: boolean;
locale: string;
}
export const readingDirections = [{text: 'Left to Right', value: ReadingDirection.LeftToRight}, {text: 'Right to Left', value: ReadingDirection.RightToLeft}];
export const bookWritingStyles = [{text: 'Horizontal', value: WritingStyle.Horizontal}, {text: 'Vertical', value: WritingStyle.Vertical}];
export const scalingOptions = [{text: 'Automatic', value: ScalingOption.Automatic}, {text: 'Fit to Height', value: ScalingOption.FitToHeight}, {text: 'Fit to Width', value: ScalingOption.FitToWidth}, {text: 'Original', value: ScalingOption.Original}];
export const pageSplitOptions = [{text: 'Fit to Screen', value: PageSplitOption.FitSplit}, {text: 'Right to Left', value: PageSplitOption.SplitRightToLeft}, {text: 'Left to Right', value: PageSplitOption.SplitLeftToRight}, {text: 'No Split', value: PageSplitOption.NoSplit}];
export const readingModes = [{text: 'Left to Right', value: ReaderMode.LeftRight}, {text: 'Up to Down', value: ReaderMode.UpDown}, {text: 'Webtoon', value: ReaderMode.Webtoon}];
export const layoutModes = [{text: 'Single', value: LayoutMode.Single}, {text: 'Double', value: LayoutMode.Double}, {text: 'Double (Manga)', value: LayoutMode.DoubleReversed}]; // , {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover}
export const bookLayoutModes = [{text: 'Scroll', value: BookPageLayoutMode.Default}, {text: '1 Column', value: BookPageLayoutMode.Column1}, {text: '2 Column', value: BookPageLayoutMode.Column2}];
export const pageLayoutModes = [{text: 'Cards', value: PageLayoutMode.Cards}, {text: 'List', value: PageLayoutMode.List}];
export const readingDirections = [{text: 'left-to-right', value: ReadingDirection.LeftToRight}, {text: 'right-to-left', value: ReadingDirection.RightToLeft}];
export const bookWritingStyles = [{text: 'horizontal', value: WritingStyle.Horizontal}, {text: 'vertical', value: WritingStyle.Vertical}];
export const scalingOptions = [{text: 'automatic', value: ScalingOption.Automatic}, {text: 'fit-to-height', value: ScalingOption.FitToHeight}, {text: 'fit-to-width', value: ScalingOption.FitToWidth}, {text: 'original', value: ScalingOption.Original}];
export const pageSplitOptions = [{text: 'fit-to-screen', value: PageSplitOption.FitSplit}, {text: 'right-to-left', value: PageSplitOption.SplitRightToLeft}, {text: 'left-to-right', value: PageSplitOption.SplitLeftToRight}, {text: 'no-split', value: PageSplitOption.NoSplit}];
export const readingModes = [{text: 'left-to-right', value: ReaderMode.LeftRight}, {text: 'up-to-down', value: ReaderMode.UpDown}, {text: 'webtoon', value: ReaderMode.Webtoon}];
export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // , {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover}
export const bookLayoutModes = [{text: 'scroll', value: BookPageLayoutMode.Default}, {text: '1-column', value: BookPageLayoutMode.Column1}, {text: '2-column', value: BookPageLayoutMode.Column2}];
export const pageLayoutModes = [{text: 'cards', value: PageLayoutMode.Cards}, {text: 'list', value: PageLayoutMode.List}];

View file

@ -1,4 +1,3 @@
import { Series } from "../../series";
import { CblBookResult } from "./cbl-book-result";
import { CblImportResult } from "./cbl-import-result.enum";

View file

@ -14,8 +14,8 @@ export interface ScrobbleEvent {
scrobbleEventType: ScrobbleEventType;
rating: number | null;
processedDateUtc: string;
lastModified: string;
created: string;
lastModifiedUtc: string;
createdUtc: string;
volumeNumber: number | null;
chapterNumber: number | null;
}

View file

@ -5,8 +5,8 @@ export interface Volume {
id: number;
number: number;
name: string;
created: string;
lastModified: string;
createdUtc: string;
lastModifiedUtc: string;
pages: number;
pagesRead: number;
chapters: Array<Chapter>;

View file

@ -32,7 +32,8 @@ export class AccountService {
private readonly destroyRef = inject(DestroyRef);
baseUrl = environment.apiUrl;
userKey = 'kavita-user';
public lastLoginKey = 'kavita-lastlogin';
public static lastLoginKey = 'kavita-lastlogin';
public static localeKey = 'kavita-locale';
private currentUser: User | undefined;
// Stores values, when someone subscribes gives (1) of last values seen.
@ -134,7 +135,7 @@ export class AccountService {
Array.isArray(roles) ? user.roles = roles : user.roles.push(roles);
localStorage.setItem(this.userKey, JSON.stringify(user));
localStorage.setItem(this.lastLoginKey, user.username);
localStorage.setItem(AccountService.lastLoginKey, user.username);
if (user.preferences && user.preferences.theme) {
this.themeService.setTheme(user.preferences.theme.name);
} else {
@ -147,15 +148,11 @@ export class AccountService {
this.currentUser = user;
this.currentUserSource.next(user);
if (user) {
this.messageHub.createHubConnection(user, this.hasAdminRole(user));
}
this.hasValidLicense().subscribe();
this.stopRefreshTokenTimer();
if (this.currentUser !== undefined) {
if (this.currentUser) {
this.messageHub.createHubConnection(this.currentUser, this.hasAdminRole(this.currentUser));
this.hasValidLicense().subscribe();
this.startRefreshTokenTimer();
}
}
@ -270,6 +267,9 @@ export class AccountService {
if (this.currentUser !== undefined && this.currentUser !== null) {
this.currentUser.preferences = settings;
this.setCurrentUser(this.currentUser);
// Update the locale on disk (for logout and compact-number pipe)
localStorage.setItem(AccountService.localeKey, this.currentUser.preferences.locale);
}
return settings;
}), takeUntilDestroyed(this.destroyRef));

View file

@ -192,27 +192,27 @@ export class ActionFactoryService {
this.libraryActions = [
{
action: Action.Scan,
title: 'Scan Library',
title: 'scan-library',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.Submenu,
title: 'Others',
title: 'others',
callback: this.dummyCallback,
requiresAdmin: true,
children: [
{
action: Action.RefreshMetadata,
title: 'Refresh Covers',
title: 'refresh-covers',
callback: this.dummyCallback,
requiresAdmin: true,
children: [],
},
{
action: Action.AnalyzeFiles,
title: 'Analyze Files',
title: 'analyze-files',
callback: this.dummyCallback,
requiresAdmin: true,
children: [],
@ -221,7 +221,7 @@ export class ActionFactoryService {
},
{
action: Action.Edit,
title: 'Settings',
title: 'settings',
callback: this.dummyCallback,
requiresAdmin: true,
children: [],
@ -231,7 +231,7 @@ export class ActionFactoryService {
this.collectionTagActions = [
{
action: Action.Edit,
title: 'Edit',
title: 'edit',
callback: this.dummyCallback,
requiresAdmin: true,
children: [],
@ -241,55 +241,55 @@ export class ActionFactoryService {
this.seriesActions = [
{
action: Action.MarkAsRead,
title: 'Mark as Read',
title: 'mark-as-read',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.MarkAsUnread,
title: 'Mark as Unread',
title: 'mark-as-unread',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.Scan,
title: 'Scan Series',
title: 'scan-series',
callback: this.dummyCallback,
requiresAdmin: true,
children: [],
},
{
action: Action.Submenu,
title: 'Add to',
title: 'add-to',
callback: this.dummyCallback,
requiresAdmin: false,
children: [
{
action: Action.AddToWantToReadList,
title: 'Add to Want to Read',
title: 'add-to-want-to-read',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.RemoveFromWantToReadList,
title: 'Remove from Want to Read',
title: 'remove-from-want-to-read',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.AddToReadingList,
title: 'Add to Reading List',
title: 'add-to-reading-list',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.AddToCollection,
title: 'Add to Collection',
title: 'add-to-collection',
callback: this.dummyCallback,
requiresAdmin: true,
children: [],
@ -298,7 +298,7 @@ export class ActionFactoryService {
},
{
action: Action.Submenu,
title: 'Send To',
title: 'send-to',
callback: this.dummyCallback,
requiresAdmin: false,
children: [
@ -316,27 +316,27 @@ export class ActionFactoryService {
},
{
action: Action.Submenu,
title: 'Others',
title: 'others',
callback: this.dummyCallback,
requiresAdmin: true,
children: [
{
action: Action.RefreshMetadata,
title: 'Refresh Covers',
title: 'refresh-covers',
callback: this.dummyCallback,
requiresAdmin: true,
children: [],
},
{
action: Action.AnalyzeFiles,
title: 'Analyze Files',
title: 'analyze-files',
callback: this.dummyCallback,
requiresAdmin: true,
children: [],
},
{
action: Action.Delete,
title: 'Delete',
title: 'delete',
callback: this.dummyCallback,
requiresAdmin: true,
class: 'danger',
@ -346,14 +346,14 @@ export class ActionFactoryService {
},
{
action: Action.Download,
title: 'Download',
title: 'download',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.Edit,
title: 'Edit',
title: 'edit',
callback: this.dummyCallback,
requiresAdmin: true,
children: [],
@ -363,34 +363,34 @@ export class ActionFactoryService {
this.volumeActions = [
{
action: Action.IncognitoRead,
title: 'Read Incognito',
title: 'read-incognito',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.MarkAsRead,
title: 'Mark as Read',
title: 'mark-as-read',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.MarkAsUnread,
title: 'Mark as Unread',
title: 'mark-as-unread',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.Submenu,
title: 'Add to',
title: 'add-to',
callback: this.dummyCallback,
requiresAdmin: false,
children: [
{
action: Action.AddToReadingList,
title: 'Add to Reading List',
title: 'add-to-reading-list',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
@ -399,7 +399,7 @@ export class ActionFactoryService {
},
{
action: Action.Submenu,
title: 'Send To',
title: 'send-to',
callback: this.dummyCallback,
requiresAdmin: false,
children: [
@ -417,14 +417,14 @@ export class ActionFactoryService {
},
{
action: Action.Download,
title: 'Download',
title: 'download',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.Edit,
title: 'Details',
title: 'details',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
@ -434,34 +434,34 @@ export class ActionFactoryService {
this.chapterActions = [
{
action: Action.IncognitoRead,
title: 'Read Incognito',
title: 'read-incognito',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.MarkAsRead,
title: 'Mark as Read',
title: 'mark-as-read',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.MarkAsUnread,
title: 'Mark as Unread',
title: 'mark-as-unread',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.Submenu,
title: 'Add to',
title: 'add-to',
callback: this.dummyCallback,
requiresAdmin: false,
children: [
{
action: Action.AddToReadingList,
title: 'Add to Reading List',
title: 'add-to-reading-list',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
@ -470,7 +470,7 @@ export class ActionFactoryService {
},
{
action: Action.Submenu,
title: 'Send To',
title: 'send-to',
callback: this.dummyCallback,
requiresAdmin: false,
children: [
@ -489,14 +489,14 @@ export class ActionFactoryService {
// RBS will handle rendering this, so non-admins with download are appicable
{
action: Action.Download,
title: 'Download',
title: 'download',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.Edit,
title: 'Details',
title: 'details',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
@ -506,14 +506,14 @@ export class ActionFactoryService {
this.readingListActions = [
{
action: Action.Edit,
title: 'Edit',
title: 'edit',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.Delete,
title: 'Delete',
title: 'delete',
callback: this.dummyCallback,
requiresAdmin: false,
class: 'danger',
@ -524,21 +524,21 @@ export class ActionFactoryService {
this.bookmarkActions = [
{
action: Action.ViewSeries,
title: 'View Series',
title: 'view-series',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.DownloadBookmark,
title: 'Download',
title: 'download',
callback: this.dummyCallback,
requiresAdmin: false,
children: [],
},
{
action: Action.Delete,
title: 'Clear',
title: 'clear',
callback: this.dummyCallback,
class: 'danger',
requiresAdmin: false,

Some files were not shown because too many files have changed in this diff Show more