From ec5b9f2941219faecdf52beca642aa0b197ccb97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 21:29:28 +0000 Subject: [PATCH 01/30] Bump brace-expansion from 1.1.11 to 1.1.12 in /UI/Web Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12. - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12) --- updated-dependencies: - dependency-name: brace-expansion dependency-version: 1.1.12 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- UI/Web/package-lock.json | 50 ++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index cfce8cded..445982f5c 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -541,7 +541,6 @@ "version": "19.2.5", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.5.tgz", "integrity": "sha512-b2cG41r6lilApXLlvja1Ra2D00dM3BxmQhoElKC1tOnpD6S3/krlH1DOnBB2I55RBn9iv4zdmPz1l8zPUSh7DQ==", - "dev": true, "dependencies": { "@babel/core": "7.26.9", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -569,7 +568,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", - "dev": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -584,7 +582,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", - "dev": true, "engines": { "node": ">= 14.16.0" }, @@ -1442,10 +1439,11 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1524,10 +1522,11 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4522,9 +4521,10 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -4906,8 +4906,7 @@ "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "node_modules/cosmiconfig": { "version": "8.3.6", @@ -5354,7 +5353,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -5364,7 +5362,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -5572,10 +5569,11 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6622,10 +6620,11 @@ } }, "node_modules/karma-coverage/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8181,8 +8180,7 @@ "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "dev": true + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, "node_modules/replace-in-file": { "version": "7.1.0", @@ -8403,7 +8401,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "devOptional": true }, "node_modules/sass": { "version": "1.85.0", @@ -8468,7 +8466,6 @@ "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -9093,7 +9090,6 @@ "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From c52ed1f65de28a7df8729634b4c7b1dea726d084 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Sat, 14 Jun 2025 12:14:04 -0500 Subject: [PATCH 02/30] Browse by Genre/Tag/Person with new metadata system for People (#3835) Co-authored-by: Stepan Goremykin Co-authored-by: goremykin Co-authored-by: Christopher <39032787+MrRobotjs@users.noreply.github.com> Co-authored-by: Fesaa <77553571+Fesaa@users.noreply.github.com> --- API.Tests/Services/ImageServiceTests.cs | 8 +- API/API.csproj | 2 +- API/Controllers/MetadataController.cs | 34 + API/Controllers/PersonController.cs | 10 +- API/DTOs/Filtering/PersonSortField.cs | 8 + API/DTOs/Filtering/SortOptions.cs | 9 + API/DTOs/Filtering/v2/FilterField.cs | 9 +- API/DTOs/Filtering/v2/FilterStatementDto.cs | 11 +- API/DTOs/Filtering/v2/FilterV2Dto.cs | 2 +- .../KavitaPlus/Manage/ManageMatchFilterDto.cs | 4 + API/DTOs/Metadata/Browse/BrowseGenreDto.cs | 13 + .../Browse}/BrowsePersonDto.cs | 6 +- API/DTOs/Metadata/Browse/BrowseTagDto.cs | 13 + .../Browse/Requests/BrowsePersonFilterDto.cs | 27 + API/DTOs/Metadata/GenreTagDto.cs | 2 +- API/DTOs/Metadata/TagDto.cs | 2 +- API/DTOs/ReadingLists/ReadingListDto.cs | 5 + API/DTOs/UserReadingProfileDto.cs | 3 + ...DisableWidthOverrideBreakPoint.Designer.cs | 3701 +++++++++++++++++ ...ngProfileDisableWidthOverrideBreakPoint.cs | 29 + .../Migrations/DataContextModelSnapshot.cs | 5 +- .../ExternalSeriesMetadataRepository.cs | 6 +- API/Data/Repositories/GenreRepository.cs | 27 + API/Data/Repositories/PersonRepository.cs | 100 +- API/Data/Repositories/SeriesRepository.cs | 6 +- API/Data/Repositories/TagRepository.cs | 27 + API/Data/Seed.cs | 2 +- API/Entities/AppUserReadingProfile.cs | 17 + API/Entities/SideNavStreamType.cs | 2 +- API/Extensions/EnumerableExtensions.cs | 13 + .../QueryExtensions/Filtering/PersonFilter.cs | 136 + .../QueryExtensions/QueryableExtensions.cs | 24 + .../RestrictByAgeExtensions.cs | 38 +- API/Helpers/AutoMapperProfiles.cs | 3 +- .../PersonFilterFieldValueConverter.cs | 31 + API/Services/ImageService.cs | 6 +- API/Services/Plus/ExternalMetadataService.cs | 28 +- API/Services/Plus/LicenseService.cs | 13 +- API/Services/ReadingProfileService.cs | 1 + API/Services/Tasks/Metadata/CoverDbService.cs | 42 +- API/Services/Tasks/Scanner/Parser/Parser.cs | 6 + Kavita.Common/Configuration.cs | 2 +- README.md | 7 +- UI/Web/src/_tag-card-common.scss | 30 + .../_models/kavitaplus/manage-match-filter.ts | 2 + UI/Web/src/app/_models/library/library.ts | 2 + .../_models/metadata/browse/browse-genre.ts | 6 + .../browse}/browse-person.ts | 4 +- .../app/_models/metadata/browse/browse-tag.ts | 6 + UI/Web/src/app/_models/metadata/language.ts | 5 +- UI/Web/src/app/_models/metadata/person.ts | 20 +- .../src/app/_models/metadata/series-filter.ts | 12 +- .../metadata/v2/browse-person-filter.ts | 8 + .../app/_models/metadata/v2/filter-field.ts | 3 +- .../_models/metadata/v2/filter-statement.ts | 9 +- .../src/app/_models/metadata/v2/filter-v2.ts | 11 + .../metadata/v2/person-filter-field.ts | 12 + .../_models/metadata/v2/person-sort-field.ts | 9 + .../_models/metadata/v2/series-filter-v2.ts | 11 - .../app/_models/metadata/v2/sort-options.ts | 17 + .../_models/preferences/reading-profiles.ts | 3 + UI/Web/src/app/_models/wiki.ts | 3 +- UI/Web/src/app/_pipes/breakpoint.pipe.ts | 25 + UI/Web/src/app/_pipes/browse-title.pipe.ts | 78 + .../app/_pipes/generic-filter-field.pipe.ts | 108 + UI/Web/src/app/_pipes/person-role.pipe.ts | 8 +- UI/Web/src/app/_pipes/sort-field.pipe.ts | 30 +- .../src/app/_resolvers/url-filter.resolver.ts | 22 + .../app/_routes/all-series-routing.module.ts | 12 +- .../app/_routes/bookmark-routing.module.ts | 12 +- .../_routes/browse-authors-routing.module.ts | 8 - .../src/app/_routes/browse-routing.module.ts | 24 + .../app/_routes/collections-routing.module.ts | 14 +- .../_routes/library-detail-routing.module.ts | 21 +- .../_routes/want-to-read-routing.module.ts | 10 +- UI/Web/src/app/_services/account.service.ts | 27 + UI/Web/src/app/_services/filter.service.ts | 10 +- UI/Web/src/app/_services/jumpbar.service.ts | 22 +- UI/Web/src/app/_services/metadata.service.ts | 157 +- UI/Web/src/app/_services/person.service.ts | 20 +- UI/Web/src/app/_services/reader.service.ts | 5 +- UI/Web/src/app/_services/series.service.ts | 50 +- UI/Web/src/app/_services/toggle.service.ts | 8 +- .../match-series-result-item.component.html | 2 +- .../sort-button/sort-button.component.html | 9 + .../sort-button/sort-button.component.scss | 0 .../sort-button/sort-button.component.ts | 21 + .../manage-matched-metadata.component.html | 13 +- .../manage-matched-metadata.component.ts | 26 +- .../all-series/all-series.component.html | 4 +- .../all-series/all-series.component.ts | 69 +- UI/Web/src/app/app-routing.module.ts | 13 +- UI/Web/src/app/app.component.ts | 1 + .../bookmarks/bookmarks.component.ts | 45 +- .../browse-people/browse-authors.component.ts | 87 - .../browse-genres.component.html | 34 + .../browse-genres.component.scss | 1 + .../browse-genres/browse-genres.component.ts | 68 + .../browse-people.component.html} | 13 +- .../browse-people.component.scss} | 2 + .../browse-people/browse-people.component.ts | 128 + .../browse-tags/browse-tags.component.html | 34 + .../browse-tags/browse-tags.component.scss | 1 + .../browse-tags/browse-tags.component.ts | 67 + .../card-detail-layout.component.html | 30 +- .../card-detail-layout.component.ts | 86 +- .../cards/card-item/card-item.component.ts | 2 +- .../person-card/person-card.component.ts | 2 +- .../collection-detail.component.ts | 41 +- .../_components/dashboard.component.ts | 8 +- .../library-detail.component.ts | 31 +- .../infinite-scroller.component.html | 3 +- .../infinite-scroller.component.ts | 53 +- .../manga-reader/manga-reader.component.html | 6 +- .../manga-reader/manga-reader.component.ts | 14 +- .../single-renderer.component.html | 2 +- .../single-renderer.component.ts | 38 +- .../metadata-builder.component.html | 4 +- .../metadata-builder.component.ts | 23 +- .../metadata-filter-row.component.html | 17 +- .../metadata-filter-row.component.ts | 303 +- .../app/metadata-filter/filter-settings.ts | 36 +- .../metadata-filter.component.html | 28 +- .../metadata-filter.component.ts | 185 +- .../nav-header/nav-header.component.html | 2 + .../nav-header/nav-header.component.ts | 7 +- .../person-detail/person-detail.component.ts | 10 +- .../reading-lists/reading-lists.component.ts | 3 +- .../series-detail/series-detail.component.ts | 17 - .../app/shared/_services/download.service.ts | 32 +- .../_services/filter-utilities.service.ts | 305 +- .../app/shared/_services/utility.service.ts | 65 +- .../dashboard-stream-list-item.component.ts | 5 +- .../side-nav/side-nav.component.html | 2 +- .../side-nav/side-nav.component.ts | 2 +- .../sidenav-stream-list-item.component.ts | 1 - .../library-settings-modal.component.ts | 18 +- .../preference-nav.component.html | 2 +- .../preference-nav.component.ts | 9 +- .../manage-reading-profiles.component.html | 28 +- .../manage-reading-profiles.component.scss | 2 - .../manage-reading-profiles.component.ts | 13 +- .../volume-detail.component.html | 2 +- .../want-to-read/want-to-read.component.ts | 87 +- UI/Web/src/assets/langs/en.json | 86 +- UI/Web/src/theme/components/_sidenav.scss | 32 +- UI/Web/src/theme/themes/dark.scss | 6 + 147 files changed, 6612 insertions(+), 958 deletions(-) create mode 100644 API/DTOs/Filtering/PersonSortField.cs create mode 100644 API/DTOs/Metadata/Browse/BrowseGenreDto.cs rename API/DTOs/{Person => Metadata/Browse}/BrowsePersonDto.cs (71%) create mode 100644 API/DTOs/Metadata/Browse/BrowseTagDto.cs create mode 100644 API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs create mode 100644 API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs create mode 100644 API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs create mode 100644 API/Extensions/QueryExtensions/Filtering/PersonFilter.cs create mode 100644 API/Helpers/Converters/PersonFilterFieldValueConverter.cs create mode 100644 UI/Web/src/_tag-card-common.scss create mode 100644 UI/Web/src/app/_models/metadata/browse/browse-genre.ts rename UI/Web/src/app/_models/{person => metadata/browse}/browse-person.ts (52%) create mode 100644 UI/Web/src/app/_models/metadata/browse/browse-tag.ts create mode 100644 UI/Web/src/app/_models/metadata/v2/browse-person-filter.ts create mode 100644 UI/Web/src/app/_models/metadata/v2/filter-v2.ts create mode 100644 UI/Web/src/app/_models/metadata/v2/person-filter-field.ts create mode 100644 UI/Web/src/app/_models/metadata/v2/person-sort-field.ts delete mode 100644 UI/Web/src/app/_models/metadata/v2/series-filter-v2.ts create mode 100644 UI/Web/src/app/_models/metadata/v2/sort-options.ts create mode 100644 UI/Web/src/app/_pipes/breakpoint.pipe.ts create mode 100644 UI/Web/src/app/_pipes/browse-title.pipe.ts create mode 100644 UI/Web/src/app/_pipes/generic-filter-field.pipe.ts create mode 100644 UI/Web/src/app/_resolvers/url-filter.resolver.ts delete mode 100644 UI/Web/src/app/_routes/browse-authors-routing.module.ts create mode 100644 UI/Web/src/app/_routes/browse-routing.module.ts create mode 100644 UI/Web/src/app/_single-module/sort-button/sort-button.component.html create mode 100644 UI/Web/src/app/_single-module/sort-button/sort-button.component.scss create mode 100644 UI/Web/src/app/_single-module/sort-button/sort-button.component.ts delete mode 100644 UI/Web/src/app/browse-people/browse-authors.component.ts create mode 100644 UI/Web/src/app/browse/browse-genres/browse-genres.component.html create mode 100644 UI/Web/src/app/browse/browse-genres/browse-genres.component.scss create mode 100644 UI/Web/src/app/browse/browse-genres/browse-genres.component.ts rename UI/Web/src/app/{browse-people/browse-authors.component.html => browse/browse-people/browse-people.component.html} (60%) rename UI/Web/src/app/{browse-people/browse-authors.component.scss => browse/browse-people/browse-people.component.scss} (64%) create mode 100644 UI/Web/src/app/browse/browse-people/browse-people.component.ts create mode 100644 UI/Web/src/app/browse/browse-tags/browse-tags.component.html create mode 100644 UI/Web/src/app/browse/browse-tags/browse-tags.component.scss create mode 100644 UI/Web/src/app/browse/browse-tags/browse-tags.component.ts diff --git a/API.Tests/Services/ImageServiceTests.cs b/API.Tests/Services/ImageServiceTests.cs index a1073a55b..f2c87e1ad 100644 --- a/API.Tests/Services/ImageServiceTests.cs +++ b/API.Tests/Services/ImageServiceTests.cs @@ -161,10 +161,10 @@ public class ImageServiceTests private static void GenerateColorImage(string hexColor, string outputPath) { - var color = ImageService.HexToRgb(hexColor); - using var colorImage = Image.Black(200, 100); - using var output = colorImage + new[] { color.R / 255.0, color.G / 255.0, color.B / 255.0 }; - output.WriteToFile(outputPath); + var (r, g, b) = ImageService.HexToRgb(hexColor); + using var blackImage = Image.Black(200, 100); + using var colorImage = blackImage.NewFromImage(r, g, b); + colorImage.WriteToFile(outputPath); } private void GenerateHtmlFileForColorScape() diff --git a/API/API.csproj b/API/API.csproj index f9a889d74..4eed66f22 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -97,9 +97,9 @@ + - diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index 10a5f393a..cab33692a 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -6,8 +6,10 @@ using System.Threading.Tasks; using API.Constants; using API.Data; using API.Data.Repositories; +using API.DTOs; using API.DTOs.Filtering; using API.DTOs.Metadata; +using API.DTOs.Metadata.Browse; using API.DTOs.Person; using API.DTOs.Recommendation; using API.DTOs.SeriesDetail; @@ -46,6 +48,22 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(User.GetUserId(), ids, context)); } + /// + /// Returns a list of Genres with counts for counts when Genre is on Series/Chapter + /// + /// + [HttpPost("genres-with-counts")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)] + public async Task>> GetBrowseGenres(UserParams? userParams = null) + { + userParams ??= UserParams.Default; + + var list = await unitOfWork.GenreRepository.GetBrowseableGenre(User.GetUserId(), userParams); + Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); + + return Ok(list); + } + /// /// Fetches people from the instance by role /// @@ -95,6 +113,22 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc return Ok(await unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(User.GetUserId())); } + /// + /// Returns a list of Tags with counts for counts when Tag is on Series/Chapter + /// + /// + [HttpPost("tags-with-counts")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)] + public async Task>> GetBrowseTags(UserParams? userParams = null) + { + userParams ??= UserParams.Default; + + var list = await unitOfWork.TagRepository.GetBrowseableTag(User.GetUserId(), userParams); + Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); + + return Ok(list); + } + /// /// Fetches all age ratings from the instance /// diff --git a/API/Controllers/PersonController.cs b/API/Controllers/PersonController.cs index a2ab3bf88..bf3cc1814 100644 --- a/API/Controllers/PersonController.cs +++ b/API/Controllers/PersonController.cs @@ -4,6 +4,9 @@ using System.Threading.Tasks; using API.Data; using API.Data.Repositories; using API.DTOs; +using API.DTOs.Filtering.v2; +using API.DTOs.Metadata.Browse; +using API.DTOs.Metadata.Browse.Requests; using API.DTOs.Person; using API.Entities.Enums; using API.Extensions; @@ -77,11 +80,13 @@ public class PersonController : BaseApiController /// /// [HttpPost("all")] - public async Task>> GetAuthorsForBrowse([FromQuery] UserParams? userParams) + public async Task>> GetPeopleForBrowse(BrowsePersonFilterDto filter, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; - var list = await _unitOfWork.PersonRepository.GetAllWritersAndSeriesCount(User.GetUserId(), userParams); + + var list = await _unitOfWork.PersonRepository.GetBrowsePersonDtos(User.GetUserId(), filter, userParams); Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); + return Ok(list); } @@ -112,6 +117,7 @@ public class PersonController : BaseApiController person.Name = dto.Name?.Trim(); + person.NormalizedName = person.Name.ToNormalized(); person.Description = dto.Description ?? string.Empty; person.CoverImageLocked = dto.CoverImageLocked; diff --git a/API/DTOs/Filtering/PersonSortField.cs b/API/DTOs/Filtering/PersonSortField.cs new file mode 100644 index 000000000..5268a1bf9 --- /dev/null +++ b/API/DTOs/Filtering/PersonSortField.cs @@ -0,0 +1,8 @@ +namespace API.DTOs.Filtering; + +public enum PersonSortField +{ + Name = 1, + SeriesCount = 2, + ChapterCount = 3 +} diff --git a/API/DTOs/Filtering/SortOptions.cs b/API/DTOs/Filtering/SortOptions.cs index a08e2968e..18f2b17ea 100644 --- a/API/DTOs/Filtering/SortOptions.cs +++ b/API/DTOs/Filtering/SortOptions.cs @@ -8,3 +8,12 @@ public sealed record SortOptions public SortField SortField { get; set; } public bool IsAscending { get; set; } = true; } + +/// +/// All Sorting Options for a query related to Person Entity +/// +public sealed record PersonSortOptions +{ + public PersonSortField SortField { get; set; } + public bool IsAscending { get; set; } = true; +} diff --git a/API/DTOs/Filtering/v2/FilterField.cs b/API/DTOs/Filtering/v2/FilterField.cs index 5323f2b48..246a92a90 100644 --- a/API/DTOs/Filtering/v2/FilterField.cs +++ b/API/DTOs/Filtering/v2/FilterField.cs @@ -56,5 +56,12 @@ public enum FilterField /// Last time User Read /// ReadLast = 32, - +} + +public enum PersonFilterField +{ + Role = 1, + Name = 2, + SeriesCount = 3, + ChapterCount = 4, } diff --git a/API/DTOs/Filtering/v2/FilterStatementDto.cs b/API/DTOs/Filtering/v2/FilterStatementDto.cs index ebe6d16af..8c99bd24c 100644 --- a/API/DTOs/Filtering/v2/FilterStatementDto.cs +++ b/API/DTOs/Filtering/v2/FilterStatementDto.cs @@ -1,4 +1,6 @@ -namespace API.DTOs.Filtering.v2; +using API.DTOs.Metadata.Browse.Requests; + +namespace API.DTOs.Filtering.v2; public sealed record FilterStatementDto { @@ -6,3 +8,10 @@ public sealed record FilterStatementDto public FilterField Field { get; set; } public string Value { get; set; } } + +public sealed record PersonFilterStatementDto +{ + public FilterComparison Comparison { get; set; } + public PersonFilterField Field { get; set; } + public string Value { get; set; } +} diff --git a/API/DTOs/Filtering/v2/FilterV2Dto.cs b/API/DTOs/Filtering/v2/FilterV2Dto.cs index 11dc42a6b..a247a17a6 100644 --- a/API/DTOs/Filtering/v2/FilterV2Dto.cs +++ b/API/DTOs/Filtering/v2/FilterV2Dto.cs @@ -16,7 +16,7 @@ public sealed record FilterV2Dto /// The name of the filter /// public string? Name { get; set; } - public ICollection Statements { get; set; } = new List(); + public ICollection Statements { get; set; } = []; public FilterCombination Combination { get; set; } = FilterCombination.And; public SortOptions? SortOptions { get; set; } diff --git a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs b/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs index 8eb38c98a..c394cf8d4 100644 --- a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs +++ b/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs @@ -15,5 +15,9 @@ public enum MatchStateOption public sealed record ManageMatchFilterDto { public MatchStateOption MatchStateOption { get; set; } = MatchStateOption.All; + /// + /// Library Type in int form. -1 indicates to ignore the field. + /// + public int LibraryType { get; set; } = -1; public string SearchTerm { get; set; } = string.Empty; } diff --git a/API/DTOs/Metadata/Browse/BrowseGenreDto.cs b/API/DTOs/Metadata/Browse/BrowseGenreDto.cs new file mode 100644 index 000000000..8044c7914 --- /dev/null +++ b/API/DTOs/Metadata/Browse/BrowseGenreDto.cs @@ -0,0 +1,13 @@ +namespace API.DTOs.Metadata.Browse; + +public sealed record BrowseGenreDto : GenreTagDto +{ + /// + /// Number of Series this Entity is on + /// + public int SeriesCount { get; set; } + /// + /// Number of Chapters this Entity is on + /// + public int ChapterCount { get; set; } +} diff --git a/API/DTOs/Person/BrowsePersonDto.cs b/API/DTOs/Metadata/Browse/BrowsePersonDto.cs similarity index 71% rename from API/DTOs/Person/BrowsePersonDto.cs rename to API/DTOs/Metadata/Browse/BrowsePersonDto.cs index c7d318e79..20f84b783 100644 --- a/API/DTOs/Person/BrowsePersonDto.cs +++ b/API/DTOs/Metadata/Browse/BrowsePersonDto.cs @@ -1,6 +1,6 @@ using API.DTOs.Person; -namespace API.DTOs; +namespace API.DTOs.Metadata.Browse; /// /// Used to browse writers and click in to see their series @@ -12,7 +12,7 @@ public class BrowsePersonDto : PersonDto /// public int SeriesCount { get; set; } /// - /// Number or Issues this Person is the Writer for + /// Number of Issues this Person is the Writer for /// - public int IssueCount { get; set; } + public int ChapterCount { get; set; } } diff --git a/API/DTOs/Metadata/Browse/BrowseTagDto.cs b/API/DTOs/Metadata/Browse/BrowseTagDto.cs new file mode 100644 index 000000000..9a71876e3 --- /dev/null +++ b/API/DTOs/Metadata/Browse/BrowseTagDto.cs @@ -0,0 +1,13 @@ +namespace API.DTOs.Metadata.Browse; + +public sealed record BrowseTagDto : TagDto +{ + /// + /// Number of Series this Entity is on + /// + public int SeriesCount { get; set; } + /// + /// Number of Chapters this Entity is on + /// + public int ChapterCount { get; set; } +} diff --git a/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs b/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs new file mode 100644 index 000000000..d41cf37f3 --- /dev/null +++ b/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using API.DTOs.Filtering; +using API.DTOs.Filtering.v2; +using API.Entities.Enums; + +namespace API.DTOs.Metadata.Browse.Requests; +#nullable enable + +public sealed record BrowsePersonFilterDto +{ + /// + /// Not used - For parity with Series Filter + /// + public int Id { get; set; } + /// + /// Not used - For parity with Series Filter + /// + public string? Name { get; set; } + public ICollection Statements { get; set; } = []; + public FilterCombination Combination { get; set; } = FilterCombination.And; + public PersonSortOptions? SortOptions { get; set; } + + /// + /// Limit the number of rows returned. Defaults to not applying a limit (aka 0) + /// + public int LimitTo { get; set; } = 0; +} diff --git a/API/DTOs/Metadata/GenreTagDto.cs b/API/DTOs/Metadata/GenreTagDto.cs index 4846048d2..13a339d38 100644 --- a/API/DTOs/Metadata/GenreTagDto.cs +++ b/API/DTOs/Metadata/GenreTagDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Metadata; -public sealed record GenreTagDto +public record GenreTagDto { public int Id { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/Metadata/TagDto.cs b/API/DTOs/Metadata/TagDto.cs index f8deb6913..f5c925e1f 100644 --- a/API/DTOs/Metadata/TagDto.cs +++ b/API/DTOs/Metadata/TagDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Metadata; -public sealed record TagDto +public record TagDto { public int Id { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/ReadingLists/ReadingListDto.cs b/API/DTOs/ReadingLists/ReadingListDto.cs index cbc16275d..47a526411 100644 --- a/API/DTOs/ReadingLists/ReadingListDto.cs +++ b/API/DTOs/ReadingLists/ReadingListDto.cs @@ -49,6 +49,11 @@ public sealed record ReadingListDto : IHasCoverImage /// public required AgeRating AgeRating { get; set; } = AgeRating.Unknown; + /// + /// Username of the User that owns (in the case of a promoted list) + /// + public string OwnerUserName { get; set; } + public void ResetColorScape() { PrimaryColor = string.Empty; diff --git a/API/DTOs/UserReadingProfileDto.cs b/API/DTOs/UserReadingProfileDto.cs index 23f67ce4d..24dbf1c34 100644 --- a/API/DTOs/UserReadingProfileDto.cs +++ b/API/DTOs/UserReadingProfileDto.cs @@ -64,6 +64,9 @@ public sealed record UserReadingProfileDto /// public int? WidthOverride { get; set; } + /// + public BreakPoint DisableWidthOverride { get; set; } = BreakPoint.Never; + #endregion #region EpubReader diff --git a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs b/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs new file mode 100644 index 000000000..0e9f00b4e --- /dev/null +++ b/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs @@ -0,0 +1,3701 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint")] + partial class AppUserReadingProfileDisableWidthOverrideBreakPoint + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("HasRunScrobbleEventGeneration") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("MalAccessToken") + .HasColumnType("TEXT"); + + b.Property("MalUserName") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventGenerationRan") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserChapterRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("MissingSeriesFromSource") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("SourceUrl") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TotalSourceCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserCollection"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(4); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserDashboardStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Host") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserExternalSource"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserOnDeckRemoval"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAutomaticWebtoonReaderDetection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AniListScrobblingEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BlurUnreadSummaries") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("CollapseSeriesRelationships") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("en"); + + b.Property("NoTransitions") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PdfScrollMode") + .HasColumnType("INTEGER"); + + b.Property("PdfSpreadMode") + .HasColumnType("INTEGER"); + + b.Property("PdfTheme") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.Property("WantToReadSync") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAutomaticWebtoonReaderDetection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("DisableWidthOverride") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("LibraryIds") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PdfScrollMode") + .HasColumnType("INTEGER"); + + b.Property("PdfSpreadMode") + .HasColumnType("INTEGER"); + + b.Property("PdfTheme") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("SeriesIds") + .HasColumnType("TEXT"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("WidthOverride") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserReadingProfiles"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserWantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AverageExternalRating") + .HasColumnType("REAL"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("ISBNLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ReleaseDateLocked") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("REAL"); + + b.Property("SortOrderLocked") + .HasColumnType("INTEGER"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TitleNameLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DeliveryStatus") + .HasColumnType("TEXT"); + + b.Property("EmailTemplate") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SendDate") + .HasColumnType("TEXT"); + + b.Property("Sent") + .HasColumnType("INTEGER"); + + b.Property("Subject") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); + + b.ToTable("EmailHistory"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("RanAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ManualMigrationHistory"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowMetadataMatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Pattern") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryExcludePattern"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FileTypeGroup") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryFileTypeGroup"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bytes") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("AverageScore") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ProviderUrl") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalRating"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("CoverUrl") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("ExternalRecommendation"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("RawBody") + .HasColumnType("TEXT"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("SiteUrl") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalReview"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AverageExternalRating") + .HasColumnType("INTEGER"); + + b.Property("CbrId") + .HasColumnType("INTEGER"); + + b.Property("GoogleBooksId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("ValidUntilUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.ToTable("ExternalSeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastChecked") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("SeriesBlacklist"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DestinationType") + .HasColumnType("INTEGER"); + + b.Property("DestinationValue") + .HasColumnType("TEXT"); + + b.Property("ExcludeFromSource") + .HasColumnType("INTEGER"); + + b.Property("MetadataSettingsId") + .HasColumnType("INTEGER"); + + b.Property("SourceType") + .HasColumnType("INTEGER"); + + b.Property("SourceValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MetadataSettingsId"); + + b.ToTable("MetadataFieldMapping"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRatingMappings") + .HasColumnType("TEXT"); + + b.Property("Blacklist") + .HasColumnType("TEXT"); + + b.Property("EnableChapterCoverImage") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterPublisher") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterReleaseDate") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterTitle") + .HasColumnType("INTEGER"); + + b.Property("EnableCoverImage") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("EnableGenres") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalizedName") + .HasColumnType("INTEGER"); + + b.Property("EnablePeople") + .HasColumnType("INTEGER"); + + b.Property("EnablePublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("EnableRelationships") + .HasColumnType("INTEGER"); + + b.Property("EnableStartDate") + .HasColumnType("INTEGER"); + + b.Property("EnableSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableTags") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("FirstLastPeopleNaming") + .HasColumnType("INTEGER"); + + b.Property("Overrides") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("PersonRoles") + .HasColumnType("TEXT"); + + b.Property("Whitelist") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("ChapterPeople"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("Asin") + .HasColumnType("TEXT"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("HardcoverId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Alias") + .HasColumnType("TEXT"); + + b.Property("NormalizedAlias") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PersonId"); + + b.ToTable("PersonAlias"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.Property("SeriesMetadataId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.HasKey("SeriesMetadataId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ErrorDetails") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsErrored") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DontMatch") + .HasColumnType("INTEGER"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsBlacklisted") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("LowestFolderPath") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("CompatibleVersion") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("GitHubPath") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PreviewUrls") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ShaHash") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LookupName") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.Property("CollectionsId") + .HasColumnType("INTEGER"); + + b.Property("ItemsId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionsId", "ItemsId"); + + b.HasIndex("ItemsId"); + + b.ToTable("AppUserCollectionSeries"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.Property("ExternalRatingsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRatingExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.Property("ExternalRecommendationsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRecommendationExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.Property("ExternalReviewsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalReviewExternalSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ChapterRatings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Ratings") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Collections") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingProfiles") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryExcludePatterns") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryFileTypes") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalRatings") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalReviews") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("ExternalSeriesMetadata") + .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") + .WithMany("FieldMappings") + .HasForeignKey("MetadataSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("People") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("ChapterPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("Aliases") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("SeriesMetadataPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") + .WithMany("People") + .HasForeignKey("SeriesMetadataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + + b.Navigation("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.HasOne("API.Entities.AppUserCollection", null) + .WithMany() + .HasForeignKey("CollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRating", null) + .WithMany() + .HasForeignKey("ExternalRatingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) + .WithMany() + .HasForeignKey("ExternalRecommendationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("ChapterRatings"); + + b.Navigation("Collections"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ReadingProfiles"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("ExternalRatings"); + + b.Navigation("ExternalReviews"); + + b.Navigation("Files"); + + b.Navigation("People"); + + b.Navigation("Ratings"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("LibraryExcludePatterns"); + + b.Navigation("LibraryFileTypes"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Navigation("FieldMappings"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Navigation("Aliases"); + + b.Navigation("ChapterPeople"); + + b.Navigation("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("ExternalSeriesMetadata"); + + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("RelationOf"); + + b.Navigation("Relations"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs b/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs new file mode 100644 index 000000000..11a554bdf --- /dev/null +++ b/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class AppUserReadingProfileDisableWidthOverrideBreakPoint : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DisableWidthOverride", + table: "AppUserReadingProfiles", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DisableWidthOverride", + table: "AppUserReadingProfiles"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 25db64e2a..e777bbf7c 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -665,6 +665,9 @@ namespace API.Data.Migrations .HasColumnType("TEXT") .HasDefaultValue("Dark"); + b.Property("DisableWidthOverride") + .HasColumnType("INTEGER"); + b.Property("EmulateBook") .HasColumnType("INTEGER"); @@ -704,7 +707,7 @@ namespace API.Data.Migrations b.Property("ScalingOption") .HasColumnType("INTEGER"); - b.PrimitiveCollection("SeriesIds") + b.Property("SeriesIds") .HasColumnType("TEXT"); b.Property("ShowScreenHints") diff --git a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs index 45882b5c4..377344a3c 100644 --- a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs +++ b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs @@ -108,14 +108,17 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor public async Task NeedsDataRefresh(int seriesId) { + // TODO: Add unit test var row = await _context.ExternalSeriesMetadata .Where(s => s.SeriesId == seriesId) .FirstOrDefaultAsync(); + return row == null || row.ValidUntilUtc <= DateTime.UtcNow; } public async Task GetSeriesDetailPlusDto(int seriesId) { + // TODO: Add unit test var seriesDetailDto = await _context.ExternalSeriesMetadata .Where(m => m.SeriesId == seriesId) .Include(m => m.ExternalRatings) @@ -144,7 +147,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); - IEnumerable reviews = new List(); + IEnumerable reviews = []; if (seriesDetailDto.ExternalReviews != null && seriesDetailDto.ExternalReviews.Any()) { reviews = seriesDetailDto.ExternalReviews @@ -231,6 +234,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor .Include(s => s.ExternalSeriesMetadata) .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) .Where(s => s.Library.AllowMetadataMatching) + .WhereIf(filter.LibraryType >= 0, s => s.Library.Type == (LibraryType) filter.LibraryType) .FilterMatchState(filter.MatchStateOption) .OrderBy(s => s.NormalizedName) .ProjectTo(_mapper.ConfigurationProvider) diff --git a/API/Data/Repositories/GenreRepository.cs b/API/Data/Repositories/GenreRepository.cs index d9bc20c99..3e645cb2e 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/API/Data/Repositories/GenreRepository.cs @@ -3,9 +3,11 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.DTOs.Metadata; +using API.DTOs.Metadata.Browse; using API.Entities; using API.Extensions; using API.Extensions.QueryExtensions; +using API.Helpers; using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; @@ -27,6 +29,7 @@ public interface IGenreRepository Task GetRandomGenre(); Task GetGenreById(int id); Task> GetAllGenresNotInListAsync(ICollection genreNames); + Task> GetBrowseableGenre(int userId, UserParams userParams); } public class GenreRepository : IGenreRepository @@ -165,4 +168,28 @@ public class GenreRepository : IGenreRepository // Return the original non-normalized genres for the missing ones return missingGenres.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList(); } + + public async Task> GetBrowseableGenre(int userId, UserParams userParams) + { + var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + + var query = _context.Genre + .RestrictAgainstAgeRestriction(ageRating) + .Select(g => new BrowseGenreDto + { + Id = g.Id, + Title = g.Title, + SeriesCount = g.SeriesMetadatas + .Select(sm => sm.Id) + .Distinct() + .Count(), + ChapterCount = g.Chapters + .Select(ch => ch.Id) + .Distinct() + .Count() + }) + .OrderBy(g => g.Title); + + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + } } diff --git a/API/Data/Repositories/PersonRepository.cs b/API/Data/Repositories/PersonRepository.cs index dce3f86ef..6954ccf03 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/API/Data/Repositories/PersonRepository.cs @@ -2,13 +2,19 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using API.Data.Misc; using API.DTOs; +using API.DTOs.Filtering.v2; +using API.DTOs.Metadata.Browse; +using API.DTOs.Metadata.Browse.Requests; using API.DTOs.Person; using API.Entities.Enums; using API.Entities.Person; using API.Extensions; using API.Extensions.QueryExtensions; +using API.Extensions.QueryExtensions.Filtering; using API.Helpers; +using API.Helpers.Converters; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; @@ -45,7 +51,7 @@ public interface IPersonRepository Task GetCoverImageAsync(int personId); Task GetCoverImageByNameAsync(string name); Task> GetRolesForPersonByName(int personId, int userId); - Task> GetAllWritersAndSeriesCount(int userId, UserParams userParams); + Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams); Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None); Task GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases); /// @@ -194,36 +200,82 @@ public class PersonRepository : IPersonRepository return chapterRoles.Union(seriesRoles).Distinct(); } - public async Task> GetAllWritersAndSeriesCount(int userId, UserParams userParams) + public async Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams) { - List roles = [PersonRole.Writer, PersonRole.CoverArtist]; var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var query = _context.Person - .Where(p => p.SeriesMetadataPeople.Any(smp => roles.Contains(smp.Role)) || p.ChapterPeople.Any(cmp => roles.Contains(cmp.Role))) - .RestrictAgainstAgeRestriction(ageRating) - .Select(p => new BrowsePersonDto - { - Id = p.Id, - Name = p.Name, - Description = p.Description, - CoverImage = p.CoverImage, - SeriesCount = p.SeriesMetadataPeople - .Where(smp => roles.Contains(smp.Role)) - .Select(smp => smp.SeriesMetadata.SeriesId) - .Distinct() - .Count(), - IssueCount = p.ChapterPeople - .Where(cp => roles.Contains(cp.Role)) - .Select(cp => cp.Chapter.Id) - .Distinct() - .Count() - }) - .OrderBy(p => p.Name); + var query = CreateFilteredPersonQueryable(userId, filter, ageRating); return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } + private IQueryable CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating) + { + var query = _context.Person.AsNoTracking(); + + // Apply filtering based on statements + query = BuildPersonFilterQuery(userId, filter, query); + + // Apply age restriction + query = query.RestrictAgainstAgeRestriction(ageRating); + + // Apply sorting and limiting + var sortedQuery = query.SortBy(filter.SortOptions); + + var limitedQuery = ApplyPersonLimit(sortedQuery, filter.LimitTo); + + // Project to DTO + var projectedQuery = limitedQuery.Select(p => new BrowsePersonDto + { + Id = p.Id, + Name = p.Name, + Description = p.Description, + CoverImage = p.CoverImage, + SeriesCount = p.SeriesMetadataPeople + .Select(smp => smp.SeriesMetadata.SeriesId) + .Distinct() + .Count(), + ChapterCount = p.ChapterPeople + .Select(cp => cp.Chapter.Id) + .Distinct() + .Count() + }); + + return projectedQuery; + } + + private static IQueryable BuildPersonFilterQuery(int userId, BrowsePersonFilterDto filterDto, IQueryable query) + { + if (filterDto.Statements == null || filterDto.Statements.Count == 0) return query; + + var queries = filterDto.Statements + .Select(statement => BuildPersonFilterGroup(userId, statement, query)) + .ToList(); + + return filterDto.Combination == FilterCombination.And + ? queries.Aggregate((q1, q2) => q1.Intersect(q2)) + : queries.Aggregate((q1, q2) => q1.Union(q2)); + } + + private static IQueryable BuildPersonFilterGroup(int userId, PersonFilterStatementDto statement, IQueryable query) + { + var value = PersonFilterFieldValueConverter.ConvertValue(statement.Field, statement.Value); + + return statement.Field switch + { + PersonFilterField.Name => query.HasPersonName(true, statement.Comparison, (string)value), + PersonFilterField.Role => query.HasPersonRole(true, statement.Comparison, (IList)value), + PersonFilterField.SeriesCount => query.HasPersonSeriesCount(true, statement.Comparison, (int)value), + PersonFilterField.ChapterCount => query.HasPersonChapterCount(true, statement.Comparison, (int)value), + _ => throw new ArgumentOutOfRangeException(nameof(statement.Field), $"Unexpected value for field: {statement.Field}") + }; + } + + private static IQueryable ApplyPersonLimit(IQueryable query, int limit) + { + return limit <= 0 ? query : query.Take(limit); + } + public async Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None) { return await _context.Person.Where(p => p.Id == personId) diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index e04c944e3..e2eab0976 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -735,6 +735,7 @@ public class SeriesRepository : ISeriesRepository { return await _context.Series .Where(s => s.Id == seriesId) + .Include(s => s.ExternalSeriesMetadata) .Select(series => new PlusSeriesRequestDto() { MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format), @@ -744,6 +745,7 @@ public class SeriesRepository : ISeriesRepository ScrobblingService.AniListWeblinkWebsite), MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.MalWeblinkWebsite), + CbrId = series.ExternalSeriesMetadata.CbrId, GoogleBooksId = ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.GoogleBooksWeblinkWebsite), MangaDexId = ScrobblingService.ExtractId(series.Metadata.WebLinks, @@ -1088,8 +1090,6 @@ public class SeriesRepository : ISeriesRepository return query.Where(s => false); } - - // First setup any FilterField.Libraries in the statements, as these don't have any traditional query statements applied here query = ApplyLibraryFilter(filter, query); @@ -1290,7 +1290,7 @@ public class SeriesRepository : ISeriesRepository FilterField.ReadingDate => query.HasReadingDate(true, statement.Comparison, (DateTime) value, userId), FilterField.ReadLast => query.HasReadLast(true, statement.Comparison, (int) value, userId), FilterField.AverageRating => query.HasAverageRating(true, statement.Comparison, (float) value), - _ => throw new ArgumentOutOfRangeException() + _ => throw new ArgumentOutOfRangeException(nameof(statement.Field), $"Unexpected value for field: {statement.Field}") }; } diff --git a/API/Data/Repositories/TagRepository.cs b/API/Data/Repositories/TagRepository.cs index c4f189957..ea39d2b0d 100644 --- a/API/Data/Repositories/TagRepository.cs +++ b/API/Data/Repositories/TagRepository.cs @@ -2,9 +2,11 @@ using System.Linq; using System.Threading.Tasks; using API.DTOs.Metadata; +using API.DTOs.Metadata.Browse; using API.Entities; using API.Extensions; using API.Extensions.QueryExtensions; +using API.Helpers; using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; @@ -23,6 +25,7 @@ public interface ITagRepository Task RemoveAllTagNoLongerAssociated(); Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null); Task> GetAllTagsNotInListAsync(ICollection tags); + Task> GetBrowseableTag(int userId, UserParams userParams); } public class TagRepository : ITagRepository @@ -104,6 +107,30 @@ public class TagRepository : ITagRepository return missingTags.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList(); } + public async Task> GetBrowseableTag(int userId, UserParams userParams) + { + var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + + var query = _context.Tag + .RestrictAgainstAgeRestriction(ageRating) + .Select(g => new BrowseTagDto + { + Id = g.Id, + Title = g.Title, + SeriesCount = g.SeriesMetadatas + .Select(sm => sm.Id) + .Distinct() + .Count(), + ChapterCount = g.Chapters + .Select(ch => ch.Id) + .Distinct() + .Count() + }) + .OrderBy(g => g.Title); + + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + } + public async Task> GetAllTagsAsync() { return await _context.Tag.ToListAsync(); diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 74bfbb296..c08f80afa 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -120,7 +120,7 @@ public static class Seed new AppUserSideNavStream() { Name = "browse-authors", - StreamType = SideNavStreamType.BrowseAuthors, + StreamType = SideNavStreamType.BrowsePeople, Order = 6, IsProvided = true, Visible = true diff --git a/API/Entities/AppUserReadingProfile.cs b/API/Entities/AppUserReadingProfile.cs index ad2548661..9b238b4f5 100644 --- a/API/Entities/AppUserReadingProfile.cs +++ b/API/Entities/AppUserReadingProfile.cs @@ -1,9 +1,22 @@ using System.Collections.Generic; +using System.ComponentModel; using API.Entities.Enums; using API.Entities.Enums.UserPreferences; namespace API.Entities; +public enum BreakPoint +{ + [Description("Never")] + Never = 0, + [Description("Mobile")] + Mobile = 1, + [Description("Tablet")] + Tablet = 2, + [Description("Desktop")] + Desktop = 3, +} + public class AppUserReadingProfile { public int Id { get; set; } @@ -72,6 +85,10 @@ public class AppUserReadingProfile /// Manga Reader Option: Optional fixed width override /// public int? WidthOverride { get; set; } = null; + /// + /// Manga Reader Option: Disable the width override if the screen is past the breakpoint + /// + public BreakPoint DisableWidthOverride { get; set; } = BreakPoint.Never; #endregion diff --git a/API/Entities/SideNavStreamType.cs b/API/Entities/SideNavStreamType.cs index 545c630d8..62f429889 100644 --- a/API/Entities/SideNavStreamType.cs +++ b/API/Entities/SideNavStreamType.cs @@ -10,5 +10,5 @@ public enum SideNavStreamType ExternalSource = 6, AllSeries = 7, WantToRead = 8, - BrowseAuthors = 9 + BrowsePeople = 9 } diff --git a/API/Extensions/EnumerableExtensions.cs b/API/Extensions/EnumerableExtensions.cs index 4e84e2fa5..8beec88ca 100644 --- a/API/Extensions/EnumerableExtensions.cs +++ b/API/Extensions/EnumerableExtensions.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text.RegularExpressions; using API.Data.Misc; using API.Entities.Enums; +using API.Entities.Metadata; namespace API.Extensions; #nullable enable @@ -42,4 +43,16 @@ public static class EnumerableExtensions return q; } + + public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) + { + if (restriction.AgeRating == AgeRating.NotApplicable) return items; + var q = items.Where(s => s.AgeRating <= restriction.AgeRating); + if (!restriction.IncludeUnknowns) + { + return q.Where(s => s.AgeRating != AgeRating.Unknown); + } + + return q; + } } diff --git a/API/Extensions/QueryExtensions/Filtering/PersonFilter.cs b/API/Extensions/QueryExtensions/Filtering/PersonFilter.cs new file mode 100644 index 000000000..c36164d9d --- /dev/null +++ b/API/Extensions/QueryExtensions/Filtering/PersonFilter.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using API.DTOs.Filtering.v2; +using API.Entities.Enums; +using API.Entities.Person; +using Kavita.Common; +using Microsoft.EntityFrameworkCore; + +namespace API.Extensions.QueryExtensions.Filtering; + +public static class PersonFilter +{ + public static IQueryable HasPersonName(this IQueryable queryable, bool condition, + FilterComparison comparison, string queryString) + { + if (string.IsNullOrEmpty(queryString) || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(p => p.Name.Equals(queryString)), + FilterComparison.BeginsWith => queryable.Where(p => EF.Functions.Like(p.Name, $"{queryString}%")), + FilterComparison.EndsWith => queryable.Where(p => EF.Functions.Like(p.Name, $"%{queryString}")), + FilterComparison.Matches => queryable.Where(p => EF.Functions.Like(p.Name, $"%{queryString}%")), + FilterComparison.NotEqual => queryable.Where(p => p.Name != queryString), + FilterComparison.NotContains or FilterComparison.GreaterThan or FilterComparison.GreaterThanEqual + or FilterComparison.LessThan or FilterComparison.LessThanEqual or FilterComparison.Contains + or FilterComparison.IsBefore or FilterComparison.IsAfter or FilterComparison.IsInLast + or FilterComparison.IsNotInLast or FilterComparison.MustContains + or FilterComparison.IsEmpty => + throw new KavitaException($"{comparison} not applicable for Person.Name"), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, + "Filter Comparison is not supported") + }; + } + public static IQueryable HasPersonRole(this IQueryable queryable, bool condition, + FilterComparison comparison, IList roles) + { + if (roles == null || roles.Count == 0 || !condition) return queryable; + + return comparison switch + { + FilterComparison.Contains or FilterComparison.MustContains => queryable.Where(p => + p.SeriesMetadataPeople.Any(smp => roles.Contains(smp.Role)) || + p.ChapterPeople.Any(cmp => roles.Contains(cmp.Role))), + FilterComparison.NotContains => queryable.Where(p => + !p.SeriesMetadataPeople.Any(smp => roles.Contains(smp.Role)) && + !p.ChapterPeople.Any(cmp => roles.Contains(cmp.Role))), + FilterComparison.Equal or FilterComparison.NotEqual or FilterComparison.BeginsWith + or FilterComparison.EndsWith or FilterComparison.Matches or FilterComparison.GreaterThan + or FilterComparison.GreaterThanEqual or FilterComparison.LessThan or FilterComparison.LessThanEqual + or FilterComparison.IsBefore or FilterComparison.IsAfter or FilterComparison.IsInLast + or FilterComparison.IsNotInLast + or FilterComparison.IsEmpty => + throw new KavitaException($"{comparison} not applicable for Person.Role"), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, + "Filter Comparison is not supported") + }; + } + + public static IQueryable HasPersonSeriesCount(this IQueryable queryable, bool condition, + FilterComparison comparison, int count) + { + if (!condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(p => p.SeriesMetadataPeople + .Select(smp => smp.SeriesMetadata.SeriesId) + .Distinct() + .Count() == count), + FilterComparison.GreaterThan => queryable.Where(p => p.SeriesMetadataPeople + .Select(smp => smp.SeriesMetadata.SeriesId) + .Distinct() + .Count() > count), + FilterComparison.GreaterThanEqual => queryable.Where(p => p.SeriesMetadataPeople + .Select(smp => smp.SeriesMetadata.SeriesId) + .Distinct() + .Count() >= count), + FilterComparison.LessThan => queryable.Where(p => p.SeriesMetadataPeople + .Select(smp => smp.SeriesMetadata.SeriesId) + .Distinct() + .Count() < count), + FilterComparison.LessThanEqual => queryable.Where(p => p.SeriesMetadataPeople + .Select(smp => smp.SeriesMetadata.SeriesId) + .Distinct() + .Count() <= count), + FilterComparison.NotEqual => queryable.Where(p => p.SeriesMetadataPeople + .Select(smp => smp.SeriesMetadata.SeriesId) + .Distinct() + .Count() != count), + FilterComparison.BeginsWith or FilterComparison.EndsWith or FilterComparison.Matches + or FilterComparison.Contains or FilterComparison.NotContains or FilterComparison.IsBefore + or FilterComparison.IsAfter or FilterComparison.IsInLast or FilterComparison.IsNotInLast + or FilterComparison.MustContains + or FilterComparison.IsEmpty => throw new KavitaException( + $"{comparison} not applicable for Person.SeriesCount"), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported") + }; + } + + public static IQueryable HasPersonChapterCount(this IQueryable queryable, bool condition, + FilterComparison comparison, int count) + { + if (!condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(p => + p.ChapterPeople.Select(cp => cp.Chapter.Id).Distinct().Count() == count), + FilterComparison.GreaterThan => queryable.Where(p => p.ChapterPeople + .Select(cp => cp.Chapter.Id) + .Distinct() + .Count() > count), + FilterComparison.GreaterThanEqual => queryable.Where(p => p.ChapterPeople + .Select(cp => cp.Chapter.Id) + .Distinct() + .Count() >= count), + FilterComparison.LessThan => queryable.Where(p => + p.ChapterPeople.Select(cp => cp.Chapter.Id).Distinct().Count() < count), + FilterComparison.LessThanEqual => queryable.Where(p => p.ChapterPeople + .Select(cp => cp.Chapter.Id) + .Distinct() + .Count() <= count), + FilterComparison.NotEqual => queryable.Where(p => + p.ChapterPeople.Select(cp => cp.Chapter.Id).Distinct().Count() != count), + FilterComparison.BeginsWith or FilterComparison.EndsWith or FilterComparison.Matches + or FilterComparison.Contains or FilterComparison.NotContains or FilterComparison.IsBefore + or FilterComparison.IsAfter or FilterComparison.IsInLast or FilterComparison.IsNotInLast + or FilterComparison.MustContains + or FilterComparison.IsEmpty => throw new KavitaException( + $"{comparison} not applicable for Person.ChapterCount"), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported") + }; + } +} diff --git a/API/Extensions/QueryExtensions/QueryableExtensions.cs b/API/Extensions/QueryExtensions/QueryableExtensions.cs index a2db1dde7..ef2af721f 100644 --- a/API/Extensions/QueryExtensions/QueryableExtensions.cs +++ b/API/Extensions/QueryExtensions/QueryableExtensions.cs @@ -5,10 +5,13 @@ using System.Linq.Expressions; using System.Threading.Tasks; using API.Data.Misc; using API.Data.Repositories; +using API.DTOs; using API.DTOs.Filtering; using API.DTOs.KavitaPlus.Manage; +using API.DTOs.Metadata.Browse; using API.Entities; using API.Entities.Enums; +using API.Entities.Person; using API.Entities.Scrobble; using Microsoft.EntityFrameworkCore; @@ -273,6 +276,27 @@ public static class QueryableExtensions }; } + public static IQueryable SortBy(this IQueryable query, PersonSortOptions? sort) + { + if (sort == null) + { + return query.OrderBy(p => p.Name); + } + + return sort.SortField switch + { + PersonSortField.Name when sort.IsAscending => query.OrderBy(p => p.Name), + PersonSortField.Name => query.OrderByDescending(p => p.Name), + PersonSortField.SeriesCount when sort.IsAscending => query.OrderBy(p => p.SeriesMetadataPeople.Count), + PersonSortField.SeriesCount => query.OrderByDescending(p => p.SeriesMetadataPeople.Count), + PersonSortField.ChapterCount when sort.IsAscending => query.OrderBy(p => p.ChapterPeople.Count), + PersonSortField.ChapterCount => query.OrderByDescending(p => p.ChapterPeople.Count), + _ => query.OrderBy(p => p.Name) + }; + + + } + /// /// Performs either OrderBy or OrderByDescending on the given query based on the value of SortOptions.IsAscending. /// diff --git a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs index fc3314f58..aef595596 100644 --- a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs +++ b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs @@ -3,6 +3,7 @@ using System.Linq; using API.Data.Misc; using API.Entities; using API.Entities.Enums; +using API.Entities.Metadata; using API.Entities.Person; namespace API.Extensions.QueryExtensions; @@ -26,6 +27,7 @@ public static class RestrictByAgeExtensions return q; } + public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) { if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; @@ -39,20 +41,6 @@ public static class RestrictByAgeExtensions return q; } - [Obsolete] - public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; - - if (restriction.IncludeUnknowns) - { - return queryable.Where(c => c.SeriesMetadatas.All(sm => - sm.AgeRating <= restriction.AgeRating)); - } - - return queryable.Where(c => c.SeriesMetadatas.All(sm => - sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown)); - } public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) { @@ -74,12 +62,15 @@ public static class RestrictByAgeExtensions if (restriction.IncludeUnknowns) { - return queryable.Where(c => c.SeriesMetadatas.All(sm => - sm.AgeRating <= restriction.AgeRating)); + return queryable.Where(c => + c.SeriesMetadatas.Any(sm => sm.AgeRating <= restriction.AgeRating) || + c.Chapters.Any(cp => cp.AgeRating <= restriction.AgeRating)); } - return queryable.Where(c => c.SeriesMetadatas.All(sm => - sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown)); + return queryable.Where(c => + c.SeriesMetadatas.Any(sm => sm.AgeRating <= restriction.AgeRating && sm.AgeRating != AgeRating.Unknown) || + c.Chapters.Any(cp => cp.AgeRating <= restriction.AgeRating && cp.AgeRating != AgeRating.Unknown) + ); } public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) @@ -88,12 +79,15 @@ public static class RestrictByAgeExtensions if (restriction.IncludeUnknowns) { - return queryable.Where(c => c.SeriesMetadatas.All(sm => - sm.AgeRating <= restriction.AgeRating)); + return queryable.Where(c => + c.SeriesMetadatas.Any(sm => sm.AgeRating <= restriction.AgeRating) || + c.Chapters.Any(cp => cp.AgeRating <= restriction.AgeRating)); } - return queryable.Where(c => c.SeriesMetadatas.All(sm => - sm.AgeRating <= restriction.AgeRating && sm.AgeRating > AgeRating.Unknown)); + return queryable.Where(c => + c.SeriesMetadatas.Any(sm => sm.AgeRating <= restriction.AgeRating && sm.AgeRating != AgeRating.Unknown) || + c.Chapters.Any(cp => cp.AgeRating <= restriction.AgeRating && cp.AgeRating != AgeRating.Unknown) + ); } public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index d25444a51..bb7511c64 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -286,7 +286,8 @@ public class AutoMapperProfiles : Profile CreateMap(); CreateMap() - .ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count)); + .ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count)) + .ForMember(dest => dest.OwnerUserName, opt => opt.MapFrom(src => src.AppUser.UserName)); CreateMap(); CreateMap(); CreateMap(); diff --git a/API/Helpers/Converters/PersonFilterFieldValueConverter.cs b/API/Helpers/Converters/PersonFilterFieldValueConverter.cs new file mode 100644 index 000000000..822ce105a --- /dev/null +++ b/API/Helpers/Converters/PersonFilterFieldValueConverter.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using API.DTOs.Filtering.v2; +using API.Entities.Enums; + +namespace API.Helpers.Converters; + +public static class PersonFilterFieldValueConverter +{ + public static object ConvertValue(PersonFilterField field, string value) + { + return field switch + { + PersonFilterField.Name => value, + PersonFilterField.Role => ParsePersonRoles(value), + PersonFilterField.SeriesCount => int.Parse(value), + PersonFilterField.ChapterCount => int.Parse(value), + _ => throw new ArgumentOutOfRangeException(nameof(field), field, "Field is not supported") + }; + } + + private static IList ParsePersonRoles(string value) + { + if (string.IsNullOrEmpty(value)) return []; + + return value.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(v => Enum.Parse(v.Trim())) + .ToList(); + } +} diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 0255b785d..544efa4ce 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -10,11 +10,9 @@ using API.Entities.Interfaces; using API.Extensions; using Microsoft.Extensions.Logging; using NetVips; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; -using Color = System.Drawing.Color; using Image = NetVips.Image; namespace API.Services; @@ -750,7 +748,7 @@ public class ImageService : IImageService } - public static Color HexToRgb(string? hex) + public static (int R, int G, int B) HexToRgb(string? hex) { if (string.IsNullOrEmpty(hex)) throw new ArgumentException("Hex cannot be null"); @@ -774,7 +772,7 @@ public class ImageService : IImageService var g = Convert.ToInt32(hex.Substring(2, 2), 16); var b = Convert.ToInt32(hex.Substring(4, 2), 16); - return Color.FromArgb(r, g, b); + return (r, g, b); } diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index a1e3750dd..435727bda 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -200,6 +200,9 @@ public class ExternalMetadataService : IExternalMetadataService /// /// Returns the match results for a Series from UI Flow /// + /// + /// Will extract alternative names like Localized name, year will send as ReleaseYear but fallback to Comic Vine syntax if applicable + /// /// /// public async Task> MatchSeries(MatchSeriesDto dto) @@ -212,19 +215,26 @@ public class ExternalMetadataService : IExternalMetadataService var potentialAnilistId = ScrobblingService.ExtractId(dto.Query, ScrobblingService.AniListWeblinkWebsite); var potentialMalId = ScrobblingService.ExtractId(dto.Query, ScrobblingService.MalWeblinkWebsite); - List altNames = [series.LocalizedName, series.OriginalName]; - if (potentialAnilistId == null && potentialMalId == null && !string.IsNullOrEmpty(dto.Query)) + var format = series.Library.Type.ConvertToPlusMediaFormat(series.Format); + var otherNames = ExtractAlternativeNames(series); + + var year = series.Metadata.ReleaseYear; + if (year == 0 && format == PlusMediaFormat.Comic && !string.IsNullOrWhiteSpace(series.Name)) { - altNames.Add(dto.Query); + var potentialYear = Parser.ParseYear(series.Name); + if (!string.IsNullOrEmpty(potentialYear)) + { + year = int.Parse(potentialYear); + } } var matchRequest = new MatchSeriesRequestDto() { - Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), + Format = format, Query = dto.Query, SeriesName = series.Name, - AlternativeNames = altNames.Where(s => !string.IsNullOrEmpty(s)).ToList(), - Year = series.Metadata.ReleaseYear, + AlternativeNames = otherNames, + Year = year, AniListId = potentialAnilistId ?? ScrobblingService.GetAniListId(series), MalId = potentialMalId ?? ScrobblingService.GetMalId(series) }; @@ -254,6 +264,12 @@ public class ExternalMetadataService : IExternalMetadataService return ArraySegment.Empty; } + private static List ExtractAlternativeNames(Series series) + { + List altNames = [series.LocalizedName, series.OriginalName]; + return altNames.Where(s => !string.IsNullOrEmpty(s)).Distinct().ToList(); + } + /// /// Retrieves Metadata about a Recommended External Series diff --git a/API/Services/Plus/LicenseService.cs b/API/Services/Plus/LicenseService.cs index 774103518..91f5a8fdd 100644 --- a/API/Services/Plus/LicenseService.cs +++ b/API/Services/Plus/LicenseService.cs @@ -130,22 +130,23 @@ public class LicenseService( if (cacheValue.HasValue) return cacheValue.Value; } + var result = false; try { var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - var result = await IsLicenseValid(serverSetting.Value); - await provider.FlushAsync(); - await provider.SetAsync(CacheKey, result, _licenseCacheTimeout); - return result; + result = await IsLicenseValid(serverSetting.Value); } catch (Exception ex) { logger.LogError(ex, "There was an issue connecting to Kavita+"); + } + finally + { await provider.FlushAsync(); - await provider.SetAsync(CacheKey, false, _licenseCacheTimeout); + await provider.SetAsync(CacheKey, result, _licenseCacheTimeout); } - return false; + return result; } /// diff --git a/API/Services/ReadingProfileService.cs b/API/Services/ReadingProfileService.cs index fbfabad70..4c3dab006 100644 --- a/API/Services/ReadingProfileService.cs +++ b/API/Services/ReadingProfileService.cs @@ -432,6 +432,7 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService existingProfile.SwipeToPaginate = dto.SwipeToPaginate; existingProfile.AllowAutomaticWebtoonReaderDetection = dto.AllowAutomaticWebtoonReaderDetection; existingProfile.WidthOverride = dto.WidthOverride; + existingProfile.DisableWidthOverride = dto.DisableWidthOverride; // Book Reader existingProfile.BookReaderMargin = dto.BookReaderMargin; diff --git a/API/Services/Tasks/Metadata/CoverDbService.cs b/API/Services/Tasks/Metadata/CoverDbService.cs index 99d02401b..015613965 100644 --- a/API/Services/Tasks/Metadata/CoverDbService.cs +++ b/API/Services/Tasks/Metadata/CoverDbService.cs @@ -206,17 +206,12 @@ public class CoverDbService : ICoverDbService throw new KavitaException($"Could not grab publisher image for {publisherName}"); } - _logger.LogTrace("Fetching publisher image from {Url}", publisherLink.Sanitize()); - // Download the publisher file using Flurl - var publisherStream = await publisherLink - .AllowHttpStatus("2xx,304") - .GetStreamAsync(); - // Create the destination file path - using var image = Image.NewFromStream(publisherStream); var filename = ImageService.GetPublisherFormat(publisherName, encodeFormat); - image.WriteToFile(Path.Combine(_directoryService.PublisherDirectory, filename)); + _logger.LogTrace("Fetching publisher image from {Url}", publisherLink.Sanitize()); + await DownloadImageFromUrl(publisherName, encodeFormat, publisherLink, _directoryService.PublisherDirectory); + _logger.LogDebug("Publisher image for {PublisherName} downloaded and saved successfully", publisherName.Sanitize()); return filename; @@ -302,7 +297,27 @@ public class CoverDbService : ICoverDbService .GetStreamAsync(); using var image = Image.NewFromStream(imageStream); - image.WriteToFile(targetFile); + try + { + image.WriteToFile(targetFile); + } + catch (Exception ex) + { + switch (encodeFormat) + { + case EncodeFormat.PNG: + image.Pngsave(Path.Combine(_directoryService.FaviconDirectory, filename)); + break; + case EncodeFormat.WEBP: + image.Webpsave(Path.Combine(_directoryService.FaviconDirectory, filename)); + break; + case EncodeFormat.AVIF: + image.Heifsave(Path.Combine(_directoryService.FaviconDirectory, filename)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null); + } + } return filename; } @@ -385,14 +400,13 @@ public class CoverDbService : ICoverDbService private async Task FallbackToKavitaReaderPublisher(string publisherName) { const string publisherFileName = "publishers.txt"; - var externalLink = string.Empty; var allOverrides = await GetCachedData(publisherFileName) ?? await $"{NewHost}publishers/{publisherFileName}".GetStringAsync(); // Cache immediately await CacheDataAsync(publisherFileName, allOverrides); - if (string.IsNullOrEmpty(allOverrides)) return externalLink; + if (string.IsNullOrEmpty(allOverrides)) return string.Empty; var externalFile = allOverrides .Split("\n") @@ -415,7 +429,7 @@ public class CoverDbService : ICoverDbService throw new KavitaException($"Could not grab publisher image for {publisherName}"); } - return $"{NewHost}publishers/{externalLink}"; + return $"{NewHost}publishers/{externalFile}"; } private async Task CacheDataAsync(string fileName, string? content) @@ -572,8 +586,7 @@ public class CoverDbService : ICoverDbService var choseNewImage = string.Equals(betterImage, tempFullPath, StringComparison.OrdinalIgnoreCase); if (choseNewImage) { - - // Don't delete series cover, unless it's an override, otherwise the first chapter cover will be null + // Don't delete the Series cover unless it is an override, otherwise the first chapter will be null if (existingPath.Contains(ImageService.GetSeriesFormat(series.Id))) { _directoryService.DeleteFiles([existingPath]); @@ -624,6 +637,7 @@ public class CoverDbService : ICoverDbService } } + // TODO: Refactor this to IHasCoverImage instead of a hard entity type public async Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false) { if (!string.IsNullOrEmpty(url)) diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 12987b18b..c8eb010b3 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -1159,6 +1159,12 @@ public static partial class Parser return !string.IsNullOrEmpty(name) && SeriesAndYearRegex.IsMatch(name); } + /// + /// Parse a Year from a Comic Series: Series Name (YEAR) + /// + /// Harley Quinn (2024) returns 2024 + /// + /// public static string ParseYear(string? name) { if (string.IsNullOrEmpty(name)) return string.Empty; diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index 3450cdce3..f2d64cde6 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -17,7 +17,7 @@ public static class Configuration private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename()); public static readonly string KavitaPlusApiUrl = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development - ? "https://plus.kavitareader.com" : "https://plus.kavitareader.com"; + ? "http://localhost:5020" : "https://plus.kavitareader.com"; public static readonly string StatsApiUrl = "https://stats.kavitareader.com"; public static int Port diff --git a/README.md b/README.md index bff8f0f5c..ffff8d831 100644 --- a/README.md +++ b/README.md @@ -107,13 +107,10 @@ Support this project by becoming a sponsor. Your logo will show up here with a l ## Mega Sponsors -## JetBrains -Thank you to [ JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools. - -* [ Rider](http://www.jetbrains.com/rider/) +## Powered By +[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSource) ### License - * [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) * Copyright 2020-2024 diff --git a/UI/Web/src/_tag-card-common.scss b/UI/Web/src/_tag-card-common.scss new file mode 100644 index 000000000..07f37c2a0 --- /dev/null +++ b/UI/Web/src/_tag-card-common.scss @@ -0,0 +1,30 @@ +.tag-card { + background-color: var(--bs-card-color, #2c2c2c); + padding: 1rem; + border-radius: 12px; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + transition: transform 0.2s ease, background 0.3s ease; + cursor: pointer; +} + +.tag-card:hover { + background-color: #3a3a3a; + //transform: translateY(-3px); // Cool effect but has a weird background issue. ROBBIE: Fix this +} + +.tag-name { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.5rem; + max-height: 8rem; + height: 8rem; + overflow: hidden; + text-overflow: ellipsis; +} + +.tag-meta { + font-size: 0.85rem; + display: flex; + justify-content: space-between; + color: var(--text-muted-color, #bbb); +} diff --git a/UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts b/UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts index a8dc1ce06..05a4041c8 100644 --- a/UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts +++ b/UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts @@ -1,6 +1,8 @@ import {MatchStateOption} from "./match-state-option"; +import {LibraryType} from "../library/library"; export interface ManageMatchFilter { matchStateOption: MatchStateOption; + libraryType: LibraryType | -1; searchTerm: string; } diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts index 06ba86cf2..bad83f54b 100644 --- a/UI/Web/src/app/_models/library/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -13,6 +13,8 @@ export enum LibraryType { } export const allLibraryTypes = [LibraryType.Manga, LibraryType.ComicVine, LibraryType.Comic, LibraryType.Book, LibraryType.LightNovel, LibraryType.Images]; +export const allKavitaPlusMetadataApplicableTypes = [LibraryType.Manga, LibraryType.LightNovel, LibraryType.ComicVine, LibraryType.Comic]; +export const allKavitaPlusScrobbleEligibleTypes = [LibraryType.Manga, LibraryType.LightNovel]; export interface Library { id: number; diff --git a/UI/Web/src/app/_models/metadata/browse/browse-genre.ts b/UI/Web/src/app/_models/metadata/browse/browse-genre.ts new file mode 100644 index 000000000..e7bb0d915 --- /dev/null +++ b/UI/Web/src/app/_models/metadata/browse/browse-genre.ts @@ -0,0 +1,6 @@ +import {Genre} from "../genre"; + +export interface BrowseGenre extends Genre { + seriesCount: number; + chapterCount: number; +} diff --git a/UI/Web/src/app/_models/person/browse-person.ts b/UI/Web/src/app/_models/metadata/browse/browse-person.ts similarity index 52% rename from UI/Web/src/app/_models/person/browse-person.ts rename to UI/Web/src/app/_models/metadata/browse/browse-person.ts index aeddac7cd..886f9455b 100644 --- a/UI/Web/src/app/_models/person/browse-person.ts +++ b/UI/Web/src/app/_models/metadata/browse/browse-person.ts @@ -1,6 +1,6 @@ -import {Person} from "../metadata/person"; +import {Person} from "../person"; export interface BrowsePerson extends Person { seriesCount: number; - issueCount: number; + chapterCount: number; } diff --git a/UI/Web/src/app/_models/metadata/browse/browse-tag.ts b/UI/Web/src/app/_models/metadata/browse/browse-tag.ts new file mode 100644 index 000000000..4d87370ee --- /dev/null +++ b/UI/Web/src/app/_models/metadata/browse/browse-tag.ts @@ -0,0 +1,6 @@ +import {Tag} from "../../tag"; + +export interface BrowseTag extends Tag { + seriesCount: number; + chapterCount: number; +} diff --git a/UI/Web/src/app/_models/metadata/language.ts b/UI/Web/src/app/_models/metadata/language.ts index 8b68c7233..28ab2b598 100644 --- a/UI/Web/src/app/_models/metadata/language.ts +++ b/UI/Web/src/app/_models/metadata/language.ts @@ -4,7 +4,10 @@ export interface Language { } export interface KavitaLocale { - fileName: string; // isoCode aka what maps to the file on disk and what transloco loads + /** + * isoCode aka what maps to the file on disk and what transloco loads + */ + fileName: string; renderName: string; translationCompletion: number; isRtL: boolean; diff --git a/UI/Web/src/app/_models/metadata/person.ts b/UI/Web/src/app/_models/metadata/person.ts index 6b098de19..efc8df914 100644 --- a/UI/Web/src/app/_models/metadata/person.ts +++ b/UI/Web/src/app/_models/metadata/person.ts @@ -2,7 +2,6 @@ import {IHasCover} from "../common/i-has-cover"; export enum PersonRole { Other = 1, - Artist = 2, Writer = 3, Penciller = 4, Inker = 5, @@ -32,3 +31,22 @@ export interface Person extends IHasCover { primaryColor: string; secondaryColor: string; } + +/** + * Excludes Other as it's not in use + */ +export const allPeopleRoles = [ + PersonRole.Writer, + PersonRole.Penciller, + PersonRole.Inker, + PersonRole.Colorist, + PersonRole.Letterer, + PersonRole.CoverArtist, + PersonRole.Editor, + PersonRole.Publisher, + PersonRole.Character, + PersonRole.Translator, + PersonRole.Imprint, + PersonRole.Team, + PersonRole.Location +] diff --git a/UI/Web/src/app/_models/metadata/series-filter.ts b/UI/Web/src/app/_models/metadata/series-filter.ts index 7d043aa3c..7875732b7 100644 --- a/UI/Web/src/app/_models/metadata/series-filter.ts +++ b/UI/Web/src/app/_models/metadata/series-filter.ts @@ -1,5 +1,5 @@ import {MangaFormat} from "../manga-format"; -import {SeriesFilterV2} from "./v2/series-filter-v2"; +import {FilterV2} from "./v2/filter-v2"; export interface FilterItem { title: string; @@ -7,10 +7,6 @@ export interface FilterItem { selected: boolean; } -export interface SortOptions { - sortField: SortField; - isAscending: boolean; -} export enum SortField { SortName = 1, @@ -27,7 +23,7 @@ export enum SortField { Random = 9 } -export const allSortFields = Object.keys(SortField) +export const allSeriesSortFields = Object.keys(SortField) .filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0) .map(key => parseInt(key, 10)) as SortField[]; @@ -54,8 +50,8 @@ export const mangaFormatFilters = [ } ]; -export interface FilterEvent { - filterV2: SeriesFilterV2; +export interface FilterEvent { + filterV2: FilterV2; isFirst: boolean; } diff --git a/UI/Web/src/app/_models/metadata/v2/browse-person-filter.ts b/UI/Web/src/app/_models/metadata/v2/browse-person-filter.ts new file mode 100644 index 000000000..bb5edc9ce --- /dev/null +++ b/UI/Web/src/app/_models/metadata/v2/browse-person-filter.ts @@ -0,0 +1,8 @@ +import {PersonRole} from "../person"; +import {PersonSortOptions} from "./sort-options"; + +export interface BrowsePersonFilter { + roles: Array; + query?: string; + sortOptions?: PersonSortOptions; +} diff --git a/UI/Web/src/app/_models/metadata/v2/filter-field.ts b/UI/Web/src/app/_models/metadata/v2/filter-field.ts index 08005d5c8..eeb8c7853 100644 --- a/UI/Web/src/app/_models/metadata/v2/filter-field.ts +++ b/UI/Web/src/app/_models/metadata/v2/filter-field.ts @@ -48,7 +48,7 @@ const enumArray = Object.keys(FilterField) enumArray.sort((a, b) => a.value.localeCompare(b.value)); -export const allFields = enumArray +export const allSeriesFilterFields = enumArray .map(key => parseInt(key.key, 10))as FilterField[]; export const allPeople = [ @@ -66,7 +66,6 @@ export const allPeople = [ export const personRoleForFilterField = (role: PersonRole) => { switch (role) { - case PersonRole.Artist: return FilterField.CoverArtist; case PersonRole.Character: return FilterField.Characters; case PersonRole.Colorist: return FilterField.Colorist; case PersonRole.CoverArtist: return FilterField.CoverArtist; diff --git a/UI/Web/src/app/_models/metadata/v2/filter-statement.ts b/UI/Web/src/app/_models/metadata/v2/filter-statement.ts index d031927a2..b14fe564d 100644 --- a/UI/Web/src/app/_models/metadata/v2/filter-statement.ts +++ b/UI/Web/src/app/_models/metadata/v2/filter-statement.ts @@ -1,8 +1,7 @@ -import { FilterComparison } from "./filter-comparison"; -import { FilterField } from "./filter-field"; +import {FilterComparison} from "./filter-comparison"; -export interface FilterStatement { +export interface FilterStatement { comparison: FilterComparison; - field: FilterField; + field: T; value: string; -} \ No newline at end of file +} diff --git a/UI/Web/src/app/_models/metadata/v2/filter-v2.ts b/UI/Web/src/app/_models/metadata/v2/filter-v2.ts new file mode 100644 index 000000000..77c064450 --- /dev/null +++ b/UI/Web/src/app/_models/metadata/v2/filter-v2.ts @@ -0,0 +1,11 @@ +import {FilterStatement} from "./filter-statement"; +import {FilterCombination} from "./filter-combination"; +import {SortOptions} from "./sort-options"; + +export interface FilterV2 { + name?: string; + statements: Array>; + combination: FilterCombination; + sortOptions?: SortOptions; + limitTo: number; +} diff --git a/UI/Web/src/app/_models/metadata/v2/person-filter-field.ts b/UI/Web/src/app/_models/metadata/v2/person-filter-field.ts new file mode 100644 index 000000000..6bfb5a0c1 --- /dev/null +++ b/UI/Web/src/app/_models/metadata/v2/person-filter-field.ts @@ -0,0 +1,12 @@ +export enum PersonFilterField { + Role = 1, + Name = 2, + SeriesCount = 3, + ChapterCount = 4, +} + + +export const allPersonFilterFields = Object.keys(PersonFilterField) + .filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0) + .map(key => parseInt(key, 10)) as PersonFilterField[]; + diff --git a/UI/Web/src/app/_models/metadata/v2/person-sort-field.ts b/UI/Web/src/app/_models/metadata/v2/person-sort-field.ts new file mode 100644 index 000000000..6bcb66925 --- /dev/null +++ b/UI/Web/src/app/_models/metadata/v2/person-sort-field.ts @@ -0,0 +1,9 @@ +export enum PersonSortField { + Name = 1, + SeriesCount = 2, + ChapterCount = 3 +} + +export const allPersonSortFields = Object.keys(PersonSortField) + .filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0) + .map(key => parseInt(key, 10)) as PersonSortField[]; diff --git a/UI/Web/src/app/_models/metadata/v2/series-filter-v2.ts b/UI/Web/src/app/_models/metadata/v2/series-filter-v2.ts deleted file mode 100644 index c13244644..000000000 --- a/UI/Web/src/app/_models/metadata/v2/series-filter-v2.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { SortOptions } from "../series-filter"; -import {FilterStatement} from "./filter-statement"; -import {FilterCombination} from "./filter-combination"; - -export interface SeriesFilterV2 { - name?: string; - statements: Array; - combination: FilterCombination; - sortOptions?: SortOptions; - limitTo: number; -} diff --git a/UI/Web/src/app/_models/metadata/v2/sort-options.ts b/UI/Web/src/app/_models/metadata/v2/sort-options.ts new file mode 100644 index 000000000..ed68d6b9d --- /dev/null +++ b/UI/Web/src/app/_models/metadata/v2/sort-options.ts @@ -0,0 +1,17 @@ +import {PersonSortField} from "./person-sort-field"; + +/** + * Series-based Sort options + */ +export interface SortOptions { + sortField: TSort; + isAscending: boolean; +} + +/** + * Person-based Sort Options + */ +export interface PersonSortOptions { + sortField: PersonSortField; + isAscending: boolean; +} diff --git a/UI/Web/src/app/_models/preferences/reading-profiles.ts b/UI/Web/src/app/_models/preferences/reading-profiles.ts index d81b8cc88..dad02946f 100644 --- a/UI/Web/src/app/_models/preferences/reading-profiles.ts +++ b/UI/Web/src/app/_models/preferences/reading-profiles.ts @@ -12,6 +12,7 @@ import {PdfLayoutMode} from "./pdf-layout-mode"; import {PdfSpreadMode} from "./pdf-spread-mode"; import {Series} from "../series"; import {Library} from "../library/library"; +import {UserBreakpoint} from "../../shared/_services/utility.service"; export enum ReadingProfileKind { Default = 0, @@ -39,6 +40,7 @@ export interface ReadingProfile { swipeToPaginate: boolean; allowAutomaticWebtoonReaderDetection: boolean; widthOverride?: number; + disableWidthOverride: UserBreakpoint; // Book Reader bookReaderMargin: number; @@ -75,3 +77,4 @@ export const pdfLayoutModes = [{text: 'pdf-multiple', value: PdfLayoutMode.Multi export const pdfScrollModes = [{text: 'pdf-vertical', value: PdfScrollMode.Vertical}, {text: 'pdf-horizontal', value: PdfScrollMode.Horizontal}, {text: 'pdf-page', value: PdfScrollMode.Page}]; export const pdfSpreadModes = [{text: 'pdf-none', value: PdfSpreadMode.None}, {text: 'pdf-odd', value: PdfSpreadMode.Odd}, {text: 'pdf-even', value: PdfSpreadMode.Even}]; export const pdfThemes = [{text: 'pdf-light', value: PdfTheme.Light}, {text: 'pdf-dark', value: PdfTheme.Dark}]; +export const breakPoints = [UserBreakpoint.Never, UserBreakpoint.Mobile, UserBreakpoint.Tablet, UserBreakpoint.Desktop] diff --git a/UI/Web/src/app/_models/wiki.ts b/UI/Web/src/app/_models/wiki.ts index 21b669f0c..a01267cf3 100644 --- a/UI/Web/src/app/_models/wiki.ts +++ b/UI/Web/src/app/_models/wiki.ts @@ -20,5 +20,6 @@ export enum WikiLink { UpdateNative = 'https://wiki.kavitareader.com/guides/updating/updating-native', UpdateDocker = 'https://wiki.kavitareader.com/guides/updating/updating-docker', OpdsClients = 'https://wiki.kavitareader.com/guides/features/opds/#opds-capable-clients', - Guides = 'https://wiki.kavitareader.com/guides' + Guides = 'https://wiki.kavitareader.com/guides', + ReadingProfiles = "https://wiki.kavitareader.com/guides/user-settings/reading-profiles/", } diff --git a/UI/Web/src/app/_pipes/breakpoint.pipe.ts b/UI/Web/src/app/_pipes/breakpoint.pipe.ts new file mode 100644 index 000000000..1897b773c --- /dev/null +++ b/UI/Web/src/app/_pipes/breakpoint.pipe.ts @@ -0,0 +1,25 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {translate} from "@jsverse/transloco"; +import {UserBreakpoint} from "../shared/_services/utility.service"; + +@Pipe({ + name: 'breakpoint' +}) +export class BreakpointPipe implements PipeTransform { + + transform(value: UserBreakpoint): string { + const v = parseInt(value + '', 10) as UserBreakpoint; + switch (v) { + case UserBreakpoint.Never: + return translate('breakpoint-pipe.never'); + case UserBreakpoint.Mobile: + return translate('breakpoint-pipe.mobile'); + case UserBreakpoint.Tablet: + return translate('breakpoint-pipe.tablet'); + case UserBreakpoint.Desktop: + return translate('breakpoint-pipe.desktop'); + } + throw new Error("unknown breakpoint value: " + value); + } + +} diff --git a/UI/Web/src/app/_pipes/browse-title.pipe.ts b/UI/Web/src/app/_pipes/browse-title.pipe.ts new file mode 100644 index 000000000..0495e8b8a --- /dev/null +++ b/UI/Web/src/app/_pipes/browse-title.pipe.ts @@ -0,0 +1,78 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FilterField} from "../_models/metadata/v2/filter-field"; +import {translate} from "@jsverse/transloco"; + +/** + * Responsible for taking a filter field and value (as a string) and translating into a "Browse X" heading for All Series page + * Example: Genre & "Action" -> Browse Action + * Example: Artist & "Joe Shmo" -> Browse Joe Shmo Works + */ +@Pipe({ + name: 'browseTitle' +}) +export class BrowseTitlePipe implements PipeTransform { + + transform(field: FilterField, value: string): string { + switch (field) { + case FilterField.PublicationStatus: + return translate('browse-title-pipe.publication-status', {value}); + case FilterField.AgeRating: + return translate('browse-title-pipe.age-rating', {value}); + case FilterField.UserRating: + return translate('browse-title-pipe.user-rating', {value}); + case FilterField.Tags: + return translate('browse-title-pipe.tag', {value}); + case FilterField.Translators: + return translate('browse-title-pipe.translator', {value}); + case FilterField.Characters: + return translate('browse-title-pipe.character', {value}); + case FilterField.Publisher: + return translate('browse-title-pipe.publisher', {value}); + case FilterField.Editor: + return translate('browse-title-pipe.editor', {value}); + case FilterField.CoverArtist: + return translate('browse-title-pipe.artist', {value}); + case FilterField.Letterer: + return translate('browse-title-pipe.letterer', {value}); + case FilterField.Colorist: + return translate('browse-title-pipe.colorist', {value}); + case FilterField.Inker: + return translate('browse-title-pipe.inker', {value}); + case FilterField.Penciller: + return translate('browse-title-pipe.penciller', {value}); + case FilterField.Writers: + return translate('browse-title-pipe.writer', {value}); + case FilterField.Genres: + return translate('browse-title-pipe.genre', {value}); + case FilterField.Libraries: + return translate('browse-title-pipe.library', {value}); + case FilterField.Formats: + return translate('browse-title-pipe.format', {value}); + case FilterField.ReleaseYear: + return translate('browse-title-pipe.release-year', {value}); + case FilterField.Imprint: + return translate('browse-title-pipe.imprint', {value}); + case FilterField.Team: + return translate('browse-title-pipe.team', {value}); + case FilterField.Location: + return translate('browse-title-pipe.location', {value}); + + // These have no natural links in the app to demand a richer title experience + case FilterField.Languages: + case FilterField.CollectionTags: + case FilterField.ReadProgress: + case FilterField.ReadTime: + case FilterField.Path: + case FilterField.FilePath: + case FilterField.WantToRead: + case FilterField.ReadingDate: + case FilterField.AverageRating: + case FilterField.ReadLast: + case FilterField.Summary: + case FilterField.SeriesName: + default: + return ''; + } + } + +} diff --git a/UI/Web/src/app/_pipes/generic-filter-field.pipe.ts b/UI/Web/src/app/_pipes/generic-filter-field.pipe.ts new file mode 100644 index 000000000..f342c0034 --- /dev/null +++ b/UI/Web/src/app/_pipes/generic-filter-field.pipe.ts @@ -0,0 +1,108 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {FilterField} from "../_models/metadata/v2/filter-field"; +import {translate} from "@jsverse/transloco"; +import {ValidFilterEntity} from "../metadata-filter/filter-settings"; +import {PersonFilterField} from "../_models/metadata/v2/person-filter-field"; + +@Pipe({ + name: 'genericFilterField' +}) +export class GenericFilterFieldPipe implements PipeTransform { + + transform(value: T, entityType: ValidFilterEntity): string { + + switch (entityType) { + case "series": + return this.translateFilterField(value as FilterField); + case "person": + return this.translatePersonFilterField(value as PersonFilterField); + } + } + + private translatePersonFilterField(value: PersonFilterField) { + switch (value) { + case PersonFilterField.Role: + return translate('generic-filter-field-pipe.person-role'); + case PersonFilterField.Name: + return translate('generic-filter-field-pipe.person-name'); + case PersonFilterField.SeriesCount: + return translate('generic-filter-field-pipe.person-series-count'); + case PersonFilterField.ChapterCount: + return translate('generic-filter-field-pipe.person-chapter-count'); + } + } + + private translateFilterField(value: FilterField) { + switch (value) { + case FilterField.AgeRating: + return translate('filter-field-pipe.age-rating'); + case FilterField.Characters: + return translate('filter-field-pipe.characters'); + case FilterField.CollectionTags: + return translate('filter-field-pipe.collection-tags'); + case FilterField.Colorist: + return translate('filter-field-pipe.colorist'); + case FilterField.CoverArtist: + return translate('filter-field-pipe.cover-artist'); + case FilterField.Editor: + return translate('filter-field-pipe.editor'); + case FilterField.Formats: + return translate('filter-field-pipe.formats'); + case FilterField.Genres: + return translate('filter-field-pipe.genres'); + case FilterField.Inker: + return translate('filter-field-pipe.inker'); + case FilterField.Imprint: + return translate('filter-field-pipe.imprint'); + case FilterField.Team: + return translate('filter-field-pipe.team'); + case FilterField.Location: + return translate('filter-field-pipe.location'); + case FilterField.Languages: + return translate('filter-field-pipe.languages'); + case FilterField.Libraries: + return translate('filter-field-pipe.libraries'); + case FilterField.Letterer: + return translate('filter-field-pipe.letterer'); + case FilterField.PublicationStatus: + return translate('filter-field-pipe.publication-status'); + case FilterField.Penciller: + return translate('filter-field-pipe.penciller'); + case FilterField.Publisher: + return translate('filter-field-pipe.publisher'); + case FilterField.ReadProgress: + return translate('filter-field-pipe.read-progress'); + case FilterField.ReadTime: + return translate('filter-field-pipe.read-time'); + case FilterField.ReleaseYear: + return translate('filter-field-pipe.release-year'); + case FilterField.SeriesName: + return translate('filter-field-pipe.series-name'); + case FilterField.Summary: + return translate('filter-field-pipe.summary'); + case FilterField.Tags: + return translate('filter-field-pipe.tags'); + case FilterField.Translators: + return translate('filter-field-pipe.translators'); + case FilterField.UserRating: + return translate('filter-field-pipe.user-rating'); + case FilterField.Writers: + return translate('filter-field-pipe.writers'); + case FilterField.Path: + return translate('filter-field-pipe.path'); + case FilterField.FilePath: + return translate('filter-field-pipe.file-path'); + case FilterField.WantToRead: + return translate('filter-field-pipe.want-to-read'); + case FilterField.ReadingDate: + return translate('filter-field-pipe.read-date'); + case FilterField.ReadLast: + return translate('filter-field-pipe.read-last'); + case FilterField.AverageRating: + return translate('filter-field-pipe.average-rating'); + default: + throw new Error(`Invalid FilterField value: ${value}`); + } + } + +} diff --git a/UI/Web/src/app/_pipes/person-role.pipe.ts b/UI/Web/src/app/_pipes/person-role.pipe.ts index c1395ae5b..1b9ee2163 100644 --- a/UI/Web/src/app/_pipes/person-role.pipe.ts +++ b/UI/Web/src/app/_pipes/person-role.pipe.ts @@ -1,6 +1,6 @@ -import {inject, Pipe, PipeTransform} from '@angular/core'; -import { PersonRole } from '../_models/metadata/person'; -import {translate, TranslocoService} from "@jsverse/transloco"; +import {Pipe, PipeTransform} from '@angular/core'; +import {PersonRole} from '../_models/metadata/person'; +import {translate} from "@jsverse/transloco"; @Pipe({ name: 'personRole', @@ -10,8 +10,6 @@ export class PersonRolePipe implements PipeTransform { transform(value: PersonRole): string { switch (value) { - case PersonRole.Artist: - return translate('person-role-pipe.artist'); case PersonRole.Character: return translate('person-role-pipe.character'); case PersonRole.Colorist: diff --git a/UI/Web/src/app/_pipes/sort-field.pipe.ts b/UI/Web/src/app/_pipes/sort-field.pipe.ts index 13ff4f758..d032de9c8 100644 --- a/UI/Web/src/app/_pipes/sort-field.pipe.ts +++ b/UI/Web/src/app/_pipes/sort-field.pipe.ts @@ -1,6 +1,8 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; import {SortField} from "../_models/metadata/series-filter"; import {TranslocoService} from "@jsverse/transloco"; +import {ValidFilterEntity} from "../metadata-filter/filter-settings"; +import {PersonSortField} from "../_models/metadata/v2/person-sort-field"; @Pipe({ name: 'sortField', @@ -11,7 +13,30 @@ export class SortFieldPipe implements PipeTransform { constructor(private translocoService: TranslocoService) { } - transform(value: SortField): string { + transform(value: T, entityType: ValidFilterEntity): string { + + switch (entityType) { + case 'series': + return this.seriesSortFields(value as SortField); + case 'person': + return this.personSortFields(value as PersonSortField); + + } + } + + private personSortFields(value: PersonSortField) { + switch (value) { + case PersonSortField.Name: + return this.translocoService.translate('sort-field-pipe.person-name'); + case PersonSortField.SeriesCount: + return this.translocoService.translate('sort-field-pipe.person-series-count'); + case PersonSortField.ChapterCount: + return this.translocoService.translate('sort-field-pipe.person-chapter-count'); + + } + } + + private seriesSortFields(value: SortField) { switch (value) { case SortField.SortName: return this.translocoService.translate('sort-field-pipe.sort-name'); @@ -32,7 +57,6 @@ export class SortFieldPipe implements PipeTransform { case SortField.Random: return this.translocoService.translate('sort-field-pipe.random'); } - } } diff --git a/UI/Web/src/app/_resolvers/url-filter.resolver.ts b/UI/Web/src/app/_resolvers/url-filter.resolver.ts new file mode 100644 index 000000000..16bc5c752 --- /dev/null +++ b/UI/Web/src/app/_resolvers/url-filter.resolver.ts @@ -0,0 +1,22 @@ +import {Injectable} from "@angular/core"; +import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from "@angular/router"; +import {Observable, of} from "rxjs"; +import {FilterV2} from "../_models/metadata/v2/filter-v2"; +import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service"; + +/** + * Checks the url for a filter and resolves one if applicable, otherwise returns null. + * It is up to the consumer to cast appropriately. + */ +@Injectable({ + providedIn: 'root' +}) +export class UrlFilterResolver implements Resolve { + + constructor(private filterUtilitiesService: FilterUtilitiesService) {} + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + if (!state.url.includes('?')) return of(null); + return this.filterUtilitiesService.decodeFilter(state.url.split('?')[1]); + } +} diff --git a/UI/Web/src/app/_routes/all-series-routing.module.ts b/UI/Web/src/app/_routes/all-series-routing.module.ts index d9dfaaf96..5c4804251 100644 --- a/UI/Web/src/app/_routes/all-series-routing.module.ts +++ b/UI/Web/src/app/_routes/all-series-routing.module.ts @@ -1,7 +1,13 @@ -import { Routes } from "@angular/router"; -import { AllSeriesComponent } from "../all-series/_components/all-series/all-series.component"; +import {Routes} from "@angular/router"; +import {AllSeriesComponent} from "../all-series/_components/all-series/all-series.component"; +import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; export const routes: Routes = [ - {path: '', component: AllSeriesComponent, pathMatch: 'full'}, + {path: '', component: AllSeriesComponent, pathMatch: 'full', + runGuardsAndResolvers: 'always', + resolve: { + filter: UrlFilterResolver + } + }, ]; diff --git a/UI/Web/src/app/_routes/bookmark-routing.module.ts b/UI/Web/src/app/_routes/bookmark-routing.module.ts index 6da971e08..2c7c52036 100644 --- a/UI/Web/src/app/_routes/bookmark-routing.module.ts +++ b/UI/Web/src/app/_routes/bookmark-routing.module.ts @@ -1,6 +1,12 @@ -import { Routes } from "@angular/router"; -import { BookmarksComponent } from "../bookmark/_components/bookmarks/bookmarks.component"; +import {Routes} from "@angular/router"; +import {BookmarksComponent} from "../bookmark/_components/bookmarks/bookmarks.component"; +import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; export const routes: Routes = [ - {path: '', component: BookmarksComponent, pathMatch: 'full'}, + {path: '', component: BookmarksComponent, pathMatch: 'full', + resolve: { + filter: UrlFilterResolver + }, + runGuardsAndResolvers: 'always', + }, ]; diff --git a/UI/Web/src/app/_routes/browse-authors-routing.module.ts b/UI/Web/src/app/_routes/browse-authors-routing.module.ts deleted file mode 100644 index e7aab1b57..000000000 --- a/UI/Web/src/app/_routes/browse-authors-routing.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Routes } from "@angular/router"; -import { AllSeriesComponent } from "../all-series/_components/all-series/all-series.component"; -import {BrowseAuthorsComponent} from "../browse-people/browse-authors.component"; - - -export const routes: Routes = [ - {path: '', component: BrowseAuthorsComponent, pathMatch: 'full'}, -]; diff --git a/UI/Web/src/app/_routes/browse-routing.module.ts b/UI/Web/src/app/_routes/browse-routing.module.ts new file mode 100644 index 000000000..be96e8193 --- /dev/null +++ b/UI/Web/src/app/_routes/browse-routing.module.ts @@ -0,0 +1,24 @@ +import {Routes} from "@angular/router"; +import {BrowsePeopleComponent} from "../browse/browse-people/browse-people.component"; +import {BrowseGenresComponent} from "../browse/browse-genres/browse-genres.component"; +import {BrowseTagsComponent} from "../browse/browse-tags/browse-tags.component"; +import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; + + +export const routes: Routes = [ + // Legacy route + {path: 'authors', component: BrowsePeopleComponent, pathMatch: 'full', + resolve: { + filter: UrlFilterResolver + }, + runGuardsAndResolvers: 'always', + }, + {path: 'people', component: BrowsePeopleComponent, pathMatch: 'full', + resolve: { + filter: UrlFilterResolver + }, + runGuardsAndResolvers: 'always', + }, + {path: 'genres', component: BrowseGenresComponent, pathMatch: 'full'}, + {path: 'tags', component: BrowseTagsComponent, pathMatch: 'full'}, +]; diff --git a/UI/Web/src/app/_routes/collections-routing.module.ts b/UI/Web/src/app/_routes/collections-routing.module.ts index 80510c8f6..2b3b0ffd7 100644 --- a/UI/Web/src/app/_routes/collections-routing.module.ts +++ b/UI/Web/src/app/_routes/collections-routing.module.ts @@ -1,9 +1,15 @@ -import { Routes } from '@angular/router'; -import { AllCollectionsComponent } from '../collections/_components/all-collections/all-collections.component'; -import { CollectionDetailComponent } from '../collections/_components/collection-detail/collection-detail.component'; +import {Routes} from '@angular/router'; +import {AllCollectionsComponent} from '../collections/_components/all-collections/all-collections.component'; +import {CollectionDetailComponent} from '../collections/_components/collection-detail/collection-detail.component'; +import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; export const routes: Routes = [ {path: '', component: AllCollectionsComponent, pathMatch: 'full'}, - {path: ':id', component: CollectionDetailComponent}, + {path: ':id', component: CollectionDetailComponent, + resolve: { + filter: UrlFilterResolver + }, + runGuardsAndResolvers: 'always', + }, ]; diff --git a/UI/Web/src/app/_routes/library-detail-routing.module.ts b/UI/Web/src/app/_routes/library-detail-routing.module.ts index 04cb3c9dd..3c09a71ee 100644 --- a/UI/Web/src/app/_routes/library-detail-routing.module.ts +++ b/UI/Web/src/app/_routes/library-detail-routing.module.ts @@ -1,7 +1,8 @@ -import { Routes } from '@angular/router'; -import { AuthGuard } from '../_guards/auth.guard'; -import { LibraryAccessGuard } from '../_guards/library-access.guard'; -import { LibraryDetailComponent } from '../library-detail/library-detail.component'; +import {Routes} from '@angular/router'; +import {AuthGuard} from '../_guards/auth.guard'; +import {LibraryAccessGuard} from '../_guards/library-access.guard'; +import {LibraryDetailComponent} from '../library-detail/library-detail.component'; +import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; export const routes: Routes = [ @@ -9,12 +10,18 @@ export const routes: Routes = [ path: ':libraryId', runGuardsAndResolvers: 'always', canActivate: [AuthGuard, LibraryAccessGuard], - component: LibraryDetailComponent + component: LibraryDetailComponent, + resolve: { + filter: UrlFilterResolver + }, }, { path: '', runGuardsAndResolvers: 'always', canActivate: [AuthGuard, LibraryAccessGuard], - component: LibraryDetailComponent - } + component: LibraryDetailComponent, + resolve: { + filter: UrlFilterResolver + }, + }, ]; diff --git a/UI/Web/src/app/_routes/want-to-read-routing.module.ts b/UI/Web/src/app/_routes/want-to-read-routing.module.ts index b3301d9f9..b593172c0 100644 --- a/UI/Web/src/app/_routes/want-to-read-routing.module.ts +++ b/UI/Web/src/app/_routes/want-to-read-routing.module.ts @@ -1,6 +1,10 @@ -import { Routes } from '@angular/router'; -import { WantToReadComponent } from '../want-to-read/_components/want-to-read/want-to-read.component'; +import {Routes} from '@angular/router'; +import {WantToReadComponent} from '../want-to-read/_components/want-to-read/want-to-read.component'; +import {UrlFilterResolver} from "../_resolvers/url-filter.resolver"; export const routes: Routes = [ - {path: '', component: WantToReadComponent, pathMatch: 'full'}, + {path: '', component: WantToReadComponent, pathMatch: 'full', runGuardsAndResolvers: 'always', resolve: { + filter: UrlFilterResolver + } + }, ]; diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index 8e8576069..f1f91143f 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -132,6 +132,33 @@ export class AccountService { return roles.some(role => user.roles.includes(role)); } + /** + * If User or Admin, will return false + * @param user + * @param restrictedRoles + */ + hasAnyRestrictedRole(user: User, restrictedRoles: Array = []) { + if (!user || !user.roles) { + return true; + } + + if (restrictedRoles.length === 0) { + return false; + } + + // If the user is an admin, they have the role + if (this.hasAdminRole(user)) { + return false; + } + + + if (restrictedRoles.length > 0 && restrictedRoles.some(role => user.roles.includes(role))) { + return true; + } + + return false; + } + hasAdminRole(user: User) { return user && user.roles.includes(Role.Admin); } diff --git a/UI/Web/src/app/_services/filter.service.ts b/UI/Web/src/app/_services/filter.service.ts index e76c1926f..2b9681e90 100644 --- a/UI/Web/src/app/_services/filter.service.ts +++ b/UI/Web/src/app/_services/filter.service.ts @@ -1,8 +1,7 @@ -import { Injectable } from '@angular/core'; -import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; +import {Injectable} from '@angular/core'; +import {FilterV2} from "../_models/metadata/v2/filter-v2"; import {environment} from "../../environments/environment"; -import { HttpClient } from "@angular/common/http"; -import {JumpKey} from "../_models/jumpbar/jump-key"; +import {HttpClient} from "@angular/common/http"; import {SmartFilter} from "../_models/metadata/v2/smart-filter"; @Injectable({ @@ -13,7 +12,7 @@ export class FilterService { baseUrl = environment.apiUrl; constructor(private httpClient: HttpClient) { } - saveFilter(filter: SeriesFilterV2) { + saveFilter(filter: FilterV2) { return this.httpClient.post(this.baseUrl + 'filter/update', filter); } getAllFilters() { @@ -26,5 +25,4 @@ export class FilterService { renameSmartFilter(filter: SmartFilter) { return this.httpClient.post(this.baseUrl + `filter/rename?filterId=${filter.id}&name=${filter.name.trim()}`, {}); } - } diff --git a/UI/Web/src/app/_services/jumpbar.service.ts b/UI/Web/src/app/_services/jumpbar.service.ts index d9919ff57..48ca08705 100644 --- a/UI/Web/src/app/_services/jumpbar.service.ts +++ b/UI/Web/src/app/_services/jumpbar.service.ts @@ -1,5 +1,5 @@ -import { Injectable } from '@angular/core'; -import { JumpKey } from '../_models/jumpbar/jump-key'; +import {Injectable} from '@angular/core'; +import {JumpKey} from '../_models/jumpbar/jump-key'; const keySize = 25; // Height of the JumpBar button @@ -105,14 +105,18 @@ export class JumpbarService { getJumpKeys(data :Array, keySelector: (data: any) => string) { const keys: {[key: string]: number} = {}; data.forEach(obj => { - let ch = keySelector(obj).charAt(0).toUpperCase(); - if (/\d|\#|!|%|@|\(|\)|\^|\.|_|\*/g.test(ch)) { - ch = '#'; + try { + let ch = keySelector(obj).charAt(0).toUpperCase(); + if (/\d|\#|!|%|@|\(|\)|\^|\.|_|\*/g.test(ch)) { + ch = '#'; + } + if (!keys.hasOwnProperty(ch)) { + keys[ch] = 0; + } + keys[ch] += 1; + } catch (e) { + console.error('Failed to calculate jump key for ', obj, e); } - if (!keys.hasOwnProperty(ch)) { - keys[ch] = 0; - } - keys[ch] += 1; }); return Object.keys(keys).map(k => { k = k.toUpperCase(); diff --git a/UI/Web/src/app/_services/metadata.service.ts b/UI/Web/src/app/_services/metadata.service.ts index 314e5c37b..fe0702219 100644 --- a/UI/Web/src/app/_services/metadata.service.ts +++ b/UI/Web/src/app/_services/metadata.service.ts @@ -1,33 +1,54 @@ -import {HttpClient} from '@angular/common/http'; -import {Injectable} from '@angular/core'; +import {HttpClient, HttpParams} from '@angular/common/http'; +import {inject, Injectable} from '@angular/core'; import {tap} from 'rxjs/operators'; -import {of} from 'rxjs'; +import {map, of} from 'rxjs'; import {environment} from 'src/environments/environment'; import {Genre} from '../_models/metadata/genre'; import {AgeRatingDto} from '../_models/metadata/age-rating-dto'; import {Language} from '../_models/metadata/language'; import {PublicationStatusDto} from '../_models/metadata/publication-status-dto'; -import {Person, PersonRole} from '../_models/metadata/person'; +import {allPeopleRoles, Person, PersonRole} from '../_models/metadata/person'; import {Tag} from '../_models/tag'; import {FilterComparison} from '../_models/metadata/v2/filter-comparison'; import {FilterField} from '../_models/metadata/v2/filter-field'; -import {SortField} from "../_models/metadata/series-filter"; +import {mangaFormatFilters, SortField} from "../_models/metadata/series-filter"; import {FilterCombination} from "../_models/metadata/v2/filter-combination"; -import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; +import {FilterV2} from "../_models/metadata/v2/filter-v2"; import {FilterStatement} from "../_models/metadata/v2/filter-statement"; import {SeriesDetailPlus} from "../_models/series-detail/series-detail-plus"; import {LibraryType} from "../_models/library/library"; import {IHasCast} from "../_models/common/i-has-cast"; import {TextResonse} from "../_types/text-response"; import {QueryContext} from "../_models/metadata/v2/query-context"; +import {AgeRatingPipe} from "../_pipes/age-rating.pipe"; +import {MangaFormatPipe} from "../_pipes/manga-format.pipe"; +import {TranslocoService} from "@jsverse/transloco"; +import {LibraryService} from './library.service'; +import {CollectionTagService} from "./collection-tag.service"; +import {PaginatedResult} from "../_models/pagination"; +import {UtilityService} from "../shared/_services/utility.service"; +import {BrowseGenre} from "../_models/metadata/browse/browse-genre"; +import {BrowseTag} from "../_models/metadata/browse/browse-tag"; +import {ValidFilterEntity} from "../metadata-filter/filter-settings"; +import {PersonFilterField} from "../_models/metadata/v2/person-filter-field"; +import {PersonRolePipe} from "../_pipes/person-role.pipe"; +import {PersonSortField} from "../_models/metadata/v2/person-sort-field"; @Injectable({ providedIn: 'root' }) export class MetadataService { + private readonly translocoService = inject(TranslocoService); + private readonly libraryService = inject(LibraryService); + private readonly collectionTagService = inject(CollectionTagService); + private readonly utilityService = inject(UtilityService); + baseUrl = environment.apiUrl; private validLanguages: Array = []; + private ageRatingPipe = new AgeRatingPipe(); + private mangaFormatPipe = new MangaFormatPipe(this.translocoService); + private personRolePipe = new PersonRolePipe(); constructor(private httpClient: HttpClient) { } @@ -74,6 +95,28 @@ export class MetadataService { return this.httpClient.get>(this.baseUrl + method); } + getGenreWithCounts(pageNum?: number, itemsPerPage?: number) { + let params = new HttpParams(); + params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); + + return this.httpClient.post>(this.baseUrl + 'metadata/genres-with-counts', {}, {observe: 'response', params}).pipe( + map((response: any) => { + return this.utilityService.createPaginatedResult(response) as PaginatedResult; + }) + ); + } + + getTagWithCounts(pageNum?: number, itemsPerPage?: number) { + let params = new HttpParams(); + params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); + + return this.httpClient.post>(this.baseUrl + 'metadata/tags-with-counts', {}, {observe: 'response', params}).pipe( + map((response: any) => { + return this.utilityService.createPaginatedResult(response) as PaginatedResult; + }) + ); + } + getAllLanguages(libraries?: Array) { let method = 'metadata/languages' if (libraries != undefined && libraries.length > 0) { @@ -110,19 +153,28 @@ export class MetadataService { return this.httpClient.get>(this.baseUrl + 'metadata/people-by-role?role=' + role); } - createDefaultFilterDto(): SeriesFilterV2 { + createDefaultFilterDto(entityType: ValidFilterEntity): FilterV2 { return { - statements: [] as FilterStatement[], + statements: [] as FilterStatement[], combination: FilterCombination.And, limitTo: 0, sortOptions: { isAscending: true, - sortField: SortField.SortName + sortField: (entityType === 'series' ? SortField.SortName : PersonSortField.Name) as TSort } }; } - createDefaultFilterStatement(field: FilterField = FilterField.SeriesName, comparison = FilterComparison.Equal, value = '') { + createDefaultFilterStatement(entityType: ValidFilterEntity) { + switch (entityType) { + case 'series': + return this.createFilterStatement(FilterField.SeriesName); + case 'person': + return this.createFilterStatement(PersonFilterField.Role, FilterComparison.Contains, `${PersonRole.CoverArtist},${PersonRole.Writer}`); + } + } + + createFilterStatement(field: T, comparison = FilterComparison.Equal, value = '') { return { comparison: comparison, field: field, @@ -130,7 +182,7 @@ export class MetadataService { }; } - updateFilter(arr: Array, index: number, filterStmt: FilterStatement) { + updateFilter(arr: Array>, index: number, filterStmt: FilterStatement) { arr[index].comparison = filterStmt.comparison; arr[index].field = filterStmt.field; arr[index].value = filterStmt.value ? filterStmt.value + '' : ''; @@ -140,8 +192,6 @@ export class MetadataService { switch (role) { case PersonRole.Other: break; - case PersonRole.Artist: - break; case PersonRole.CoverArtist: entity.coverArtists = persons; break; @@ -183,4 +233,85 @@ export class MetadataService { break; } } + + /** + * Used to get the underlying Options (for Metadata Filter Dropdowns) + * @param filterField + * @param entityType + */ + getOptionsForFilterField(filterField: T, entityType: ValidFilterEntity) { + + switch (entityType) { + case 'series': + return this.getSeriesOptionsForFilterField(filterField as FilterField); + case 'person': + return this.getPersonOptionsForFilterField(filterField as PersonFilterField); + } + } + + private getPersonOptionsForFilterField(field: PersonFilterField) { + switch (field) { + case PersonFilterField.Role: + return of(allPeopleRoles.map(r => {return {value: r, label: this.personRolePipe.transform(r)}})); + } + return of([]) + } + + private getSeriesOptionsForFilterField(field: FilterField) { + switch (field) { + case FilterField.PublicationStatus: + return this.getAllPublicationStatus().pipe(map(pubs => pubs.map(pub => { + return {value: pub.value, label: pub.title} + }))); + case FilterField.AgeRating: + return this.getAllAgeRatings().pipe(map(ratings => ratings.map(rating => { + return {value: rating.value, label: this.ageRatingPipe.transform(rating.value)} + }))); + case FilterField.Genres: + return this.getAllGenres().pipe(map(genres => genres.map(genre => { + return {value: genre.id, label: genre.title} + }))); + case FilterField.Languages: + return this.getAllLanguages().pipe(map(statuses => statuses.map(status => { + return {value: status.isoCode, label: status.title + ` (${status.isoCode})`} + }))); + case FilterField.Formats: + return of(mangaFormatFilters).pipe(map(statuses => statuses.map(status => { + return {value: status.value, label: this.mangaFormatPipe.transform(status.value)} + }))); + case FilterField.Libraries: + return this.libraryService.getLibraries().pipe(map(libs => libs.map(lib => { + return {value: lib.id, label: lib.name} + }))); + case FilterField.Tags: + return this.getAllTags().pipe(map(statuses => statuses.map(status => { + return {value: status.id, label: status.title} + }))); + case FilterField.CollectionTags: + return this.collectionTagService.allCollections().pipe(map(statuses => statuses.map(status => { + return {value: status.id, label: status.title} + }))); + case FilterField.Characters: return this.getPersonOptions(PersonRole.Character); + case FilterField.Colorist: return this.getPersonOptions(PersonRole.Colorist); + case FilterField.CoverArtist: return this.getPersonOptions(PersonRole.CoverArtist); + case FilterField.Editor: return this.getPersonOptions(PersonRole.Editor); + case FilterField.Inker: return this.getPersonOptions(PersonRole.Inker); + case FilterField.Letterer: return this.getPersonOptions(PersonRole.Letterer); + case FilterField.Penciller: return this.getPersonOptions(PersonRole.Penciller); + case FilterField.Publisher: return this.getPersonOptions(PersonRole.Publisher); + case FilterField.Imprint: return this.getPersonOptions(PersonRole.Imprint); + case FilterField.Team: return this.getPersonOptions(PersonRole.Team); + case FilterField.Location: return this.getPersonOptions(PersonRole.Location); + case FilterField.Translators: return this.getPersonOptions(PersonRole.Translator); + case FilterField.Writers: return this.getPersonOptions(PersonRole.Writer); + } + + return of([]); + } + + private getPersonOptions(role: PersonRole) { + return this.getAllPeopleByRole(role).pipe(map(people => people.map(person => { + return {value: person.id, label: person.name} + }))); + } } diff --git a/UI/Web/src/app/_services/person.service.ts b/UI/Web/src/app/_services/person.service.ts index 0ac58b178..fc9148135 100644 --- a/UI/Web/src/app/_services/person.service.ts +++ b/UI/Web/src/app/_services/person.service.ts @@ -6,9 +6,12 @@ import {PaginatedResult} from "../_models/pagination"; import {Series} from "../_models/series"; import {map} from "rxjs/operators"; import {UtilityService} from "../shared/_services/utility.service"; -import {BrowsePerson} from "../_models/person/browse-person"; +import {BrowsePerson} from "../_models/metadata/browse/browse-person"; import {StandaloneChapter} from "../_models/standalone-chapter"; import {TextResonse} from "../_types/text-response"; +import {FilterV2} from "../_models/metadata/v2/filter-v2"; +import {PersonFilterField} from "../_models/metadata/v2/person-filter-field"; +import {PersonSortField} from "../_models/metadata/v2/person-sort-field"; @Injectable({ providedIn: 'root' @@ -43,17 +46,28 @@ export class PersonService { return this.httpClient.get>(this.baseUrl + `person/chapters-by-role?personId=${personId}&role=${role}`); } - getAuthorsToBrowse(pageNum?: number, itemsPerPage?: number) { + getAuthorsToBrowse(filter: FilterV2, pageNum?: number, itemsPerPage?: number) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); - return this.httpClient.post>(this.baseUrl + 'person/all', {}, {observe: 'response', params}).pipe( + return this.httpClient.post>(this.baseUrl + `person/all`, filter, {observe: 'response', params}).pipe( map((response: any) => { return this.utilityService.createPaginatedResult(response) as PaginatedResult; }) ); } + // getAuthorsToBrowse(filter: BrowsePersonFilter, pageNum?: number, itemsPerPage?: number) { + // let params = new HttpParams(); + // params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); + // + // return this.httpClient.post>(this.baseUrl + `person/all`, filter, {observe: 'response', params}).pipe( + // map((response: any) => { + // return this.utilityService.createPaginatedResult(response) as PaginatedResult; + // }) + // ); + // } + downloadCover(personId: number) { return this.httpClient.post(this.baseUrl + 'person/fetch-cover?personId=' + personId, {}, TextResonse); } diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 9941cd005..05958ee61 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -16,13 +16,14 @@ import {TextResonse} from '../_types/text-response'; import {AccountService} from './account.service'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {PersonalToC} from "../_models/readers/personal-toc"; -import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; +import {FilterV2} from "../_models/metadata/v2/filter-v2"; import NoSleep from 'nosleep.js'; import {FullProgress} from "../_models/readers/full-progress"; import {Volume} from "../_models/volume"; import {UtilityService} from "../shared/_services/utility.service"; import {translate} from "@jsverse/transloco"; import {ToastrService} from "ngx-toastr"; +import {FilterField} from "../_models/metadata/v2/filter-field"; export const CHAPTER_ID_DOESNT_EXIST = -1; @@ -107,7 +108,7 @@ export class ReaderService { return this.httpClient.post(this.baseUrl + 'reader/unbookmark', {seriesId, volumeId, chapterId, page}); } - getAllBookmarks(filter: SeriesFilterV2 | undefined) { + getAllBookmarks(filter: FilterV2 | undefined) { return this.httpClient.post(this.baseUrl + 'reader/all-bookmarks', filter); } diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index b440b1eb7..39e3b720b 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -1,28 +1,26 @@ -import { HttpClient, HttpParams } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { environment } from 'src/environments/environment'; -import { UtilityService } from '../shared/_services/utility.service'; -import { Chapter } from '../_models/chapter'; -import { PaginatedResult } from '../_models/pagination'; -import { Series } from '../_models/series'; -import { RelatedSeries } from '../_models/series-detail/related-series'; -import { SeriesDetail } from '../_models/series-detail/series-detail'; -import { SeriesGroup } from '../_models/series-group'; -import { SeriesMetadata } from '../_models/metadata/series-metadata'; -import { Volume } from '../_models/volume'; -import { ImageService } from './image.service'; -import { TextResonse } from '../_types/text-response'; -import { SeriesFilterV2 } from '../_models/metadata/v2/series-filter-v2'; -import {UserReview} from "../_single-module/review-card/user-review"; +import {HttpClient, HttpParams} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {environment} from 'src/environments/environment'; +import {UtilityService} from '../shared/_services/utility.service'; +import {Chapter} from '../_models/chapter'; +import {PaginatedResult} from '../_models/pagination'; +import {Series} from '../_models/series'; +import {RelatedSeries} from '../_models/series-detail/related-series'; +import {SeriesDetail} from '../_models/series-detail/series-detail'; +import {SeriesGroup} from '../_models/series-group'; +import {SeriesMetadata} from '../_models/metadata/series-metadata'; +import {Volume} from '../_models/volume'; +import {TextResonse} from '../_types/text-response'; +import {FilterV2} from '../_models/metadata/v2/filter-v2'; import {Rating} from "../_models/rating"; import {Recommendation} from "../_models/series-detail/recommendation"; import {ExternalSeriesDetail} from "../_models/series-detail/external-series-detail"; import {NextExpectedChapter} from "../_models/series-detail/next-expected-chapter"; import {QueryContext} from "../_models/metadata/v2/query-context"; -import {ExternalSeries} from "../_models/series-detail/external-series"; import {ExternalSeriesMatch} from "../_models/series-detail/external-series-match"; +import {FilterField} from "../_models/metadata/v2/filter-field"; @Injectable({ providedIn: 'root' @@ -33,10 +31,9 @@ export class SeriesService { paginatedResults: PaginatedResult = new PaginatedResult(); paginatedSeriesForTagsResults: PaginatedResult = new PaginatedResult(); - constructor(private httpClient: HttpClient, private imageService: ImageService, - private utilityService: UtilityService) { } + constructor(private httpClient: HttpClient, private utilityService: UtilityService) { } - getAllSeriesV2(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2, context: QueryContext = QueryContext.None) { + getAllSeriesV2(pageNum?: number, itemsPerPage?: number, filter?: FilterV2, context: QueryContext = QueryContext.None) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); const data = filter || {}; @@ -48,7 +45,7 @@ export class SeriesService { ); } - getSeriesForLibraryV2(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2) { + getSeriesForLibraryV2(pageNum?: number, itemsPerPage?: number, filter?: FilterV2) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); const data = filter || {}; @@ -100,7 +97,7 @@ export class SeriesService { return this.httpClient.post(this.baseUrl + 'reader/mark-unread', {seriesId}); } - getRecentlyAdded(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2) { + getRecentlyAdded(pageNum?: number, itemsPerPage?: number, filter?: FilterV2) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); @@ -116,7 +113,7 @@ export class SeriesService { return this.httpClient.post(this.baseUrl + 'series/recently-updated-series', {}); } - getWantToRead(pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2): Observable> { + getWantToRead(pageNum?: number, itemsPerPage?: number, filter?: FilterV2): Observable> { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); const data = filter || {}; @@ -134,7 +131,7 @@ export class SeriesService { })); } - getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilterV2) { + getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: FilterV2) { let params = new HttpParams(); params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage); const data = filter || {}; @@ -230,5 +227,4 @@ export class SeriesService { updateDontMatch(seriesId: number, dontMatch: boolean) { return this.httpClient.post(this.baseUrl + `series/dont-match?seriesId=${seriesId}&dontMatch=${dontMatch}`, {}, TextResonse); } - } diff --git a/UI/Web/src/app/_services/toggle.service.ts b/UI/Web/src/app/_services/toggle.service.ts index 8b335394a..0ad9813e3 100644 --- a/UI/Web/src/app/_services/toggle.service.ts +++ b/UI/Web/src/app/_services/toggle.service.ts @@ -1,6 +1,6 @@ -import { Injectable } from '@angular/core'; -import { NavigationStart, Router } from '@angular/router'; -import { filter, ReplaySubject, take } from 'rxjs'; +import {Injectable} from '@angular/core'; +import {NavigationStart, Router} from '@angular/router'; +import {filter, ReplaySubject, take} from 'rxjs'; @Injectable({ providedIn: 'root' @@ -29,7 +29,7 @@ export class ToggleService { this.toggleState = !state; this.toggleStateSource.next(this.toggleState); }); - + } set(state: boolean) { diff --git a/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.html b/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.html index 2d94dd848..322a16bd8 100644 --- a/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.html +++ b/UI/Web/src/app/_single-module/match-series-result-item/match-series-result-item.component.html @@ -33,10 +33,10 @@ } @else {
@if ((item.series.volumes || 0) > 0 || (item.series.chapters || 0) > 0) { - {{t('volume-count', {num: item.series.volumes})}} @if (item.series.plusMediaFormat === PlusMediaFormat.Comic) { {{t('issue-count', {num: item.series.chapters})}} } @else { + {{t('volume-count', {num: item.series.volumes})}} {{t('chapter-count', {num: item.series.chapters})}} } } @else { diff --git a/UI/Web/src/app/_single-module/sort-button/sort-button.component.html b/UI/Web/src/app/_single-module/sort-button/sort-button.component.html new file mode 100644 index 000000000..bc02c743d --- /dev/null +++ b/UI/Web/src/app/_single-module/sort-button/sort-button.component.html @@ -0,0 +1,9 @@ + + + diff --git a/UI/Web/src/app/_single-module/sort-button/sort-button.component.scss b/UI/Web/src/app/_single-module/sort-button/sort-button.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/UI/Web/src/app/_single-module/sort-button/sort-button.component.ts b/UI/Web/src/app/_single-module/sort-button/sort-button.component.ts new file mode 100644 index 000000000..230a0ee6f --- /dev/null +++ b/UI/Web/src/app/_single-module/sort-button/sort-button.component.ts @@ -0,0 +1,21 @@ +import {ChangeDetectionStrategy, Component, input, model} from '@angular/core'; +import {TranslocoDirective} from "@jsverse/transloco"; + +@Component({ + selector: 'app-sort-button', + imports: [ + TranslocoDirective + ], + templateUrl: './sort-button.component.html', + styleUrl: './sort-button.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SortButtonComponent { + + disabled = input(false); + isAscending = model(true); + + updateSortOrder() { + this.isAscending.set(!this.isAscending()); + } +} diff --git a/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.html b/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.html index 60f494f38..2ae9ea45b 100644 --- a/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.html +++ b/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.html @@ -3,8 +3,17 @@
-
- +
+ + +
+
+ - @for (field of availableFields; track field) { - + @for (field of filterFieldOptions(); track field.value) { + }
@@ -18,7 +18,7 @@
- @if (IsEmptySelected) { + @if (isEmptySelected()) { @if (predicateType$ | async; as predicateType) { @switch (predicateType) { @case (PredicateType.Text) { @@ -50,7 +50,7 @@ @@ -62,10 +62,11 @@
- @if (UiLabel !== null) { - {{t(UiLabel.unit)}} - @if (UiLabel.tooltip) { - + @let label = uiLabel(); + @if (label !== null) { + {{t(label.unit)}} + @if (label.tooltip) { + } }
diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts index 34a1b7db8..4fcca8eac 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts +++ b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts @@ -2,32 +2,41 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + computed, DestroyRef, EventEmitter, inject, + Injector, + input, Input, OnInit, Output, + Signal, } from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; import {FilterStatement} from '../../../_models/metadata/v2/filter-statement'; import {BehaviorSubject, distinctUntilChanged, filter, map, Observable, of, startWith, switchMap, tap} from 'rxjs'; import {MetadataService} from 'src/app/_services/metadata.service'; -import {mangaFormatFilters} from 'src/app/_models/metadata/series-filter'; -import {PersonRole} from 'src/app/_models/metadata/person'; -import {LibraryService} from 'src/app/_services/library.service'; -import {CollectionTagService} from 'src/app/_services/collection-tag.service'; import {FilterComparison} from 'src/app/_models/metadata/v2/filter-comparison'; -import {allFields, FilterField} from 'src/app/_models/metadata/v2/filter-field'; +import {FilterField} from 'src/app/_models/metadata/v2/filter-field'; import {AsyncPipe} from "@angular/common"; -import {FilterFieldPipe} from "../../../_pipes/filter-field.pipe"; import {FilterComparisonPipe} from "../../../_pipes/filter-comparison.pipe"; -import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop"; import {Select2, Select2Option} from "ng-select2-component"; import {NgbDate, NgbDateParserFormatter, NgbInputDatepicker, NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; import {TranslocoDirective, TranslocoService} from "@jsverse/transloco"; -import {MangaFormatPipe} from "../../../_pipes/manga-format.pipe"; -import {AgeRatingPipe} from "../../../_pipes/age-rating.pipe"; +import {ValidFilterEntity} from "../../filter-settings"; +import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service"; + +interface FieldConfig { + type: PredicateType; + baseComparisons: FilterComparison[]; + defaultValue: any; + allowsDateComparisons?: boolean; + allowsNumberComparisons?: boolean; + excludesMustContains?: boolean; + allowsIsEmpty?: boolean; +} enum PredicateType { Text = 1, @@ -54,42 +63,42 @@ const unitLabels: Map = new Map([ [FilterField.ReadLast, new FilterRowUi('unit-read-last')], ]); -const StringFields = [FilterField.SeriesName, FilterField.Summary, FilterField.Path, FilterField.FilePath]; -const NumberFields = [ - FilterField.ReadTime, FilterField.ReleaseYear, FilterField.ReadProgress, - FilterField.UserRating, FilterField.AverageRating, FilterField.ReadLast -]; -const DropdownFields = [ - FilterField.PublicationStatus, FilterField.Languages, FilterField.AgeRating, - FilterField.Translators, FilterField.Characters, FilterField.Publisher, - FilterField.Editor, FilterField.CoverArtist, FilterField.Letterer, - FilterField.Colorist, FilterField.Inker, FilterField.Penciller, - FilterField.Writers, FilterField.Genres, FilterField.Libraries, - FilterField.Formats, FilterField.CollectionTags, FilterField.Tags, - FilterField.Imprint, FilterField.Team, FilterField.Location -]; -const BooleanFields = [FilterField.WantToRead]; -const DateFields = [FilterField.ReadingDate]; - -const DropdownFieldsWithoutMustContains = [ - FilterField.Libraries, FilterField.Formats, FilterField.AgeRating, FilterField.PublicationStatus -]; -const DropdownFieldsThatIncludeNumberComparisons = [ - FilterField.AgeRating -]; -const NumberFieldsThatIncludeDateComparisons = [ - FilterField.ReleaseYear -]; - -const FieldsThatShouldIncludeIsEmpty = [ - FilterField.Summary, FilterField.UserRating, FilterField.Genres, - FilterField.CollectionTags, FilterField.Tags, FilterField.ReleaseYear, - FilterField.Translators, FilterField.Characters, FilterField.Publisher, - FilterField.Editor, FilterField.CoverArtist, FilterField.Letterer, - FilterField.Colorist, FilterField.Inker, FilterField.Penciller, - FilterField.Writers, FilterField.Imprint, FilterField.Team, - FilterField.Location, -]; +// const StringFields = [FilterField.SeriesName, FilterField.Summary, FilterField.Path, FilterField.FilePath, PersonFilterField.Name]; +// const NumberFields = [ +// FilterField.ReadTime, FilterField.ReleaseYear, FilterField.ReadProgress, +// FilterField.UserRating, FilterField.AverageRating, FilterField.ReadLast +// ]; +// const DropdownFields = [ +// FilterField.PublicationStatus, FilterField.Languages, FilterField.AgeRating, +// FilterField.Translators, FilterField.Characters, FilterField.Publisher, +// FilterField.Editor, FilterField.CoverArtist, FilterField.Letterer, +// FilterField.Colorist, FilterField.Inker, FilterField.Penciller, +// FilterField.Writers, FilterField.Genres, FilterField.Libraries, +// FilterField.Formats, FilterField.CollectionTags, FilterField.Tags, +// FilterField.Imprint, FilterField.Team, FilterField.Location, PersonFilterField.Role +// ]; +// const BooleanFields = [FilterField.WantToRead]; +// const DateFields = [FilterField.ReadingDate]; +// +// const DropdownFieldsWithoutMustContains = [ +// FilterField.Libraries, FilterField.Formats, FilterField.AgeRating, FilterField.PublicationStatus +// ]; +// const DropdownFieldsThatIncludeNumberComparisons = [ +// FilterField.AgeRating +// ]; +// const NumberFieldsThatIncludeDateComparisons = [ +// FilterField.ReleaseYear +// ]; +// +// const FieldsThatShouldIncludeIsEmpty = [ +// FilterField.Summary, FilterField.UserRating, FilterField.Genres, +// FilterField.CollectionTags, FilterField.Tags, FilterField.ReleaseYear, +// FilterField.Translators, FilterField.Characters, FilterField.Publisher, +// FilterField.Editor, FilterField.CoverArtist, FilterField.Letterer, +// FilterField.Colorist, FilterField.Inker, FilterField.Penciller, +// FilterField.Writers, FilterField.Imprint, FilterField.Team, +// FilterField.Location, +// ]; const StringComparisons = [ FilterComparison.Equal, @@ -126,7 +135,6 @@ const BooleanComparisons = [ imports: [ ReactiveFormsModule, AsyncPipe, - FilterFieldPipe, FilterComparisonPipe, NgbTooltip, TranslocoDirective, @@ -135,60 +143,75 @@ const BooleanComparisons = [ ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class MetadataFilterRowComponent implements OnInit { - - protected readonly FilterComparison = FilterComparison; - protected readonly PredicateType = PredicateType; +export class MetadataFilterRowComponent implements OnInit { private readonly cdRef = inject(ChangeDetectorRef); private readonly destroyRef = inject(DestroyRef); private readonly dateParser = inject(NgbDateParserFormatter); private readonly metadataService = inject(MetadataService); - private readonly libraryService = inject(LibraryService); - private readonly collectionTagService = inject(CollectionTagService); private readonly translocoService = inject(TranslocoService); + private readonly filterUtilitiesService = inject(FilterUtilitiesService); + private readonly injector = inject(Injector); - - @Input() index: number = 0; // This is only for debugging /** * Slightly misleading as this is the initial state and will be updated on the filterStatement event emitter */ - @Input() preset!: FilterStatement; - @Input() availableFields: Array = allFields; - @Output() filterStatement = new EventEmitter(); + @Input() preset!: FilterStatement; + entityType = input.required(); + @Output() filterStatement = new EventEmitter>(); - formGroup: FormGroup = new FormGroup({ - 'comparison': new FormControl(FilterComparison.Equal, []), - 'filterValue': new FormControl('', []), - }); + formGroup!: FormGroup; validComparisons$: BehaviorSubject = new BehaviorSubject([FilterComparison.Equal] as FilterComparison[]); predicateType$: BehaviorSubject = new BehaviorSubject(PredicateType.Text as PredicateType); dropdownOptions$ = of([]); loaded: boolean = false; - private readonly mangaFormatPipe = new MangaFormatPipe(this.translocoService); - private readonly ageRatingPipe = new AgeRatingPipe(); - - get IsEmptySelected() { - return parseInt(this.formGroup.get('comparison')?.value + '', 10) !== FilterComparison.IsEmpty; - } - get UiLabel(): FilterRowUi | null { - const field = parseInt(this.formGroup.get('input')!.value, 10) as FilterField; - if (!unitLabels.has(field)) return null; - return unitLabels.get(field) as FilterRowUi; - } + private comparisonSignal!: Signal; + private inputSignal!: Signal; - get MultipleDropdownAllowed() { - const comp = parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison; - return comp === FilterComparison.Contains || comp === FilterComparison.NotContains || comp === FilterComparison.MustContains; - } + isEmptySelected: Signal = computed(() => false); + uiLabel: Signal = computed(() => null); + isMultiSelectDropdownAllowed: Signal = computed(() => false); + filterFieldOptions: Signal<{title: string, value: TFilter}[]> = computed(() => []); ngOnInit() { - this.formGroup.addControl('input', new FormControl(FilterField.SeriesName, [])); + + this.formGroup = new FormGroup({ + 'comparison': new FormControl(FilterComparison.Equal, []), + 'filterValue': new FormControl('', []), + 'input': new FormControl(this.filterUtilitiesService.getDefaultFilterField(this.entityType()), []) + }); + + this.comparisonSignal = toSignal( + this.formGroup.get('comparison')!.valueChanges.pipe( + startWith(this.formGroup.get('comparison')!.value), + map(d => parseInt(d + '', 10) as FilterComparison) + ) + , {requireSync: true, injector: this.injector}); + this.inputSignal = toSignal( + this.formGroup.get('input')!.valueChanges.pipe( + startWith(this.formGroup.get('input')!.value), + map(d => parseInt(d + '', 10) as TFilter) + ) + , {requireSync: true, injector: this.injector}); + + this.isEmptySelected = computed(() => this.comparisonSignal() !== FilterComparison.IsEmpty); + this.uiLabel = computed(() => { + if (!unitLabels.has(this.inputSignal())) return null; + return unitLabels.get(this.inputSignal()) as FilterRowUi; + }); + + this.isMultiSelectDropdownAllowed = computed(() => { + return this.comparisonSignal() === FilterComparison.Contains || this.comparisonSignal() === FilterComparison.NotContains || this.comparisonSignal() === FilterComparison.MustContains; + }); + + this.filterFieldOptions = computed(() => { + return this.filterUtilitiesService.getFilterFields(this.entityType()); + }); this.formGroup.get('input')?.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe((val: string) => this.handleFieldChange(val)); this.populateFromPreset(); @@ -200,14 +223,14 @@ export class MetadataFilterRowComponent implements OnInit { startWith(this.preset.value), distinctUntilChanged(), filter(() => { - const inputVal = parseInt(this.formGroup.get('input')?.value, 10) as FilterField; - return DropdownFields.includes(inputVal); + return this.filterUtilitiesService.getDropdownFields(this.entityType()).includes(this.inputSignal()); }), switchMap((_) => this.getDropdownObservable()), takeUntilDestroyed(this.destroyRef) ); + this.formGroup!.valueChanges.pipe( distinctUntilChanged(), tap(_ => this.propagateFilterUpdate()), @@ -221,11 +244,13 @@ export class MetadataFilterRowComponent implements OnInit { propagateFilterUpdate() { const stmt = { comparison: parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison, - field: parseInt(this.formGroup.get('input')?.value, 10) as FilterField, + field: parseInt(this.formGroup.get('input')?.value, 10) as TFilter, value: this.formGroup.get('filterValue')?.value! }; - if (typeof stmt.value === 'object' && DateFields.includes(stmt.field)) { + const dateFields = this.filterUtilitiesService.getDateFields(this.entityType()); + const booleanFields = this.filterUtilitiesService.getBooleanFields(this.entityType()); + if (typeof stmt.value === 'object' && dateFields.includes(stmt.field)) { stmt.value = this.dateParser.format(stmt.value); } @@ -239,7 +264,7 @@ export class MetadataFilterRowComponent implements OnInit { } if (stmt.comparison !== FilterComparison.IsEmpty) { - if (!stmt.value && (![FilterField.SeriesName, FilterField.Summary].includes(stmt.field) && !BooleanFields.includes(stmt.field))) return; + if (!stmt.value && (![FilterField.SeriesName, FilterField.Summary].includes(stmt.field) && !booleanFields.includes(stmt.field))) return; } this.filterStatement.emit(stmt); @@ -250,15 +275,20 @@ export class MetadataFilterRowComponent implements OnInit { this.formGroup.get('comparison')?.patchValue(this.preset.comparison); this.formGroup.get('input')?.patchValue(this.preset.field); - if (StringFields.includes(this.preset.field)) { + const dropdownFields = this.filterUtilitiesService.getDropdownFields(this.entityType()); + const stringFields = this.filterUtilitiesService.getStringFields(this.entityType()); + const dateFields = this.filterUtilitiesService.getDateFields(this.entityType()); + const booleanFields = this.filterUtilitiesService.getBooleanFields(this.entityType()); + + if (stringFields.includes(this.preset.field)) { this.formGroup.get('filterValue')?.patchValue(val); - } else if (BooleanFields.includes(this.preset.field)) { + } else if (booleanFields.includes(this.preset.field)) { this.formGroup.get('filterValue')?.patchValue(val); - } else if (DateFields.includes(this.preset.field)) { + } else if (dateFields.includes(this.preset.field)) { this.formGroup.get('filterValue')?.patchValue(this.dateParser.parse(val)); } - else if (DropdownFields.includes(this.preset.field)) { - if (this.MultipleDropdownAllowed || val.includes(',')) { + else if (dropdownFields.includes(this.preset.field)) { + if (this.isMultiSelectDropdownAllowed() || val.includes(',')) { this.formGroup.get('filterValue')?.patchValue(val.split(',').map(d => parseInt(d, 10))); } else { if (this.preset.field === FilterField.Languages) { @@ -276,72 +306,28 @@ export class MetadataFilterRowComponent implements OnInit { } getDropdownObservable(): Observable { - const filterField = parseInt(this.formGroup.get('input')?.value, 10) as FilterField; - switch (filterField) { - case FilterField.PublicationStatus: - return this.metadataService.getAllPublicationStatus().pipe(map(pubs => pubs.map(pub => { - return {value: pub.value, label: pub.title} - }))); - case FilterField.AgeRating: - return this.metadataService.getAllAgeRatings().pipe(map(ratings => ratings.map(rating => { - return {value: rating.value, label: this.ageRatingPipe.transform(rating.value)} - }))); - case FilterField.Genres: - return this.metadataService.getAllGenres().pipe(map(genres => genres.map(genre => { - return {value: genre.id, label: genre.title} - }))); - case FilterField.Languages: - return this.metadataService.getAllLanguages().pipe(map(statuses => statuses.map(status => { - return {value: status.isoCode, label: status.title + ` (${status.isoCode})`} - }))); - case FilterField.Formats: - return of(mangaFormatFilters).pipe(map(statuses => statuses.map(status => { - return {value: status.value, label: this.mangaFormatPipe.transform(status.value)} - }))); - case FilterField.Libraries: - return this.libraryService.getLibraries().pipe(map(libs => libs.map(lib => { - return {value: lib.id, label: lib.name} - }))); - case FilterField.Tags: - return this.metadataService.getAllTags().pipe(map(statuses => statuses.map(status => { - return {value: status.id, label: status.title} - }))); - case FilterField.CollectionTags: - return this.collectionTagService.allCollections().pipe(map(statuses => statuses.map(status => { - return {value: status.id, label: status.title} - }))); - case FilterField.Characters: return this.getPersonOptions(PersonRole.Character); - case FilterField.Colorist: return this.getPersonOptions(PersonRole.Colorist); - case FilterField.CoverArtist: return this.getPersonOptions(PersonRole.CoverArtist); - case FilterField.Editor: return this.getPersonOptions(PersonRole.Editor); - case FilterField.Inker: return this.getPersonOptions(PersonRole.Inker); - case FilterField.Letterer: return this.getPersonOptions(PersonRole.Letterer); - case FilterField.Penciller: return this.getPersonOptions(PersonRole.Penciller); - case FilterField.Publisher: return this.getPersonOptions(PersonRole.Publisher); - case FilterField.Imprint: return this.getPersonOptions(PersonRole.Imprint); - case FilterField.Team: return this.getPersonOptions(PersonRole.Team); - case FilterField.Location: return this.getPersonOptions(PersonRole.Location); - case FilterField.Translators: return this.getPersonOptions(PersonRole.Translator); - case FilterField.Writers: return this.getPersonOptions(PersonRole.Writer); - } - return of([]); + const filterField = this.inputSignal(); + return this.metadataService.getOptionsForFilterField(filterField, this.entityType()); } - getPersonOptions(role: PersonRole) { - return this.metadataService.getAllPeopleByRole(role).pipe(map(people => people.map(person => { - return {value: person.id, label: person.name} - }))); - } - - handleFieldChange(val: string) { - const inputVal = parseInt(val, 10) as FilterField; + const inputVal = parseInt(val, 10) as TFilter; + const stringFields = this.filterUtilitiesService.getStringFields(this.entityType()); + const dropdownFields = this.filterUtilitiesService.getDropdownFields(this.entityType()); + const numberFields = this.filterUtilitiesService.getNumberFields(this.entityType()); + const booleanFields = this.filterUtilitiesService.getBooleanFields(this.entityType()); + const dateFields = this.filterUtilitiesService.getDateFields(this.entityType()); + const fieldsThatShouldIncludeIsEmpty = this.filterUtilitiesService.getFieldsThatShouldIncludeIsEmpty(this.entityType()); + const numberFieldsThatIncludeDateComparisons = this.filterUtilitiesService.getNumberFieldsThatIncludeDateComparisons(this.entityType()); + const dropdownFieldsThatIncludeDateComparisons = this.filterUtilitiesService.getDropdownFieldsThatIncludeDateComparisons(this.entityType()); + const dropdownFieldsWithoutMustContains = this.filterUtilitiesService.getDropdownFieldsWithoutMustContains(this.entityType()); + const dropdownFieldsThatIncludeNumberComparisons = this.filterUtilitiesService.getDropdownFieldsThatIncludeNumberComparisons(this.entityType()); - if (StringFields.includes(inputVal)) { + if (stringFields.includes(inputVal)) { let comps = [...StringComparisons]; - if (FieldsThatShouldIncludeIsEmpty.includes(inputVal)) { + if (fieldsThatShouldIncludeIsEmpty.includes(inputVal)) { comps.push(FilterComparison.IsEmpty); } @@ -356,13 +342,13 @@ export class MetadataFilterRowComponent implements OnInit { return; } - if (NumberFields.includes(inputVal)) { + if (numberFields.includes(inputVal)) { const comps = [...NumberComparisons]; - if (NumberFieldsThatIncludeDateComparisons.includes(inputVal)) { + if (numberFieldsThatIncludeDateComparisons.includes(inputVal)) { comps.push(...DateComparisons); } - if (FieldsThatShouldIncludeIsEmpty.includes(inputVal)) { + if (fieldsThatShouldIncludeIsEmpty.includes(inputVal)) { comps.push(FilterComparison.IsEmpty); } @@ -378,9 +364,9 @@ export class MetadataFilterRowComponent implements OnInit { return; } - if (DateFields.includes(inputVal)) { + if (dateFields.includes(inputVal)) { const comps = [...DateComparisons]; - if (FieldsThatShouldIncludeIsEmpty.includes(inputVal)) { + if (fieldsThatShouldIncludeIsEmpty.includes(inputVal)) { comps.push(FilterComparison.IsEmpty); } @@ -395,9 +381,9 @@ export class MetadataFilterRowComponent implements OnInit { return; } - if (BooleanFields.includes(inputVal)) { + if (booleanFields.includes(inputVal)) { let comps = [...DateComparisons]; - if (FieldsThatShouldIncludeIsEmpty.includes(inputVal)) { + if (fieldsThatShouldIncludeIsEmpty.includes(inputVal)) { comps.push(FilterComparison.IsEmpty); } @@ -413,15 +399,15 @@ export class MetadataFilterRowComponent implements OnInit { return; } - if (DropdownFields.includes(inputVal)) { + if (dropdownFields.includes(inputVal)) { let comps = [...DropdownComparisons]; - if (DropdownFieldsThatIncludeNumberComparisons.includes(inputVal)) { + if (dropdownFieldsThatIncludeNumberComparisons.includes(inputVal)) { comps.push(...NumberComparisons); } - if (DropdownFieldsWithoutMustContains.includes(inputVal)) { + if (dropdownFieldsWithoutMustContains.includes(inputVal)) { comps = comps.filter(c => c !== FilterComparison.MustContains); } - if (FieldsThatShouldIncludeIsEmpty.includes(inputVal)) { + if (fieldsThatShouldIncludeIsEmpty.includes(inputVal)) { comps.push(FilterComparison.IsEmpty); } @@ -443,4 +429,7 @@ export class MetadataFilterRowComponent implements OnInit { updateIfDateFilled() { this.propagateFilterUpdate(); } + + protected readonly FilterComparison = FilterComparison; + protected readonly PredicateType = PredicateType; } diff --git a/UI/Web/src/app/metadata-filter/filter-settings.ts b/UI/Web/src/app/metadata-filter/filter-settings.ts index 452abee71..092ec4740 100644 --- a/UI/Web/src/app/metadata-filter/filter-settings.ts +++ b/UI/Web/src/app/metadata-filter/filter-settings.ts @@ -1,11 +1,39 @@ -import { SeriesFilterV2 } from "../_models/metadata/v2/series-filter-v2"; +import {FilterV2} from "../_models/metadata/v2/filter-v2"; +import {SortField} from "../_models/metadata/series-filter"; +import {PersonSortField} from "../_models/metadata/v2/person-sort-field"; +import {PersonFilterField} from "../_models/metadata/v2/person-filter-field"; +import {FilterField} from "../_models/metadata/v2/filter-field"; -export class FilterSettings { +/** + * The set of entities that are supported for rich filtering. Each entity must have its own distinct SortField and FilterField enums. + */ +export type ValidFilterEntity = 'series' | 'person'; + +export class FilterSettingsBase { + presetsV2: FilterV2 | undefined; sortDisabled = false; - presetsV2: SeriesFilterV2 | undefined; /** * The number of statements that can be on the filter. Set to 1 to disable adding more. */ statementLimit: number = 0; saveDisabled: boolean = false; - } + type: ValidFilterEntity = 'series'; + supportsSmartFilter: boolean = false; +} + +/** + * Filter Settings for Series entity + */ +export class SeriesFilterSettings extends FilterSettingsBase { + type: ValidFilterEntity = 'series'; + supportsSmartFilter = true; +} + +/** + * Filter Settings for People entity + */ +export class PersonFilterSettings extends FilterSettingsBase { + type: ValidFilterEntity = 'person'; +} + + diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.html b/UI/Web/src/app/metadata-filter/metadata-filter.component.html index 3ec9dbde1..3ef84ce22 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html @@ -26,8 +26,8 @@
@@ -41,23 +41,21 @@
- +
-
- - -
+ + @if (filterSettings().supportsSmartFilter) { +
+ + +
+ } + @if (utilityService.getActiveBreakpoint() > Breakpoint.Tablet) { @@ -82,7 +80,7 @@
- diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts index c65bb5c16..d1fc264ef 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts @@ -2,55 +2,61 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + computed, ContentChild, DestroyRef, + effect, EventEmitter, inject, + input, Input, OnInit, - Output + Output, + Signal } from '@angular/core'; import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {NgbCollapse} from '@ng-bootstrap/ng-bootstrap'; import {Breakpoint, UtilityService} from '../shared/_services/utility.service'; import {Library} from '../_models/library/library'; -import {allSortFields, FilterEvent, FilterItem, SortField} from '../_models/metadata/series-filter'; +import {FilterEvent, FilterItem} from '../_models/metadata/series-filter'; import {ToggleService} from '../_services/toggle.service'; -import {FilterSettings} from './filter-settings'; -import {SeriesFilterV2} from '../_models/metadata/v2/series-filter-v2'; +import {FilterV2} from '../_models/metadata/v2/filter-v2'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {DrawerComponent} from '../shared/drawer/drawer.component'; import {AsyncPipe, NgClass, NgTemplateOutlet} from '@angular/common'; import {translate, TranslocoModule, TranslocoService} from "@jsverse/transloco"; -import {SortFieldPipe} from "../_pipes/sort-field.pipe"; import {MetadataBuilderComponent} from "./_components/metadata-builder/metadata-builder.component"; -import {allFields} from "../_models/metadata/v2/filter-field"; import {FilterService} from "../_services/filter.service"; import {ToastrService} from "ngx-toastr"; import {animate, style, transition, trigger} from "@angular/animations"; +import {SortButtonComponent} from "../_single-module/sort-button/sort-button.component"; +import {FilterSettingsBase} from "./filter-settings"; +import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service"; + @Component({ - selector: 'app-metadata-filter', - templateUrl: './metadata-filter.component.html', - styleUrls: ['./metadata-filter.component.scss'], - animations: [ - trigger('inOutAnimation', [ - transition(':enter', [ - style({ height: 0, opacity: 0 }), - animate('.5s ease-out', style({ height: 300, opacity: 1 })) - ]), - transition(':leave', [ - style({ height: 300, opacity: 1 }), - animate('.5s ease-in', style({ height: 0, opacity: 0 })) - ]) - ]), - ], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgTemplateOutlet, DrawerComponent, - ReactiveFormsModule, FormsModule, AsyncPipe, TranslocoModule, - MetadataBuilderComponent, NgClass] + selector: 'app-metadata-filter', + templateUrl: './metadata-filter.component.html', + styleUrls: ['./metadata-filter.component.scss'], + animations: [ + trigger('inOutAnimation', [ + transition(':enter', [ + style({ height: 0, opacity: 0 }), + animate('.5s ease-out', style({ height: 300, opacity: 1 })) + ]), + transition(':leave', [ + style({ height: 300, opacity: 1 }), + animate('.5s ease-in', style({ height: 0, opacity: 0 })) + ]) + ]), + ], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgTemplateOutlet, DrawerComponent, + ReactiveFormsModule, FormsModule, AsyncPipe, TranslocoModule, + MetadataBuilderComponent, NgClass, SortButtonComponent] }) -export class MetadataFilterComponent implements OnInit { +export class MetadataFilterComponent implements OnInit { + private readonly destroyRef = inject(DestroyRef); public readonly utilityService = inject(UtilityService); @@ -59,18 +65,16 @@ export class MetadataFilterComponent implements OnInit { private readonly filterService = inject(FilterService); protected readonly toggleService = inject(ToggleService); protected readonly translocoService = inject(TranslocoService); - private readonly sortFieldPipe = new SortFieldPipe(this.translocoService); + protected readonly filterUtilitiesService = inject(FilterUtilitiesService); /** * This toggles the opening/collapsing of the metadata filter code */ @Input() filterOpen: EventEmitter = new EventEmitter(); - /** - * Should filtering be shown on the page - */ - @Input() filteringDisabled: boolean = false; - @Input({required: true}) filterSettings!: FilterSettings; - @Output() applyFilter: EventEmitter = new EventEmitter(); + + filterSettings = input.required>(); + + @Output() applyFilter: EventEmitter> = new EventEmitter(); @ContentChild('[ngbCollapse]') collapse!: NgbCollapse; @@ -86,20 +90,23 @@ export class MetadataFilterComponent implements OnInit { updateApplied: number = 0; fullyLoaded: boolean = false; - filterV2: SeriesFilterV2 | undefined; + filterV2: FilterV2 | undefined; + sortFieldOptions: Signal<{title: string, value: number}[]> = computed(() => []); + filterFieldOptions: Signal<{title: string, value: number}[]> = computed(() => []); + + constructor() { + effect(() => { + const settings = this.filterSettings(); + if (settings?.presetsV2) { + this.filterV2 = this.deepClone(settings.presetsV2); + this.cdRef.markForCheck(); + } + }) + } - protected readonly allSortFields = allSortFields.map(f => { - return {title: this.sortFieldPipe.transform(f), value: f}; - }).sort((a, b) => a.title.localeCompare(b.title)); - protected readonly allFilterFields = allFields; ngOnInit(): void { - if (this.filterSettings === undefined) { - this.filterSettings = new FilterSettings(); - this.cdRef.markForCheck(); - } - if (this.filterOpen) { this.filterOpen.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(openState => { this.filteringCollapsed = !openState; @@ -109,42 +116,19 @@ export class MetadataFilterComponent implements OnInit { } + this.filterFieldOptions = computed(() => { + return this.filterUtilitiesService.getFilterFields(this.filterSettings().type); + }); + + this.sortFieldOptions = computed(() => { + return this.filterUtilitiesService.getSortFields(this.filterSettings().type); + }); + + this.loadFromPresetsAndSetup(); } - // loadSavedFilter(event: Select2UpdateEvent) { - // // Load the filter from the backend and update the screen - // if (event.value === undefined || typeof(event.value) === 'string') return; - // const smartFilter = event.value as SmartFilter; - // this.filterV2 = this.filterUtilitiesService.decodeSeriesFilter(smartFilter.filter); - // this.cdRef.markForCheck(); - // console.log('update event: ', event); - // } - // - // createFilterValue(event: Select2AutoCreateEvent) { - // // Create a new name and filter - // if (!this.filterV2) return; - // this.filterV2.name = event.value; - // this.filterService.saveFilter(this.filterV2).subscribe(() => { - // - // const item = { - // value: { - // filter: this.filterUtilitiesService.encodeSeriesFilter(this.filterV2!), - // name: event.value, - // } as SmartFilter, - // label: event.value - // }; - // this.smartFilters.push(item); - // this.sortGroup.get('name')?.setValue(item); - // this.cdRef.markForCheck(); - // this.toastr.success(translate('toasts.smart-filter-updated')); - // this.apply(); - // }); - // - // console.log('create event: ', event); - // } - close() { this.filterOpen.emit(false); @@ -177,7 +161,7 @@ export class MetadataFilterComponent implements OnInit { return clonedObj; } - handleFilters(filter: SeriesFilterV2) { + handleFilters(filter: FilterV2) { this.filterV2 = filter; } @@ -185,29 +169,34 @@ export class MetadataFilterComponent implements OnInit { loadFromPresetsAndSetup() { this.fullyLoaded = false; - this.filterV2 = this.deepClone(this.filterSettings.presetsV2); + const currentFilterSettings = this.filterSettings(); + this.filterV2 = this.deepClone(currentFilterSettings.presetsV2); + + const defaultSortField = this.sortFieldOptions()[0].value; this.sortGroup = new FormGroup({ - sortField: new FormControl({value: this.filterV2?.sortOptions?.sortField || SortField.SortName, disabled: this.filterSettings.sortDisabled}, []), + sortField: new FormControl({value: this.filterV2?.sortOptions?.sortField || defaultSortField, disabled: this.filterSettings().sortDisabled}, []), limitTo: new FormControl(this.filterV2?.limitTo || 0, []), name: new FormControl(this.filterV2?.name || '', []) }); - if (this.filterSettings?.presetsV2?.sortOptions) { - this.isAscendingSort = this.filterSettings?.presetsV2?.sortOptions!.isAscending; + + if (this.filterSettings()?.presetsV2?.sortOptions) { + this.isAscendingSort = this.filterSettings()?.presetsV2?.sortOptions!.isAscending || true; } + this.cdRef.markForCheck(); this.sortGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { - if (this.filterV2?.sortOptions === null) { - this.filterV2.sortOptions = { - isAscending: this.isAscendingSort, - sortField: parseInt(this.sortGroup.get('sortField')?.value, 10) - }; - } - this.filterV2!.sortOptions!.sortField = parseInt(this.sortGroup.get('sortField')?.value, 10); - this.filterV2!.limitTo = Math.max(parseInt(this.sortGroup.get('limitTo')?.value || '0', 10), 0); - this.filterV2!.name = this.sortGroup.get('name')?.value || ''; - this.cdRef.markForCheck(); + if (this.filterV2?.sortOptions === null) { + this.filterV2.sortOptions = { + isAscending: this.isAscendingSort, + sortField: parseInt(this.sortGroup.get('sortField')?.value, 10) as TSort + }; + } + this.filterV2!.sortOptions!.sortField = parseInt(this.sortGroup.get('sortField')?.value, 10) as TSort; + this.filterV2!.limitTo = Math.max(parseInt(this.sortGroup.get('limitTo')?.value || '0', 10), 0); + this.filterV2!.name = this.sortGroup.get('name')?.value || ''; + this.cdRef.markForCheck(); }); this.fullyLoaded = true; @@ -215,13 +204,16 @@ export class MetadataFilterComponent implements OnInit { } - updateSortOrder() { - if (this.filterSettings.sortDisabled) return; - this.isAscendingSort = !this.isAscendingSort; + updateSortOrder(isAscending: boolean) { + if (this.filterSettings().sortDisabled) return; + this.isAscendingSort = isAscending; + if (this.filterV2?.sortOptions === null) { + const defaultSortField = this.sortFieldOptions()[0].value as TSort; + this.filterV2.sortOptions = { isAscending: this.isAscendingSort, - sortField: SortField.SortName + sortField: defaultSortField } } @@ -235,7 +227,7 @@ export class MetadataFilterComponent implements OnInit { } apply() { - this.applyFilter.emit({isFirst: this.updateApplied === 0, filterV2: this.filterV2!}); + this.applyFilter.emit({isFirst: this.updateApplied === 0, filterV2: this.filterV2!} as FilterEvent); if (this.utilityService.getActiveBreakpoint() === Breakpoint.Mobile && this.updateApplied !== 0) { this.toggleSelected(); @@ -259,9 +251,6 @@ export class MetadataFilterComponent implements OnInit { this.cdRef.markForCheck(); } - setToggle(event: any) { - this.toggleService.set(!this.filteringCollapsed); - } protected readonly Breakpoint = Breakpoint; } diff --git a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html index 9e0f26a0a..089262875 100644 --- a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html +++ b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html @@ -206,6 +206,8 @@
{{t('all-filters')}} + {{t('browse-genres')}} + {{t('browse-tags')}} {{t('announcements')}} {{t('help')}} {{t('logout')}} diff --git a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts index 980a1be55..fd4af01f0 100644 --- a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts +++ b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts @@ -47,6 +47,7 @@ import {SettingsTabId} from "../../../sidenav/preference-nav/preference-nav.comp import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service"; import {WikiLink} from "../../../_models/wiki"; import {NavLinkModalComponent} from "../nav-link-modal/nav-link-modal.component"; +import {MetadataService} from "../../../_services/metadata.service"; @Component({ selector: 'app-nav-header', @@ -70,6 +71,8 @@ export class NavHeaderComponent implements OnInit { protected readonly imageService = inject(ImageService); protected readonly utilityService = inject(UtilityService); protected readonly modalService = inject(NgbModal); + protected readonly metadataService = inject(MetadataService); + protected readonly FilterField = FilterField; protected readonly WikiLink = WikiLink; @@ -159,9 +162,9 @@ export class NavHeaderComponent implements OnInit { }); } - goTo(statement: FilterStatement) { + goTo(statement: FilterStatement) { let params: any = {}; - const filter = this.filterUtilityService.createSeriesV2Filter(); + const filter = this.metadataService.createDefaultFilterDto('series'); filter.statements = [statement]; params['page'] = 1; this.clearSearch(); diff --git a/UI/Web/src/app/person-detail/person-detail.component.ts b/UI/Web/src/app/person-detail/person-detail.component.ts index 31b7f976b..ab3c486bd 100644 --- a/UI/Web/src/app/person-detail/person-detail.component.ts +++ b/UI/Web/src/app/person-detail/person-detail.component.ts @@ -23,7 +23,7 @@ import {PersonRolePipe} from "../_pipes/person-role.pipe"; import {CarouselReelComponent} from "../carousel/_components/carousel-reel/carousel-reel.component"; import {FilterComparison} from "../_models/metadata/v2/filter-comparison"; import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service"; -import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; +import {FilterV2} from "../_models/metadata/v2/filter-v2"; import {allPeople, FilterField, personRoleForFilterField} from "../_models/metadata/v2/filter-field"; import {Series} from "../_models/series"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; @@ -44,6 +44,7 @@ import {SafeUrlPipe} from "../_pipes/safe-url.pipe"; import {MergePersonModalComponent} from "./_modal/merge-person-modal/merge-person-modal.component"; import {EVENTS, MessageHubService} from "../_services/message-hub.service"; import {BadgeExpanderComponent} from "../shared/badge-expander/badge-expander.component"; +import {MetadataService} from "../_services/metadata.service"; interface PersonMergeEvent { srcId: number, @@ -87,6 +88,7 @@ export class PersonDetailComponent implements OnInit { private readonly themeService = inject(ThemeService); private readonly toastr = inject(ToastrService); private readonly messageHubService = inject(MessageHubService) + private readonly metadataService = inject(MetadataService) protected readonly FilterField = FilterField; @@ -98,7 +100,7 @@ export class PersonDetailComponent implements OnInit { roles$: Observable | null = null; roles: PersonRole[] | null = null; works$: Observable | null = null; - filter: SeriesFilterV2 | null = null; + filter: FilterV2 | null = null; personActions: Array> = this.actionService.getPersonActions(this.handleAction.bind(this)); chaptersByRole: any = {}; anilistUrl: string = ''; @@ -181,7 +183,7 @@ export class PersonDetailComponent implements OnInit { } createFilter(roles: PersonRole[]) { - const filter: SeriesFilterV2 = this.filterUtilityService.createSeriesV2Filter(); + const filter = this.metadataService.createDefaultFilterDto('series'); filter.combination = FilterCombination.Or; filter.limitTo = 20; @@ -217,7 +219,7 @@ export class PersonDetailComponent implements OnInit { params['page'] = 1; params['title'] = translate('person-detail.browse-person-by-role-title', {name: this.person!.name, role: personPipe.transform(role)}); - const searchFilter = this.filterUtilityService.createSeriesV2Filter(); + const searchFilter = this.metadataService.createDefaultFilterDto('series'); searchFilter.limitTo = 0; searchFilter.combination = FilterCombination.Or; diff --git a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts index e02a22fca..976bebbda 100644 --- a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts @@ -31,7 +31,8 @@ import {User} from "../../../_models/user"; templateUrl: './reading-lists.component.html', styleUrls: ['./reading-lists.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [SideNavCompanionBarComponent, CardActionablesComponent, CardDetailLayoutComponent, CardItemComponent, DecimalPipe, TranslocoDirective, BulkOperationsComponent] + imports: [SideNavCompanionBarComponent, CardActionablesComponent, CardDetailLayoutComponent, CardItemComponent, + DecimalPipe, TranslocoDirective, BulkOperationsComponent] }) export class ReadingListsComponent implements OnInit { protected readonly WikiLink = WikiLink; diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 5992df828..4035f2f41 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -895,10 +895,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { this.cdRef.markForCheck(); } - - - - this.isLoading = false; this.cdRef.markForCheck(); }); @@ -1092,19 +1088,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { } - openVolume(volume: Volume) { - if (this.bulkSelectionService.hasSelections()) return; - if (volume.chapters === undefined || volume.chapters?.length === 0) { - this.toastr.error(this.translocoService.translate('series-detail.no-chapters')); - return; - } - - this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'volume', volume.id]); - return; - - - this.readerService.readVolume(this.libraryId, this.seriesId, volume, false); - } openEditChapter(chapter: Chapter) { const ref = this.modalService.open(EditChapterModalComponent, DefaultModalOptions); diff --git a/UI/Web/src/app/shared/_services/download.service.ts b/UI/Web/src/app/shared/_services/download.service.ts index 49d57efbc..184f31094 100644 --- a/UI/Web/src/app/shared/_services/download.service.ts +++ b/UI/Web/src/app/shared/_services/download.service.ts @@ -1,24 +1,16 @@ -import { HttpClient } from '@angular/common/http'; +import {HttpClient} from '@angular/common/http'; import {DestroyRef, inject, Inject, Injectable} from '@angular/core'; -import { Series } from 'src/app/_models/series'; -import { environment } from 'src/environments/environment'; -import { ConfirmService } from '../confirm.service'; -import { Chapter } from 'src/app/_models/chapter'; -import { Volume } from 'src/app/_models/volume'; -import { - asyncScheduler, - BehaviorSubject, - Observable, - tap, - finalize, - of, - filter, -} from 'rxjs'; -import { download, Download } from '../_models/download'; -import { PageBookmark } from 'src/app/_models/readers/page-bookmark'; +import {Series} from 'src/app/_models/series'; +import {environment} from 'src/environments/environment'; +import {ConfirmService} from '../confirm.service'; +import {Chapter} from 'src/app/_models/chapter'; +import {Volume} from 'src/app/_models/volume'; +import {asyncScheduler, BehaviorSubject, filter, finalize, Observable, of, tap,} from 'rxjs'; +import {download, Download} from '../_models/download'; +import {PageBookmark} from 'src/app/_models/readers/page-bookmark'; import {switchMap, take, takeWhile, throttleTime} from 'rxjs/operators'; -import { AccountService } from 'src/app/_services/account.service'; -import { BytesPipe } from 'src/app/_pipes/bytes.pipe'; +import {AccountService} from 'src/app/_services/account.service'; +import {BytesPipe} from 'src/app/_pipes/bytes.pipe'; import {translate} from "@jsverse/transloco"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {SAVER, Saver} from "../../_providers/saver.provider"; @@ -26,7 +18,7 @@ import {UtilityService} from "./utility.service"; import {UserCollection} from "../../_models/collection-tag"; import {RecentlyAddedItem} from "../../_models/recently-added-item"; import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter"; -import {BrowsePerson} from "../../_models/person/browse-person"; +import {BrowsePerson} from "../../_models/metadata/browse/browse-person"; export const DEBOUNCE_TIME = 100; diff --git a/UI/Web/src/app/shared/_services/filter-utilities.service.ts b/UI/Web/src/app/shared/_services/filter-utilities.service.ts index a8c615149..559a70ab1 100644 --- a/UI/Web/src/app/shared/_services/filter-utilities.service.ts +++ b/UI/Web/src/app/shared/_services/filter-utilities.service.ts @@ -1,18 +1,27 @@ import {inject, Injectable} from '@angular/core'; -import {ActivatedRouteSnapshot, Params, Router} from '@angular/router'; -import {SortField, SortOptions} from 'src/app/_models/metadata/series-filter'; +import {Params, Router} from '@angular/router'; +import {allSeriesSortFields, SortField} from 'src/app/_models/metadata/series-filter'; import {MetadataService} from "../../_services/metadata.service"; -import {SeriesFilterV2} from "../../_models/metadata/v2/series-filter-v2"; -import {FilterStatement} from "../../_models/metadata/v2/filter-statement"; +import {FilterV2} from "../../_models/metadata/v2/filter-v2"; import {FilterCombination} from "../../_models/metadata/v2/filter-combination"; -import {FilterField} from "../../_models/metadata/v2/filter-field"; +import {allSeriesFilterFields, FilterField} from "../../_models/metadata/v2/filter-field"; import {FilterComparison} from "../../_models/metadata/v2/filter-comparison"; -import { HttpClient } from "@angular/common/http"; +import {HttpClient} from "@angular/common/http"; import {TextResonse} from "../../_types/text-response"; import {environment} from "../../../environments/environment"; import {map, tap} from "rxjs/operators"; -import {of, switchMap} from "rxjs"; -import {Location} from "@angular/common"; +import {switchMap} from "rxjs"; +import {allPersonFilterFields, PersonFilterField} from "../../_models/metadata/v2/person-filter-field"; +import {allPersonSortFields} from "../../_models/metadata/v2/person-sort-field"; +import { + FilterSettingsBase, + PersonFilterSettings, + SeriesFilterSettings, + ValidFilterEntity +} from "../../metadata-filter/filter-settings"; +import {SortFieldPipe} from "../../_pipes/sort-field.pipe"; +import {GenericFilterFieldPipe} from "../../_pipes/generic-filter-field.pipe"; +import {TranslocoService} from "@jsverse/transloco"; @Injectable({ @@ -20,59 +29,64 @@ import {Location} from "@angular/common"; }) export class FilterUtilitiesService { - private readonly location = inject(Location); private readonly router = inject(Router); private readonly metadataService = inject(MetadataService); private readonly http = inject(HttpClient); + private readonly translocoService = inject(TranslocoService); - private apiUrl = environment.apiUrl; + private readonly sortFieldPipe = new SortFieldPipe(this.translocoService); + private readonly genericFilterFieldPipe = new GenericFilterFieldPipe(); - encodeFilter(filter: SeriesFilterV2 | undefined) { + private readonly apiUrl = environment.apiUrl; + + encodeFilter(filter: FilterV2 | undefined) { return this.http.post(this.apiUrl + 'filter/encode', filter, TextResonse); } decodeFilter(encodedFilter: string) { - return this.http.post(this.apiUrl + 'filter/decode', {encodedFilter}).pipe(map(filter => { + return this.http.post(this.apiUrl + 'filter/decode', {encodedFilter}).pipe(map(filter => { if (filter == null) { - filter = this.metadataService.createDefaultFilterDto(); - filter.statements.push(this.createSeriesV2DefaultStatement()); + filter = this.metadataService.createDefaultFilterDto('series'); + filter.statements.push(this.metadataService.createDefaultFilterStatement('series')); } return filter; })) } - updateUrlFromFilter(filter: SeriesFilterV2 | undefined) { + /** + * Encodes the filter and patches into the url + * @param filter + */ + updateUrlFromFilter(filter: FilterV2 | undefined) { return this.encodeFilter(filter).pipe(tap(encodedFilter => { window.history.replaceState(window.location.href, '', window.location.href.split('?')[0]+ '?' + encodedFilter); })); } - filterPresetsFromUrl(snapshot: ActivatedRouteSnapshot) { - const filter = this.metadataService.createDefaultFilterDto(); - filter.statements.push(this.createSeriesV2DefaultStatement()); - if (!window.location.href.includes('?')) return of(filter); - - return this.decodeFilter(window.location.href.split('?')[1]); - } - /** - * Applies and redirects to the passed page with the filter encoded + * Applies and redirects to the passed page with the filter encoded (Series only) * @param page * @param filter * @param comparison * @param value */ applyFilter(page: Array, filter: FilterField, comparison: FilterComparison, value: string) { - const dto = this.createSeriesV2Filter(); - dto.statements.push(this.metadataService.createDefaultFilterStatement(filter, comparison, value + '')); + const dto = this.metadataService.createDefaultFilterDto('series'); + dto.statements.push(this.metadataService.createFilterStatement(filter, comparison, value + '')); return this.encodeFilter(dto).pipe(switchMap(encodedFilter => { return this.router.navigateByUrl(page.join('/') + '?' + encodedFilter); })); } - applyFilterWithParams(page: Array, filter: SeriesFilterV2, extraParams: Params) { + /** + * (Series only) + * @param page + * @param filter + * @param extraParams + */ + applyFilterWithParams(page: Array, filter: FilterV2, extraParams: Params) { return this.encodeFilter(filter).pipe(switchMap(encodedFilter => { let url = page.join('/') + '?' + encodedFilter; url += Object.keys(extraParams).map(k => `&${k}=${extraParams[k]}`).join(''); @@ -81,23 +95,228 @@ export class FilterUtilitiesService { })); } - createSeriesV2Filter(): SeriesFilterV2 { - return { - combination: FilterCombination.And, - statements: [], - limitTo: 0, - sortOptions: { - isAscending: true, - sortField: SortField.SortName - }, - }; + + createPersonV2Filter(): FilterV2 { + return { + combination: FilterCombination.And, + statements: [], + limitTo: 0, + sortOptions: { + isAscending: true, + sortField: SortField.SortName + }, + }; } - createSeriesV2DefaultStatement(): FilterStatement { - return { - comparison: FilterComparison.Equal, - value: '', - field: FilterField.SeriesName - } + /** + * Returns the Sort Fields for the Metadata filter based on the entity. + * @param type + */ + getSortFields(type: ValidFilterEntity) { + switch (type) { + case 'series': + return allSeriesSortFields.map(f => { + return {title: this.sortFieldPipe.transform(f, type), value: f}; + }).sort((a, b) => a.title.localeCompare(b.title)) as unknown as {title: string, value: T}[]; + case 'person': + return allPersonSortFields.map(f => { + return {title: this.sortFieldPipe.transform(f, type), value: f}; + }).sort((a, b) => a.title.localeCompare(b.title)) as unknown as {title: string, value: T}[]; + default: + return [] as {title: string, value: T}[]; + } + } + + /** + * Returns the Filter Fields for the Metadata filter based on the entity. + * @param type + */ + getFilterFields(type: ValidFilterEntity): {title: string, value: T}[] { + switch (type) { + case 'series': + return allSeriesFilterFields.map(f => { + return {title: this.genericFilterFieldPipe.transform(f, type), value: f}; + }).sort((a, b) => a.title.localeCompare(b.title)) as unknown as {title: string, value: T}[]; + case 'person': + return allPersonFilterFields.map(f => { + return {title: this.genericFilterFieldPipe.transform(f, type), value: f}; + }).sort((a, b) => a.title.localeCompare(b.title)) as unknown as {title: string, value: T}[]; + default: + return [] as {title: string, value: T}[]; + } + } + + /** + * Returns the default field for the Series or Person entity aka what should be there if there are no statements + * @param type + */ + getDefaultFilterField(type: ValidFilterEntity) { + switch (type) { + case 'series': + return FilterField.SeriesName as unknown as T; + case 'person': + return PersonFilterField.Role as unknown as T; + } + } + + /** + * Returns the appropriate Dropdown Fields based on the entity type + * @param type + */ + getDropdownFields(type: ValidFilterEntity) { + switch (type) { + case 'series': + return [ + FilterField.PublicationStatus, FilterField.Languages, FilterField.AgeRating, + FilterField.Translators, FilterField.Characters, FilterField.Publisher, + FilterField.Editor, FilterField.CoverArtist, FilterField.Letterer, + FilterField.Colorist, FilterField.Inker, FilterField.Penciller, + FilterField.Writers, FilterField.Genres, FilterField.Libraries, + FilterField.Formats, FilterField.CollectionTags, FilterField.Tags, + FilterField.Imprint, FilterField.Team, FilterField.Location + ] as unknown as T[]; + case 'person': + return [ + PersonFilterField.Role + ] as unknown as T[]; + } + } + + /** + * Returns the applicable String fields + * @param type + */ + getStringFields(type: ValidFilterEntity) { + switch (type) { + case 'series': + return [ + FilterField.SeriesName, FilterField.Summary, FilterField.Path, FilterField.FilePath, PersonFilterField.Name + ] as unknown as T[]; + case 'person': + return [ + PersonFilterField.Name + ] as unknown as T[]; + } + } + + getNumberFields(type: ValidFilterEntity) { + switch (type) { + case 'series': + return [ + FilterField.ReadTime, FilterField.ReleaseYear, FilterField.ReadProgress, + FilterField.UserRating, FilterField.AverageRating, FilterField.ReadLast + ] as unknown as T[]; + case 'person': + return [ + PersonFilterField.ChapterCount, PersonFilterField.SeriesCount + ] as unknown as T[]; + } + } + + getBooleanFields(type: ValidFilterEntity) { + switch (type) { + case 'series': + return [ + FilterField.WantToRead + ] as unknown as T[]; + case 'person': + return [ + + ] as unknown as T[]; + } + } + + getDateFields(type: ValidFilterEntity) { + switch (type) { + case 'series': + return [ + FilterField.ReadingDate + ] as unknown as T[]; + case 'person': + return [ + + ] as unknown as T[]; + } + } + + getNumberFieldsThatIncludeDateComparisons(type: ValidFilterEntity) { + switch (type) { + case 'series': + return [ + FilterField.ReleaseYear + ] as unknown as T[]; + case 'person': + return [ + + ] as unknown as T[]; + } + } + + getDropdownFieldsThatIncludeDateComparisons(type: ValidFilterEntity) { + switch (type) { + case 'series': + return [ + FilterField.AgeRating + ] as unknown as T[]; + case 'person': + return [ + + ] as unknown as T[]; + } + } + + getDropdownFieldsWithoutMustContains(type: ValidFilterEntity) { + switch (type) { + case 'series': + return [ + FilterField.Libraries, FilterField.Formats, FilterField.AgeRating, FilterField.PublicationStatus + ] as unknown as T[]; + case 'person': + return [ + + ] as unknown as T[]; + } + } + + getDropdownFieldsThatIncludeNumberComparisons(type: ValidFilterEntity) { + switch (type) { + case 'series': + return [ + FilterField.AgeRating + ] as unknown as T[]; + case 'person': + return [ + + ] as unknown as T[]; + } + } + + getFieldsThatShouldIncludeIsEmpty(type: ValidFilterEntity) { + switch (type) { + case 'series': + return [ + FilterField.Summary, FilterField.UserRating, FilterField.Genres, + FilterField.CollectionTags, FilterField.Tags, FilterField.ReleaseYear, + FilterField.Translators, FilterField.Characters, FilterField.Publisher, + FilterField.Editor, FilterField.CoverArtist, FilterField.Letterer, + FilterField.Colorist, FilterField.Inker, FilterField.Penciller, + FilterField.Writers, FilterField.Imprint, FilterField.Team, + FilterField.Location + ] as unknown as T[]; + case 'person': + return [] as unknown as T[]; + } + } + + getDefaultSettings(entityType: ValidFilterEntity | "other" | undefined): FilterSettingsBase { + if (entityType === 'other' || entityType === undefined) { + // It doesn't matter, return series type + return new SeriesFilterSettings(); + } + + if (entityType == 'series') return new SeriesFilterSettings(); + if (entityType == 'person') return new PersonFilterSettings(); + + return new SeriesFilterSettings(); } } diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts index afb63ab1d..da90ca412 100644 --- a/UI/Web/src/app/shared/_services/utility.service.ts +++ b/UI/Web/src/app/shared/_services/utility.service.ts @@ -1,5 +1,5 @@ import {HttpParams} from '@angular/common/http'; -import {Injectable} from '@angular/core'; +import {Inject, Injectable, signal, Signal} from '@angular/core'; import {Chapter} from 'src/app/_models/chapter'; import {LibraryType} from 'src/app/_models/library/library'; import {MangaFormat} from 'src/app/_models/manga-format'; @@ -8,6 +8,8 @@ import {Series} from 'src/app/_models/series'; import {Volume} from 'src/app/_models/volume'; import {translate} from "@jsverse/transloco"; import {debounceTime, ReplaySubject, shareReplay} from "rxjs"; +import {DOCUMENT} from "@angular/common"; +import getComputedStyle from "@popperjs/core/lib/dom-utils/getComputedStyle"; export enum KEY_CODES { RIGHT_ARROW = 'ArrowRight', @@ -27,12 +29,37 @@ export enum KEY_CODES { SHIFT = 'Shift' } +/** + * Use {@link UserBreakpoint} and {@link UtilityService.activeUserBreakpoint} for breakpoint that should depend on user settings + */ export enum Breakpoint { Mobile = 768, Tablet = 1280, Desktop = 1440 } +/* +Breakpoints, but they're derived from css vars in the theme + */ +export enum UserBreakpoint { + /** + * This is to be used in the UI/as value to disable the functionality with breakpoint, will not actually be set as a breakpoint + */ + Never = 0, + /** + * --mobile-breakpoint + */ + Mobile = 1, + /** + * --tablet-breakpoint + */ + Tablet = 2, + /** + * --desktop-breakpoint, does not actually matter as everything that's not mobile or tablet will be desktop + */ + Desktop = 3, +} + @Injectable({ providedIn: 'root' @@ -42,11 +69,19 @@ export class UtilityService { public readonly activeBreakpointSource = new ReplaySubject(1); public readonly activeBreakpoint$ = this.activeBreakpointSource.asObservable().pipe(debounceTime(60), shareReplay({bufferSize: 1, refCount: true})); + /** + * The currently active breakpoint, is {@link UserBreakpoint.Never} until the app has loaded + */ + public readonly activeUserBreakpoint = signal(UserBreakpoint.Never); + // TODO: I need an isPhone/Tablet so that I can easily trigger different views mangaFormatKeys: string[] = []; + constructor(@Inject(DOCUMENT) private document: Document) { + } + sortChapters = (a: Chapter, b: Chapter) => { return a.minNumber - b.minNumber; @@ -132,6 +167,34 @@ export class UtilityService { return Breakpoint.Desktop; } + updateUserBreakpoint(): void { + this.activeUserBreakpoint.set(this.getActiveUserBreakpoint()); + } + + private getActiveUserBreakpoint(): UserBreakpoint { + const style = getComputedStyle(this.document.body) + const mobileBreakPoint = this.parseOrDefault(style.getPropertyValue('--setting-mobile-breakpoint'), Breakpoint.Mobile); + const tabletBreakPoint = this.parseOrDefault(style.getPropertyValue('--setting-tablet-breakpoint'), Breakpoint.Tablet); + //const desktopBreakPoint = this.parseOrDefault(style.getPropertyValue('--setting-desktop-breakpoint'), Breakpoint.Desktop); + + if (window.innerWidth <= mobileBreakPoint) { + return UserBreakpoint.Mobile; + } else if (window.innerWidth <= tabletBreakPoint) { + return UserBreakpoint.Tablet; + } + + // Fallback to desktop + return UserBreakpoint.Desktop; + } + + private parseOrDefault(s: string, def: T): T { + const ret = parseInt(s, 10); + if (isNaN(ret)) { + return def; + } + return ret as T; + } + isInViewport(element: Element, additionalTopOffset: number = 0) { const rect = element.getBoundingClientRect(); return ( diff --git a/UI/Web/src/app/sidenav/_components/dashboard-stream-list-item/dashboard-stream-list-item.component.ts b/UI/Web/src/app/sidenav/_components/dashboard-stream-list-item/dashboard-stream-list-item.component.ts index acc1ca28c..267c5123c 100644 --- a/UI/Web/src/app/sidenav/_components/dashboard-stream-list-item/dashboard-stream-list-item.component.ts +++ b/UI/Web/src/app/sidenav/_components/dashboard-stream-list-item/dashboard-stream-list-item.component.ts @@ -1,14 +1,13 @@ import {ChangeDetectionStrategy, Component, EventEmitter, inject, Input, Output} from '@angular/core'; -import {APP_BASE_HREF, NgClass, NgIf} from '@angular/common'; +import {APP_BASE_HREF, NgClass} from '@angular/common'; import {TranslocoDirective} from "@jsverse/transloco"; import {DashboardStream} from "../../../_models/dashboard/dashboard-stream"; import {StreamNamePipe} from "../../../_pipes/stream-name.pipe"; -import {SideNavStreamType} from "../../../_models/sidenav/sidenav-stream-type.enum"; import {StreamType} from "../../../_models/dashboard/stream-type.enum"; @Component({ selector: 'app-dashboard-stream-list-item', - imports: [TranslocoDirective, StreamNamePipe, NgClass, NgIf], + imports: [TranslocoDirective, StreamNamePipe, NgClass], templateUrl: './dashboard-stream-list-item.component.html', styleUrls: ['./dashboard-stream-list-item.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush diff --git a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.html b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.html index a20571b91..7e3deb5af 100644 --- a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.html +++ b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.html @@ -76,7 +76,7 @@ @case (SideNavStreamType.BrowseAuthors) { + [cdkDragDisabled]="!editMode" [cdkDragData]="navStream" [editMode]="editMode" icon="fa-users" [title]="t('browse-people')" link="/browse/authors/"> } @case (SideNavStreamType.SmartFilter) { diff --git a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts index 617db2500..be9d841b5 100644 --- a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts +++ b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts @@ -38,7 +38,7 @@ import {ReadingProfileService} from "../../../_services/reading-profile.service" export class SideNavComponent implements OnInit { protected readonly WikiLink = WikiLink; - protected readonly ItemLimit = 10; + protected readonly ItemLimit = 13; protected readonly SideNavStreamType = SideNavStreamType; protected readonly SettingsTabId = SettingsTabId; protected readonly Breakpoint = Breakpoint; diff --git a/UI/Web/src/app/sidenav/_components/sidenav-stream-list-item/sidenav-stream-list-item.component.ts b/UI/Web/src/app/sidenav/_components/sidenav-stream-list-item/sidenav-stream-list-item.component.ts index 6118eae83..ad86bef89 100644 --- a/UI/Web/src/app/sidenav/_components/sidenav-stream-list-item/sidenav-stream-list-item.component.ts +++ b/UI/Web/src/app/sidenav/_components/sidenav-stream-list-item/sidenav-stream-list-item.component.ts @@ -4,7 +4,6 @@ import {SideNavStream} from "../../../_models/sidenav/sidenav-stream"; import {StreamNamePipe} from "../../../_pipes/stream-name.pipe"; import {TranslocoDirective} from "@jsverse/transloco"; import {SideNavStreamType} from "../../../_models/sidenav/sidenav-stream-type.enum"; -import {RouterLink} from "@angular/router"; @Component({ selector: 'app-sidenav-stream-list-item', diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts index 2333dee25..797124c4f 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts @@ -20,7 +20,13 @@ import { } from 'src/app/admin/_modals/directory-picker/directory-picker.component'; import {ConfirmService} from 'src/app/shared/confirm.service'; import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service'; -import {allLibraryTypes, Library, LibraryType} from 'src/app/_models/library/library'; +import { + allKavitaPlusMetadataApplicableTypes, + allKavitaPlusScrobbleEligibleTypes, + allLibraryTypes, + Library, + LibraryType +} from 'src/app/_models/library/library'; import {ImageService} from 'src/app/_services/image.service'; import {LibraryService} from 'src/app/_services/library.service'; import {UploadService} from 'src/app/_services/upload.service'; @@ -103,8 +109,8 @@ export class LibrarySettingsModalComponent implements OnInit { includeInDashboard: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), includeInRecommended: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), includeInSearch: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - manageCollections: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - manageReadingLists: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), + manageCollections: new FormControl(false, { nonNullable: true, validators: [Validators.required] }), + manageReadingLists: new FormControl(false, { nonNullable: true, validators: [Validators.required] }), allowScrobbling: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), allowMetadataMatching: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), collapseSeriesRelationships: new FormControl(false, { nonNullable: true, validators: [Validators.required] }), @@ -125,13 +131,12 @@ export class LibrarySettingsModalComponent implements OnInit { get IsKavitaPlusEligible() { const libType = parseInt(this.libraryForm.get('type')?.value + '', 10) as LibraryType; - return libType === LibraryType.Manga || libType === LibraryType.LightNovel; + return allKavitaPlusScrobbleEligibleTypes.includes(libType); } get IsMetadataDownloadEligible() { const libType = parseInt(this.libraryForm.get('type')?.value + '', 10) as LibraryType; - return libType === LibraryType.Manga || libType === LibraryType.LightNovel - || libType === LibraryType.ComicVine || libType === LibraryType.Comic; + return allKavitaPlusMetadataApplicableTypes.includes(libType); } ngOnInit(): void { @@ -232,6 +237,7 @@ export class LibrarySettingsModalComponent implements OnInit { this.libraryForm.get('allowMetadataMatching')?.disable(); } + this.cdRef.markForCheck(); }), takeUntilDestroyed(this.destroyRef) diff --git a/UI/Web/src/app/sidenav/preference-nav/preference-nav.component.html b/UI/Web/src/app/sidenav/preference-nav/preference-nav.component.html index 5e4f9b2cd..b8c1ec8af 100644 --- a/UI/Web/src/app/sidenav/preference-nav/preference-nav.component.html +++ b/UI/Web/src/app/sidenav/preference-nav/preference-nav.component.html @@ -9,7 +9,7 @@ @for(section of sections; track section.title + section.children.length; let idx = $index;) { @if (hasAnyChildren(user, section)) { -
{{t(section.title)}}
+
{{t(section.title)}}
@for(item of section.children; track item.fragment) { @if (accountService.hasAnyRole(user, item.roles, item.restrictRoles)) { diff --git a/UI/Web/src/app/sidenav/preference-nav/preference-nav.component.ts b/UI/Web/src/app/sidenav/preference-nav/preference-nav.component.ts index 98ba48968..d76a84cc8 100644 --- a/UI/Web/src/app/sidenav/preference-nav/preference-nav.component.ts +++ b/UI/Web/src/app/sidenav/preference-nav/preference-nav.component.ts @@ -192,6 +192,7 @@ export class PreferenceNavComponent implements AfterViewInit { } else { return this.manageService.getAllKavitaPlusSeries({ matchStateOption: MatchStateOption.Error, + libraryType: -1, searchTerm: '' }).pipe( takeUntilDestroyed(this.destroyRef), @@ -272,13 +273,11 @@ export class PreferenceNavComponent implements AfterViewInit { hasAnyChildren(user: User, section: PrefSection) { // Filter out items where the user has a restricted role const visibleItems = section.children.filter(item => - item.restrictRoles.length === 0 || !this.accountService.hasAnyRole(user, item.restrictRoles) + (item.restrictRoles.length === 0 || !this.accountService.hasAnyRestrictedRole(user, item.restrictRoles)) && + (item.roles.length === 0 || this.accountService.hasAnyRole(user, item.roles)) ); - // Check if the user has any allowed roles in the remaining items - return visibleItems.some(item => - this.accountService.hasAnyRole(user, item.roles) - ); + return visibleItems.length > 0; } collapse() { diff --git a/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.html b/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.html index 7acbe9196..d7f4c6bb9 100644 --- a/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.html +++ b/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.html @@ -10,13 +10,13 @@

{{t('description')}}

-

{{t('extra-tip')}}

+

{{t('extra-tip')}} {{t('wiki-title')}}

-
-
+
+
@if (readingProfiles.length < virtualScrollerBreakPoint) { @for (readingProfile of readingProfiles; track readingProfile.id) { @@ -32,7 +32,7 @@
-
+
@if (selectedProfile === null) {

{{t('no-selected')}}

@@ -46,7 +46,9 @@
- {{readingProfileForm.get('name')!.value}} + + {{readingProfileForm.get('name')!.value}} + @@ -250,6 +252,22 @@
} +
+ + + {{readingProfileForm.get('disableWidthOverride')!.value | breakpoint}} + + + + + +
+
} diff --git a/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.scss b/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.scss index 13f341a32..ba232dbae 100644 --- a/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.scss +++ b/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.scss @@ -1,7 +1,5 @@ @use '../../../series-detail-common'; - - .group-item { background-color: transparent; diff --git a/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.ts b/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.ts index 2bc4ece7d..e64e938f6 100644 --- a/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.ts +++ b/UI/Web/src/app/user-settings/manage-reading-profiles/manage-reading-profiles.component.ts @@ -2,7 +2,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, OnIni import {ReadingProfileService} from "../../_services/reading-profile.service"; import { bookLayoutModes, - bookWritingStyles, + bookWritingStyles, breakPoints, layoutModes, pageSplitOptions, pdfScrollModes, @@ -47,6 +47,8 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {LoadingComponent} from "../../shared/loading/loading.component"; import {ToastrService} from "ngx-toastr"; import {ConfirmService} from "../../shared/confirm.service"; +import {WikiLink} from "../../_models/wiki"; +import {BreakpointPipe} from "../../_pipes/breakpoint.pipe"; enum TabId { ImageReader = "image-reader", @@ -85,6 +87,7 @@ enum TabId { NgbNavOutlet, LoadingComponent, NgbTooltip, + BreakpointPipe, ], templateUrl: './manage-reading-profiles.component.html', styleUrl: './manage-reading-profiles.component.scss', @@ -193,6 +196,7 @@ export class ManageReadingProfilesComponent implements OnInit { this.readingProfileForm.addControl('backgroundColor', new FormControl(this.selectedProfile.backgroundColor, [])); this.readingProfileForm.addControl('allowAutomaticWebtoonReaderDetection', new FormControl(this.selectedProfile.allowAutomaticWebtoonReaderDetection, [])); this.readingProfileForm.addControl('widthOverride', new FormControl(this.selectedProfile.widthOverride, [Validators.min(0), Validators.max(100)])); + this.readingProfileForm.addControl('disableWidthOverride', new FormControl(this.selectedProfile.disableWidthOverride, [])) // Epub reader this.readingProfileForm.addControl('bookReaderFontFamily', new FormControl(this.selectedProfile.bookReaderFontFamily, [])); @@ -237,10 +241,10 @@ export class ManageReadingProfilesComponent implements OnInit { } else { const profile = this.packData(); this.readingProfileService.updateProfile(profile).subscribe({ - next: _ => { + next: newProfile => { this.readingProfiles = this.readingProfiles.map(p => { if (p.id !== profile.id) return p; - return profile; + return newProfile; }); this.cdRef.markForCheck(); }, @@ -260,6 +264,7 @@ export class ManageReadingProfilesComponent implements OnInit { data.pageSplitOption = parseInt(data.pageSplitOption as unknown as string); data.readerMode = parseInt(data.readerMode as unknown as string); data.layoutMode = parseInt(data.layoutMode as unknown as string); + data.disableWidthOverride = parseInt(data.disableWidthOverride as unknown as string); data.bookReaderReadingDirection = parseInt(data.bookReaderReadingDirection as unknown as string); data.bookReaderWritingStyle = parseInt(data.bookReaderWritingStyle as unknown as string); @@ -316,4 +321,6 @@ export class ManageReadingProfilesComponent implements OnInit { protected readonly pdfScrollModes = pdfScrollModes; protected readonly TabId = TabId; protected readonly ReadingProfileKind = ReadingProfileKind; + protected readonly WikiLink = WikiLink; + protected readonly breakPoints = breakPoints; } diff --git a/UI/Web/src/app/volume-detail/volume-detail.component.html b/UI/Web/src/app/volume-detail/volume-detail.component.html index 8ef4f814c..56742bc57 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.html +++ b/UI/Web/src/app/volume-detail/volume-detail.component.html @@ -170,7 +170,7 @@ [libraryId]="libraryId" [libraryType]="libraryType" [actions]="chapterActions" - (selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, volume.chapters.length, $event)" + (selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, volume!.chapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true" > } diff --git a/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.ts b/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.ts index 91fc9f143..b16d1f9fc 100644 --- a/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.ts +++ b/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.ts @@ -1,43 +1,49 @@ -import { DOCUMENT, NgStyle, NgIf, DecimalPipe } from '@angular/common'; +import {DecimalPipe, DOCUMENT, NgStyle} from '@angular/common'; import { AfterContentChecked, ChangeDetectionStrategy, ChangeDetectorRef, - Component, DestroyRef, + Component, + DestroyRef, ElementRef, EventEmitter, - HostListener, inject, Inject, OnInit, ViewChild } from '@angular/core'; -import { Title } from '@angular/platform-browser'; -import { Router, ActivatedRoute } from '@angular/router'; -import { take, debounceTime } from 'rxjs'; -import { BulkSelectionService } from 'src/app/cards/bulk-selection.service'; -import { FilterSettings } from 'src/app/metadata-filter/filter-settings'; -import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service'; -import { UtilityService, KEY_CODES } from 'src/app/shared/_services/utility.service'; -import { SeriesRemovedEvent } from 'src/app/_models/events/series-removed-event'; -import { JumpKey } from 'src/app/_models/jumpbar/jump-key'; -import { Pagination } from 'src/app/_models/pagination'; -import { Series } from 'src/app/_models/series'; -import { FilterEvent } from 'src/app/_models/metadata/series-filter'; -import { Action, ActionItem } from 'src/app/_services/action-factory.service'; -import { ActionService } from 'src/app/_services/action.service'; -import { ImageService } from 'src/app/_services/image.service'; -import { JumpbarService } from 'src/app/_services/jumpbar.service'; -import { MessageHubService, EVENTS } from 'src/app/_services/message-hub.service'; -import { ScrollService } from 'src/app/_services/scroll.service'; -import { SeriesService } from 'src/app/_services/series.service'; +import {Title} from '@angular/platform-browser'; +import {ActivatedRoute, Router} from '@angular/router'; +import {debounceTime, take} from 'rxjs'; +import {BulkSelectionService} from 'src/app/cards/bulk-selection.service'; +import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service'; +import {UtilityService} from 'src/app/shared/_services/utility.service'; +import {SeriesRemovedEvent} from 'src/app/_models/events/series-removed-event'; +import {JumpKey} from 'src/app/_models/jumpbar/jump-key'; +import {Pagination} from 'src/app/_models/pagination'; +import {Series} from 'src/app/_models/series'; +import {FilterEvent, SortField} from 'src/app/_models/metadata/series-filter'; +import {Action, ActionItem} from 'src/app/_services/action-factory.service'; +import {ActionService} from 'src/app/_services/action.service'; +import {ImageService} from 'src/app/_services/image.service'; +import {JumpbarService} from 'src/app/_services/jumpbar.service'; +import {EVENTS, MessageHubService} from 'src/app/_services/message-hub.service'; +import {ScrollService} from 'src/app/_services/scroll.service'; +import {SeriesService} from 'src/app/_services/series.service'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import { SeriesCardComponent } from '../../../cards/series-card/series-card.component'; -import { CardDetailLayoutComponent } from '../../../cards/card-detail-layout/card-detail-layout.component'; -import { BulkOperationsComponent } from '../../../cards/bulk-operations/bulk-operations.component'; -import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component'; +import {SeriesCardComponent} from '../../../cards/series-card/series-card.component'; +import {CardDetailLayoutComponent} from '../../../cards/card-detail-layout/card-detail-layout.component'; +import {BulkOperationsComponent} from '../../../cards/bulk-operations/bulk-operations.component'; +import { + SideNavCompanionBarComponent +} from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component'; import {translate, TranslocoDirective} from "@jsverse/transloco"; -import {SeriesFilterV2} from "../../../_models/metadata/v2/series-filter-v2"; +import {FilterV2} from "../../../_models/metadata/v2/filter-v2"; +import {FilterField} from "../../../_models/metadata/v2/filter-field"; +import {SeriesFilterSettings} from "../../../metadata-filter/filter-settings"; +import {MetadataService} from "../../../_services/metadata.service"; +import {FilterStatement} from "../../../_models/metadata/v2/filter-statement"; +import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; @Component({ @@ -52,15 +58,16 @@ export class WantToReadComponent implements OnInit, AfterContentChecked { @ViewChild('scrollingBlock') scrollingBlock: ElementRef | undefined; @ViewChild('companionBar') companionBar: ElementRef | undefined; private readonly destroyRef = inject(DestroyRef); + private readonly metadataService = inject(MetadataService); isLoading: boolean = true; series: Array = []; pagination: Pagination = new Pagination(); - filter: SeriesFilterV2 | undefined = undefined; - filterSettings: FilterSettings = new FilterSettings(); + filter: FilterV2 | undefined = undefined; + filterSettings: SeriesFilterSettings = new SeriesFilterSettings(); refresh: EventEmitter = new EventEmitter(); - filterActiveCheck!: SeriesFilterV2; + filterActiveCheck!: FilterV2; filterActive: boolean = false; jumpbarKeys: Array = []; @@ -105,13 +112,23 @@ export class WantToReadComponent implements OnInit, AfterContentChecked { this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.titleService.setTitle('Kavita - ' + translate('want-to-read.title')); - this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => { - this.filter = filter; - this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter(); - this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement()); + this.route.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(data => { + this.filter = data['filter'] as FilterV2; + + const defaultStmt = {field: FilterField.WantToRead, value: 'true', comparison: FilterComparison.Equal} as FilterStatement; + + if (this.filter == null) { + this.filter = this.metadataService.createDefaultFilterDto('series'); + this.filter.statements.push(defaultStmt); + } + + + this.filterActiveCheck = this.metadataService.createDefaultFilterDto('series'); + this.filterActiveCheck!.statements.push(defaultStmt); this.filterSettings.presetsV2 = this.filter; + this.cdRef.markForCheck(); }); @@ -165,7 +182,7 @@ export class WantToReadComponent implements OnInit, AfterContentChecked { }); } - updateFilter(data: FilterEvent) { + updateFilter(data: FilterEvent) { if (data.filterV2 === undefined) return; this.filter = data.filterV2; diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index b9ab24ae5..2a2d40c4f 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -719,6 +719,8 @@ "library-name-header": "Library", "valid-until-header": "Next Refresh", "actions-header": "Actions", + "library-type": "Library Type", + "matched-state-label": "Match State", "matched-status-label": "Matched", "unmatched-status-label": "Not Matched", "blacklist-status-label": "Needs Manual Match", @@ -1034,7 +1036,7 @@ "collections": "Collections", "reading-lists": "Reading Lists", "bookmarks": "Bookmarks", - "browse-authors": "Browse Authors", + "browse-people": "Browse People", "filter-label": "{{common.filter}}", "all-series": "All Series", "clear": "{{common.clear}}", @@ -1047,10 +1049,31 @@ "edit": "{{common.edit}}" }, - "browse-authors": { - "title": "Browse Authors & Writers", + "browse-people": { + "title": "Browse People", "author-count": "{{num}} People", - "cover-image-description": "{{edit-series-modal.cover-image-description}}" + "cover-image-description": "{{edit-series-modal.cover-image-description}}", + "issue-count": "{{common.issue-count}}", + "series-count": "{{common.series-count}}", + "roles-label": "Roles", + "sort-label": "Sort", + "name-label": "Name", + "issue-count-label": "Issue Count", + "series-count-label": "Series Count" + }, + + "browse-genres": { + "title": "Browse Genres", + "genre-count": "{{num}} Genres", + "issue-count": "{{common.issue-count}}", + "series-count": "{{common.series-count}}" + }, + + "browse-tags": { + "title": "Browse Tags", + "genre-count": "{{num}} Tags", + "issue-count": "{{common.issue-count}}", + "series-count": "{{common.series-count}}" }, "person-detail": { @@ -1832,7 +1855,9 @@ "all-filters": "Smart Filters", "nav-link-header": "Navigation Options", "close": "{{common.close}}", - "person-aka-status": "Matches an alias" + "person-aka-status": "Matches an alias", + "browse-genres": "Browse Genres", + "browse-tags": "Browse Tags" }, "promoted-icon": { @@ -2060,7 +2085,10 @@ "release-year": "Release Year", "read-progress": "Last Read", "average-rating": "Average Rating", - "random": "Random" + "random": "Random", + "person-name": "Name", + "person-series-count": "Series Count", + "person-chapter-count": "Chapter Count" }, "edit-series-modal": { @@ -2483,7 +2511,7 @@ "reading-lists": "{{side-nav.reading-lists}}", "bookmarks": "{{side-nav.bookmarks}}", "all-series": "{{side-nav.all-series}}", - "browse-authors": "{{side-nav.browse-authors}}" + "browse-authors": "{{side-nav.browse-people}}" }, "filter-field-pipe": { @@ -2559,6 +2587,38 @@ "prompt": "Question" }, + "browse-title-pipe": { + "publication-status": "{{value}} works", + "age-rating": "Rated {{value}}", + "user-rating": "{{value}} star rating", + "tag": "Has Tag {{value}}", + "translator": "Translated by {{value}}", + "character": "Has character {{value}}", + "publisher": "Published by {{value}}", + "editor": "Edited by {{value}}", + "artist": "Drawn by {{value}}", + "letterer": "Lettered by {{value}}", + "colorist": "Colored by {{value}}", + "inker": "Inked by {{value}}", + "penciller": "Pencilled by {{value}}", + "writer": "Written by {{value}}", + "genre": "Has Genre {{value}}", + "library": "Within {{value}} library", + "format": "Format of {{value}}", + "release-year": "Released in {{value}}", + "imprint": "Imprint of {{value}}", + "team": "Team {{value}}", + "location": "In {{value}} location" + }, + + "generic-filter-field-pipe": { + "person-role": "Role", + "person-name": "Name", + "person-series-count": "Series Count", + "person-chapter-count": "Chapter Count" + }, + + "toasts": { "regen-cover": "A job has been enqueued to regenerate the cover image", "no-pages": "There are no pages. Kavita was not able to read this archive.", @@ -2822,10 +2882,17 @@ "pdf-light": "Light", "pdf-dark": "Dark" }, + "breakpoint-pipe": { + "never": "Never", + "mobile": "Mobile", + "tablet": "Tablet", + "desktop": "Desktop" + }, "manage-reading-profiles": { "description": "Not all your series may be read in the same way, set up distinct reading profiles per library or series to make getting back in your series as seamless as possible.", - "extra-tip": "Assign reading profiles via the action menu on series and libraries, or in bulk. When changing settings in a reader, a hidden profile is created that remembers your choices for that series (not for pdfs). This profile is removed when you assign or update one of your own reading profiles to the series.", + "extra-tip": "Assign reading profiles via the action menu on series and libraries, or in bulk. When changing settings in a reader, a hidden profile is created that remembers your choices for that series (not for pdfs). This profile is removed when you assign one of your own reading profiles to the series. More information can be found on the", + "wiki-title": "wiki", "profiles-title": "Your reading profiles", "default-profile": "Default", "add": "{{common.add}}", @@ -2860,6 +2927,8 @@ "allow-auto-webtoon-reader-tooltip": "Switch into Webtoon Reader mode if pages look like a webtoon. Some false positives may occur.", "width-override-label": "{{manga-reader.width-override-label}}", "width-override-tooltip": "Override width of images in the reader", + "disable-width-override-label": "Disable width override", + "disable-width-override-tooltip": "Prevent the width override from taking effect when your screen is at least the configured breakpoint or smaller", "reset": "{{common.reset}}", "book-reader-settings-title": "Book Reader", @@ -2971,6 +3040,7 @@ "author-count": "{{num}} Authors", "item-count": "{{num}} Items", "chapter-count": "{{num}} Chapters", + "issue-count": "{{num}} Issues", "no-data": "No Data", "book-num": "Book", diff --git a/UI/Web/src/theme/components/_sidenav.scss b/UI/Web/src/theme/components/_sidenav.scss index c724cec17..33faf7d82 100644 --- a/UI/Web/src/theme/components/_sidenav.scss +++ b/UI/Web/src/theme/components/_sidenav.scss @@ -28,7 +28,7 @@ } //START closed state of the sidebar &.closed { - width: 2.825rem; + width: 3.125rem; overflow-x: hidden; overflow-y: hidden; background-color: var(--side-nav-closed-bg-color); @@ -36,6 +36,11 @@ height: calc((var(--vh) * 100) - 6.5rem); border-radius: unset; + // For Firefox + @supports (-moz-appearance: none) { + width: 2.5rem; + } + .side-nav { .side-nav-item { color: var(--side-nav-item-closed-color); @@ -46,6 +51,10 @@ } } + .phone-hidden:first-of-type { + margin-left: unset; + } + .active-highlight { opacity: 0; } @@ -83,10 +92,10 @@ display: flex; &:first-of-type { - text-align: center; width: 2.5rem; min-width: 2.5rem; margin-left: 0.3rem; + justify-content: center; } &:last-child { @@ -95,9 +104,7 @@ } div { - align-items: center; height: 100%; - justify-content: inherit; padding: 0 0.625rem; i { @@ -193,10 +200,12 @@ .side-nav { overflow-x: hidden; padding-bottom: 0.625rem; + padding-left: 1.125rem; .side-nav-header { color: #ffffff; font-size: 1rem; + margin-left: unset; &:first-of-type { margin-top: 0.7rem; @@ -207,7 +216,6 @@ font-size: 1rem; min-height: 1.875rem; justify-content: unset; - margin-left: 1.125rem; &.active { .side-nav-text { @@ -220,6 +228,14 @@ margin-left: 0.75rem; font-size: 0.9rem; color: #999999; + + div { + display: flex; + + .badge { + align-self: center; + } + } } .card-actions { @@ -278,9 +294,9 @@ } } } - //START sidebar closed + //START sidebar bottom closed &.closed { - width: 3.4375rem; + width: 2.5rem; overflow-x: hidden; overflow-y: auto; background-color: unset; @@ -295,7 +311,7 @@ } } } - //END sidebar closed + //END sidebar bottom closed } //END kavita+ subscription bottom heart button diff --git a/UI/Web/src/theme/themes/dark.scss b/UI/Web/src/theme/themes/dark.scss index c0cba6def..7ec33d613 100644 --- a/UI/Web/src/theme/themes/dark.scss +++ b/UI/Web/src/theme/themes/dark.scss @@ -442,4 +442,10 @@ /** Search **/ --input-hint-border-color: #aeaeae; --input-hint-text-color: lightgrey; + + /** Breakpoint **/ + --setting-mobile-breakpoint: 768; + --setting-tablet-breakpoint: 1280; + --setting-desktop-breakpoint: 1440; + } From 59e461fc96edbbe4fcb048406ddd7665d9eb8de2 Mon Sep 17 00:00:00 2001 From: majora2007 Date: Sat, 14 Jun 2025 17:14:50 +0000 Subject: [PATCH 03/30] Bump versions by dotnet-bump-version. --- Kavita.Common/Kavita.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 0f0819e07..5d612e6b7 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -3,7 +3,7 @@ net9.0 kavitareader.com Kavita - 0.8.6.14 + 0.8.6.15 en true From 10280c5487d09d2d94f19c8a9918d30832f5733d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 14 Jun 2025 17:15:51 +0000 Subject: [PATCH 04/30] Update OpenAPI documentation --- openapi.json | 344 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 340 insertions(+), 4 deletions(-) diff --git a/openapi.json b/openapi.json index d5831d74b..bfcb28ef8 100644 --- a/openapi.json +++ b/openapi.json @@ -2,12 +2,12 @@ "openapi": "3.0.4", "info": { "title": "Kavita", - "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.6.12", + "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.6.14", "license": { "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.8.6.12" + "version": "0.8.6.14" }, "servers": [ { @@ -4065,6 +4065,64 @@ } } }, + "/api/Metadata/genres-with-counts": { + "post": { + "tags": [ + "Metadata" + ], + "summary": "Returns a list of Genres with counts for counts when Genre is on Series/Chapter", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserParams" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UserParams" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UserParams" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BrowseGenreDto" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BrowseGenreDto" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BrowseGenreDto" + } + } + } + } + } + } + } + }, "/api/Metadata/people-by-role": { "get": { "tags": [ @@ -4229,6 +4287,64 @@ } } }, + "/api/Metadata/tags-with-counts": { + "post": { + "tags": [ + "Metadata" + ], + "summary": "Returns a list of Tags with counts for counts when Tag is on Series/Chapter", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserParams" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UserParams" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UserParams" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BrowseTagDto" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BrowseTagDto" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BrowseTagDto" + } + } + } + } + } + } + } + }, "/api/Metadata/age-ratings": { "get": { "tags": [ @@ -5644,6 +5760,25 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrowsePersonFilterDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/BrowsePersonFilterDto" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/BrowsePersonFilterDto" + } + } + } + }, "responses": { "200": { "description": "OK", @@ -17065,6 +17200,17 @@ "format": "int32", "nullable": true }, + "disableWidthOverride": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer", + "description": "Manga Reader Option: Disable the width override if the screen is past the breakpoint", + "format": "int32" + }, "bookReaderMargin": { "type": "integer", "description": "Book Reader Option: Override extra Margin", @@ -17595,6 +17741,33 @@ }, "additionalProperties": false }, + "BrowseGenreDto": { + "required": [ + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "seriesCount": { + "type": "integer", + "description": "Number of Series this Entity is on", + "format": "int32" + }, + "chapterCount": { + "type": "integer", + "description": "Number of Chapters this Entity is on", + "format": "int32" + } + }, + "additionalProperties": false + }, "BrowsePersonDto": { "required": [ "name" @@ -17660,15 +17833,81 @@ "description": "Number of Series this Person is the Writer for", "format": "int32" }, - "issueCount": { + "chapterCount": { "type": "integer", - "description": "Number or Issues this Person is the Writer for", + "description": "Number of Issues this Person is the Writer for", "format": "int32" } }, "additionalProperties": false, "description": "Used to browse writers and click in to see their series" }, + "BrowsePersonFilterDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Not used - For parity with Series Filter", + "format": "int32" + }, + "name": { + "type": "string", + "description": "Not used - For parity with Series Filter", + "nullable": true + }, + "statements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonFilterStatementDto" + }, + "nullable": true + }, + "combination": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "format": "int32" + }, + "sortOptions": { + "$ref": "#/components/schemas/PersonSortOptions" + }, + "limitTo": { + "type": "integer", + "description": "Limit the number of rows returned. Defaults to not applying a limit (aka 0)", + "format": "int32" + } + }, + "additionalProperties": false + }, + "BrowseTagDto": { + "required": [ + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + }, + "seriesCount": { + "type": "integer", + "description": "Number of Series this Entity is on", + "format": "int32" + }, + "chapterCount": { + "type": "integer", + "description": "Number of Chapters this Entity is on", + "format": "int32" + } + }, + "additionalProperties": false + }, "BulkActionDto": { "type": "object", "properties": { @@ -21235,6 +21474,11 @@ "description": "Represents an option in the UI layer for Filtering", "format": "int32" }, + "libraryType": { + "type": "integer", + "description": "Library Type in int form. -1 indicates to ignore the field.", + "format": "int32" + }, "searchTerm": { "type": "string", "nullable": true @@ -22032,6 +22276,49 @@ }, "additionalProperties": false }, + "PersonFilterStatementDto": { + "type": "object", + "properties": { + "comparison": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16 + ], + "type": "integer", + "format": "int32" + }, + "field": { + "enum": [ + 1, + 2, + 3, + 4 + ], + "type": "integer", + "format": "int32" + }, + "value": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "PersonMergeDto": { "required": [ "destId", @@ -22052,6 +22339,25 @@ }, "additionalProperties": false }, + "PersonSortOptions": { + "type": "object", + "properties": { + "sortField": { + "enum": [ + 1, + 2, + 3 + ], + "type": "integer", + "format": "int32" + }, + "isAscending": { + "type": "boolean" + } + }, + "additionalProperties": false, + "description": "All Sorting Options for a query related to Person Entity" + }, "PersonalToCDto": { "required": [ "chapterId", @@ -22484,6 +22790,11 @@ "type": "integer", "description": "The highest age rating from all Series within the reading list", "format": "int32" + }, + "ownerUserName": { + "type": "string", + "description": "Username of the User that owns (in the case of a promoted list)", + "nullable": true } }, "additionalProperties": false @@ -26674,6 +26985,21 @@ }, "additionalProperties": false }, + "UserParams": { + "type": "object", + "properties": { + "pageNumber": { + "type": "integer", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "If set to 0, will set as MaxInt", + "format": "int32" + } + }, + "additionalProperties": false + }, "UserPreferencesDto": { "required": [ "blurUnreadSummaries", @@ -26885,6 +27211,16 @@ "format": "int32", "nullable": true }, + "disableWidthOverride": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer", + "format": "int32" + }, "bookReaderMargin": { "type": "integer", "format": "int32" From b6d004614a88a3dc25a92e876d9dafcad8f9ec6e Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 16 Jun 2025 13:36:01 +0200 Subject: [PATCH 05/30] [skip ci] Weblate Changes (#3804) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adam Havránek Co-authored-by: Adam Kleizer Co-authored-by: Aindriú Mac Giolla Eoin Co-authored-by: Bora Atıcı Co-authored-by: DR Co-authored-by: Dark77 Co-authored-by: Frozehunter Co-authored-by: Havokdan Co-authored-by: Itsmechinmoy Co-authored-by: Yoan Jacquemin Co-authored-by: lin49931104 Co-authored-by: peter cerny Co-authored-by: Михаил Co-authored-by: 無情天 --- API/I18N/as.json | 1 + API/I18N/cs.json | 3 +- API/I18N/ga.json | 3 +- API/I18N/he.json | 3 +- API/I18N/pt_BR.json | 3 +- API/I18N/ru.json | 16 +- API/I18N/sk.json | 93 ++++++- API/I18N/zh_Hans.json | 3 +- UI/Web/src/assets/langs/ca.json | 23 +- UI/Web/src/assets/langs/cs.json | 292 +++++++++++++++------- UI/Web/src/assets/langs/de.json | 235 +++++++++++------- UI/Web/src/assets/langs/es.json | 59 +---- UI/Web/src/assets/langs/fi.json | 30 +-- UI/Web/src/assets/langs/fr.json | 88 ++----- UI/Web/src/assets/langs/ga.json | 308 ++++++++++++++++------- UI/Web/src/assets/langs/hu.json | 39 ++- UI/Web/src/assets/langs/id.json | 33 --- UI/Web/src/assets/langs/it.json | 59 +---- UI/Web/src/assets/langs/ja.json | 51 ---- UI/Web/src/assets/langs/ko.json | 61 +---- UI/Web/src/assets/langs/nl.json | 37 --- UI/Web/src/assets/langs/pl.json | 61 +---- UI/Web/src/assets/langs/pt.json | 61 +---- UI/Web/src/assets/langs/pt_BR.json | 296 +++++++++++++++------- UI/Web/src/assets/langs/ru.json | 125 ++++++---- UI/Web/src/assets/langs/sk.json | 356 +++++++++++++++++++-------- UI/Web/src/assets/langs/sv.json | 59 ----- UI/Web/src/assets/langs/ta.json | 57 ----- UI/Web/src/assets/langs/th.json | 37 --- UI/Web/src/assets/langs/tr.json | 123 ++++++--- UI/Web/src/assets/langs/uk.json | 49 ---- UI/Web/src/assets/langs/vi.json | 57 ----- UI/Web/src/assets/langs/zh_Hans.json | 300 +++++++++++++++------- UI/Web/src/assets/langs/zh_Hant.json | 95 ++----- 34 files changed, 1600 insertions(+), 1516 deletions(-) create mode 100644 API/I18N/as.json diff --git a/API/I18N/as.json b/API/I18N/as.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/API/I18N/as.json @@ -0,0 +1 @@ +{} diff --git a/API/I18N/cs.json b/API/I18N/cs.json index 9825ab074..e136d8e75 100644 --- a/API/I18N/cs.json +++ b/API/I18N/cs.json @@ -208,5 +208,6 @@ "smart-filter-name-required": "Vyžaduje se název chytrého filtru", "smart-filter-system-name": "Nelze použít název streamu poskytovaného systémem", "sidenav-stream-only-delete-smart-filter": "Z postranní navigace lze odstranit pouze streamy chytrých filtrů", - "aliases-have-overlap": "Jeden nebo více aliasů se překrývají s jinými osobami, nelze je aktualizovat" + "aliases-have-overlap": "Jeden nebo více aliasů se překrývají s jinými osobami, nelze je aktualizovat", + "generated-reading-profile-name": "Generováno z {0}" } diff --git a/API/I18N/ga.json b/API/I18N/ga.json index 79d0d271e..142425aec 100644 --- a/API/I18N/ga.json +++ b/API/I18N/ga.json @@ -208,5 +208,6 @@ "sidenav-stream-only-delete-smart-filter": "Ní féidir ach sruthanna cliste scagaire a scriosadh as an SideNav", "dashboard-stream-only-delete-smart-filter": "Ní féidir ach sruthanna cliste scagaire a scriosadh ón deais", "smart-filter-name-required": "Ainm Scagaire Cliste ag teastáil", - "aliases-have-overlap": "Tá forluí idir ceann amháin nó níos mó de na leasainmneacha agus daoine eile, ní féidir iad a nuashonrú" + "aliases-have-overlap": "Tá forluí idir ceann amháin nó níos mó de na leasainmneacha agus daoine eile, ní féidir iad a nuashonrú", + "generated-reading-profile-name": "Gineadh ó {0}" } diff --git a/API/I18N/he.json b/API/I18N/he.json index 41a9a7de7..3b2386bf6 100644 --- a/API/I18N/he.json +++ b/API/I18N/he.json @@ -21,5 +21,6 @@ "age-restriction-update": "אירעה תקלה בעת עדכון הגבלת גיל", "generic-user-update": "אירעה תקלה בעת עדכון משתמש", "user-already-registered": "משתמש רשום כבר בתור {0}", - "manual-setup-fail": "לא מתאפשר להשלים הגדרה ידנית. יש לבטל וליצור מחדש את ההזמנה" + "manual-setup-fail": "לא מתאפשר להשלים הגדרה ידנית. יש לבטל וליצור מחדש את ההזמנה", + "email-taken": "דואר אלקטרוני כבר בשימוש" } diff --git a/API/I18N/pt_BR.json b/API/I18N/pt_BR.json index b67d926b0..418e0ea3b 100644 --- a/API/I18N/pt_BR.json +++ b/API/I18N/pt_BR.json @@ -208,5 +208,6 @@ "dashboard-stream-only-delete-smart-filter": "Somente fluxos de filtros inteligentes podem ser excluídos do painel", "smart-filter-system-name": "Você não pode usar o nome de um fluxo fornecido pelo sistema", "sidenav-stream-only-delete-smart-filter": "Somente fluxos de filtros inteligentes podem ser excluídos do Navegador Lateral", - "aliases-have-overlap": "Um ou mais dos pseudônimos se sobrepõem a outras pessoas, não pode atualizar" + "aliases-have-overlap": "Um ou mais dos pseudônimos se sobrepõem a outras pessoas, não pode atualizar", + "generated-reading-profile-name": "Gerado a partir de {0}" } diff --git a/API/I18N/ru.json b/API/I18N/ru.json index 92d842336..fdea5920f 100644 --- a/API/I18N/ru.json +++ b/API/I18N/ru.json @@ -1,5 +1,5 @@ { - "confirm-email": "Вы обязаны сначала подтвердить свою почту", + "confirm-email": "Сначала Вы обязаны подтвердить свою электронную почту", "generate-token": "Возникла проблема с генерацией токена подтверждения электронной почты. Смотрите журналы", "invalid-password": "Неверный пароль", "invalid-email-confirmation": "Неверное подтверждение электронной почты", @@ -35,15 +35,15 @@ "no-user": "Пользователь не существует", "generic-invite-user": "Возникла проблема с приглашением пользователя. Пожалуйста, проверьте журналы.", "permission-denied": "Вам запрещено выполнять эту операцию", - "invalid-access": "Недопустимый доступ", + "invalid-access": "В доступе отказано", "reading-list-name-exists": "Такой список для чтения уже существует", "perform-scan": "Пожалуйста, выполните сканирование этой серии или библиотеки и повторите попытку", "generic-device-create": "При создании устройства возникла ошибка", "generic-read-progress": "Возникла проблема с сохранением прогресса", "file-doesnt-exist": "Файл не существует", "admin-already-exists": "Администратор уже существует", - "send-to-kavita-email": "Отправка на устройство не может быть использована с почтовым сервисом Kavita. Пожалуйста, настройте свой собственный.", - "no-image-for-page": "Нет такого изображения для страницы {0}. Попробуйте обновить, чтобы обновить кэш.", + "send-to-kavita-email": "Отправка на устройство не может быть использована без настройки электронной почты", + "no-image-for-page": "Нет такого изображения для страницы {0}. Попробуйте обновить, для повторного кеширования.", "reading-list-permission": "У вас нет прав на этот список чтения или список не существует", "volume-doesnt-exist": "Том не существует", "generic-library": "Возникла критическая проблема. Пожалуйста, попробуйте еще раз.", @@ -57,7 +57,7 @@ "generic-reading-list-create": "Возникла проблема с созданием списка для чтения", "no-cover-image": "Изображение на обложке отсутствует", "collection-updated": "Коллекция успешно обновлена", - "critical-email-migration": "Возникла проблема при переносе электронной почты. Обратитесь в службу поддержки", + "critical-email-migration": "Возникла проблема при смене электронной почты. Обратитесь в службу поддержки", "cache-file-find": "Не удалось найти изображение в кэше. Перезагрузитесь и попробуйте снова.", "duplicate-bookmark": "Дублирующая закладка уже существует", "collection-tag-duplicate": "Такая коллекция уже существует", @@ -72,7 +72,7 @@ "pdf-doesnt-exist": "PDF не существует, когда он должен существовать", "generic-device-delete": "При удалении устройства возникла ошибка", "bookmarks-empty": "Закладки не могут быть пустыми", - "valid-number": "Должен быть действительный номер страницы", + "valid-number": "Номер страницы должен быть действительным", "series-doesnt-exist": "Серия не существует", "no-library-access": "Пользователь не имеет доступа к этой библиотеке", "reading-list-item-delete": "Не удалось удалить элемент(ы)", @@ -155,7 +155,7 @@ "generic-user-delete": "Не удалось удалить пользователя", "generic-cover-reading-list-save": "Невозможно сохранить изображение обложки в списке для чтения", "unable-to-register-k+": "Невозможно зарегистрировать лицензию из-за ошибки. Обратитесь в службу поддержки Кавита+", - "encode-as-warning": "Конвертировать в PNG невозможно. Для обложек используйте Обновить Обложки. Закладки и фавиконки нельзя закодировать обратно.", + "encode-as-warning": "Вы не можете конвертировать в формат PNG. Для обновления обложек используйте команду \"Обновить обложку\". Закладки и значки не могут быть закодированы обратно.", "want-to-read": "Хотите прочитать", "generic-user-pref": "Возникла проблема с сохранением предпочтений", "external-sources": "Внешние источники", @@ -197,7 +197,7 @@ "kavita+-data-refresh": "Обновление данных Kavita+", "kavitaplus-restricted": "Это доступно только для Kavita+", "person-doesnt-exist": "Персона не существует", - "generic-cover-volume-save": "Не удается сохранить обложку для раздела", + "generic-cover-volume-save": "Не удается сохранить обложку для тома", "generic-cover-person-save": "Не удается сохранить изображение обложки для Персоны", "person-name-unique": "Имя персоны должно быть уникальным", "person-image-doesnt-exist": "Персона не существует в CoversDB", diff --git a/API/I18N/sk.json b/API/I18N/sk.json index 0967ef424..a48add072 100644 --- a/API/I18N/sk.json +++ b/API/I18N/sk.json @@ -1 +1,92 @@ -{} +{ + "disabled-account": "Váš účet je zakázaný. Kontaktujte správcu servera.", + "register-user": "Niečo sa pokazilo pri registrácii užívateľa", + "confirm-email": "Najprv musíte potvrdiť svoj e-mail", + "locked-out": "Boli ste zamknutí z dôvodu veľkého počtu neúspešných pokusov o prihlásenie. Počkajte 10 minút.", + "validate-email": "Pri validácii vášho e-mailu sa vyskytla chyba: {0}", + "confirm-token-gen": "Pri vytváraní potvrdzovacieho tokenu sa vyskytla chyba", + "permission-denied": "Na vykonanie tejto úlohy nemáte oprávnenie", + "password-required": "Ak nie ste administrátor, musíte na vykonanie zmien vo vašom profile zadať vaše aktuálne heslo", + "invalid-password": "Nesprávne heslo", + "invalid-token": "Nesprávny token", + "unable-to-reset-key": "Niečo sa pokazilo, kľúč nie je možné resetovať", + "invalid-payload": "Nesprávny payload", + "nothing-to-do": "Nič na vykonanie", + "share-multiple-emails": "Nemôžete zdielať e-maily medzi rôznymi účtami", + "generate-token": "Pri generovaní potvrdzovacieho tokenu e-mailu sa vyskytla chyba. Pozrite záznamy udalostí", + "age-restriction-update": "Pri aktualizovaní vekového obmedzenia sa vyskytla chyba", + "no-user": "Používateľ neexistuje", + "generic-user-update": "Aktualizácia používateľa prebehla s výnimkou", + "username-taken": "Používateľské meno už existuje", + "user-already-confirmed": "Používateľ je už potvrdený", + "user-already-registered": "Používateľ je už registrovaný ako {0}", + "user-already-invited": "Používateľ je už pod týmto e-mailom pozvaný a musí ešte prijať pozvanie.", + "generic-password-update": "Pri potvrdení nového hesla sa vyskytla neočakávaná chyba", + "generic-invite-user": "Pri pozývaní tohto používateľa sa vyskytla chyba. Pozrite záznamy udalostí.", + "password-updated": "Heslo aktualizované", + "forgot-password-generic": "E-mail bude odoslaný na zadanú adresu len v prípade, ak existuje v databáze", + "invalid-email-confirmation": "Neplatné potvrdenie e-mailu", + "not-accessible-password": "Váš server nie je dostupný. Odkaz na resetovanie vášho hesla je v záznamoch udalostí", + "email-taken": "Zadaný e-mail už existuje", + "denied": "Nepovolené", + "manual-setup-fail": "Manuálne nastavenie nie je možné dokončiť. Prosím zrušte aktuálny postup a znovu vytvorte pozvánku", + "generic-user-email-update": "Nemožno aktualizovať e-mail používateľa. Skontrolujte záznamy udalostí.", + "email-not-enabled": "E-mail nie je na tomto serveri povolený. Preto túto akciu nemôžete vykonať.", + "collection-updated": "Zbierka bola úspešne aktualizovaná", + "device-doesnt-exist": "Zariadenie neexistuje", + "generic-device-delete": "Pri odstraňovaní zariadenia sa vyskytla chyba", + "greater-0": "{0} musí byť väčší ako 0", + "send-to-size-limit": "Snažíte sa odoslať súbor(y), ktoré sú príliš veľké pre vášho e-mailového poskytovateľa", + "send-to-device-status": "Prenos súborov do vášho zariadenia", + "no-cover-image": "Žiadny prebal", + "must-be-defined": "{0} musí byť definovaný", + "generic-favicon": "Pri získavaní favicon-u domény sa vyskytla chyba", + "no-library-access": "Pozužívateľ nemá prístup do tejto knižnice", + "user-doesnt-exist": "Používateľ neexistuje", + "collection-already-exists": "Zbierka už existuje", + "not-accessible": "Váš server nie je dostupný z vonkajšieho prostredia", + "email-sent": "E-mail odoslaný", + "user-migration-needed": "Uvedený používateľ potrebuje migrovať. Odhláste ho a opäť prihláste na spustenie migrácie", + "generic-invite-email": "Pri opakovanom odosielaní pozývacieho e-mailu sa vyskytla chyba", + "email-settings-invalid": "V nastaveniach e-mailu chýbajú potrebné údaje. Uistite sa, že všetky nastavenia e-mailu sú uložené.", + "chapter-doesnt-exist": "Kapitola neexistuje", + "critical-email-migration": "Počas migrácie e-mailu sa vyskytla chyba. Kontaktujte podporu", + "collection-deleted": "Zbierka bola vymazaná", + "generic-error": "Niečo sa pokazilo, skúste to znova", + "collection-doesnt-exist": "Zbierka neexistuje", + "generic-device-update": "Pri aktualizácii zariadenia sa vyskytla chyba", + "bookmark-doesnt-exist": "Záložka neexistuje", + "person-doesnt-exist": "Osoba neexistuje", + "send-to-kavita-email": "Odoslanie do zariadenia nemôže byť použité bez nastavenia e-amilu", + "send-to-unallowed": "Nemôžete odosielať do zariadenia, ktoré nie je vaše", + "generic-library": "Vyskytla sa kritická chyba. Prosím skúste to opäť.", + "pdf-doesnt-exist": "PDF neexistuje, hoci by malo", + "generic-library-update": "Počas aktualizácie knižnice sa vyskytla kritická chyba.", + "invalid-access": "Neplatný prístup", + "perform-scan": "Prosím, vykonajte opakovaný sken na tejto sérii alebo knižnici", + "generic-read-progress": "Pri ukladaní aktuálneho stavu sa vyskytla chyba", + "generic-clear-bookmarks": "Záložky nie je možné vymazať", + "bookmark-permission": "Nemáte oprávnenie na vkladanie/odstraňovanie záložiek", + "bookmark-save": "Nemožno uložiť záložku", + "bookmarks-empty": "Záložky nemôžu byť prázdne", + "library-doesnt-exist": "Knižnica neexistuje", + "invalid-path": "Neplatné umiestnenie", + "generic-send-to": "Pri odosielaní súboru(-ov) do vášho zariadenia sa vyskytla chyba", + "no-image-for-page": "Žiadny taký obrázok pre stránku {0}. Pokúste sa ju obnoviť, aby ste ju mohli nanovo uložiť.", + "delete-library-while-scan": "Nemôžete odstrániť knižnicu počas prebiehajúceho skenovania. Prosím, vyčkajte na dokončenie skenovania alebo reštartujte Kavitu a skúste ju opäť odstrániť", + "invalid-username": "Neplatné používateľské meno", + "account-email-invalid": "E-mail uvedený v údajoch administrátora nie je platným e-mailom. Nie je možné zaslať testovací e-mail.", + "admin-already-exists": "Administrátor už existuje", + "invalid-filename": "Neplatný názov súboru", + "file-doesnt-exist": "Súbor neexistuje", + "invalid-email": "E-mail v záznamoch pre používateľov nie platný e-mail. Odkazy sú uvedené v záznamoch udalostí.", + "file-missing": "Súbor nebol nájdený v knihe", + "error-import-stack": "Pri importovaní MAL balíka sa vyskytla chyba", + "person-name-required": "Meno osoby je povinné a nesmie byť prázdne", + "person-name-unique": "Meno osoby musí byť jedinečné", + "person-image-doesnt-exist": "Osoba neexistuje v databáze CoversDB", + "generic-device-create": "Pri vytváraní zariadenia sa vyskytla chyba", + "series-doesnt-exist": "Séria neexistuje", + "volume-doesnt-exist": "Zväzok neexistuje", + "library-name-exists": "Názov knižnice už existuje. Prosím, vyberte si pre daný server jedinečný názov." +} diff --git a/API/I18N/zh_Hans.json b/API/I18N/zh_Hans.json index 070a87855..14c8c902e 100644 --- a/API/I18N/zh_Hans.json +++ b/API/I18N/zh_Hans.json @@ -208,5 +208,6 @@ "smart-filter-name-required": "需要智能筛选器名称", "smart-filter-system-name": "您不能使用系统提供的流名称", "sidenav-stream-only-delete-smart-filter": "只能从侧边栏删除智能筛选器流", - "aliases-have-overlap": "一个或多个别名与其他人有重叠,无法更新" + "aliases-have-overlap": "一个或多个别名与其他人有重叠,无法更新", + "generated-reading-profile-name": "由 {0} 生成" } diff --git a/UI/Web/src/assets/langs/ca.json b/UI/Web/src/assets/langs/ca.json index 7291e3e2a..1b929cb53 100644 --- a/UI/Web/src/assets/langs/ca.json +++ b/UI/Web/src/assets/langs/ca.json @@ -31,29 +31,11 @@ "success-toast": "S'han actualitzat les preferències d'ús", "global-settings-title": "Paràmetres globals", "locale-label": "Configuració local", - "scaling-option-label": "Opcions d'escala", - "background-color-label": "Color de fons", - "writing-style-label": "Estil d'escriptura", - "font-size-book-label": "Mida de la lletra", - "line-height-book-label": "Interlineat", - "pdf-scroll-mode-label": "Mode de desplaçament", - "pdf-theme-label": "Tema", - "immersive-mode-label": "Mode immersiu", "locale-tooltip": "La llengua que ha d'usar el Kavita", "prompt-on-download-label": "Pregunta en baixar", "disable-animations-label": "Desactiva les animacions", - "auto-close-menu-label": "Tanca automàticament el menú", - "font-family-label": "Família tipogràfica", - "margin-book-label": "Marge", "clients-api-key-label": "Clau de l'API", - "clients-api-key-tooltip": "La clau de l'API és com una contrasenya. Reinicialitzar-la invalidarà tots els clients existents.", - "image-reader-settings-title": "Lector d'imatges", - "reading-direction-label": "Direcció de lectura", - "layout-mode-label": "Mode de disposició", - "book-reader-settings-title": "Lector de llibres", - "reading-direction-book-label": "Direcció de lectura", - "layout-mode-book-label": "Mode de disposició", - "pdf-reader-settings-title": "Lector de PDF" + "clients-api-key-tooltip": "La clau de l'API és com una contrasenya. Reinicialitzar-la invalidarà tots els clients existents." }, "theme-manager": { "title": "Gestor de temes", @@ -493,8 +475,7 @@ "manga-reader": { "incognito-title": "Mode incògnit:", "next-page-tooltip": "Pàgina següent", - "back": "Enrere", - "save-globally": "Desa globalment" + "back": "Enrere" }, "pdf-scroll-mode-pipe": { "vertical": "Vertical", diff --git a/UI/Web/src/assets/langs/cs.json b/UI/Web/src/assets/langs/cs.json index dfa2db0fc..05a877a23 100644 --- a/UI/Web/src/assets/langs/cs.json +++ b/UI/Web/src/assets/langs/cs.json @@ -83,7 +83,7 @@ }, "user-preferences": { "title": "Uživatelský panel", - "pref-description": "Toto jsou globální nastavení, která jsou vázána na váš účet.", + "pref-description": "Jedná se o globální nastavení, která jsou vázána na váš účet. Nastavení čtečky se nachází v sekci Čtecí profily.", "account-tab": "{{tabs.account-tab}}", "preferences-tab": "{{tabs.preferences-tab}}", "theme-tab": "{{tabs.theme-tab}}", @@ -107,70 +107,19 @@ "collapse-series-relationships-tooltip": "Měla by Kavita ukázat seriály, které nemají žádné vztahy nebo jsou rodičem/prequelem", "share-series-reviews-label": "Sdílejte recenze Serií", "share-series-reviews-tooltip": "Má Kavita zahrnout vaše recenze na Series pro ostatní uživatele", - "image-reader-settings-title": "Čtečka obrázků", - "reading-direction-label": "Směr čtení", - "reading-direction-tooltip": "Kliknutím na směr přejdete na další stránku. Zprava doleva znamená, že kliknutím na levou stranu obrazovky přejdete na další stránku.", - "scaling-option-label": "Možnosti škálování", - "scaling-option-tooltip": "Jak změnit měřítko obrázku na obrazovku.", - "page-splitting-label": "Rozdělení stránky", - "page-splitting-tooltip": "Jak rozdělit obrázek v plné šířce (tj. oba obrázky jsou kombinovány)", - "reading-mode-label": "Režim čtení", - "reading-mode-tooltip": "Změna stránkování na vertikální, horizontální nebo nekonečné posunování", - "layout-mode-label": "Režim rozvržení", - "layout-mode-tooltip": "Vykreslení jednoho obrázku na obrazovku nebo dvou obrázků vedle sebe", - "background-color-label": "Barva pozadí", - "background-color-tooltip": "Barva pozadí čtečky obrázků", - "auto-close-menu-label": "Nabídka automatického zavření", - "auto-close-menu-tooltip": "Mělo by se menu automaticky zavřít", - "show-screen-hints-label": "Zobrazit tipy na obrazovce", - "show-screen-hints-tooltip": "Zobrazení překryvného okna pro lepší orientaci v oblasti a směru stránkování", - "emulate-comic-book-label": "Emulovat komiks", - "swipe-to-paginate-label": "Přejeďte prstem na stránkování", - "book-reader-settings-title": "Čtečka knih", - "tap-to-paginate-label": "Klepnutím můžete stránkovat", - "tap-to-paginate-tooltip": "Pokud strany obrazovky čtečky knih umožňují klepnutím na ni přejít na předchozí/další stránku", - "immersive-mode-label": "Imerzní režim", - "immersive-mode-tooltip": "Tím se nabídka skryje za kliknutí na dokument čtečky a otočením klepnutím zapněte stránkování", - "reading-direction-book-label": "Směr čtení", - "reading-direction-book-tooltip": "Směrem ke kliknutí se přesunete na další stránku. Zprava doleva znamená, že kliknutím na levou stranu obrazovky přejdete na další stránku.", - "font-family-label": "Rodina písem", - "font-family-tooltip": "Rodina písem k načtení. Výchozí načte výchozí písmo knihy", - "writing-style-label": "Styl psaní", - "writing-style-tooltip": "Změní směr textu. Vodorovně je zleva doprava, svisle shora dolů.", - "layout-mode-book-label": "Režim rozvržení", - "layout-mode-book-tooltip": "Jak by měl být obsah rozvržen. Scroll je takový, jak ho kniha balí. 1 nebo 2 sloupce se přizpůsobí výšce zařízení a vejdou se 1 nebo 2 sloupce textu na stránku", - "color-theme-book-label": "Barevný motiv", - "color-theme-book-tooltip": "Jaký barevný motiv použít na obsah a nabídku čtečky knih", - "font-size-book-label": "Velikost písma", - "line-height-book-label": "Řádkování", - "line-height-book-tooltip": "Kolik mezer mezi řádky knihy", - "margin-book-label": "Okraj", - "margin-book-tooltip": "Kolik mezer na každé straně obrazovky. To bude přepsáno na 0 na mobilních zařízeních bez ohledu na toto nastavení.", "clients-opds-alert": "OPDS není na tomto serveru povoleno. Toto nebude mít vliv na uživatele Tachiyomi.", "clients-opds-description": "Všichni klienti třetích stran budou používat klíč API nebo níže uvedenou adresu URL připojení. Jsou to jako hesla, udržujte je v soukromí.", "clients-api-key-tooltip": "Klíč API je jako heslo. Jeho resetováním dojde ke zneplatnění všech stávajících klientů.", "clients-opds-url-tooltip": "Viz seznam podporovaných klientů OPDS: ", "reset": "{{common.reset}}", "save": "{{common.save}}", - "swipe-to-paginate-tooltip": "Mělo by přejetí prstem po obrazovce způsobit spuštění další nebo předchozí stránky", - "emulate-comic-book-tooltip": "Používá stínový efekt, který napodobuje čtení z knihy", - "pdf-scroll-mode-tooltip": "Způsob procházení stránek. Vertikální/horizontální a stránkování klepnutím (bez posouvání)", - "pdf-spread-mode-label": "Režim rozložení", - "pdf-reader-settings-title": "PDF čtečka", - "pdf-scroll-mode-label": "Režim posunu", - "font-size-book-tooltip": "Procento měřítka, které se použije na písmo v knize", - "pdf-spread-mode-tooltip": "Jak by měly být stránky uspořádány. Jednoduché nebo dvojité (liché/sudé)", - "pdf-theme-label": "Motiv", - "pdf-theme-tooltip": "Barevný motiv čtečky", "clients-opds-url-label": "URL OPDS", "clients-api-key-label": "Klíč API", "kavitaplus-settings-title": "Kavita+", "anilist-scrobbling-label": "AniList Scrobblování", "anilist-scrobbling-tooltip": "Povolit Kavitě Scrobble (jednosměrná synchronizace) průběhu čtení a hodnocení do AniListu", "want-to-read-sync-label": "Chci číst Synchronizace", - "want-to-read-sync-tooltip": "Povolit Kavitě přidávat položky do seznamu Chci číst na základě AniListu a sérií MAL v seznamu čekajících čtení", - "allow-auto-webtoon-reader-label": "Režim automatické čtečky webových komiksů", - "allow-auto-webtoon-reader-tooltip": "Pokud stránky vypadají jako webový komiks, přepněte se do režimu čtečky webových komiksů. Mohou se vyskytnout některé falešně pozitivní výsledky." + "want-to-read-sync-tooltip": "Povolit Kavitě přidávat položky do seznamu Chci číst na základě AniListu a sérií MAL v seznamu čekajících čtení" }, "user-holds": { "title": "Scrobble drží", @@ -680,7 +629,10 @@ "incognito-mode-label": "Režim inkognito", "next": "Další", "previous": "Předchozí", - "go-to-page-prompt": "Existuje {{totalPages}} stránek. Na kterou stránku chcete přejít?" + "go-to-page-prompt": "Existuje {{totalPages}} stránek. Na kterou stránku chcete přejít?", + "go-to-first-page": "Přejít na první stránku", + "go-to-section": "Přejít do sekce", + "go-to-section-prompt": "Je zde {{totalSections}} sekcí. Do které sekce chcete přejít?" }, "personal-table-of-contents": { "no-data": "V záložkách zatím nic není", @@ -728,7 +680,7 @@ "series-detail": { "page-settings-title": "Nastavení stránky", "close": "{{common.close}}", - "layout-mode-label": "{{user-preferences.layout-mode-book-label}}", + "layout-mode-label": "{{manage-reading-profiles.layout-mode-book-label}}", "layout-mode-option-card": "Karta", "layout-mode-option-list": "Seznam", "continue-from": "Pokračovat {{title}}", @@ -839,7 +791,6 @@ "customize": "{{settings.customize}}", "reading-lists": "Seznamy četby", "bookmarks": "Záložky", - "browse-authors": "Procházet autory", "donate": "Přispět", "more": "Více", "back": "Zpět", @@ -849,7 +800,8 @@ "collections": "Sbírky", "all-series": "Všechny série", "cancel-edit": "Zavřít úpravu", - "edit": "{{common.edit}}" + "edit": "{{common.edit}}", + "browse-people": "Procházet Osoby" }, "library-settings-modal": { "close": "{{common.close}}", @@ -905,14 +857,14 @@ "allow-metadata-matching-tooltip": "Měla by Kavita stáhnout metadata pro série v rámci této knihovny. K tomu dojde pouze v případě, že má server aktivní předplatné Kavita+." }, "reader-settings": { - "font-family-label": "{{user-preferences.font-family-label}}", - "font-size-label": "{{user-preferences.font-size-book-label}}", - "line-spacing-label": "{{user-preferences.line-height-book-label}}", - "margin-label": "{{user-preferences.margin-book-label}}", - "reading-direction-label": "{{user-preferences.reading-direction-book-label}}", - "writing-style-label": "{{user-preferences.writing-style-label}}", - "immersive-mode-label": "{{user-preferences.immersive-mode-label}}", - "layout-mode-label": "{{user-preferences.layout-mode-book-label}}", + "font-family-label": "{{manage-reading-profiles.font-family-label}}", + "font-size-label": "{{manage-reading-profiles.font-size-book-label}}", + "line-spacing-label": "{{manage-reading-profiles.line-height-book-label}}", + "margin-label": "{{manage-reading-profiles.margin-book-label}}", + "reading-direction-label": "{{manage-reading-profiles.reading-direction-book-label}}", + "writing-style-label": "{{manage-reading-profiles.writing-style-label}}", + "immersive-mode-label": "{{manage-reading-profiles.immersive-mode-label}}", + "layout-mode-label": "{{manage-reading-profiles.layout-mode-book-label}}", "right-to-left": "Zprava doleva", "writing-style-tooltip": "Změní směr textu. Vodorovný je zleva doprava, svislý je shora dolů.", "general-settings-title": "Obecná nastavení", @@ -938,7 +890,15 @@ "color-theme-title": "Barevné motivy", "theme-white": "Bílý", "theme-paper": "Papír", - "theme-black": "Černý" + "theme-black": "Černý", + "create-new-tooltip": "Vytvořte nový spravovatelný profil z vašeho aktuálního implicitního profilu", + "update-parent": "Uložit do {{name}}", + "loading": "načítání", + "create-new": "Nový profil z implicitního", + "reading-profile-updated": "Profil čtení aktualizován", + "reading-profile-promoted": "Profil čtení propagován", + "line-spacing-min-label": "1x", + "line-spacing-max-label": "2.5x" }, "bookmarks": { "title": "{{side-nav.bookmarks}}", @@ -1337,7 +1297,8 @@ "admin-manage-tokens": "Správa uživatelských tokenů", "scrobble-holds": "Podržení Scrobble", "account": "Účet", - "admin-metadata": "Správa metadat" + "admin-metadata": "Správa metadat", + "reading-profiles": "Profily čtení" }, "collection-detail": { "item-count": "{{common.item-count}}", @@ -1452,7 +1413,9 @@ "server-settings": "Nastavení serveru", "settings": "Nastavení", "announcements": "Oznámení", - "person-aka-status": "Shoduje se s aliasem" + "person-aka-status": "Shoduje se s aliasem", + "browse-tags": "Procházet štítky", + "browse-genres": "Procházet žánry" }, "promoted-icon": { "promoted": "{{common.promoted}}" @@ -1524,8 +1487,8 @@ "dry-run-description": "Toto je zkušební verze, která ukazuje, co se stane, pokud stisknete tlačítko Další a provedete import. Všechny chyby nebudou importovány." }, "manga-reader": { - "auto-close-menu-label": "{{user-preferences.auto-close-menu-label}}", - "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", + "auto-close-menu-label": "{{manage-reading-profiles.auto-close-menu-label}}", + "emulate-comic-book-label": "{{manage-reading-profiles.emulate-comic-book-label}}", "next-page-tooltip": "Další stránka", "first-time-reading-manga": "Klepnutím na obrázek kdykoli otevřete nabídku. Klepnutím na ukazatel průběhu můžete konfigurovat různá nastavení nebo přejít na stránku. Klepnutím na strany obrázku přejdete na další/předchozí stránku.", "back": "Zpět", @@ -1536,7 +1499,6 @@ "no-prev-chapter": "Žádná předchozí kapitola", "incognito-title": "Anonymní režim:", "left-to-right-alt": "Zleva doprava", - "save-globally": "Uložit globálně", "shortcuts-menu-alt": "Modální klávesové zkratky", "prev-page-tooltip": "Předchozí stránka", "prev-chapter-tooltip": "Předchozí kapitola/svazek", @@ -1555,14 +1517,19 @@ "incognito-alt": "Anonymní režim je zapnutý. Přepnutím jej vypnete.", "image-splitting-label": "Rozdělení obrazu", "width-override-label": "Přepsání šířky", - "off": "Vypnuto", + "off": "{{reader-settings.off}}", "enable-comic-book-label": "Napodobit komiks", "brightness-label": "Jas", "bookmark-page-tooltip": "Založit stránku", "unbookmark-page-tooltip": "Zrušení záložky stránky", "bookmarks-title": "Záložky", "layout-mode-switched": "Režim rozvržení přepnutý na jednoduchý z důvodu nedostatku místa pro vykreslení dvojitého rozvržení", - "user-preferences-updated": "Aktualizace uživatelských předvoleb" + "create-new": "{{reader-settings.create-new}}", + "update-parent": "{{reader-settings.update-parent}}", + "loading": "{{reader-settings.loading}}", + "create-new-tooltip": "{{reader-settings.create-new-tooltip}}", + "reading-profile-updated": "Profil čtení aktualizován", + "reading-profile-promoted": "Profil čtení propagován" }, "metadata-filter": { "filter-title": "{{common.filter}}", @@ -1744,8 +1711,8 @@ "cover-image-tab": "{{tabs.cover-tab}}", "tasks-tab": "{{tabs.tasks-tab}}", "info-tab": "{{tabs.info-tab}}", - "pages-label": "{{edit-chapter-modal.pages-count}}", - "words-label": "{{edit-chapter-modal.length-title}}", + "pages-label": "{{edit-chapter-modal.pages-label}}", + "words-label": "{{edit-chapter-modal.words-label}}", "pages-count": "{{edit-chapter-modal.pages-count}}", "words-count": "{{edit-chapter-modal.words-count}}", "reading-time-label": "{{edit-chapter-modal.reading-time-label}}", @@ -1898,7 +1865,7 @@ "reading-lists": "{{side-nav.reading-lists}}", "bookmarks": "{{side-nav.bookmarks}}", "all-series": "{{side-nav.all-series}}", - "browse-authors": "{{side-nav.browse-authors}}" + "browse-authors": "{{side-nav.browse-people}}" }, "filter-field-pipe": { "age-rating": "{{metadata-fields.age-rating-title}}", @@ -2004,7 +1971,13 @@ "reorder": "Přeuspořádat", "rename": "Přejmenovat", "rename-tooltip": "Přejmenovat inteligentní filtr", - "merge": "Sloučit" + "merge": "Sloučit", + "reading-profiles": "Profily čtení", + "set-reading-profile": "Nastavit profil čtení", + "set-reading-profile-tooltip": "Připojit profil čtení k této knihovně", + "clear-reading-profile": "Vymazat profil čtení", + "clear-reading-profile-tooltip": "Vymazat profil čtení pro tuto knihovnu", + "cleared-profile": "Vyčištěný profil čtení" }, "changelog-update-item": { "changed": "Změněno", @@ -2093,11 +2066,6 @@ "epub": "Epub", "image": "Obrázek" }, - "browse-authors": { - "title": "Procházet autory a spisovatele", - "author-count": "{{num}} lidí", - "cover-image-description": "{{edit-series-modal.cover-image-description}}" - }, "carousel-reel": { "prev-items": "Předchozí položky", "next-items": "Další položky" @@ -2121,7 +2089,9 @@ "dont-match-status-label": "{{dont-match-label}}", "match-alt": "Spáruj {{seriesName}}", "actions-header": "Akce", - "library-name-header": "Knihovna" + "library-name-header": "Knihovna", + "library-type": "Typ knihovny", + "matched-state-label": "Stav shody" }, "match-series-result-item": { "volume-count": "{{server-stats.volume-count}}", @@ -2154,7 +2124,10 @@ "last-modified": "Naposledy upraveno", "time-to-read": "Čas k přečtení", "read-progress": "Naposledy čteno", - "average-rating": "Průměrné hodnocení" + "average-rating": "Průměrné hodnocení", + "person-name": "Jméno", + "person-series-count": "Počet sérií", + "person-chapter-count": "Počet kapitol" }, "edit-person-modal": { "name-label": "{{edit-series-modal.name-label}}", @@ -2461,7 +2434,9 @@ "webtoon-override": "Přepnutí do režimu Webtoon kvůli obrázkům představujícím webtoon.", "scrobble-gen-init": "Vytvořena úloha pro generování událostí scrobble z historie čtení, hodnocení v minulosti a jejich synchronizaci s připojenými službami.", "confirm-delete-multiple-volumes": "Jste si jisti, že chcete odstranit {{count}} svazků? Soubory na disku se tím nezmění.", - "series-added-want-to-read": "Série přidána ze seznamu Chci číst" + "series-added-want-to-read": "Série přidána ze seznamu Chci číst", + "series-bound-to-reading-profile": "Série vázaná na čtenářský profil {{name}}", + "library-bound-to-reading-profile": "Knihovna vázaná na čtenářský profil {{name}}" }, "preferences": { "split-right-to-left": "Rozdělit zprava doleva", @@ -2543,7 +2518,8 @@ "submit": "Odeslat", "email": "E-mail", "chapter-count": "{{num}} Kapitol", - "no-data": "Žádná data" + "no-data": "Žádná data", + "issue-count": "{{num}} Vydání" }, "entity-type": { "logs": "záznamy", @@ -2582,7 +2558,8 @@ "confirm": "Potvrdit", "info": "Informace", "cancel": "{{common.cancel}}", - "ok": "Ok" + "ok": "Ok", + "prompt": "Otázka" }, "manage-metadata-settings": { "enabled-tooltip": "Povolte systému Kavita stahovat metadata a zapisovat do jeho databáze.", @@ -2690,5 +2667,146 @@ "merge-warning": "Pokud budete pokračovat, vybraná osoba bude odstraněna. Jméno vybrané osoby bude přidáno jako alias a všechny její role budou převedeny.", "alias-title": "Nové aliasy", "known-for-title": "Známý pro" + }, + "manage-reading-profiles": { + "extra-tip": "Přiřaďte profily čtení pomocí akčního menu v sériích a knihovnách nebo hromadně. Při změně nastavení v čtečce se vytvoří skrytý profil, který si pamatuje vaše volby pro danou sérii (ne pro soubory PDF). Tento profil se odstraní, když přiřadíte k sérii jeden ze svých vlastních profilů čtení. Více informací naleznete na stránce", + "reading-direction-tooltip": "Směr kliknutí pro přechod na další stránku. Zprava doleva znamená, že kliknete na levou stranu obrazovky, abyste přejeli na další stránku.", + "layout-mode-book-tooltip": "Jak by měl být obsah rozložen. Posouvání je stejné jako u knihy. 1 nebo 2 sloupce se přizpůsobí výšce zařízení a na každou stránku se vejde 1 nebo 2 sloupce textu", + "description": "Ne všechny vaše série lze číst stejným způsobem, proto nastavte pro každou knihovnu nebo sérii samostatné profily čtení, aby se vám co nejlépe dařilo navázat na předchozí díly série.", + "reading-direction-book-tooltip": "Směr kliknutí pro přechod na další stránku. Zprava doleva znamená, že kliknete na levou stranu obrazovky, abyste přejeli na další stránku.", + "font-family-tooltip": "Rodina písem, která se má načíst. Výchozí nastavení načte výchozí písmo knihy", + "pdf-theme-tooltip": "Barevné motivy čtečky", + "reading-profile-series-settings-title": "Série", + "reading-direction-book-label": "Směr čtení", + "margin-book-tooltip": "Jak velký odstup na každé straně obrazovky. Na mobilních zařízeních se tato hodnota bez ohledu na nastavení nastaví na 0.", + "allow-auto-webtoon-reader-tooltip": "Pokud stránky vypadají jako webtoon, přepněte do režimu Webtoon čtečky. Mohou se vyskytnout některé falešně pozitivní výsledky.", + "reset": "{{common.reset}}", + "tap-to-paginate-tooltip": "Měly by strany obrazovky čtečky knih umožňovat klepnutí pro přechod na předchozí/následující stránku", + "delete": "{{common.delete}}", + "profiles-title": "Vaše profily čtení", + "default-profile": "Výchozí", + "add": "{{common.add}}", + "add-tooltip": "Váš nový profil bude uložen po provedení změn", + "make-default": "Nastavit jako výchozí", + "no-selected": "Není vybrán žádný profil", + "confirm": "Opravdu chcete smazat profil čtení {{name}}?", + "selection-tip": "Vyberte profil ze seznamu nebo vytvořte nový v pravém horním rohu", + "image-reader-settings-title": "Čtečka obrázků", + "reading-direction-label": "Směr čtení", + "scaling-option-label": "Možnosti škálování", + "scaling-option-tooltip": "Jak přizpůsobit velikost obrázku vaší obrazovce.", + "page-splitting-label": "Rozdělení stránky", + "page-splitting-tooltip": "Jak rozdělit obrázek v plné šířce (tj. kombinace levého a pravého obrázku)", + "reading-mode-label": "Režim čtení", + "reading-mode-tooltip": "Změňte čtečku tak, aby stránkovala vertikálně, horizontálně nebo měla nekonečné posouvání", + "layout-mode-label": "Režim rozvržení", + "layout-mode-tooltip": "Zobrazit jeden obrázek na obrazovce nebo dva obrázky vedle sebe", + "background-color-label": "Barva pozadí", + "background-color-tooltip": "Barva pozadí čtečky obrázků", + "auto-close-menu-label": "Automatické zavření nabídky", + "auto-close-menu-tooltip": "Mělo by se menu automaticky zavřít", + "show-screen-hints-label": "Zobrazit nápovědu na obrazovce", + "show-screen-hints-tooltip": "Zobrazit překryvnou vrstvu, která pomůže pochopit oblast a směr stránkování", + "emulate-comic-book-label": "Napodobit komiks", + "emulate-comic-book-tooltip": "Použije stínový efekt, který napodobuje čtení z knihy", + "swipe-to-paginate-label": "Přejetím prstem přejděte na stránku", + "swipe-to-paginate-tooltip": "Má přejetí prstem po obrazovce vyvolat přechod na další nebo předchozí stránku", + "allow-auto-webtoon-reader-label": "Režim automatického čtení webtoonů", + "width-override-label": "{{manga-reader.width-override-label}}", + "width-override-tooltip": "Přepsat šířku obrázků v čtečce", + "book-reader-settings-title": "Čtečka knih", + "tap-to-paginate-label": "Klepnutím na stránku posouvat", + "immersive-mode-label": "Imersivní režim", + "immersive-mode-tooltip": "Tím se skryje nabídka za kliknutím na dokument čtečky a zapne se listování klepnutím", + "font-family-label": "Rodina písem", + "writing-style-label": "Styl psaní", + "writing-style-tooltip": "Změní směr textu. Horizontální směr je zleva doprava, vertikální směr je shora dolů.", + "layout-mode-book-label": "Režim rozvržení", + "color-theme-book-label": "Barevné téma", + "color-theme-book-tooltip": "Jaké barevné schéma použít pro obsah čtečky knih a nabídku", + "font-size-book-label": "Velikost písma", + "font-size-book-tooltip": "Procento zmenšení, které se použije na písmo v knize", + "line-height-book-label": "Řádkování", + "line-height-book-tooltip": "Jak velký je odstup mezi řádky v knize", + "margin-book-label": "Odsazení", + "pdf-reader-settings-title": "Čtečka PDF", + "pdf-scroll-mode-label": "Režim posouvání", + "pdf-scroll-mode-tooltip": "Jak procházíte stránky. Vertikálně/horizontálně a klepnutím na stránku (bez posouvání)", + "pdf-spread-mode-label": "Režim rozložení", + "pdf-spread-mode-tooltip": "Jak by měly být stránky rozvrženy. Jednoduché nebo dvojité (liché/sudé)", + "pdf-theme-label": "Motiv", + "reading-profile-library-settings-title": "Knihovna", + "wiki-title": "wiki", + "disable-width-override-label": "Zakázat přepsání šířky", + "disable-width-override-tooltip": "Zabraňte tomu, aby se přepsání šířky projevilo, pokud je vaše obrazovka alespoň v konfigurovaném bodě zlomu nebo menší" + }, + "bulk-set-reading-profile-modal": { + "title": "Nastavit profil čtení", + "close": "{{common.close}}", + "filter-label": "{{common.filter}}", + "clear": "{{common.clear}}", + "no-data": "Zatím nebyly vytvořeny žádné sbírky", + "loading": "{{common.loading}}", + "create": "{{common.create}}", + "bound": "Vázaný" + }, + "browse-people": { + "title": "Procházet Osoby", + "author-count": "{{num}} osob", + "issue-count": "{{common.issue-count}}", + "roles-label": "Role", + "sort-label": "Třídit", + "name-label": "Jméno", + "issue-count-label": "Počet vydání", + "series-count-label": "Počet sérií", + "cover-image-description": "{{edit-series-modal.cover-image-description}}", + "series-count": "{{common.series-count}}" + }, + "browse-genres": { + "title": "Procházet žánry", + "genre-count": "{{num}} žánrů", + "issue-count": "{{common.issue-count}}", + "series-count": "{{common.series-count}}" + }, + "browse-tags": { + "title": "Procházet štítky", + "genre-count": "{{num}} štítků", + "issue-count": "{{common.issue-count}}", + "series-count": "{{common.series-count}}" + }, + "browse-title-pipe": { + "publication-status": "{{value}} prací", + "age-rating": "Hodnoceno {{value}}", + "tag": "Má štítek {{value}}", + "translator": "Přeložil {{value}}", + "character": "Má postavu {{value}}", + "editor": "Úpravy provedl {{value}}", + "artist": "Nakreslil {{value}}", + "letterer": "Vepsal text {{value}}", + "inker": "Inkoustoval {{value}}", + "genre": "Má žánr {{value}}", + "library": "V rámci knihovny {{value}}", + "release-year": "Vydáno v roce {{value}}", + "imprint": "Otisk {{value}}", + "team": "Tým {{value}}", + "location": "V místě {{value}}", + "user-rating": "Hodnoceno {{value}} hvězdičkami", + "format": "Formát {{value}}", + "colorist": "Vybarvil {{value}}", + "publisher": "Vydáno {{value}}", + "writer": "Napsal {{value}}", + "penciller": "Nakreslil tužkou {{hodnota}}" + }, + "generic-filter-field-pipe": { + "person-name": "Jméno", + "person-series-count": "Počet sérií", + "person-role": "Role", + "person-chapter-count": "Počet kapitol" + }, + "breakpoint-pipe": { + "never": "Nikdy", + "mobile": "Mobilní", + "tablet": "Tablet", + "desktop": "Stolní počítač" } } diff --git a/UI/Web/src/assets/langs/de.json b/UI/Web/src/assets/langs/de.json index 376e33a7e..77663373a 100644 --- a/UI/Web/src/assets/langs/de.json +++ b/UI/Web/src/assets/langs/de.json @@ -72,7 +72,8 @@ "your-review": "Dies ist deine Rezension", "external-review": "Externe Rezension", "local-review": "Lokale Rezension", - "rating-percentage": "Bewertung {{r}}%" + "rating-percentage": "Bewertung {{r}}%", + "critic": "Kritiker" }, "want-to-read": { "title": "Favoriten", @@ -106,55 +107,6 @@ "collapse-series-relationships-tooltip": "Sollte Kavita Serien zeigen, die keine Verbindungen haben, oder ist das Elternteil/Prequel", "share-series-reviews-label": "Bewertungen von Serien teilen", "share-series-reviews-tooltip": "Soll Kavita deine Rezensionen zu Serien für andere Nutzer aufnehmen", - "image-reader-settings-title": "Bildleser", - "reading-direction-label": "Leserichtung", - "reading-direction-tooltip": "Richtung, in die geklicken werden müssen, um zur nächsten Seite zu gelangen. Von rechts nach links bedeutet, dass man auf die linke Seite des Bildschirms klickt, um zur nächsten Seite zu gelangen.", - "scaling-option-label": "Skalierungsoptionen", - "scaling-option-tooltip": "So wird das Bild auf den Bildschirm skaliert.", - "page-splitting-label": "Seitenaufteilung", - "page-splitting-tooltip": "Wie wird ein Bild in voller Breite geteilt (d.h. linkes und rechtes Bild werden kombiniert)", - "reading-mode-label": "Lesemodus", - "reading-mode-tooltip": "Ändern Sie den Reader so, dass er vertikal oder horizontal paginiert oder einen unendlichen Bildlauf hat", - "layout-mode-label": "Layoutmodus", - "layout-mode-tooltip": "Rendert ein einzelnes Bild auf dem Bildschirm oder zwei nebeneinander liegende Bilder", - "background-color-label": "Hintergrundfarbe", - "background-color-tooltip": "Hintergrundfarbe des Bildlesers", - "auto-close-menu-label": "Menü Automatisch schließen", - "auto-close-menu-tooltip": "Soll sich das Menü automatisch schließen", - "show-screen-hints-label": "Bildschirmtipps anzeigen", - "show-screen-hints-tooltip": "Einblenden eines Overlays zum besseren Verständnis des Seitenbereichs und der Seitenausrichtung", - "emulate-comic-book-label": "Comicbuch nachbilden", - "emulate-comic-book-tooltip": "Wendet einen Schatteneffekt an, um das Lesen aus einem Buch zu simulieren", - "swipe-to-paginate-label": "Zum Umblättern wischen", - "swipe-to-paginate-tooltip": "Soll die nächste oder vorherige Seite durch Wischen über den Bildschirm aufgerufen werden", - "book-reader-settings-title": "Buch-Reader", - "tap-to-paginate-label": "Tippen zum Umblättern", - "tap-to-paginate-tooltip": "Sollten die Seiten des Buchlesebildschirms ein Antippen erlauben, um zur vorherigen/nächsten Seite zu gelangen", - "immersive-mode-label": "Immersiver Modus", - "immersive-mode-tooltip": "Das Menü wird nach einem Klick auf das Reader-Dokument ausgeblendet und das Tippen zum Umblättern eingeschaltet", - "reading-direction-book-label": "Leserichtung", - "reading-direction-book-tooltip": "Richtung, in die zu klicken ist, um zur nächsten Seite zu gelangen. Von rechts nach links bedeutet, dass man auf die linke Seite des Bildschirms klickt, um zur nächsten Seite zu gelangen.", - "font-family-label": "Schriftart", - "font-family-tooltip": "Schriftart, die geladen werden soll. Standard lädt die Standardschriftart des Buches", - "writing-style-label": "Schreibstil", - "writing-style-tooltip": "Ändert die Richtung des Textes. Horizontal ist von links nach rechts, vertikal von oben nach unten.", - "layout-mode-book-label": "Layoutmodus", - "layout-mode-book-tooltip": "Wie soll der Inhalt aufgebaut sein. Scrollen ist so, wie das Buch es verpackt. 1- oder 2-spaltig entspricht der Höhe des Geräts und fasst 1 oder 2 Spalten Text pro Seite", - "color-theme-book-label": "Farbe Motiv", - "color-theme-book-tooltip": "Welches Farb Motiv soll für den Inhalt und das Menü des Buchlesers verwendet werden", - "font-size-book-label": "Schriftgröße", - "font-size-book-tooltip": "Prozentsatz der Skalierung, der auf die Schrift im Buch anzuwenden ist", - "line-height-book-label": "Zeilenabstände", - "line-height-book-tooltip": "Wie viel Abstand zwischen den Zeilen im Buch", - "margin-book-label": "Seitenrand", - "margin-book-tooltip": "Wie viel Abstand auf jeder Seite des Bildschirms. Auf mobilen Geräten wird dieser Wert unabhängig von dieser Einstellung auf 0 gesetzt.", - "pdf-reader-settings-title": "PDF Viewer", - "pdf-scroll-mode-label": "Scroll Modus", - "pdf-scroll-mode-tooltip": "So Scrollen sie durch Seiten. Vertikal/Horizontal und Tippen zum Paginieren (kein Scrollen)", - "pdf-spread-mode-label": "Ausbreitungs Modus", - "pdf-spread-mode-tooltip": "Wie sollten die Seiten angeordnet werden. Einzeln oder Doppelt (Gerade/Ungrade)", - "pdf-theme-label": "Thema", - "pdf-theme-tooltip": "Farbschema des Lesers", "clients-opds-alert": "OPDS ist auf diesem Server nicht aktiviert. Tachiyomi-Benutzer sind davon nicht betroffen.", "clients-opds-description": "Alle Clients von Drittanbietern verwenden entweder den API-Schlüssel oder die unten stehende Verbindungs-URL. Diese sind wie Passwörter, vertraulich zu behandeln.", "clients-api-key-tooltip": "Der API-Schlüssel ist wie ein Passwort. Wenn Sie ihn zurücksetzen, werden alle bestehenden Kunden ungültig.", @@ -167,9 +119,7 @@ "want-to-read-sync-label": "Willst du lesen synchronisieren", "kavitaplus-settings-title": "Kavita+", "want-to-read-sync-tooltip": "Erlaube Kavita, basierend auf AniList und MAL-Serien Elemente zu deiner „Want to Read“-Liste in „Pending readlist“ hinzuzufügen", - "anilist-scrobbling-tooltip": "Erlaube Kavita, ihren Lesefortschritt und ihre Bewertungen zu AniList zu scrobbeln (Einweg-Synchronisierung)", - "allow-auto-webtoon-reader-tooltip": "Wechsle in den Webtoon-Lesemodus, wenn die Seiten wie ein Webtoon aussehen. Es kann zu einer falschen Positivrate kommen.", - "allow-auto-webtoon-reader-label": "Automatischer Webtoon-Lesemodus" + "anilist-scrobbling-tooltip": "Erlaube Kavita, ihren Lesefortschritt und ihre Bewertungen zu AniList zu scrobbeln (Einweg-Synchronisierung)" }, "user-holds": { "title": "Scrobble pausiert", @@ -679,7 +629,10 @@ "incognito-mode-label": "Unsichtbarkeits Modus", "next": "Nächste", "previous": "Vorherige", - "go-to-page-prompt": "Es gibt {{totalPages}} Seiten. Auf welche Seite möchten sie gehen?" + "go-to-page-prompt": "Es gibt {{totalPages}} Seiten. Auf welche Seite möchten sie gehen?", + "go-to-first-page": "Zur ersten Seite gehen", + "go-to-section": "Zum Abschnitt gehen", + "go-to-section-prompt": "Es gibt {{totalSections}} Abschnitte. Zu welchem Abschnitt möchten Sie springen?" }, "personal-table-of-contents": { "no-data": "Noch nichts mit Lesezeichen versehen", @@ -727,7 +680,7 @@ "series-detail": { "page-settings-title": "Seiteneinstellungen", "close": "{{common.close}}", - "layout-mode-label": "{{user-preferences.layout-mode-book-label}}", + "layout-mode-label": "{{manage-reading-profiles.layout-mode-book-label}}", "layout-mode-option-card": "Karte", "layout-mode-option-list": "Liste", "continue-from": "Fortsetzen: {{title}}", @@ -849,7 +802,6 @@ "back": "Zurück", "more": "Mehr", "customize": "{{settings.customize}}", - "browse-authors": "Autoren durchsuchen", "edit": "{{common.edit}}", "cancel-edit": "Schließen Neu ordnen" }, @@ -914,10 +866,8 @@ }, "reader-settings": { "general-settings-title": "Allgemeine Einstellungen", - "font-family-label": "{{user-preferences.font-family-label}}", - "font-size-label": "{{user-preferences.font-size-book-label}}", - "line-spacing-label": "{{user-preferences.line-height-book-label}}", - "margin-label": "{{user-preferences.margin-book-label}}", + "line-spacing-label": "{{manage-reading-profiles.line-height-book-label}}", + "margin-label": "{{manage-reading-profiles.margin-book-label}}", "reset-to-defaults": "Auf Standardwerte zurücksetzen", "reader-settings-title": "Reader-Einstellungen", "reading-direction-label": "{{user-preferences.reading-direction-book-label}}", @@ -946,7 +896,15 @@ "theme-dark": "Dunkel", "theme-black": "Schwarz", "theme-white": "Weiß", - "theme-paper": "Papier" + "theme-paper": "Papier", + "line-spacing-max-label": "2.5x", + "create-new-tooltip": "Erstellen Sie aus Ihrem aktuellen impliziten Profil ein neues verwaltbares Profil", + "update-parent": "Unter {{name}} speichern", + "loading": "laden", + "create-new": "Neues Profil von impliziert", + "reading-profile-updated": "Leseprofil aktualisiert", + "reading-profile-promoted": "Leseprofil beworben", + "line-spacing-min-label": "1x" }, "table-of-contents": { "no-data": "Dieses Buch hat kein Inhaltsverzeichnis in den Metadaten oder eine toc-Datei" @@ -1387,7 +1345,8 @@ "admin-matched-metadata": "Passende Metadaten", "admin-manage-tokens": "Verwalte Benutzertoken", "scrobble-holds": "Scrobble-Holds", - "admin-metadata": "Verwalte Metadaten" + "admin-metadata": "Verwalte Metadaten", + "reading-profiles": "Leseprofile" }, "collection-detail": { "no-data": "Es sind keine Artikel vorhanden. Versuchen Sie, eine Serie hinzuzufügen.", @@ -1511,7 +1470,8 @@ "logout": "Abmelden", "all-filters": "Intelligente Filter", "nav-link-header": "Navigation Optionen", - "close": "{{common.close}}" + "close": "{{common.close}}", + "person-aka-status": "Entspricht einem Alias" }, "promoted-icon": { "promoted": "{{common.promoted}}" @@ -1622,7 +1582,6 @@ }, "manga-reader": { "back": "Zurück", - "save-globally": "Global speichern", "incognito-alt": "Der Inkognito-Modus ist eingeschaltet. Zum Ausschalten schalten Sie um.", "incognito-title": "Inkognito-Modus:", "shortcuts-menu-alt": "Tastaturkurzbefehle Modal", @@ -1644,9 +1603,7 @@ "height": "Höhe", "width": "Breite", "width-override-label": "Breite Override", - "off": "Aus", "original": "Original", - "auto-close-menu-label": "{{user-preferences.auto-close-menu-label}}", "swipe-enabled-label": "Swipe aktiviert", "enable-comic-book-label": "Comicbuch nachahmen", "brightness-label": "Helligkeit", @@ -1657,9 +1614,9 @@ "layout-mode-switched": "Layoutmodus auf Einfach umgestellt, da nicht genügend Platz zum Rendern des Doppellayouts vorhanden ist", "no-next-chapter": "Kein nächstes Kapitel", "no-prev-chapter": "Kein vorheriges Kapitel", - "user-preferences-updated": "Benutzereinstellungen aktualisiert", - "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", - "series-progress": "Fortschritt der Serie: {{percentage}}" + "series-progress": "Fortschritt der Serie: {{percentage}}", + "reading-profile-updated": "Leseprofil aktualisiert", + "reading-profile-promoted": "Leseprofil beworben" }, "metadata-filter": { "filter-title": "{{common.filter}}", @@ -1853,8 +1810,6 @@ "cover-image-tab": "{{tabs.cover-tab}}", "tasks-tab": "{{tabs.tasks-tab}}", "info-tab": "{{tabs.info-tab}}", - "pages-label": "{{edit-chapter-modal.pages-count}}", - "words-label": "{{edit-chapter-modal.length-title}}", "pages-count": "{{edit-chapter-modal.pages-count}}", "words-count": "{{edit-chapter-modal.words-count}}", "reading-time-label": "{{edit-chapter-modal.reading-time-label}}", @@ -2249,7 +2204,11 @@ "bulk-delete-libraries": "Möchten Sie wirklich {{count}} Bibliotheken löschen?", "match-success": "Reihenfolge korrekt zugeordnet", "webtoon-override": "Wechsel in den Webtoon-Modus, da die Bilder einen Webtoon darstellen.", - "scrobble-gen-init": "Hat einen Auftrag in die Warteschlange gestellt, um Scrobble-Ereignisse aus dem bisherigen Leseverlauf und Bewertungen zu generieren und diese mit verbundenen Diensten zu synchronisieren." + "scrobble-gen-init": "Hat einen Auftrag in die Warteschlange gestellt, um Scrobble-Ereignisse aus dem bisherigen Leseverlauf und Bewertungen zu generieren und diese mit verbundenen Diensten zu synchronisieren.", + "confirm-delete-multiple-volumes": "Sind sie sicher, dass Sie {{count}} Bände löschen wollen? Die Dateien auf der Festplatte werden dadurch nicht verändert.", + "series-bound-to-reading-profile": "Serie gebunden an Leseprofil {{name}}", + "library-bound-to-reading-profile": "Bibliothek, verknüpft mit Leseprofil {{name}}", + "series-added-want-to-read": "Serie aus der Liste „Möchte lesen“ hinzugefügt" }, "read-time-pipe": { "less-than-hour": "<1 Stunde", @@ -2324,7 +2283,14 @@ "match-tooltip": "Spielserie mit Kavita+ manuell abgleichen", "reorder": "Nachbestellung", "rename": "Umbenennen", - "rename-tooltip": "Smart Filter umbenennen" + "rename-tooltip": "Smart Filter umbenennen", + "reading-profiles": "Leseprofile", + "set-reading-profile": "Leseprofil festlegen", + "set-reading-profile-tooltip": "Ein Leseprofil mit dieser Bibliothek verknüpfen", + "clear-reading-profile": "Leseprofil löschen", + "clear-reading-profile-tooltip": "Leseprofil für diese Bibliothek löschen", + "cleared-profile": "Lese-Profil gelöscht", + "merge": "Zusammenführen" }, "preferences": { "left-to-right": "Links nach rechts", @@ -2450,7 +2416,8 @@ "confirm": "Bestätigen", "info": "Info", "ok": "Ok", - "cancel": "{{common.cancel}}" + "cancel": "{{common.cancel}}", + "prompt": "Frage" }, "person-detail": { "all-roles": "Funktionen", @@ -2458,12 +2425,9 @@ "individual-role-title": "Als eine {{role}}", "browse-person-title": "Alle Werke von {{name}}", "browse-person-by-role-title": "Alle Werke von {{name}} als {{role}}", - "anilist-url": "{{edit-person-modal.anilist-tooltip}}" - }, - "browse-authors": { - "author-count": "{{num}} Personen", - "title": "Autoren & Schriftsteller durchsuchen", - "cover-image-description": "{{edit-series-modal.cover-image-description}}" + "anilist-url": "{{edit-person-modal.anilist-tooltip}}", + "aka-title": "Auch bekannt als ", + "no-info": "Keine Informationen zu dieser Person" }, "edit-person-modal": { "title": "Details zu {{personName}}", @@ -2486,7 +2450,11 @@ "name-label": "{{edit-series-modal.name-label}}", "required-field": "{{validations.required-field}}", "cover-image-description": "{{edit-series-modal.cover-image-description}}", - "save": "{{common.save}}" + "save": "{{common.save}}", + "aliases-tooltip": "Wenn eine Serie mit einem Alias einer Person versehen ist, wird diese Person zugewiesen, anstatt eine neue Person anzulegen. Wenn Sie einen Alias löschen, müssen Sie die Serie erneut scannen, damit die Änderung übernommen wird.", + "aliases-tab": "Aliases", + "aliases-label": "Aliase bearbeiten", + "alias-overlap": "Dieser Alias verweist bereits auf eine andere Person oder ist der Name dieser Person. Bitte erwäge, sie zusammenzuführen." }, "changelog-update-item": { "download": "Download", @@ -2511,7 +2479,7 @@ }, "match-series-modal": { "description": "Wähle einen Treffer aus, um die Metadaten von Kavita+ neu zu verknüpfen und Scrobble-Ereignisse neu zu generieren. „Nicht übereinstimmend“ kann verwendet werden, um Kavita daran zu hindern, Metadaten abzugleichen und zu scrobbeln.", - "query-tooltip": "Gib den Namen der Serie ein, AniList/MyAnimeList-URL. URLs werden direkt aufgerufen.", + "query-tooltip": "Geben Sie den Seriennamen und die URL von AniList/MyAnimeList/ComicBookRoundup ein. Die URLs werden direkt nachgeschlagen.", "no-results": "Es wurde keine Übereinstimmung gefunden. Versuche, die URL eines unterstützten Anbieters hinzuzufügen und versuche es erneut.", "query-label": "Abfrage", "dont-match-label": "Keine Übereinstimmung", @@ -2610,7 +2578,16 @@ "enable-cover-image-tooltip": "Erlaubt Kavita, das Titelbild für die Serie zu schreiben", "enable-cover-image-label": "Cover Bild", "overrides-label": "Überschreibungen", - "overrides-description": "Erlaube Kavita, gesperrte Felder zu überschreiben." + "overrides-description": "Erlaube Kavita, gesperrte Felder zu überschreiben.", + "enable-chapter-release-date-tooltip": "Veröffentlichungsdatum des Kapitels/der Ausgabe darf angegeben werden", + "enable-chapter-publisher-label": "Publisher", + "enable-chapter-release-date-label": "Veröffentlichungsdatum", + "enable-chapter-title-label": "Titel", + "enable-chapter-title-tooltip": "Titel des Kapitels/der Ausgabe darf geschrieben werden", + "chapter-header": "Kapitel Felder", + "enable-chapter-publisher-tooltip": "Zulassen, dass der Herausgeber des Kapitels/der Ausgabe geschrieben wird", + "enable-chapter-cover-label": "Kapitel-Cover", + "enable-chapter-cover-tooltip": "Cover von Kapitel/Ausgabe festlegen" }, "metadata-setting-field-pipe": { "covers": "Covers", @@ -2621,7 +2598,12 @@ "summary": "{{filter-field-pipe.summary}}", "publication-status": "{{edit-series-modal.publication-status-title}}", "tags": "{{metadata-fields.tags-title}}", - "localized-name": "{{edit-series-modal.localized-name-label}}" + "localized-name": "{{edit-series-modal.localized-name-label}}", + "chapter-release-date": "Veröffentlichungsdatum (Kapitel)", + "chapter-summary": "Zusammenfassung (Kapitel)", + "chapter-covers": "Covers (Kapitel)", + "chapter-title": "Titel (Kapitel)", + "chapter-publisher": "{{person-role-pipe.publisher}} (Kapitel)" }, "role-localized-pipe": { "bookmark": "Lesezeichen", @@ -2639,5 +2621,90 @@ "critical": "Kritisch", "information": "Information", "warning": "Warnung" + }, + "reviews": { + "user-reviews-local": "Lokale Bewertungen", + "user-reviews-plus": "Externe Rezension" + }, + "review-modal": { + "review-label": "Rezension", + "title": "Rezension Bearbeiten", + "min-length": "Die Rezension muss mindestens {{count}} Zeichen lang sein" + }, + "manage-reading-profiles": { + "description": "Nicht alle Ihre Serien können auf dieselbe Weise gelesen werden. Richten Sie daher für jede Bibliothek oder Serie separate Leseprofile ein, damit Sie so nahtlos wie möglich zu Ihrer Serie zurückkehren können.", + "extra-tip": "Weisen Sie Leseprofile über das Aktionsmenü in Serien und Bibliotheken oder in großen Mengen zu. Wenn Sie die Einstellungen in einem Reader ändern, wird ein verstecktes Profil erstellt, das Ihre Auswahl für diese Serie speichert (nicht für PDFs). Dieses Profil wird entfernt, wenn Sie der Serie eines Ihrer eigenen Leseprofile zuweisen oder aktualisieren.", + "tap-to-paginate-tooltip": "Sollten die Seiten des E-Book-Reader-Bildschirms durch Antippen umblätterbar sein vorherige/nächste Seite", + "show-screen-hints-tooltip": "Zeige eine Überlagerung an, um den Bereich und die Richtung der Paginierung besser zu verstehen", + "font-size-book-tooltip": "Prozentuale Skalierung für die Schriftart im Buch", + "pdf-reader-settings-title": "PDF Reader", + "line-height-book-label": "Zeilenabstand", + "layout-mode-book-tooltip": "Wie Inhalte angeordnet werden sollen. Der Bildlauf entspricht dem Aufbau des Buches. 1 oder 2 Spalten passen zur Höhe des Geräts und es passen 1 oder 2 Spalten Text pro Seite", + "profiles-title": "Ihre Leseprofil", + "default-profile": "Standard", + "add-tooltip": "Ihr neues Profil wird nach der Änderung gespeichert", + "make-default": "Als Standard festlegen", + "no-selected": "Kein Profil ausgewählt", + "confirm": "Möchten Sie das Leseprofil {{name}} wirklich löschen?", + "selection-tip": "Wählen Sie ein Profil aus der Liste aus oder erstellen Sie oben rechts ein neues Profil", + "image-reader-settings-title": "Bild Reader", + "reading-direction-label": "Leserichtung", + "reading-direction-tooltip": "Klicken Sie hier, um zur nächsten Seite zu gelangen. Von rechts nach links bedeutet, dass Sie auf die linke Seite des Bildschirms klicken, um zur nächsten Seite zu gelangen.", + "scaling-option-label": "Skalierungsoptionen", + "scaling-option-tooltip": "So skalieren Sie das Bild an Ihren Bildschirm.", + "page-splitting-label": "Seitenaufteilung", + "page-splitting-tooltip": "So teilen Sie ein Bild in voller Breite (d. h. beide Bilder, links und rechts, werden kombiniert)", + "reading-mode-label": "Lesemodus", + "reading-mode-tooltip": "Ändern Sie den Reader, um vertikal, horizontal oder unbegrenzt zu scrollen", + "layout-mode-label": "Layout-Modus", + "layout-mode-tooltip": "Ein einzelnes Bild auf dem Bildschirm oder zwei Bilder nebeneinander rendern", + "background-color-label": "Hintergrundfarbe", + "background-color-tooltip": "Hintergrundfarbe des Bildlesers", + "auto-close-menu-label": "Menü automatisch schließen", + "auto-close-menu-tooltip": "Soll das Menü automatisch schließen", + "show-screen-hints-label": "Bildschirmhinweise anzeigen", + "emulate-comic-book-label": "Comicbuch nachahmen", + "emulate-comic-book-tooltip": "Wendet einen Schatteneffekt an, um das Lesen aus einem Buch zu imitieren", + "swipe-to-paginate-label": "Zum Blättern wischen", + "swipe-to-paginate-tooltip": "Soll durch Wischen auf dem Bildschirm die nächste oder vorherige Seite aufgerufen werden", + "allow-auto-webtoon-reader-label": "Automatischer Webtoon-Lesemodus", + "allow-auto-webtoon-reader-tooltip": "Wechseln Sie in den Webtoon-Reader-Modus, wenn die Seiten wie ein Webtoon aussehen. Es können einige Fehlalarme auftreten.", + "width-override-tooltip": "Breite von Bildern im Reader überschreiben", + "book-reader-settings-title": "Buch Reader", + "tap-to-paginate-label": "Zum Blättern antippen", + "immersive-mode-label": "Immersiver Modus", + "reading-direction-book-label": "Leserichtung", + "reading-direction-book-tooltip": "Klicken Sie hier, um zur nächsten Seite zu gelangen. Von rechts nach links bedeutet, dass Sie auf die linke Seite des Bildschirms klicken, um zur nächsten Seite zu gelangen.", + "immersive-mode-tooltip": "Dadurch wird das Menü hinter einem Klick auf das Lesedokument ausgeblendet und die Seitenumblätterung per Fingertipp aktiviert.", + "font-family-label": "Schriftfamilie", + "font-family-tooltip": "Zu ladende Schriftfamilie. Standardmäßig wird die Standardschriftart des Buches geladen", + "writing-style-label": "Schreibstil", + "writing-style-tooltip": "Ändert die Ausrichtung des Textes. Horizontal bedeutet von links nach rechts, vertikal bedeutet von oben nach unten.", + "layout-mode-book-label": "Layout-Modus", + "color-theme-book-label": "Farbschema", + "color-theme-book-tooltip": "Welches Farbschema soll für den Inhalt und das Menü des Buchlesers verwendet werden", + "font-size-book-label": "Schriftgröße", + "line-height-book-tooltip": "Wie viel Abstand zwischen den Zeilen des Buches", + "margin-book-label": "Rand", + "margin-book-tooltip": "Wie viel Abstand soll auf jeder Seite des Bildschirms sein? Diese Einstellung wird auf Mobilgeräten unabhängig von dieser Einstellung auf 0 gesetzt.", + "pdf-scroll-mode-label": "Scroll-Modus", + "pdf-scroll-mode-tooltip": "Wie Sie durch die Seiten scrollen. Vertikal/horizontal und durch Antippen blättern (kein Scrollen)", + "pdf-spread-mode-label": "Verteilungsmodus", + "pdf-spread-mode-tooltip": "Wie Seiten angelegt werden sollen. Einfach oder doppelt (ungerade/gerade)", + "pdf-theme-label": "Theme", + "pdf-theme-tooltip": "Farbschema des Readers", + "reading-profile-series-settings-title": "Serie", + "reading-profile-library-settings-title": "Bibliothek" + }, + "bulk-set-reading-profile-modal": { + "no-data": "Es wurden noch keine Sammlungen erstellt", + "bound": "Gebunden", + "title": "Leseprofil festlegen" + }, + "merge-person-modal": { + "alias-title": "Neue Aliase", + "known-for-title": "Bekannt für", + "src": "Person zusammenführen", + "merge-warning": "Wenn Sie fortfahren, wird die ausgewählte Person entfernt. Der Name der ausgewählten Person wird als Alias hinzugefügt und alle ihre Rollen werden übertragen." } } diff --git a/UI/Web/src/assets/langs/es.json b/UI/Web/src/assets/langs/es.json index d9f5ca04f..2360de136 100644 --- a/UI/Web/src/assets/langs/es.json +++ b/UI/Web/src/assets/langs/es.json @@ -106,55 +106,6 @@ "collapse-series-relationships-tooltip": "Kavita debería mostrar series que no tienen relaciones o es la serie madre/secuela", "share-series-reviews-label": "Compartir reseñas de series", "share-series-reviews-tooltip": "Kavita debería incluir tus reseñas de series para otros usuarios", - "image-reader-settings-title": "Lector de imágenes", - "reading-direction-label": "Dirección de lectura", - "reading-direction-tooltip": "Dirección para hacer clic para pasar a la siguiente página. De derecha a izquierda significa que haces clic en el lado izquierdo de la pantalla para pasar a la siguiente página.", - "scaling-option-label": "Opciones de escalado", - "scaling-option-tooltip": "Cómo ajustar la imagen a tu pantalla.", - "page-splitting-label": "Separación de página", - "page-splitting-tooltip": "Cómo separar una imagen de ancho completo (es decir, ambas imágenes izquierda y derecha están combinadas)", - "reading-mode-label": "Modo de lectura", - "reading-mode-tooltip": "Cambiar el lector a paginar verticalmente, horizontalmente, o tener un desplazamiento infinito", - "layout-mode-label": "Modo de disposición", - "layout-mode-tooltip": "Renderizar una única imagen en la pantalla o dos imágenes una al lado de la otra", - "background-color-label": "Color de fondo", - "background-color-tooltip": "Color de fondo del lector de imágenes", - "auto-close-menu-label": "Cerrar menú automáticamente", - "auto-close-menu-tooltip": "¿El menú debería cerrarse automáticamente?", - "show-screen-hints-label": "Mostrar sugerencias en pantalla", - "show-screen-hints-tooltip": "Mostrar superposiciones para ayudar a comprender las áreas de paginación y las direcciones", - "emulate-comic-book-label": "Emular a un cómic", - "emulate-comic-book-tooltip": "Agrega un efecto de sombra para imitar la lectura de un libro", - "swipe-to-paginate-label": "Deslizar para paginar", - "swipe-to-paginate-tooltip": "Permitir deslizar en la pantalla para pasar a la página anterior/siguiente", - "book-reader-settings-title": "Lector de libros", - "tap-to-paginate-label": "Tocar para paginar", - "tap-to-paginate-tooltip": "Los lados de la pantalla del lector de libros deben permitir tocar para pasar a la página anterior/siguiente", - "immersive-mode-label": "Modo inmersivo", - "immersive-mode-tooltip": "Esto ocultará el menú detrás de un clic en el documento del lector y activará tocar para paginar", - "reading-direction-book-label": "Dirección de lectura", - "reading-direction-book-tooltip": "Dirección para hacer clic para pasar a la siguiente página. De derecha a izquierda significa que haces clic en el lado izquierdo de la pantalla para pasar a la siguiente página.", - "font-family-label": "Familia de fuentes", - "font-family-tooltip": "Familia de fuentes para cargar. Por defecto cargará la fuente predeterminada del libro", - "writing-style-label": "Estilo de escritura", - "writing-style-tooltip": "Cambia la dirección del texto. Horizontal es de izquierda a derecha, vertical es de arriba a abajo.", - "layout-mode-book-label": "Modo de disposición", - "layout-mode-book-tooltip": "Cómo se debe organizar el contenido. Desplazable es tal como el libro se empaqueta. 1 o 2 columnas se ajusta a la altura del dispositivo y ajusta 1 o 2 columnas de texto por página", - "color-theme-book-label": "Tema de color", - "color-theme-book-tooltip": "Qué tema de color aplicar al contenido del lector de libros y al menú", - "font-size-book-label": "Tamaño de fuente", - "font-size-book-tooltip": "Porcentaje de escalado que aplicar a la fuente del libro", - "line-height-book-label": "Espaciado entre líneas", - "line-height-book-tooltip": "Cuánto espacio habrá entre las líneas del libro", - "margin-book-label": "Margen", - "margin-book-tooltip": "Cuánto espacio habrá en cada lado de la pantalla. Esto se anulará a 0 en los dispositivos móviles sin importar esta configuración.", - "pdf-reader-settings-title": "Lector de PDF", - "pdf-scroll-mode-label": "Modo de desplazamiento", - "pdf-scroll-mode-tooltip": "Cómo se desplaza por las páginas. Vertical/Horizontal y Toque para Paginar (sin desplazamiento)", - "pdf-spread-mode-label": "Modo de propagación", - "pdf-spread-mode-tooltip": "Cómo se deben distribuir las páginas. Simple o doble (par/impar)", - "pdf-theme-label": "Tema", - "pdf-theme-tooltip": "Tema de color del lector", "clients-opds-alert": "OPDS no está habilitado en este servidor. Esto no afectará a los usuarios de Tachiyomi.", "clients-opds-description": "Todos los clientes de terceros usarán la clave API o la URL de conexión a continuación. Estos son como contraseñas, mantenlo en privado.", "clients-api-key-tooltip": "La clave API es como una contraseña. Restablecerla invalidará cualquier cliente existente.", @@ -841,8 +792,7 @@ "donate-tooltip": "Puedes eliminarlo suscribiéndote a Kavita+", "back": "Atras", "more": "Ver más", - "customize": "{{settings.customize}}", - "browse-authors": "Buscar autores" + "customize": "{{settings.customize}}" }, "library-settings-modal": { "close": "{{common.close}}", @@ -1590,7 +1540,6 @@ }, "manga-reader": { "back": "Volver", - "save-globally": "Guardar globalmente", "incognito-alt": "El modo incógnito está encendido. Desliza para apagar.", "incognito-title": "Modo incógnito:", "shortcuts-menu-alt": "Modo de atajos de teclado", @@ -1625,7 +1574,6 @@ "layout-mode-switched": "El modo de diseño se ha cambiado a Individual ya que no hay espacio suficiente para renderizar el diseño doble", "no-next-chapter": "No hay siguiente Capítulo", "no-prev-chapter": "No hay Capítulo Anterior", - "user-preferences-updated": "Preferencias de usuario actualizadas", "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", "series-progress": "Progreso de la serie: {{porcentage}}" }, @@ -2433,11 +2381,6 @@ "mal-tooltip": "https://myanimelist.net/people/{MalId}/", "asin-tooltip": "https://www.amazon.com/stores/J.K.-Rowling/author/{ASIN}" }, - "browse-authors": { - "title": "Buscar autores y escritores", - "author-count": "{{num}} personas", - "cover-image-description": "{{edit-series-modal.cover-image-description}}" - }, "email-history": { "date-header": "Fecha de envío", "template-header": "Plantilla", diff --git a/UI/Web/src/assets/langs/fi.json b/UI/Web/src/assets/langs/fi.json index 5d1f1393f..7795f3c60 100644 --- a/UI/Web/src/assets/langs/fi.json +++ b/UI/Web/src/assets/langs/fi.json @@ -22,37 +22,9 @@ "collapse-series-relationships-label": "Kutista sarjojen suhteet", "collapse-series-relationships-tooltip": "Pitäisikö Kavita näyttää sarjoja, joilla ei ole suhteita tai jotka ovat vanhempi/esiosa", "share-series-reviews-label": "Jaa sarjojen arvostelut", - "image-reader-settings-title": "Kuvanlukija", - "reading-direction-tooltip": "Napsauttamista kattavat ohjeet seuraavalle sivulle siirtymiseksi. \"Oikealta vasemmalle\" tarkoittaa, että napsautat näytön vasenta reunaa siirtyäksesi seuraavalle sivulle.", - "scaling-option-label": "Skaalausvaihtoehdot", - "scaling-option-tooltip": "Kuinka skaalata kuva ruudullesi.", - "page-splitting-label": "Sivun jakaminen", - "page-splitting-tooltip": "Täysleveän kuvan jakaminen (eli sekä vasen että oikea kuva yhdistetään)", - "reading-mode-label": "Lukutila", - "reading-mode-tooltip": "Vaihda lukija sivuttelemaan pysty- tai vaakasuunnassa tai vierittämään loputtomasti", - "layout-mode-label": "Asettelutila", - "layout-mode-tooltip": "Hahmonna ruudulle yksi kuva tai kaksi vierekkäistä kuvaa", - "background-color-label": "Taustaväri", - "background-color-tooltip": "Kuvanlukijan taustaväri", - "auto-close-menu-label": "Sulje valikko automaattisesti", - "auto-close-menu-tooltip": "Pitäisikö valikon sulkeutua automaattisesti", - "show-screen-hints-label": "Näytä näytön vihjeet", - "show-screen-hints-tooltip": "Näytä peittokuva, joka auttaa ymmärtämään sivutusalueen ja -suunnan", - "emulate-comic-book-label": "Emuloi sarjakuvaa", - "emulate-comic-book-tooltip": "Käyttää varjotehostetta jäljittelemään kirjan lukemista", - "tap-to-paginate-label": "Sivuta napauttamalla", - "book-reader-settings-title": "Kirjanlukija", - "swipe-to-paginate-label": "Sivuta pyyhkäisemällä", - "swipe-to-paginate-tooltip": "Pitäisikö näytöllä pyyhkäiseminen laukaista seuraavan tai edellisen sivun", - "immersive-mode-label": "Mukaansatempaava tila", - "immersive-mode-tooltip": "Tämä piilottaa valikon lukijan asiakirjan napsautuksen taakse ja sivuuta päälle napauttamalla", - "reading-direction-book-label": "Lukusuunta", - "reading-direction-book-tooltip": "Napsauttamista kattavat ohjeet seuraavalle sivulle siirtymiseksi. \"Oikealta vasemmalle\" tarkoittaa, että napsautat näytön vasenta reunaa siirtyäksesi seuraavalle sivulle.", "disable-animations-tooltip": "Poistaa animaatiot käytöstä sivustolla. Hyödyllinen e-musteen lukijoille.", "stats-tab": "{{tabs.stats-tab}}", - "reading-direction-label": "Lukusuunta", - "scrobbling-tab": "{{tabs.scrobbling-tab}}", - "tap-to-paginate-tooltip": "Pitäisikö kirjanlukijanäytön sivuilla napauttaa sitä siirtyäksesi edelliselle/seuraavalle sivulle" + "scrobbling-tab": "{{tabs.scrobbling-tab}}" }, "customize-dashboard-modal": { "smart-filters": "Älykkäät suodattimet", diff --git a/UI/Web/src/assets/langs/fr.json b/UI/Web/src/assets/langs/fr.json index 3ae26002d..780da243e 100644 --- a/UI/Web/src/assets/langs/fr.json +++ b/UI/Web/src/assets/langs/fr.json @@ -72,7 +72,8 @@ "your-review": "Voici votre critique", "external-review": "Critique externe", "local-review": "Critique locale", - "rating-percentage": "Évaluation {{r}}%" + "rating-percentage": "Évaluation {{r}}%", + "critic": "critique" }, "want-to-read": { "title": "À lire", @@ -106,55 +107,6 @@ "collapse-series-relationships-tooltip": "Kavita doit-il afficher des Séries qui n'ont pas de lien ou sont le parent/préquel", "share-series-reviews-label": "Partagez les critiques de la Série", "share-series-reviews-tooltip": "Kavita doit-il afficher vos commentaires sur les Séries pour les autres utilisateurs", - "image-reader-settings-title": "Lecteur d'image", - "reading-direction-label": "Sens de lecture", - "reading-direction-tooltip": "Direction dans laquelle cliquer pour passer à la page suivante. De droite à gauche signifie que vous cliquez sur le côté gauche de l'écran pour passer à la page suivante.", - "scaling-option-label": "Options de Mise à l'échelle", - "scaling-option-tooltip": "Comment ajuster la mise à l'échelle de l'image a votre écran.", - "page-splitting-label": "Séparation des Pages", - "page-splitting-tooltip": "Comment diviser une image pleine largeur (càd que les images de gauche et de droite sont combinées)", - "reading-mode-label": "Mode de Lecture", - "reading-mode-tooltip": "Modifier le lecteur pour paginer verticalement, horizontalement ou avoir un défilement infini", - "layout-mode-label": "Mode de mise en page", - "layout-mode-tooltip": "Rendu d'une seule image à l'écran ou de deux images côte à côte", - "background-color-label": "Couleur d'arrière-plan", - "background-color-tooltip": "Couleur de fond du lecteur d'image", - "auto-close-menu-label": "Fermeture automatique du Menu", - "auto-close-menu-tooltip": "Le menu devrait se fermer tout seul", - "show-screen-hints-label": "Afficher les conseils à l'écran", - "show-screen-hints-tooltip": "Afficher une superposition pour aider à comprendre la zone et la direction de la pagination", - "emulate-comic-book-label": "Imiter l'aspect Bande dessinées", - "emulate-comic-book-tooltip": "Applique un effet d'ombre pour imiter la lecture d'un livre", - "swipe-to-paginate-label": "Glisser pour paginer", - "swipe-to-paginate-tooltip": "Le glissement sur l'écran devrait provoquer le déclenchement de la page suivante ou précédente", - "book-reader-settings-title": "Lecteur de livre", - "tap-to-paginate-label": "Appuyer pour paginer", - "tap-to-paginate-tooltip": "Les bords d'écran du lecteur de livre doivent-ils permettre d'appuyer pour passer à la page précédente/suivante", - "immersive-mode-label": "Mode immersif", - "immersive-mode-tooltip": "Cela permet de masquer le menu en cliquant sur le document de lecture et d'activer la fonction de pagination", - "reading-direction-book-label": "Sens de lecture", - "reading-direction-book-tooltip": "Sens du clic pour aller à page suivante. Droite à gauche signifie que vous devez cliquer du côté gauche de l'écran pour aller à la page suivante.", - "font-family-label": "Famille de Polices", - "font-family-tooltip": "Famille de police à charger. Par Défaut chargera la police par défaut du livre", - "writing-style-label": "Style d'écriture", - "writing-style-tooltip": "Change la direction du texte. Horizontal est de gauche à droite, vertical est de haut en bas.", - "layout-mode-book-label": "Mise en page", - "layout-mode-book-tooltip": "Comment le contenu devrait être mis en page. Défilement correspond à la présentation d'origine du livre. 1 ou 2 Colonnes adapte le texte à la hauteur de l'appareil avec une 1 ou 2 colonnes par page", - "color-theme-book-label": "Couleur du thème", - "color-theme-book-tooltip": "Quel thème de couleur appliquer aux contenu et menu du lecteur de livre", - "font-size-book-label": "Taille de la Police", - "font-size-book-tooltip": "Pourcentage de mise à l'échelle à appliquer à la police du livre", - "line-height-book-label": "Espacement de l'interligne", - "line-height-book-tooltip": "Quel espace entre les lignes du livre", - "margin-book-label": "Marge", - "margin-book-tooltip": "Quel espace de chaque côté de l'écran. Cela sera remplacé par 0 sur les appareils mobiles indépendamment de ce réglage.", - "pdf-reader-settings-title": "Lecteur PDF", - "pdf-scroll-mode-label": "Mode de défilement", - "pdf-scroll-mode-tooltip": "Mode de défilement des pages. Vertical/Horizontal et Tapez pour paginer (pas de défilement)", - "pdf-spread-mode-label": "Mode d'étalement", - "pdf-spread-mode-tooltip": "Comment les pages doivent être mises en page. Simple ou double (paire/impaire)", - "pdf-theme-label": "Thème", - "pdf-theme-tooltip": "Thème de couleur du lecteur", "clients-opds-alert": "OPDS n'est pas actif sur ce serveur. Cela n'affectera pas les utilisateurs de Tachiyomi.", "clients-opds-description": "Tous les clients tiers utiliseront soit la Clé d'API ou l'Url de connexion ci-dessous. Ce sont comme des mots de passe, ne les divulguez pas.", "clients-api-key-tooltip": "La Clé d'API est comme un mot de passe. Gardez-la secrète, gardez-la en sécurité. La réinitialiser invalidera tous les clients existants.", @@ -167,9 +119,7 @@ "anilist-scrobbling-label": "Suivi de lecture AniList", "anilist-scrobbling-tooltip": "Autorise Kavita à mettre à jour le suivi de lecture et les notes sur AniList", "want-to-read-sync-label": "À lire : Synchroniser", - "want-to-read-sync-tooltip": "Permettre à Kavita d'ajouter des éléments à votre liste \"À lire\" en fonction des séries AniList et MAL de la liste de lecture \"En attente\"", - "allow-auto-webtoon-reader-label": "Mode Lecteur Webtoon Automatique", - "allow-auto-webtoon-reader-tooltip": "Passez en mode \"Lecteur Webtoon\" si les pages ressemblent à un webtoon. Des faux positifs peuvent se produire." + "want-to-read-sync-tooltip": "Permettre à Kavita d'ajouter des éléments à votre liste \"À lire\" en fonction des séries AniList et MAL de la liste de lecture \"En attente\"" }, "user-holds": { "title": "Enregistrement du suivi d'activité", @@ -849,7 +799,6 @@ "back": "Retour", "more": "Plus", "customize": "{{settings.customize}}", - "browse-authors": "Parcourir les auteurs", "edit": "{{common.edit}}", "cancel-edit": "Fermer la réorganisation" }, @@ -1622,7 +1571,6 @@ }, "manga-reader": { "back": "Retour", - "save-globally": "Sauvegarder globalement", "incognito-alt": "Le mode Incognito est activé. Basculer pour le désactiver.", "incognito-title": "Mode incognito :", "shortcuts-menu-alt": "Raccourcis clavier Modal", @@ -1657,9 +1605,8 @@ "layout-mode-switched": "Le mode de mise en page est passé à simple en raison d'un manque d'espace pour rendre la mise en page double", "no-next-chapter": "Pas de prochain chapitre", "no-prev-chapter": "Pas de chapitre précédent", - "user-preferences-updated": "Mise à jour des préférences des utilisateurs", - "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", - "series-progress": "Progression de la série : {{percentage}}" + "emulate-comic-book-label": "{{manage-reading-profiles.emulate-comic-book-label}}", + "series-progress": "Progression de la série : {{percentage}}" }, "metadata-filter": { "filter-title": "{{common.filter}}", @@ -2481,11 +2428,6 @@ "cover-image-description-extra": "Vous pouvez également télécharger une couverture à partir de CoversDB, si elle est disponible.", "download-coversdb": "Télécharger à partir de CoversDB" }, - "browse-authors": { - "title": "Parcourir les auteurs et écrivains", - "author-count": "{{num}} Personnes", - "cover-image-description": "{{edit-series-modal.cover-image-description}}" - }, "changelog-update-item": { "added": "Ajouté", "download": "Télécharger", @@ -2591,7 +2533,12 @@ "age-rating-mapping-description": "Toute chaîne de caractères à gauche, si elle est trouvée dans les Genres ou les Tags, définira la classification par âge de la Série.", "whitelist-tooltip": "N'autoriser qu'une chaîne de cette liste à être écrite pour les Tags. Assurez-vous qu'elles sont séparées par des virgules.", "age-rating-mapping-title": "Correspondance des classifications par âge", - "field-mapping-title": "Correspondance de champ" + "field-mapping-title": "Correspondance de champ", + "enable-chapter-title-tooltip": "Autoriser la modification du Titre du chapitre/volume", + "enable-chapter-title-label": "Titre", + "enable-chapter-release-date-label": "Date de diffusion", + "enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}", + "enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}" }, "match-series-result-item": { "chapter-count": "{{common.chapter-count}}", @@ -2637,5 +2584,18 @@ "trace": "Trace", "warning": "Attention", "critical": "Critique" + }, + "reviews": { + "user-reviews-local": "Avis Locaux", + "user-reviews-plus": "Avis Externes" + }, + "review-modal": { + "close": "{{common.close}}", + "save": "{{common.save}}", + "delete": "{{common.delete}}", + "required": "{{validation.required-field}}", + "review-label": "Avis", + "title": "Editer Avis", + "min-length": "L'avis doit faire au moins {{count}} caractères" } } diff --git a/UI/Web/src/assets/langs/ga.json b/UI/Web/src/assets/langs/ga.json index 94490ec07..f04733b7a 100644 --- a/UI/Web/src/assets/langs/ga.json +++ b/UI/Web/src/assets/langs/ga.json @@ -83,7 +83,7 @@ }, "user-preferences": { "title": "Painéal na nÚsáideoirí", - "pref-description": "Is socruithe domhanda iad seo atá faoi cheangal ag do chuntas.", + "pref-description": "Is socruithe domhanda iad seo atá ceangailte le do chuntas. Tá socruithe léitheoirí le fáil i bPróifílí Léitheoireachta.", "account-tab": "{{tabs.account-tab}}", "preferences-tab": "{{tabs.preferences-tab}}", "theme-tab": "{{tabs.theme-tab}}", @@ -107,55 +107,6 @@ "collapse-series-relationships-tooltip": "Ar chóir Kavita thaispeáint Sraith nach bhfuil aon chaidreamh nó go bhfuil an tuismitheoir/réamhscéal", "share-series-reviews-label": "Léirmheasanna Sraith Comhroinn", "share-series-reviews-tooltip": "Ar chóir Kavita san áireamh do athbhreithnithe ar Sraith d'úsáideoirí eile", - "image-reader-settings-title": "Léitheoir Íomhá", - "reading-direction-label": "Treo Léitheoireachta", - "reading-direction-tooltip": "Treoir chun cliceáil chun bogadh go dtí an chéad leathanach eile. Ciallaíonn Deas go Clé go bhfuil tú ag cliceáil ar thaobh clé an scáileáin chun bogadh go dtí an chéad leathanach eile.", - "scaling-option-label": "Roghanna Scálú", - "scaling-option-tooltip": "Conas an íomhá a scála ar do scáileán.", - "page-splitting-label": "Scoilteadh Leathanach", - "page-splitting-tooltip": "Conas íomhá leithead iomlán a roinnt (ie cuirtear íomhánna ar chlé agus ar dheis le chéile)", - "reading-mode-label": "Mód Léitheoireachta", - "reading-mode-tooltip": "Athraigh an léitheoir chun leathanach a dhéanamh go hingearach, go cothrománach, nó bíodh scrolla gan teorainn agat", - "layout-mode-label": "Mód Leagan Amach", - "layout-mode-tooltip": "Tabhair íomhá amháin don scáileán nó dhá íomhá taobh le taobh", - "background-color-label": "Cúlra Dath", - "background-color-tooltip": "Cúlra Dath an Léitheora Íomhá", - "auto-close-menu-label": "Dún an Roghchlár Go hUathoibríoch", - "auto-close-menu-tooltip": "Ba chóir an roghchlár a dhúnadh go huathoibríoch", - "show-screen-hints-label": "Taispeáin Leideanna Scáileáin", - "show-screen-hints-tooltip": "Taispeáin forleagan chun cabhrú le tuiscint a fháil ar limistéar agus treo pagination", - "emulate-comic-book-label": "Aithris a dhéanamh ar ghreannán", - "emulate-comic-book-tooltip": "Cuireann sé scáthéifeacht i bhfeidhm chun aithris a dhéanamh ar léamh as leabhar", - "swipe-to-paginate-label": "Svaidhpeáil go Paginate", - "swipe-to-paginate-tooltip": "Ba chóir svaidhpeáil ar an scáileán a chur faoi deara an chéad leathanach eile nó an leathanach roimhe seo a spreagadh", - "book-reader-settings-title": "Léitheoir Leabhar", - "tap-to-paginate-label": "Tapáil go Paginate", - "tap-to-paginate-tooltip": "Má cheadaíonn taobhanna scáileán an léitheora leabhar cnagadh air chun bogadh go dtí an chéad leathanach eile/an chéad leathanach eile", - "immersive-mode-label": "Mód tumtha", - "immersive-mode-tooltip": "Folóidh sé seo an roghchlár taobh thiar de chliceáil ar an gcáipéis léitheora agus casfaidh sé tapáil chun paginate a dhéanamh air", - "reading-direction-book-label": "Treo Léitheoireachta", - "reading-direction-book-tooltip": "Treoir chun cliceáil chun bogadh go dtí an chéad leathanach eile. Ciallaíonn Deas go Clé go bhfuil tú ag cliceáil ar thaobh clé an scáileáin chun bogadh go dtí an chéad leathanach eile.", - "font-family-label": "Teaghlach Cló", - "font-family-tooltip": "Clófhoireann le luchtú suas. Luchtóidh an réamhshocrú cló réamhshocraithe an leabhair", - "writing-style-label": "Stíl Scríbhneoireachta", - "writing-style-tooltip": "Athraíonn seo treo an téacs. Horizontal is left to right, tá ingearach ó bhun go barr.", - "layout-mode-book-label": "Mód Leagan Amach", - "layout-mode-book-tooltip": "Conas ba chóir ábhar a leagan amach. Scrollaigh mar a phacálann an leabhar é. 1 nó 2 Oireann Colún d'airde na feiste agus luíonn sé 1 nó 2 cholún téacs in aghaidh an leathanaigh", - "color-theme-book-label": "Téama Datha", - "color-theme-book-tooltip": "Cén téama datha atá le cur i bhfeidhm ar ábhar agus roghchlár an léitheora leabhar", - "font-size-book-label": "Clómhéid", - "font-size-book-tooltip": "Céatadán den scálú le cur i bhfeidhm ar chló sa leabhar", - "line-height-book-label": "Spásáil Líne", - "line-height-book-tooltip": "Cé mhéad spásáil idir línte an leabhair", - "margin-book-label": "Imeall", - "margin-book-tooltip": "Cé mhéad spásáil ar gach taobh den scáileán. Sáróidh sé seo 0 ar ghléasanna soghluaiste beag beann ar an socrú seo.", - "pdf-reader-settings-title": "Léitheoir PDF", - "pdf-scroll-mode-label": "Mód Scrollaigh", - "pdf-scroll-mode-tooltip": "Conas a scrollaíonn tú trí leathanaigh. Ingearach / Cothrománach agus Tapáil go Paginate (gan scrollbharra)", - "pdf-spread-mode-label": "Mód Scaipthe", - "pdf-spread-mode-tooltip": "Conas ba chóir leathanaigh a leagan amach. Singil nó dúbailte (corr / fiú)", - "pdf-theme-label": "Téama", - "pdf-theme-tooltip": "Téama datha an léitheora", "clients-opds-alert": "Níl OPDS cumasaithe ar an bhfreastalaí seo. Ní dhéanfaidh sé seo difear d'úsáideoirí Tachiyami.", "clients-opds-description": "Úsáidfidh gach cliant 3ú Páirtí an eochair API nó an Url Ceangail thíos. Tá siad seo cosúil le pasfhocail, coinnigh príobháideach é.", "clients-api-key-tooltip": "Tá an eochair API cosúil le pasfhocal. Má athshocraítear é, cuirfidh sé aon chliaint atá ann cheana ó bhail.", @@ -168,9 +119,7 @@ "anilist-scrobbling-label": "Scrobbling AniList", "anilist-scrobbling-tooltip": "Lig do Kavita Scrobble (sioncronú aontreo) dul chun cinn a léamh agus rátálacha chuig AniList", "want-to-read-sync-label": "Ag Teastáil le Léamh Sioncrónaithe", - "want-to-read-sync-tooltip": "Lig do Kavita míreanna a chur le do liosta Want to Read bunaithe ar shraith AniList agus MAL sa léiliosta ar feitheamh", - "allow-auto-webtoon-reader-tooltip": "Athraigh isteach i mód Webtoon Reader má tá cuma ar leathanaigh ghréasáin. D’fhéadfadh roinnt rudaí dearfacha bréagacha tarlú.", - "allow-auto-webtoon-reader-label": "Mód Uathoibríoch Léitheoir Webtoon" + "want-to-read-sync-tooltip": "Lig do Kavita míreanna a chur le do liosta Want to Read bunaithe ar shraith AniList agus MAL sa léiliosta ar feitheamh" }, "user-holds": { "title": "Coinníonn Scrobble", @@ -214,7 +163,7 @@ "description": "Nuair a roghnaítear iad, déanfar gach liosta sraithe agus léitheoireachta a bhfuil mír amháin ar a laghad acu atá níos mó ná an srian roghnaithe a bhriseadh ó thorthaí.", "not-applicable-for-admins": "Níl sé seo infheidhme maidir le admins.", "age-rating-label": "{{metadata-fields.age-rating-title}}", - "no-restriction": "Uimh Srianadh", + "no-restriction": "Gan Srian", "include-unknowns-label": "Cuir anaithnid san áireamh", "include-unknowns-tooltip": "Más fíor, ceadófar Anaithnid le Srian Aoise. D'fhéadfadh sé seo a bheith ina chúis le sceitheadh na meán d'úsáideoirí a bhfuil srianta Aoise orthu." }, @@ -680,7 +629,10 @@ "incognito-mode-label": "Mód Incognito", "next": "Ar Aghaidh", "previous": "Roimhe Seo", - "go-to-page-prompt": "Tá leathanaigh {{totalPages}} ann. Cén leathanach ar mhaith leat dul chuige?" + "go-to-page-prompt": "Tá leathanaigh {{totalPages}} ann. Cén leathanach ar mhaith leat dul chuige?", + "go-to-section": "Téigh go dtí an roinn", + "go-to-section-prompt": "Tá {{totalSections}} rannóg ann. Cén rannóg ar mhaith leat dul chuici?", + "go-to-first-page": "Téigh go dtí an chéad leathanach" }, "personal-table-of-contents": { "no-data": "Níl aon rud Leabharmharcáilte fós", @@ -728,7 +680,7 @@ "series-detail": { "page-settings-title": "Socruithe an Leathanaigh", "close": "{{common.close}}", - "layout-mode-label": "{{user-preferences.layout-mode-book-label}}", + "layout-mode-label": "{{manage-reading-profiles.layout-mode-book-label}}", "layout-mode-option-card": "Cárta", "layout-mode-option-list": "Liosta", "continue-from": "Lean ar aghaidh {{title}}", @@ -851,9 +803,9 @@ "back": "Ar ais", "more": "Níos mó", "customize": "{{settings.customize}}", - "browse-authors": "Brabhsáil Údair", "edit": "{{common.edit}}", - "cancel-edit": "Dún Athordú" + "cancel-edit": "Dún Athordú", + "browse-people": "Brabhsáil Daoine" }, "library-settings-modal": { "close": "{{common.close}}", @@ -916,30 +868,30 @@ }, "reader-settings": { "general-settings-title": "Socruithe Ginearálta", - "font-family-label": "{{user-preferences.font-family-label}}", - "font-size-label": "{{user-preferences.font-size-book-label}}", - "line-spacing-label": "{{user-preferences.line-height-book-label}}", - "margin-label": "{{user-preferences.margin-book-label}}", + "font-family-label": "{{manage-reading-profiles.font-family-label}}", + "font-size-label": "{{manage-reading-profiles.font-size-book-label}}", + "line-spacing-label": "{{manage-reading-profiles.line-height-book-label}}", + "margin-label": "{{manage-reading-profiles.margin-book-label}}", "reset-to-defaults": "Athshocraigh go Réamhshocruithe", "reader-settings-title": "Socruithe an Léitheora", - "reading-direction-label": "{{user-preferences.reading-direction-book-label}}", + "reading-direction-label": "{{manage-reading-profiles.reading-direction-book-label}}", "right-to-left": "Deas go Clé", "left-to-right": "Clé go Deas", "horizontal": "Cothrománach", "vertical": "Ingearach", - "writing-style-label": "{{user-preferences.writing-style-label}}", + "writing-style-label": "{{manage-reading-profiles.writing-style-label}}", "writing-style-tooltip": "Athraíonn seo treo an téacs. Horizontal is left to right, tá ingearach ó bhun go barr.", "tap-to-paginate-label": "Tapáil Pagination", "tap-to-paginate-tooltip": "Cliceáil ar imill an scáileáin chun paginate", "on": "Ar", "off": "As", - "immersive-mode-label": "{{user-preferences.immersive-mode-label}}", + "immersive-mode-label": "{{manage-reading-profiles.immersive-mode-label}}", "immersive-mode-tooltip": "Folóidh sé seo an roghchlár taobh thiar de chliceáil ar an gcáipéis léitheora agus casfaidh sé tapáil chun paginate a dhéanamh air", "fullscreen-label": "Lánscáileán", "fullscreen-tooltip": "Cuir an léitheoir i mód lánscáileáin", "exit": "Scoir", "enter": "Iontráil", - "layout-mode-label": "{{user-preferences.layout-mode-book-label}}", + "layout-mode-label": "{{manage-reading-profiles.layout-mode-book-label}}", "layout-mode-tooltip": "Scrollaigh: Scátháin comhad epub (de ghnáth leathanach fada scrollaithe amháin in aghaidh na caibidle).
1 Colún: Cruthaíonn sé leathanach fíorúil amháin ag an am.
2 Colún: Cruthaítear dhá leathanach fhíorúla ag an am atá leagtha amach taobh le taobh -taobh.", "layout-mode-option-scroll": "Scrollaigh", "layout-mode-option-1col": "1 Colún", @@ -948,7 +900,15 @@ "theme-dark": "Dorcha", "theme-black": "Dubh", "theme-white": "Bán", - "theme-paper": "Páipéar" + "theme-paper": "Páipéar", + "line-spacing-min-label": "1x", + "line-spacing-max-label": "2.5x", + "update-parent": "Sábháil chuig {{name}}", + "loading": "ag lódáil", + "create-new": "Próifíl nua ó intuigthe", + "reading-profile-updated": "Próifíl léitheoireachta nuashonraithe", + "reading-profile-promoted": "Próifíl léitheoireachta curtha chun cinn", + "create-new-tooltip": "Cruthaigh próifíl nua inbhainistithe ón gceann intuigthe atá agat faoi láthair" }, "table-of-contents": { "no-data": "Níl Clár ábhair sa leabhar seo atá leagtha síos sna meiteashonraí nó i gcomhad clár" @@ -1245,7 +1205,7 @@ "library-scan-tooltip": "Cé chomh minic a dhéanfaidh Kavita meiteashonraí a scanadh agus a athnuachan timpeall ar chomhaid leabharlainne.", "library-database-backup-label": "Cúltaca fileata", "library-database-backup-tooltip": "Cé chomh minic a dhéanfaidh Kavita cúltaca ar an mbunachar sonraí agus ar chomhaid ghaolmhara eile.", - "cleanup-label": "Glanta", + "cleanup-label": "Glanadh", "cleanup-tooltip": "Cé chomh minic a rithfidh Kavita tascanna glantacháin. D'fhéadfadh sé seo a bheith trom agus ba chóir é a dhéanamh ag meán oíche i bhformhór na gcásanna", "adhoc-tasks-title": "Tascanna Ad-hoc", "job-title-header": "Teideal an Phoist", @@ -1380,7 +1340,7 @@ "clients": "Eochair API / OPDS", "devices": "Gléasanna", "user-stats": "Stait", - "scrobbling": "Scrobbling", + "scrobbling": "Ag scrobadh", "theme": "Téama", "customize": "Saincheap", "cbl-import": "Liosta Léitheoireachta CBL", @@ -1389,7 +1349,8 @@ "admin-matched-metadata": "Meiteashonraí Meaitseála", "admin-manage-tokens": "Bainistigh Comharthaí Úsáideora", "scrobble-holds": "Scrobble i seilbh", - "admin-metadata": "Bainistigh Meiteashonraí" + "admin-metadata": "Bainistigh Meiteashonraí", + "reading-profiles": "Próifílí Léitheoireachta" }, "collection-detail": { "no-data": "Níl aon mhír ann. Bain triail as sraith a chur leis.", @@ -1514,7 +1475,9 @@ "all-filters": "Scagairí Cliste", "nav-link-header": "Roghanna Nascleanúna", "close": "{{common.close}}", - "person-aka-status": "Meaitseálann leasainm" + "person-aka-status": "Meaitseálann leasainm", + "browse-tags": "Brabhsáil Clibeanna", + "browse-genres": "Brabhsáil Seánraí" }, "promoted-icon": { "promoted": "{{common.promoted}}" @@ -1625,7 +1588,6 @@ }, "manga-reader": { "back": "Ar ais", - "save-globally": "Sábháil ar fud an domhain", "incognito-alt": "Tá mód Incognito ar siúl. Scoránaigh chun é a chasadh as.", "incognito-title": "Mód Incognito:", "shortcuts-menu-alt": "Mód Aicearraí Méarchláir", @@ -1647,9 +1609,9 @@ "height": "Airde", "width": "Leithead", "width-override-label": "Sáraigh Leithead", - "off": "As", + "off": "{{reader-settings.off}}", "original": "Bunleagan", - "auto-close-menu-label": "{{user-preferences.auto-close-menu-label}}", + "auto-close-menu-label": "{{manage-reading-profiles.auto-close-menu-label}}", "swipe-enabled-label": "Svaidhpeáil Cumasaithe", "enable-comic-book-label": "Aithris a dhéanamh ar ghreannán", "brightness-label": "Gile", @@ -1660,9 +1622,14 @@ "layout-mode-switched": "Mód leagan amach aistrithe go Singil mar gheall ar easpa spáis chun leagan amach dúbailte a dhéanamh", "no-next-chapter": "Gan aon chaibidil eile", "no-prev-chapter": "Uimh Caibidil Roimhe Seo", - "user-preferences-updated": "Nuashonraíodh sainroghanna úsáideora", - "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", - "series-progress": "Dul Chun Cinn na Sraithe: {{percentage}}" + "emulate-comic-book-label": "{{manage-reading-profiles.emulate-comic-book-label}}", + "series-progress": "Dul Chun Cinn na Sraithe: {{percentage}}", + "create-new": "{{reader-settings.create-new}}", + "update-parent": "{{reader-settings.update-parent}}", + "loading": "{{reader-settings.loading}}", + "reading-profile-updated": "Próifíl léitheoireachta nuashonraithe", + "reading-profile-promoted": "Próifíl léitheoireachta curtha chun cinn", + "create-new-tooltip": "{{reader-settings.create-new-tooltip}}" }, "metadata-filter": { "filter-title": "{{common.filter}}", @@ -1719,7 +1686,10 @@ "release-year": "Bliain Scaoilte", "read-progress": "An Léamh Is Déanaí", "average-rating": "Meánráta", - "random": "Randamach" + "random": "Randamach", + "person-name": "Ainm", + "person-series-count": "Líon na Sraitheanna", + "person-chapter-count": "Líon na gCaibidlí" }, "edit-series-modal": { "title": "{{seriesName}} Sonraí", @@ -1856,8 +1826,8 @@ "cover-image-tab": "{{tabs.cover-tab}}", "tasks-tab": "{{tabs.tasks-tab}}", "info-tab": "{{tabs.info-tab}}", - "pages-label": "{{edit-chapter-modal.pages-count}}", - "words-label": "{{edit-chapter-modal.length-title}}", + "pages-label": "{{edit-chapter-modal.pages-label}}", + "words-label": "{{edit-chapter-modal.words-label}}", "pages-count": "{{edit-chapter-modal.pages-count}}", "words-count": "{{edit-chapter-modal.words-count}}", "reading-time-label": "{{edit-chapter-modal.reading-time-label}}", @@ -2078,7 +2048,7 @@ "reading-lists": "{{side-nav.reading-lists}}", "bookmarks": "{{side-nav.bookmarks}}", "all-series": "{{side-nav.all-series}}", - "browse-authors": "{{side-nav.browse-authors}}" + "browse-authors": "{{side-nav.browse-people}}" }, "filter-field-pipe": { "age-rating": "{{metadata-fields.age-rating-title}}", @@ -2110,7 +2080,7 @@ "writers": "{{metadata-fields.writers-title}}", "path": "Cosán", "file-path": "Cosán Comhad", - "want-to-read": "Ag Teastáil le Léamh", + "want-to-read": "Ar mhaith leat léamh", "read-date": "Dáta Léitheoireachta", "average-rating": "Meánráta", "read-last": "Léigh go deireanach" @@ -2254,7 +2224,9 @@ "webtoon-override": "Ag aistriú go mód Webtoon mar gheall ar íomhánna a léiríonn webtoon.", "scrobble-gen-init": "Cheangail post chun teagmhais scrobble a ghiniúint ó stair léitheoireachta agus rátálacha san am a chuaigh thart, agus iad á sioncronú le seirbhísí ceangailte.", "confirm-delete-multiple-volumes": "An bhfuil tú cinnte gur mian leat {{count}} imleabhar a scriosadh? Ní athróidh sé comhaid ar an diosca.", - "series-added-want-to-read": "Sraith curtha leis ón liosta Ar Mhaith Leat Léamh" + "series-added-want-to-read": "Sraith curtha leis ón liosta Ar Mhaith Leat Léamh", + "series-bound-to-reading-profile": "Sraith atá ceangailte le Próifíl Léitheoireachta {{name}}", + "library-bound-to-reading-profile": "Leabharlann ceangailte le Próifíl Léitheoireachta {{name}}" }, "read-time-pipe": { "less-than-hour": "<1 Uair", @@ -2330,7 +2302,13 @@ "reorder": "Athordú", "rename": "Athainmnigh", "rename-tooltip": "Athainmnigh an Scagaire Cliste", - "merge": "Cumaisc" + "merge": "Cumaisc", + "reading-profiles": "Próifílí Léitheoireachta", + "set-reading-profile": "Socraigh Próifíl Léitheoireachta", + "set-reading-profile-tooltip": "Ceangail Próifíl Léitheoireachta leis an Leabharlann seo", + "clear-reading-profile": "Próifíl Léitheoireachta Glan", + "clear-reading-profile-tooltip": "Glan Próifíl Léitheoireachta don Leabharlann seo", + "cleared-profile": "Próifíl Léitheoireachta Glanta" }, "preferences": { "left-to-right": "Clé go Deas", @@ -2405,7 +2383,7 @@ "theme-tab": "Téama", "devices-tab": "Gléasanna", "stats-tab": "Stait", - "scrobbling-tab": "Scrobbling", + "scrobbling-tab": "Ag scrobadh", "smart-filters-tab": "Scagairí Cliste" }, "common": { @@ -2449,19 +2427,16 @@ "volume-nums": "Imleabhair", "author-count": "{{num}} Údair", "chapter-count": "{{num}} Caibidlí", - "no-data": "Uimh Sonraí" + "no-data": "Uimh Sonraí", + "issue-count": "{{num}} Fadhbanna" }, "confirm": { "alert": "Airdeall", "confirm": "Deimhnigh", "info": "Faisnéis", "cancel": "{{common.cancel}}", - "ok": "Ceart go leor" - }, - "browse-authors": { - "title": "Brabhsáil Údair & Scríbhneoirí", - "author-count": "{{num}} Daoine", - "cover-image-description": "{{edit-series-modal.cover-image-description}}" + "ok": "Ceart go leor", + "prompt": "Ceist" }, "person-detail": { "known-for-title": "Aitheanta do", @@ -2485,7 +2460,7 @@ "role-label": "Ról", "mal-id-label": "MAL Id", "anilist-id-label": "AniList Id", - "hardcover-id-label": "Hardcover Id", + "hardcover-id-label": "Id Clúdaigh Chrua", "asin-label": "ASIN", "description-label": "Cur síos", "save": "{{common.save}}", @@ -2531,7 +2506,7 @@ "match-series-modal": { "save": "{{common.save}}", "no-results": "Ní féidir meaitseáil a aimsiú. Bain triail as an url ó sholáthraí tacaithe a chur leis agus bain triail eile as.", - "query-label": "Ceist", + "query-label": "Fiosrúchán", "title": "Meaitseáil {{ seriesName}}", "description": "Roghnaigh meaitseáil chun meiteashonraí Kavita+ a athshreangú agus imeachtaí scrobble a athghiniúint. Is féidir Don't Match a úsáid chun Kavita a shrianadh ó mheiteashonraí a mheaitseáil agus scrobadh a dhéanamh.", "close": "{{common.close}}", @@ -2565,7 +2540,9 @@ "dont-match-status-label": "{{dont-match-label}}", "library-name-header": "Leabharlann", "actions-header": "Gníomhartha", - "match-alt": "Meaitseáil {{seriesName}}" + "match-alt": "Meaitseáil {{seriesName}}", + "matched-state-label": "Stát an Chomhoiriúnaithe", + "library-type": "Cineál Leabharlainne" }, "match-series-result-item": { "details": "Féach ar an leathanach", @@ -2690,5 +2667,146 @@ "src": "Cumaisc Duine", "title": "{{personName}}", "save": "{{common.save}}" + }, + "manage-reading-profiles": { + "selection-tip": "Roghnaigh próifíl ón liosta, nó cruthaigh ceann nua ag an mbarr ar dheis", + "delete": "{{common.delete}}", + "page-splitting-tooltip": "Conas íomhá lánleithid a roinnt (i.e. cuirtear na híomhánna clé agus deas le chéile)", + "auto-close-menu-label": "Dúnadh Uathoibríoch an Roghchláir", + "scaling-option-tooltip": "Conas an íomhá a scálú chuig do scáileán.", + "pdf-spread-mode-tooltip": "Leagan amach leathanaigh. Aonair nó dúbailte (corr/cothrom)", + "writing-style-tooltip": "Athraíonn sé treo an téacs. Is é an treo cothrománach ná ó chlé go deas, agus is é an treo ingearach ná ó bharr go bun.", + "reading-profile-library-settings-title": "Leabharlann", + "pdf-spread-mode-label": "Mód Scaipthe", + "layout-mode-book-tooltip": "Leagan amach an ábhair. Is é an scrollú an chaoi a bhfuil sé pacáilte sa leabhar. Oireann 1 nó 2 cholún d'airde an fheiste agus oireann sé do 1 nó 2 cholún téacs in aghaidh an leathanaigh", + "layout-mode-tooltip": "Rindreáil íomhá aonair ar an scáileán nó dhá íomhá taobh le taobh", + "color-theme-book-tooltip": "Cén téama datha atá le cur i bhfeidhm ar ábhar agus roghchlár an léitheora leabhar", + "layout-mode-label": "Mód Leagan Amach", + "description": "B’fhéidir nach léifear do shraitheanna uile ar an mbealach céanna, mar sin socraigh próifílí léitheoireachta ar leith in aghaidh an leabharlainne nó na sraithe chun go mbeidh sé chomh héasca agus is féidir filleadh ar do shraith.", + "profiles-title": "Do phróifílí léitheoireachta", + "default-profile": "Réamhshocrú", + "add": "{{common.add}}", + "add-tooltip": "Sábhálfar do phróifíl nua tar éis athrú a dhéanamh uirthi", + "make-default": "Socraigh mar réamhshocrú", + "image-reader-settings-title": "Léitheoir Íomhánna", + "scaling-option-label": "Roghanna Scálúcháin", + "page-splitting-label": "Scoilteadh Leathanaigh", + "reading-mode-label": "Mód Léitheoireachta", + "reading-mode-tooltip": "Athraigh an léitheoir chun leathanaigh a roinnt go hingearach, go cothrománach, nó scrolláil gan teorainn a bheith aige", + "background-color-tooltip": "Dath Cúlra an Léitheora Íomhá", + "auto-close-menu-tooltip": "Ar cheart go ndúnfadh an roghchlár go huathoibríoch", + "show-screen-hints-label": "Taispeáin Leideanna Scáileáin", + "show-screen-hints-tooltip": "Taispeáin forleagan chun cabhrú le limistéar agus treo na leathanaigh a thuiscint", + "emulate-comic-book-label": "Aithris leabhar grinn", + "emulate-comic-book-tooltip": "Cuireann éifeacht scátha i bhfeidhm chun léamh ó leabhar a aithris", + "swipe-to-paginate-label": "Svaidhpeáil chun leathanaigh a athrú", + "allow-auto-webtoon-reader-label": "Mód Léitheora Gréasáin Uathoibríoch", + "allow-auto-webtoon-reader-tooltip": "Athraigh go mód Léitheora Gréasáin má tá cuma gréasáin ar leathanaigh. D’fhéadfadh roinnt torthaí dearfacha bréagacha tarlú.", + "width-override-label": "{{manga-reader.width-override-label}}", + "reset": "{{common.reset}}", + "book-reader-settings-title": "Léitheoir Leabhar", + "tap-to-paginate-label": "Tapáil chun Leathanachú", + "tap-to-paginate-tooltip": "An gceadaíonn taobhanna scáileán an léitheora leabhar tapáil air chun bogadh go dtí an leathanach roimhe/an chéad leathanach eile", + "immersive-mode-label": "Mód Tumtha", + "immersive-mode-tooltip": "Cuirfidh sé seo an roghchlár i bhfolach taobh thiar de chliceáil ar an doiciméad léitheora agus casfaidh sé sconna chun leathanach a chur ar siúl", + "reading-direction-book-label": "Treo Léitheoireachta", + "reading-direction-book-tooltip": "Treo le cliceáil chun bogadh go dtí an chéad leathanach eile. Ciallaíonn Deas go Clé go gcliceálann tú ar thaobh na láimhe clé den scáileán chun bogadh go dtí an chéad leathanach eile.", + "font-family-label": "Teaghlach Clónna", + "writing-style-label": "Stíl Scríbhneoireachta", + "layout-mode-book-label": "Mód Leagan Amach", + "font-family-tooltip": "Teaghlach clónna le luchtú. Luchtófar cló réamhshocraithe an leabhair leis an rogha réamhshocraithe", + "color-theme-book-label": "Téama Dathanna", + "font-size-book-label": "Méid Cló", + "font-size-book-tooltip": "Céatadán den scálú le cur i bhfeidhm ar an gcló sa leabhar", + "line-height-book-label": "Spásáil Líne", + "line-height-book-tooltip": "Cé mhéad spás idir línte an leabhair", + "margin-book-label": "Corrlach", + "pdf-reader-settings-title": "Léitheoir PDF", + "pdf-scroll-mode-label": "Mód Scrollaigh", + "reading-profile-series-settings-title": "Sraith", + "pdf-theme-label": "Téama", + "no-selected": "Níl aon phróifíl roghnaithe", + "reading-direction-label": "Treo Léitheoireachta", + "reading-direction-tooltip": "Treo le cliceáil chun bogadh go dtí an chéad leathanach eile. Ciallaíonn Deas go Clé go gcliceálann tú ar thaobh na láimhe clé den scáileán chun bogadh go dtí an chéad leathanach eile.", + "extra-tip": "Sannadh próifílí léitheoireachta tríd an roghchlár gníomhaíochta ar shraitheanna agus ar leabharlanna, nó i mórchóir. Agus socruithe á n-athrú i léitheoir, cruthaítear próifíl fholaithe a chuimhníonn ar do roghanna don tsraith sin (ní do pdfanna). Baintear an phróifíl seo nuair a shannann tú ceann de do phróifílí léitheoireachta féin don tsraith. Is féidir tuilleadh eolais a fháil ar an", + "confirm": "An bhfuil tú cinnte gur mian leat an phróifíl léitheoireachta {{name}} a scriosadh?", + "margin-book-tooltip": "Cé mhéad spásála ar gach taobh den scáileán. Sáróidh sé seo go 0 ar ghléasanna soghluaiste beag beann ar an socrú seo.", + "swipe-to-paginate-tooltip": "An gcuirfí an leathanach seo chugainn nó an leathanach roimhe sin i ngníomh mar gheall ar shlaipeadh ar an scáileán?", + "pdf-theme-tooltip": "Téama dathanna an léitheora", + "background-color-label": "Dath an Chúlra", + "pdf-scroll-mode-tooltip": "Conas a scrollaíonn tú trí leathanaigh. Ingearach/Cothrománach agus Tapáil chun Leathanachú (gan scrolláil)", + "width-override-tooltip": "Sáraigh leithead na n-íomhánna sa léitheoir", + "wiki-title": "vicí", + "disable-width-override-label": "Díchumasaigh sárú leithead", + "disable-width-override-tooltip": "Cosc a chur ar an sárú leithead ó bheith i bhfeidhm nuair a bhíonn do scáileán ar a laghad ag an bpointe briste cumraithe nó níos lú" + }, + "bulk-set-reading-profile-modal": { + "filter-label": "{{common.filter}}", + "clear": "{{common.clear}}", + "close": "{{common.close}}", + "no-data": "Níl aon bhailiúcháin cruthaithe fós", + "create": "{{common.create}}", + "bound": "Ceangailte", + "title": "Socraigh próifíl Léitheoireachta", + "loading": "{{common.loading}}" + }, + "browse-people": { + "author-count": "{{num}} Daoine", + "cover-image-description": "{{edit-series-modal.cover-image-description}}", + "issue-count": "{{common.issue-count}}", + "series-count": "{{common.series-count}}", + "sort-label": "Sórtáil", + "name-label": "Ainm", + "series-count-label": "Líon na Sraitheanna", + "roles-label": "Róil", + "title": "Brabhsáil Daoine", + "issue-count-label": "Líon na nEisiúintí" + }, + "browse-genres": { + "series-count": "{{common.series-count}}", + "genre-count": "Seánraí {{num}}", + "issue-count": "{{common.issue-count}}", + "title": "Brabhsáil Seánraí" + }, + "browse-tags": { + "title": "Brabhsáil Clibeanna", + "genre-count": "Clibeanna {{num}}", + "series-count": "{{common.series-count}}", + "issue-count": "{{common.issue-count}}" + }, + "browse-title-pipe": { + "publication-status": "Saothar {{value}}", + "user-rating": "Rátáil réalta {{value}}", + "tag": "Tá Clib {{value}} aige", + "character": "Tá carachtar {{value}} ann", + "editor": "Eagarthóireacht déanta ag {{value}}", + "letterer": "Litrithe ag {{value}}", + "penciller": "Pencilithe ag {{value}}", + "writer": "Scríofa ag {{value}}", + "genre": "Tá Seánra {{value}} aige", + "release-year": "Scaoilte i {{value}}", + "imprint": "Inphriontáil de {{value}}", + "library": "Laistigh de leabharlann {{value}}", + "team": "Foireann {{value}}", + "age-rating": "Rátáil {{value}}", + "publisher": "Foilsithe ag {{value}}", + "inker": "Dúigh le {{value}}", + "format": "Formáid {{value}}", + "translator": "Aistrithe ag {{value}}", + "colorist": "Daite ag {{value}}", + "location": "I suíomh {{value}}", + "artist": "Tarraingthe ag {{value}}" + }, + "generic-filter-field-pipe": { + "person-role": "Ról", + "person-series-count": "Líon na Sraitheanna", + "person-chapter-count": "Líon na gCaibidlí", + "person-name": "Ainm" + }, + "breakpoint-pipe": { + "never": "Choíche", + "mobile": "Soghluaiste", + "tablet": "Táibléad", + "desktop": "Deasc" } } diff --git a/UI/Web/src/assets/langs/hu.json b/UI/Web/src/assets/langs/hu.json index 53c9632cd..7fae9daef 100644 --- a/UI/Web/src/assets/langs/hu.json +++ b/UI/Web/src/assets/langs/hu.json @@ -25,7 +25,8 @@ "cancel": "{{common.cancel}}", "saving": "Mentés…", "update": "Frissítés", - "account-detail-title": "Fiók Részletei" + "account-detail-title": "Fiók Részletei", + "invalid-email-warning": "Érvénytelen email esetén a Kavita néhány funkciója zárolva lesz" }, "user-scrobble-history": { "title": "Feldolgozás történet", @@ -44,7 +45,10 @@ "not-applicable": "Nem alkalmazható", "processed": "Feldolgozva", "not-processed": "Nincs feldolgozva", - "special": "{{entity-title.special}}" + "special": "{{entity-title.special}}", + "scrobbling-disabled": "A scrobbling ki van kapcsolva a Fiók beállításaidban.", + "description": "Itt található minden a fiókodhoz tartozó srobble esemény. Ahhoz, hogy az események létrejöjjenek, be kell állítanod egy scrobble szolgáltatót. A feldolgozott események egy hónap után törlődnek. Ha vannak fel nem dolgozott események, azok adatait valószínűleg nem sikerült egyeztetni a feltöltés során. Ilyen esetben vedd fel a kapcsolatot az adminoddal, hogy javításra kerüljenek.", + "token-expired": "Az AniList tokened lejárt! A scrobble események nem kerülnek feldolgozásra, amíg a Fiókok oldalon nem frissíted." }, "scrobble-event-type-pipe": { "chapter-read": "Olvasás folyamata", @@ -66,7 +70,8 @@ "your-review": "A te áttekintésed", "external-review": "Külső áttekintés", "local-review": "Helyi áttekintés", - "rating-percentage": "Értékelés {{r}}%" + "rating-percentage": "Értékelés {{r}}%", + "critic": "kritikus" }, "want-to-read": { "title": "Olvasandó", @@ -82,7 +87,6 @@ "stats-tab": "{{tabs.stats-tab}}", "scrobbling-tab": "{{tabs.scrobbling-tab}}", "smart-filters-tab": "{{tabs.smart-filters-tab}}", - "background-color-label": "Háttérszín", "reset": "{{common.reset}}", "save": "{{common.save}}", "locale-tooltip": "Kavita által használt nyelv", @@ -91,7 +95,19 @@ "disable-animations-tooltip": "Oldalon lévő animációk kikapcsolása. Hasznos az e-ink olvasóknak.", "prompt-on-download-label": "Letöltés esetében kérdezzen", "prompt-on-download-tooltip": "Kérdezzen, amikor a letöltés mérete túlhaladja a {{size}}MB-ot", - "collapse-series-relationships-label": "Sorozat Kapcsolatok Összezárása" + "collapse-series-relationships-label": "Sorozat Kapcsolatok Összezárása", + "title": "Felhasználó irányítópultja", + "page-layout-mode-label": "Oldal elrendezési mód", + "global-settings-title": "Globális beállítások", + "blur-unread-summaries-tooltip": "Elmossa a kötetek vagy fejezetek összefoglaló szövegét, ha még nem olvastad (a spoilerek elkerülése érdekében)", + "kavitaplus-settings-title": "Kavita+", + "page-layout-mode-tooltip": "Elemek mutatása kártyákként vagy listanézetben a Sorozat részletei oldalon.", + "anilist-scrobbling-tooltip": "Scrobble (egyirányú szinkronizáció) engedélyezése a Kavitából az AniList felé (olvasási előrehaladottság és kritikák)", + "pref-description": "Ezek olyan globális beállítások, melyek a fiókodhoz vannak rendelve.", + "success-toast": "Felhasználó beállításai frissítve", + "locale-label": "Nyelv", + "anilist-scrobbling-label": "AniList Scrobbling", + "want-to-read-sync-label": "Olvasási várólista szinkronizálás" }, "user-holds": { "no-data": "{{typeahead.no-data}}" @@ -721,5 +737,18 @@ }, "actionable": { "clear": "{{common.clear}}" + }, + "reviews": { + "user-reviews-local": "Helyi kritikák", + "user-reviews-plus": "Külsős kritikák" + }, + "review-modal": { + "title": "Kritika szerkesztése", + "review-label": "Kritika", + "close": "{{common.close}}", + "save": "{{common.save}}", + "min-length": "A kritika minimum {{count}} karakter hosszúságú kell legyen", + "delete": "{{common.delete}}", + "required": "{{validation.required-field}}" } } diff --git a/UI/Web/src/assets/langs/id.json b/UI/Web/src/assets/langs/id.json index 5843bf7da..490cd5f9f 100644 --- a/UI/Web/src/assets/langs/id.json +++ b/UI/Web/src/assets/langs/id.json @@ -83,39 +83,6 @@ "collapse-series-relationships-tooltip": "Haruskah Kavita menampilkan Seri yang tidak memiliki hubungan atau yang merupakan induk/prekuel", "share-series-reviews-label": "Bagikan Ulasan Seri", "share-series-reviews-tooltip": "Haruskah Kavita menyertakan ulasan Anda tentang Seri untuk pengguna lain", - "image-reader-settings-title": "Pembaca Gambar", - "reading-direction-label": "Orientasi Baca", - "reading-direction-tooltip": "Arah untuk mengklik agar beralih ke halaman berikutnya. Dari Kanan ke Kiri berarti Anda mengklik pada sisi kiri layar untuk beralih ke halaman berikutnya.", - "scaling-option-label": "Opsi Skala", - "scaling-option-tooltip": "Bagaimana cara menyesuaikan skala gambar ke layar Anda.", - "page-splitting-label": "Pemisahan Halaman", - "page-splitting-tooltip": "Bagaimana cara memisahkan gambar yang lebarnya penuh (gambar kiri dan kanan digabungkan)", - "reading-mode-label": "Mode Membaca", - "layout-mode-label": "Mode Tata Letak", - "layout-mode-tooltip": "Tampilkan satu gambar ke layar atau dua gambar berdampingan", - "background-color-label": "Warna Latar Belakang", - "auto-close-menu-label": "Tutup Menu Otomatis", - "show-screen-hints-label": "Tampilkan Petunjuk Layar", - "emulate-comic-book-label": "Emulasikan buku komik", - "swipe-to-paginate-label": "Geser untuk Memutar Halaman", - "book-reader-settings-title": "Pembaca Buku", - "tap-to-paginate-label": "Ketuk untuk Memutar Halaman", - "tap-to-paginate-tooltip": "Haruskan sisi layar pembaca buku bisa diketuk untuk pindah ke halaman sebelumnya/berikutnya", - "immersive-mode-label": "Mode Imersif", - "immersive-mode-tooltip": "Ini akan menyembunyikan menu dengan mengklik dokumen pembaca dan mengaktifkan ketuk untuk memutar halaman", - "reading-direction-book-label": "Arah Baca", - "reading-direction-book-tooltip": "Cara mengklik untuk beralih ke halaman berikutnya. Dari Kanan ke Kiri berarti mengklik di sisi kiri layar.", - "font-family-label": "Jenis Font", - "font-family-tooltip": "Jenis Font untuk dimuat. Bawaan akan memuat font bawaan buku", - "writing-style-label": "Gaya Penulisan", - "writing-style-tooltip": "Mengubah arah teks. Horizontal dari kiri ke kanan, vertikal dari atas ke bawah.", - "layout-mode-book-label": "Mode Tata Letak", - "layout-mode-book-tooltip": "Bagaimana konten harus ditata. Gulir itu sesuai dengan bawaan buku. 1 atau 2 Kolom itu sesuai dengan tinggi perangkat dan dimuat dalam 1 atau 2 kolom teks per halaman", - "color-theme-book-label": "Warna Tema", - "color-theme-book-tooltip": "Warna tema apa yang akan diterapkan pada konten pembaca buku dan menu", - "font-size-book-label": "Ukuran Font", - "line-height-book-label": "Jarak Baris", - "line-height-book-tooltip": "Berapa banyak spasi antara baris dalam buku", "reset": "{{common.reset}}", "save": "{{common.save}}" }, diff --git a/UI/Web/src/assets/langs/it.json b/UI/Web/src/assets/langs/it.json index bfee2d3c8..98fa183dd 100644 --- a/UI/Web/src/assets/langs/it.json +++ b/UI/Web/src/assets/langs/it.json @@ -106,63 +106,14 @@ "collapse-series-relationships-tooltip": "Kavita dovrebbe mostrare serie che non hanno relazioni o è il genitore", "share-series-reviews-label": "Condividi le recensioni della serie", "share-series-reviews-tooltip": "Kavita dovrebbe includere le tue recensioni di serie per altri utenti", - "image-reader-settings-title": "Lettore di immagini", - "reading-direction-label": "Direzione Lettura", - "reading-direction-tooltip": "Direzione da cliccare per passare alla pagina successiva. Da destra a sinistra significa che fai clic sul lato sinistro dello schermo per passare alla pagina successiva.", - "scaling-option-label": "Opzioni di ridimensionamento", - "scaling-option-tooltip": "Come ridimensionare l'immagine sullo schermo.", - "page-splitting-label": "Divisione pagine", - "page-splitting-tooltip": "Come dividere un'immagine a tutta larghezza (cioè entrambe le immagini sinistra e destra sono combinate)", - "reading-mode-label": "Modalità lettura", - "layout-mode-label": "Modalità Layout", - "layout-mode-tooltip": "Renderizza una singola immagine sullo schermo o due immagini affiancate", - "background-color-label": "Colore di sfondo", - "auto-close-menu-label": "Menu di chiusura automatica", - "show-screen-hints-label": "Mostra suggerimenti sullo schermo", - "emulate-comic-book-label": "Emula fumetto", - "swipe-to-paginate-label": "Scorri per impaginare", - "book-reader-settings-title": "Lettore di Libri", - "tap-to-paginate-label": "Tocca per impaginare", - "tap-to-paginate-tooltip": "Se i lati dello schermo del lettore di libri consentono di toccarlo per passare alla pagina precedente/successiva", - "immersive-mode-label": "Modalità Immersiva", - "immersive-mode-tooltip": "Questo nasconderà il menu. Un clic sulla pagina del lettore e si attiverà il \"tocca per impaginare\"", - "reading-direction-book-label": "Direzione di lettura", - "reading-direction-book-tooltip": "Direzione da cliccare per passare alla pagina successiva. Da destra a sinistra significa che fai clic sul lato sinistro dello schermo per passare alla pagina successiva.", - "font-family-label": "Tipologia di Font", - "font-family-tooltip": "Tipologia di caratteri da caricare. Predefinito caricherà il carattere predefinito del libro", - "writing-style-label": "Stile di Scrittura", - "writing-style-tooltip": "Cambia la direzione del testo. Orizzontale è da sinistra a destra, verticale è dall'alto verso il basso.", - "layout-mode-book-label": "Modalità di disposizione delle pagine", - "layout-mode-book-tooltip": "Come devono essere strutturati i contenuti. Sfoglia è come lo presenta il libro. 1 o 2 colonne adatta all'altezza del dispositivo ed utilizza 1 o 2 colonne di testo per pagina", - "color-theme-book-label": "Tema Colore", - "color-theme-book-tooltip": "Quale tema applicare al contenuto e al menu del lettore di libri", - "font-size-book-label": "Dimensioni del carattere", - "line-height-book-label": "Spaziatura interlinea", - "line-height-book-tooltip": "Quanta spaziatura tra le righe del libro", - "margin-book-label": "Margine", - "margin-book-tooltip": "Quanto margine lasciare su ciascun lato dello schermo. Sui dispositivi mobili verrà ignorato il valore, indipendentemente da questo settaggio.", - "pdf-reader-settings-title": "Lettore PDF", - "pdf-scroll-mode-label": "Modalità Scorrimento", - "pdf-scroll-mode-tooltip": "Come scorrere le pagine. Verticalmente/Orizzontalmente e Premi per cambiare pagina (nessuno scorrimento)", - "pdf-spread-mode-label": "Modalità Due Pagine", - "pdf-spread-mode-tooltip": "Come dovrebbero essere disposte le pagine. Singole o doppie (pari/dispari)", - "pdf-theme-label": "Tema", "clients-opds-alert": "OPDS non è abilitato su questo server. Ciò non influirà sugli utenti Tachiyomi.", "clients-opds-description": "Tutti i client di terze parti utilizzeranno la chiave API o l'URL di connessione di seguito. Sono come password, tienile private.", "clients-api-key-tooltip": "La chiave API è come una password. Reimpostandolo, tutti i client esistenti verranno invalidati.", "clients-opds-url-tooltip": "Visualizza un elenco dei client OPDS supportati: ", "reset": "{{common.reset}}", "save": "{{common.save}}", - "reading-mode-tooltip": "Cambia il lettore per impaginare verticalmente, orizzontalmente o avere uno scorrimento infinito", - "show-screen-hints-tooltip": "Mostra una sovrapposizione per aiutare a comprendere l'area e la direzione della paginazione", - "emulate-comic-book-tooltip": "Applica un effetto ombra per emulare la lettura di un libro", - "swipe-to-paginate-tooltip": "Lo scorrimento sullo schermo dovrebbe causare l'attivazione della pagina successiva o precedente", - "pdf-theme-tooltip": "Tema colore del lettore", "clients-opds-url-label": "URL OPDS", "clients-api-key-label": "Chiave API", - "background-color-tooltip": "Colore di sfondo del lettore di immagini", - "auto-close-menu-tooltip": "Il menu dovrebbe chiudersi automaticamente", - "font-size-book-tooltip": "Percentuale di ridimensionamento da applicare al font nel libro", "kavitaplus-settings-title": "Kavita+", "anilist-scrobbling-tooltip": "Permetti a Kavita di fare Scrobble (sincronizzazione unidirezionale) sul progresso della lettura e sulle valutazioni verso AniList", "anilist-scrobbling-label": "Scrobbling AniList", @@ -840,8 +791,7 @@ "donate-tooltip": "Puoi rimuoverlo iscrivendoti a Kavita+", "back": "Indietro", "more": "Altro", - "customize": "{{settings.customize}}", - "browse-authors": "Sfoglia gli Autori" + "customize": "{{settings.customize}}" }, "library-settings-modal": { "close": "{{common.close}}", @@ -1593,7 +1543,6 @@ }, "manga-reader": { "back": "Indietro", - "save-globally": "Salva Globalmente", "incognito-alt": "La modalità di navigazione in incognito è attiva. Attiva/disattiva per disattivare.", "incognito-title": "Modalità Incognito:", "shortcuts-menu-alt": "Scorciatoie da tastiera modale", @@ -1626,7 +1575,6 @@ "layout-mode-switched": "La modalità layout è passata a Singola a causa dello spazio insufficiente per eseguire il rendering del layout doppio", "no-next-chapter": "Nessun prossimo capitolo", "no-prev-chapter": "Nessun capitolo precedente", - "user-preferences-updated": "Preferenze utente aggiornate", "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", "series-progress": "Avanzamento della serie: {{percentage}}", "off": "Spegni", @@ -2501,11 +2449,6 @@ "info-tab": "Info", "metadata-tab": "Metadati" }, - "browse-authors": { - "title": "Sfoglia Autori e Scrittori", - "author-count": "{{num}} Persone", - "cover-image-description": "{{edit-series-modal.cover-image-description}}" - }, "edit-person-modal": { "title": "{{personName}} Dettagli", "general-tab": "{{edit-series-modal.general-tab}}", diff --git a/UI/Web/src/assets/langs/ja.json b/UI/Web/src/assets/langs/ja.json index 0aa935eee..5e8196f91 100644 --- a/UI/Web/src/assets/langs/ja.json +++ b/UI/Web/src/assets/langs/ja.json @@ -105,55 +105,6 @@ "collapse-series-relationships-tooltip": "Kavita上で関連するシリーズを表示する", "share-series-reviews-label": "レビューを共有する", "share-series-reviews-tooltip": "Kavitaはレビューを他のユーザーに共有する", - "image-reader-settings-title": "画像リーダー", - "reading-direction-label": "綴じ方向", - "reading-direction-tooltip": "次のページに進む場合のクリック場所。右から左にめくる場合は左端をクリックする。", - "scaling-option-label": "スケーリング設定", - "scaling-option-tooltip": "画像の表示を画面に合わせる方法。", - "page-splitting-label": "ページ分割", - "page-splitting-tooltip": "分割されたページを見開きとして表示する", - "reading-mode-label": "読書モード", - "reading-mode-tooltip": "リーダーを垂直に、水平方向に、または無限スクロールを持つように変更", - "layout-mode-label": "レイアウトモード", - "layout-mode-tooltip": "画面に単一の画像をレンダリングするか、横に並べた2つの画像をレンダリングします", - "background-color-label": "背景色", - "background-color-tooltip": "画像リーダーの背景色", - "auto-close-menu-label": "オートクローズメニュー", - "auto-close-menu-tooltip": "メニュー自動閉鎖", - "show-screen-hints-label": "画面ヒントを表示する", - "show-screen-hints-tooltip": "パジネーションエリアと方向を理解するためのオーバーレイを表示", - "emulate-comic-book-label": "コミックのエミュレート", - "emulate-comic-book-tooltip": "書籍から読書をエミュレートする影効果を適用", - "swipe-to-paginate-label": "スワイプでめくる", - "swipe-to-paginate-tooltip": "画面上にスワイプすると、次のページまたは前のページがトリガーされるようになります", - "book-reader-settings-title": "ブックリーダー", - "tap-to-paginate-label": "タップしてめくる", - "tap-to-paginate-tooltip": "画面の端をタップして前/次のページに移動できるようにする", - "immersive-mode-label": "没入モード", - "immersive-mode-tooltip": "リーダードキュメント上のクリックでメニューを非表示にし、タップでページ送りが有効になります", - "reading-direction-book-label": "読書方向", - "reading-direction-book-tooltip": "次のページに移動するにはどちらをクリックしますか?右から左の場合は、次のページに移動するために画面の左側をクリックします。", - "font-family-label": "フォントファミリー", - "font-family-tooltip": "フォントファミリーを読み込みます。デフォルトでは、本のデフォルトのフォントが読み込まれます", - "writing-style-label": "ライティングスタイル", - "writing-style-tooltip": "縦書き、横書きを切り替えます。水平は左から右、垂直は上から下です。", - "layout-mode-book-label": "レイアウトモード", - "layout-mode-book-tooltip": "コンテンツの表示方法を選択します。\"Scroll\"は本のままスクロール表示されます。1または2列はデバイスの高さに合わせ、1ページに1または2列のテキストが収まります", - "color-theme-book-label": "カラー テーマ:", - "color-theme-book-tooltip": "ブックリーダーのコンテンツとメニューに適用するカラーテーマ", - "font-size-book-label": "文字サイズ", - "font-size-book-tooltip": "本のフォントに適用するスケーリングの割合", - "line-height-book-label": "行間隔", - "line-height-book-tooltip": "書籍の行間の間隔", - "margin-book-label": "余白", - "margin-book-tooltip": "画面の各側面にどれだけの余白を設定しますか?この設定に関わらず、モバイルデバイスでは余白は0になります。", - "pdf-reader-settings-title": "PDFリーダー", - "pdf-scroll-mode-label": "スクロールモード", - "pdf-scroll-mode-tooltip": "ページをスクロールする方法 縦/横/タップでパギン(スクロールなし)", - "pdf-spread-mode-label": "見開き設定", - "pdf-spread-mode-tooltip": "ページのレイアウト。単一または見開き (奇数・偶数)", - "pdf-theme-label": "テーマ", - "pdf-theme-tooltip": "読者の色テーマ", "clients-opds-alert": "このサーバではOPDSが有効になっていません。 立ち読みユーザーには影響しません.", "clients-opds-description": "外部クライアントを使用する場合は以下のAPIもしくは接続用URLを使用してください。これはパスワードのようなものであるため、公開しないでください。", "clients-api-key-tooltip": "API キーはパスワードのようになります。 リセットすると、既存のクライアントが無効になります.", @@ -1449,7 +1400,6 @@ }, "manga-reader": { "back": "戻る", - "save-globally": "グローバルに保存", "incognito-alt": "シークレットモードがオンです。切り替えるとオフになります。", "incognito-title": "シークレットモード:", "shortcuts-menu-alt": "キーボードショートカットモデル", @@ -1482,7 +1432,6 @@ "layout-mode-switched": "レイアウトモードが見開きレイアウトをレンダリングするのに十分な余白がないため、単一レイアウトに切り替えられました。", "no-next-chapter": "次の章はありません", "no-prev-chapter": "前の章はありません", - "user-preferences-updated": "ユーザー環境設定を更新しました", "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", "series-progress": "シリーズの進捗: {{percentage}}" }, diff --git a/UI/Web/src/assets/langs/ko.json b/UI/Web/src/assets/langs/ko.json index 449fdcae2..42a181356 100644 --- a/UI/Web/src/assets/langs/ko.json +++ b/UI/Web/src/assets/langs/ko.json @@ -107,55 +107,6 @@ "collapse-series-relationships-tooltip": "Kavita는 관계가 없거나 상위/속편인 시리즈를 표시해야 합니까", "share-series-reviews-label": "시리즈 리뷰 공유", "share-series-reviews-tooltip": "Kavita에 다른 사용자를 위한 시리즈 리뷰를 포함합니까", - "image-reader-settings-title": "이미지 리더", - "reading-direction-label": "읽는 방향", - "reading-direction-tooltip": "다음 페이지로 이동할 때 클릭해야 하는 방향입니다. 오른쪽에서 왼쪽은 화면 왼쪽을 클릭하여 다음 페이지로 이동하는 것을 의미합니다.", - "scaling-option-label": "스케일링 옵션", - "scaling-option-tooltip": "화면에 맞게 이미지 크기를 조정하는 방법.", - "page-splitting-label": "페이지 분할", - "page-splitting-tooltip": "전체 너비 이미지를 분할하는 방법(예: 왼쪽 및 오른쪽 이미지가 모두 결합됨)", - "reading-mode-label": "읽기 모드", - "reading-mode-tooltip": "세로, 가로로 페이지를 매기거나 무한 스크롤이 가능하도록 리더를 변경하세요", - "layout-mode-label": "레이아웃 모드", - "layout-mode-tooltip": "단일 이미지를 화면에 렌더링하거나 두 개의 이미지를 나란히 렌더링합니다", - "background-color-label": "배경색", - "background-color-tooltip": "이미지 리더의 배경색", - "auto-close-menu-label": "자동 메뉴 닫기", - "auto-close-menu-tooltip": "메뉴가 자동으로 닫혀야 하나요", - "show-screen-hints-label": "화면에 힌트 표시", - "show-screen-hints-tooltip": "페이지 매기기 영역과 방향을 이해하는 데 도움이 되는 오버레이 표시", - "emulate-comic-book-label": "만화책 에뮬레이션", - "emulate-comic-book-tooltip": "책 읽기를 에뮬레이트하기 위해 그림자 효과를 적용합니다", - "swipe-to-paginate-label": "스와이프하여 페이지 넘김", - "swipe-to-paginate-tooltip": "화면을 스와이프하면 다음 페이지나 이전 페이지가 실행되어야 합니다", - "book-reader-settings-title": "북 리더", - "tap-to-paginate-label": "탭하여 페이지 넘김", - "tap-to-paginate-tooltip": "북 리더 화면의 측면을 탭하여 이전/다음 페이지로 이동할 수 있습니다", - "immersive-mode-label": "몰입 모드", - "immersive-mode-tooltip": "문서를 클릭하고 탭할 때 페이지 넘기기가 켜져 있으면 메뉴가 숨겨집니다", - "reading-direction-book-label": "읽는 방향", - "reading-direction-book-tooltip": "다음 페이지로 이동할 때 클릭해야 하는 방향입니다. 오른쪽에서 왼쪽은 화면 왼쪽을 클릭하여 다음 페이지로 이동하는 것을 의미합니다.", - "font-family-label": "글꼴", - "font-family-tooltip": "로드할 글꼴 모음입니다. 기본값은 책의 기본 글꼴을 로드합니다", - "writing-style-label": "작문 스타일", - "writing-style-tooltip": "텍스트의 방향을 변경합니다. 가로는 왼쪽에서 오른쪽으로, 세로는 위에서 아래로.", - "layout-mode-book-label": "레이아웃 모드", - "layout-mode-book-tooltip": "콘텐츠를 어떻게 배치할지 정합니다 스크롤은 책에 포함된 대로입니다. 1 또는 2 열은 장치의 높이에 맞게 페이지당 1 또는 2 열의 텍스트가 들어갑니다", - "color-theme-book-label": "색상 테마", - "color-theme-book-tooltip": "북 리더 콘텐츠 및 메뉴에 적용할 색상 테마", - "font-size-book-label": "글꼴 크기", - "font-size-book-tooltip": "책의 글꼴에 적용할 크기 조정 비율", - "line-height-book-label": "줄 간격", - "line-height-book-tooltip": "줄 사이의 간격", - "margin-book-label": "여백", - "margin-book-tooltip": "화면 양쪽의 간격입니다. 이 설정과 상관없이 모바일 장치에서는 0으로 재정의됩니다.", - "pdf-reader-settings-title": "PDF리더", - "pdf-scroll-mode-label": "스크롤 모드", - "pdf-scroll-mode-tooltip": "페이지를 스크롤하는 방법. 수직/수평 및 탭하여 페이지 매김(스크롤 없음)", - "pdf-spread-mode-label": "스프레드 모드", - "pdf-spread-mode-tooltip": "페이지를 배치하는 방법. 싱글 또는 더블(홀수/짝수)", - "pdf-theme-label": "테마", - "pdf-theme-tooltip": "리더의 색상 테마", "clients-opds-alert": "이 서버에서 OPDS를 사용할 수 없습니다. 이것은 Tachiyomi 사용자에게 영향을 미치지 않습니다.", "clients-opds-description": "모든 서드파티 클라이언트는 API 키 또는 아래의 연결 URL을 사용합니다. 이것은 암호와 같으므로 비공개로 유지하십시오.", "clients-api-key-tooltip": "API 키는 비밀번호와 같습니다. 재설정하면 기존 클라이언트가 모두 무효화됩니다.", @@ -168,9 +119,7 @@ "anilist-scrobbling-tooltip": "Kavita가 AniList에 읽기 진행 상황 및 평가를 스크로블(단방향 동기화)하도록 허용", "want-to-read-sync-label": "읽고 싶은 동기화", "want-to-read-sync-tooltip": "Kavita가 AniList 및 MAL의 보류 중인 읽기 목록에 있는 시리즈를 기반으로 '읽고 싶은 목록'에 항목을 추가하도록 허용합니다", - "kavitaplus-settings-title": "Kavita+", - "allow-auto-webtoon-reader-label": "자동 웹툰 리더 모드", - "allow-auto-webtoon-reader-tooltip": "페이지가 웹툰 형식으로 판단될 경우 웹툰 리더 모드로 전환합니다. 일부 잘못된 전환이 일어날 수 있습니다." + "kavitaplus-settings-title": "Kavita+" }, "user-holds": { "title": "스크러블 홀드", @@ -851,7 +800,6 @@ "back": "뒤로가기", "more": "더 보기", "customize": "{{settings.customize}}", - "browse-authors": "작가 찾아보기", "edit": "{{common.edit}}", "cancel-edit": "재정렬 닫기" }, @@ -1624,7 +1572,6 @@ }, "manga-reader": { "back": "뒤로가기", - "save-globally": "전역 설정", "incognito-alt": "시크릿 모드가 켜져 있습니다. 끄려면 전환하세요.", "incognito-title": "시크릿 모드:", "shortcuts-menu-alt": "키보드 단축키 모달", @@ -1659,7 +1606,6 @@ "layout-mode-switched": "이중 레이아웃을 렌더링할 공간이 부족하여 레이아웃 모드가 단일로 전환됨", "no-next-chapter": "다음 챕터 없음", "no-prev-chapter": "이전 챕터 없음", - "user-preferences-updated": "사용자 기본 설정 업데이트됨", "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", "series-progress": "시리즈 진행률: {{percentage}}" }, @@ -2455,11 +2401,6 @@ "cancel": "{{common.cancel}}", "ok": "Ok" }, - "browse-authors": { - "author-count": "{{num}} 사람", - "cover-image-description": "{{edit-series-modal.cover-image-description}}", - "title": "저자 및 작가 찾아보기" - }, "person-detail": { "known-for-title": "알려진", "individual-role-title": "{{role}} 로서", diff --git a/UI/Web/src/assets/langs/nl.json b/UI/Web/src/assets/langs/nl.json index 57f96555a..d9a3a4df1 100644 --- a/UI/Web/src/assets/langs/nl.json +++ b/UI/Web/src/assets/langs/nl.json @@ -106,41 +106,6 @@ "collapse-series-relationships-tooltip": "Moet Kavita series laten zien die geen relaties hebben of die geen ouder / prequel is", "share-series-reviews-label": "Deel serierecensies", "share-series-reviews-tooltip": "Moet Kavita jouw beoordelingen van Series voor andere gebruikers gebruiken", - "image-reader-settings-title": "Afbeeldingslezer", - "reading-direction-label": "Leesrichting", - "reading-direction-tooltip": "Richting om te klikken om naar de volgende pagina te gaan. Rechts naar links betekent dat u aan de linkerkant van het scherm klikt om naar de volgende pagina te gaan.", - "scaling-option-label": "Schaal opties", - "scaling-option-tooltip": "Hoe de afbeelding naar uw scherm te schalen.", - "page-splitting-label": "Pagina splitsen", - "page-splitting-tooltip": "Een afbeelding over de volledige breedte splitsen (dwz zowel de linker- als de rechterafbeelding worden gecombineerd)", - "reading-mode-label": "Leesmodus", - "layout-mode-label": "Lay-out Modus", - "layout-mode-tooltip": "Geef een enkele afbeelding of twee afbeeldingen naast elkaar weer", - "background-color-label": "Achtergrondkleur", - "auto-close-menu-label": "Menu automatisch sluiten", - "show-screen-hints-label": "Toon schermhints", - "emulate-comic-book-label": "Emuleer Stripverhaal", - "swipe-to-paginate-label": "Veeg om te bladeren", - "book-reader-settings-title": "Boekenlezer", - "tap-to-paginate-label": "Tik om te bladeren", - "tap-to-paginate-tooltip": "Tikken op de zijden van de lezen toestaan om te bladeren naar vorige/volgende pagina", - "immersive-mode-label": "Immersie-modus", - "immersive-mode-tooltip": "Hierdoor wordt het menu verborgen achter een klik op het lezerdocument en wordt de tik om te pagineren ingeschakeld", - "reading-direction-book-label": "Leesrichting", - "reading-direction-book-tooltip": "Richting waarin u klikt om naar de volgende pagina te gaan. Van rechts naar links betekent dat u aan de linkerkant van het scherm klikt om naar de volgende pagina te gaan.", - "font-family-label": "Lettertypefamilie", - "font-family-tooltip": "Lettertypefamilie om te laden. Standaard laadt het standaardlettertype van het boek", - "writing-style-label": "Schrijfstijl", - "writing-style-tooltip": "Verandert de richting van de tekst. Horizontaal is van links naar rechts, verticaal is van boven naar beneden.", - "layout-mode-book-label": "Lay-outmodus", - "layout-mode-book-tooltip": "Hoe de inhoud moet worden ingedeeld. Scroll is zoals het boek het verpakt. 1 of 2 kolommen passen bij de hoogte van het apparaat en er passen 1 of 2 kolommen tekst per pagina", - "color-theme-book-label": "Kleurenthema", - "color-theme-book-tooltip": "Welk kleurenthema moet worden toegepast op de inhoud en het menu van de boeklezer", - "font-size-book-label": "Lettertypegrootte", - "line-height-book-label": "Regelafstand", - "line-height-book-tooltip": "Hoeveel ruimte tussen de regels van het boek", - "margin-book-label": "Marge", - "margin-book-tooltip": "Hoeveel ruimte aan elke kant van het scherm. Dit overschrijft naar 0 op mobiele apparaten, ongeacht deze instelling.", "clients-opds-alert": "OPDS is niet ingeschakeld op deze server. Dit heeft geen invloed op Tachiyomi-gebruikers.", "clients-opds-description": "Alle externe clients gebruiken de API-sleutel of de onderstaande verbindings-URL. Dit zijn net wachtwoorden, houd het privé.", "clients-api-key-tooltip": "De API-sleutel is als een wachtwoord. Houd het geheim, houd het veilig.", @@ -1287,7 +1252,6 @@ }, "manga-reader": { "back": "Terug", - "save-globally": "Globaal opslaan", "incognito-alt": "De incognitomodus staat aan. Schakel om uit te schakelen.", "incognito-title": "Incognito modus:", "shortcuts-menu-alt": "Sneltoetsen Modaal", @@ -1319,7 +1283,6 @@ "layout-mode-switched": "De lay-outmodus is overgeschakeld naar Enkel omdat er onvoldoende ruimte is om een dubbele lay-out weer te geven", "no-next-chapter": "Geen volgend hoofdstuk", "no-prev-chapter": "Geen vorig hoofdstuk", - "user-preferences-updated": "Gebruikersvoorkeuren bijgewerkt", "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}" }, "metadata-filter": { diff --git a/UI/Web/src/assets/langs/pl.json b/UI/Web/src/assets/langs/pl.json index 654b6dcb4..8f242ef94 100644 --- a/UI/Web/src/assets/langs/pl.json +++ b/UI/Web/src/assets/langs/pl.json @@ -106,55 +106,6 @@ "collapse-series-relationships-tooltip": "Czy Kavita powinna pokazywać serie, które nie mają powiązań lub są rodzicem/prequelem", "share-series-reviews-label": "Udostępnij recenzje serii", "share-series-reviews-tooltip": "Czy Kavita powinna udostępniać Twoje recenzje serii innym użytkownikom?", - "image-reader-settings-title": "Czytnik obrazów", - "reading-direction-label": "Kierunek czytania", - "reading-direction-tooltip": "Kierunek kliknięcia, aby przejść do następnej strony. Od prawej do lewej oznacza, że należy kliknąć po lewej stronie ekranu, aby przejść do następnej strony.", - "scaling-option-label": "Opcje skalowania", - "scaling-option-tooltip": "Jak skalować obraz do ekranu.", - "page-splitting-label": "Podział stron", - "page-splitting-tooltip": "Jak podzielić obraz o pełnej szerokości (tj. połączyć lewy i prawy obraz)", - "reading-mode-label": "Tryb czytania", - "reading-mode-tooltip": "Zmień czytnik na podział stron pionowo, poziomo lub na nieskończone przewijanie", - "layout-mode-label": "Rodzaj układu", - "layout-mode-tooltip": "Wyświetlaj pojedynczy obraz na ekranie lub dwa obrazy obok siebie", - "background-color-label": "Kolor tła", - "background-color-tooltip": "Kolor tła czytnika obrazów", - "auto-close-menu-label": "Automatyczne zamknięcie menu", - "auto-close-menu-tooltip": "Automatyczne zamykanie menu", - "show-screen-hints-label": "Wyświetlaj podpowiedzi", - "show-screen-hints-tooltip": "Pokaż nakładkę aby lepiej zobrazować strefy paginacji i kierunek czytania", - "emulate-comic-book-label": "Naśladuj komiks", - "emulate-comic-book-tooltip": "Dodaje efekt cienia, aby imitować czytanie z książki", - "swipe-to-paginate-label": "Przesuń, aby zmienić stronę", - "swipe-to-paginate-tooltip": "Czy przesunięcie palcem po ekranie powinno powodować przejście do następnej lub poprzedniej strony", - "book-reader-settings-title": "Czytnik książek", - "tap-to-paginate-label": "Stuknij aby zmienić stronę", - "tap-to-paginate-tooltip": "Czy boki ekranu czytnika książek powinny umożliwiać dotknięcie go w celu przejścia do poprzedniej/następnej strony?", - "immersive-mode-label": "Tryb immersyjny", - "immersive-mode-tooltip": "Spowoduje to ukrycie menu po kliknięciu czytnika i włączenie funkcji podziału na strony", - "reading-direction-book-label": "Kierunek czytania", - "reading-direction-book-tooltip": "Kierunek kliknięcia, aby przejść do następnej strony. Od prawej do lewej oznacza, że należy kliknąć po lewej stronie ekranu, aby przejść do następnej strony.", - "font-family-label": "Rodzina czcionek", - "font-family-tooltip": "Rodzina czcionek do załadowania. Domyślna spowoduje załadowanie domyślnej czcionki książki", - "writing-style-label": "Kierunek tekstu", - "writing-style-tooltip": "Zmienia kierunek tekstu. Poziomo od lewej do prawej, pionowo od góry do dołu.", - "layout-mode-book-label": "Rodzaj układu", - "layout-mode-book-tooltip": "Jak powinna być ułożona treść. Przewijanie jest takie, jak w książce. 1 lub 2 kolumny dopasowują się do wysokości urządzenia i mieszczą 1 lub 2 kolumny tekstu na stronie", - "color-theme-book-label": "Kolor motywu", - "color-theme-book-tooltip": "Jaki motyw kolorystyczny zastosować do treści i menu czytnika książek?", - "font-size-book-label": "Rozmiar czcionki", - "font-size-book-tooltip": "Procent skalowania do zastosowania dla czcionki w książce", - "line-height-book-label": "Odstępy między wierszami", - "line-height-book-tooltip": "Jak duży odstęp między wierszami książki", - "margin-book-label": "Margines", - "margin-book-tooltip": "Odstępy po każdej stronie ekranu. Na urządzeniach mobilnych wartość ta zostanie zastąpiona wartością 0, niezależnie od tego ustawienia.", - "pdf-reader-settings-title": "Czytnik PDF", - "pdf-scroll-mode-label": "Tryb przewijania", - "pdf-scroll-mode-tooltip": "Sposób przewijania stron. Pionowo/poziomo i dotknij, aby zmienić stronę (bez przewijania)", - "pdf-spread-mode-label": "Tryb układu stron", - "pdf-spread-mode-tooltip": "Jak powinny być rozmieszczone strony. Pojedynczo lub podwójnie (zaczynając od nieparzystej/parzystej)", - "pdf-theme-label": "Motyw", - "pdf-theme-tooltip": "Motyw kolorystyczny czytnika", "clients-opds-alert": "OPDS nie jest włączony na tym serwerze. Nie będzie to miało wpływu na użytkowników Tachiyomi.", "clients-opds-description": "Wszystkie zewnętrzne aplikacje będą używać poniższego klucza API lub linku URL. Są one jak hasła, zachowa je w tajemnicy.", "clients-api-key-tooltip": "Klucz API jest jak hasło. Zresetowanie spowoduje unieważnienie wszystkich istniejących klientów.", @@ -167,9 +118,7 @@ "anilist-scrobbling-tooltip": "Pozwól Kavicie Scrobblować (synchronizacja jednostronna) postęp czytania i oceny do AniList", "anilist-scrobbling-label": "AniList Scrobbling", "want-to-read-sync-label": "Synchronizuj Chcę przeczytać", - "want-to-read-sync-tooltip": "Pozwól Kavicie na dodanie do twojej listy Chcę przeczytać na podstawie serii oczekujących na przeczytanie z AniList i MAL", - "allow-auto-webtoon-reader-label": "Automatyczny tryb czytnika Webtoon", - "allow-auto-webtoon-reader-tooltip": "Przełącz się w tryb czytnika webtoon, jeśli strony wyglądają jak webtoon. Mogą wystąpić błędne rozpoznania." + "want-to-read-sync-tooltip": "Pozwól Kavicie na dodanie do twojej listy Chcę przeczytać na podstawie serii oczekujących na przeczytanie z AniList i MAL" }, "user-holds": { "description": "To jest lista serii zarządzana przez użytkownika, które nie będą scrobblowane do zewnętrznych dostawców. Możesz usunąć serię w dowolnym momencie, a następne zdarzenie umożliwiające scrobblowanie (postęp czytania, ocena, status 'chcę przeczytać') wywoła scrobblowanie.", @@ -849,7 +798,6 @@ "back": "Wstecz", "more": "Więcej", "customize": "{{settings.customize}}", - "browse-authors": "Przeglądaj autorów", "edit": "{{common.edit}}", "cancel-edit": "Zamknij zmianę kolejności" }, @@ -1622,7 +1570,6 @@ }, "manga-reader": { "back": "Wstecz", - "save-globally": "Zapisz globalnie", "incognito-alt": "Tryb incognito jest włączony. Przełącz, aby go wyłączyć.", "incognito-title": "Tryb incognito:", "shortcuts-menu-alt": "Tryb skrótów klawiaturowych", @@ -1657,7 +1604,6 @@ "layout-mode-switched": "Tryb układu został przełączony na Pojedynczy z powodu niewystarczającej ilości miejsca do renderowania podwójnego układu", "no-next-chapter": "Brak następnego rozdziału", "no-prev-chapter": "Brak poprzedniego rozdziału", - "user-preferences-updated": "Zaktualizowano preferencje użytkownika", "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", "series-progress": "Postęp serii: {{percentage}}" }, @@ -2452,11 +2398,6 @@ "browse-person-by-role-title": "Wszystkie dzieła {{name}} jako {{role}}", "anilist-url": "{{edit-person-modal.anilist-tooltip}}" }, - "browse-authors": { - "cover-image-description": "{{edit-series-modal.cover-image-description}}", - "title": "Przeglądaj autorów i pisarzy", - "author-count": "{{num}} osób" - }, "confirm": { "confirm": "Potwierdź", "alert": "Uwaga", diff --git a/UI/Web/src/assets/langs/pt.json b/UI/Web/src/assets/langs/pt.json index 5acfea812..d4922b083 100644 --- a/UI/Web/src/assets/langs/pt.json +++ b/UI/Web/src/assets/langs/pt.json @@ -106,55 +106,6 @@ "collapse-series-relationships-tooltip": "O Kavita deve mostrar Séries que não tenham relações ou é o pai/prequela", "share-series-reviews-label": "Partilhar Críticas de Séries", "share-series-reviews-tooltip": "As suas críticas de Séries devem ser incluídas para outros utilizadores pelo Kavita", - "image-reader-settings-title": "Leitor de Imagens", - "reading-direction-label": "Direção de Leitura", - "reading-direction-tooltip": "Direção a clicar para ir para a próxima página. Direita para a Esquerda significa que se clica no lado esquerdo do ecrã para ir para a página seguinte.", - "scaling-option-label": "Opções de Dimensionamento", - "scaling-option-tooltip": "Como adaptar a dimensão da imagem ao seu ecrã.", - "page-splitting-label": "Separação de Páginas", - "page-splitting-tooltip": "Como separar uma imagem que ocupa a largura completa (p.e., as imagens esquerda e direita estão juntas numa só)", - "reading-mode-label": "Modo de Leitura", - "reading-mode-tooltip": "Alterar o leitor para paginar na vertical, na horizontal, ou ter um scroll infinito", - "layout-mode-label": "Modo de Exibição", - "layout-mode-tooltip": "Mostrar uma única imagem no ecrã ou mostrar duas imagens lado a lado", - "background-color-label": "Cor de Fundo", - "background-color-tooltip": "Cor de Fundo do Leitor de Imagens", - "auto-close-menu-label": "Fechar Menu Automaticamente", - "auto-close-menu-tooltip": "O menu deve fechar-se automaticamente?", - "show-screen-hints-label": "Mostrar Dicas no Ecrã", - "show-screen-hints-tooltip": "Mostrar uma indicação visual que ajude a perceber a área de paginação e a direção", - "emulate-comic-book-label": "Emular livro de BD", - "emulate-comic-book-tooltip": "Aplica um efeito de sobra para emular a leitura num livro", - "swipe-to-paginate-label": "Deslize para Paginar", - "swipe-to-paginate-tooltip": "O gesto de deslizar deve causar que a página seguinte ou anterior seja mostrada?", - "book-reader-settings-title": "Leitor de Livros", - "tap-to-paginate-label": "Toque para Paginar", - "tap-to-paginate-tooltip": "Se é permitido tocar nos lados do ecrã do leitor de livros para ir para a próxima/anterior página", - "immersive-mode-label": "Modo Imersivo", - "immersive-mode-tooltip": "O menu será escondido com um clique no documento e o toque para paginar ficará ativo", - "reading-direction-book-label": "Direção de Leitura", - "reading-direction-book-tooltip": "Direção a clicar para ir para a página seguinte. Direita para Esquerda significa que se clica no lado esquerdo do ecrã para ir para a página seguinte.", - "font-family-label": "Família de Fonte", - "font-family-tooltip": "Família de fonte a carregar. Por defeito será carregado a fonte do livro", - "writing-style-label": "Estilo de Escrita", - "writing-style-tooltip": "Muda a direção do texto. Horizontal é da esquerda para direita, vertical é do topo para o fundo.", - "layout-mode-book-label": "Modo de Exibição", - "layout-mode-book-tooltip": "Como o conteúdo deve ser exibido. A opção Scroll exibe de acordo com o definido no arquivo. As opções 1 ou 2 Colunas, ajustam à altura do dispositivo e mostram 1 ou 2 colunas de texto por página", - "color-theme-book-label": "Tema de Cor", - "color-theme-book-tooltip": "Que tema de cores aplicar ao conteúdo e menu do leitor de livros", - "font-size-book-label": "Tamanho de Fonte", - "font-size-book-tooltip": "Escala em percentagem a aplicar à fonte no livro", - "line-height-book-label": "Espaçamento Entre Linhas", - "line-height-book-tooltip": "Quanto espaçamento entre as linhas do livro", - "margin-book-label": "Margem", - "margin-book-tooltip": "Espaçamento em cada lado do ecrã. Nos dispositivos móveis o valor desta definição será sempre substituído por 0.", - "pdf-reader-settings-title": "Leitor de PDF", - "pdf-scroll-mode-label": "Modo de Rolagem", - "pdf-scroll-mode-tooltip": "Como rola pelas páginas. Vertical/Horizontal e toque para paginar (sem rolagem)", - "pdf-spread-mode-label": "Modo de Propagação", - "pdf-spread-mode-tooltip": "Como as páginas devem ser dispostas. Simples ou duplo (ímpar/par)", - "pdf-theme-label": "Tema", - "pdf-theme-tooltip": "Tema de cores do leitor", "clients-opds-alert": "O OPDS não está habilitado neste servidor. Isto não irá afectar os utilizadores do Tachiyomi.", "clients-opds-description": "Todos os clientes de terceiros utilizarão a chave de API ou URL abaixo. Estes elementos são semelhantes a palavras passe, mantenha-os privados.", "clients-api-key-tooltip": "A chave de API é como uma palavra passe. Se for redefinida, qualquer cliente existente será invalidado.", @@ -167,9 +118,7 @@ "want-to-read-sync-tooltip": "Permitir que o Kavita adicione itens à lista Leituras Futuras com base nas séries da lista de leitura Pendentes do AniList e/ou MAL", "anilist-scrobbling-tooltip": "Permitir que o Kavita faça Scrobble (sincronização unidirecional) do progresso das leituras e classificações para o AniList", "kavitaplus-settings-title": "Kavita+", - "anilist-scrobbling-label": "Scrobbling AniList", - "allow-auto-webtoon-reader-label": "Modo Automático do Leitor Webtoon", - "allow-auto-webtoon-reader-tooltip": "Muda para o modo Leitor Webtoon se as páginas parecem ser um webtoon. Podem acontecer falsos positivos." + "anilist-scrobbling-label": "Scrobbling AniList" }, "user-holds": { "title": "Retenções de Scrobble", @@ -849,7 +798,6 @@ "back": "Voltar", "more": "Mais", "customize": "{{settings.customize}}", - "browse-authors": "Explorar Autores", "edit": "{{common.edit}}", "cancel-edit": "Fechar Reordenar" }, @@ -1622,7 +1570,6 @@ }, "manga-reader": { "back": "Voltar", - "save-globally": "Guardar Globalmente", "incognito-alt": "O modo anónimo está ligado. Pressione novamente para desligar.", "incognito-title": "Modo Incógnito:", "shortcuts-menu-alt": "Modal de Atalhos de Teclado", @@ -1657,7 +1604,6 @@ "layout-mode-switched": "O modo de layout foi mudado para Individual por não haver espaço suficiente para mostrar o layout duplo", "no-next-chapter": "Não há Capítulo Seguinte", "no-prev-chapter": "Não há Capítulo Anterior", - "user-preferences-updated": "Preferência de utilizador atualizadas", "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", "series-progress": "Progresso da Série: {{percentage}}" }, @@ -2481,11 +2427,6 @@ "asin-tooltip": "https://www.amazon.com/stores/J.K.-Rowling/author/{ASIN}", "download-coversdb": "Descarregar de CoversDB" }, - "browse-authors": { - "author-count": "{{num}} Pessoas", - "cover-image-description": "{{edit-series-modal.cover-image-description}}", - "title": "Explorar Autores & Escritores" - }, "changelog-update-item": { "fixed": "Corrigido", "published-label": "Publicado: ", diff --git a/UI/Web/src/assets/langs/pt_BR.json b/UI/Web/src/assets/langs/pt_BR.json index bf6029d14..d4aa79571 100644 --- a/UI/Web/src/assets/langs/pt_BR.json +++ b/UI/Web/src/assets/langs/pt_BR.json @@ -83,7 +83,7 @@ }, "user-preferences": { "title": "Painel do Usuário", - "pref-description": "Essas são configurações globais vinculadas à sua conta.", + "pref-description": "Estas são configurações globais vinculadas à sua conta. As configurações do leitor estão localizadas em Perfis de Leitura.", "account-tab": "{{tabs.account-tab}}", "preferences-tab": "{{tabs.preferences-tab}}", "theme-tab": "{{tabs.theme-tab}}", @@ -107,55 +107,6 @@ "collapse-series-relationships-tooltip": "Kavita deve mostrar uma série que não tem relacionamentos ou é derivado/prequela", "share-series-reviews-label": "Compartilhar Análises de Séries", "share-series-reviews-tooltip": "Kavita deve incluir suas análises da série para outros usuários", - "image-reader-settings-title": "Leitor de Imagem", - "reading-direction-label": "Direção de Leitura", - "reading-direction-tooltip": "Direção para clicar para mover para a próxima página. Da direita para a esquerda significa que você clica no lado esquerdo da tela para ir para a próxima página.", - "scaling-option-label": "Opções de Escala", - "scaling-option-tooltip": "Como dimensionar a imagem para sua tela.", - "page-splitting-label": "Divisão de Página", - "page-splitting-tooltip": "Como dividir uma imagem de largura total (ou seja, as imagens esquerda e direita são combinadas)", - "reading-mode-label": "Modo de Leitura", - "reading-mode-tooltip": "Mude o leitor para paginar verticalmente, horizontalmente ou ter uma rolagem infinita", - "layout-mode-label": "Modo de Layout", - "layout-mode-tooltip": "Renderiza uma única imagem na tela ou duas imagens lado a lado", - "background-color-label": "Cor de Fundo", - "background-color-tooltip": "Cor de Fundo do Leitor de Imagens", - "auto-close-menu-label": "Fechar Automaticamente o Menu", - "auto-close-menu-tooltip": "O menu deve fechar automaticamente", - "show-screen-hints-label": "Mostrar Dicas na Tela", - "show-screen-hints-tooltip": "Mostrar uma sobreposição para entender a área e a direção da paginação", - "emulate-comic-book-label": "Emular quadrinhos", - "emulate-comic-book-tooltip": "Aplica um efeito de sombra para emular a leitura de um livro", - "swipe-to-paginate-label": "Deslizar para Paginar", - "swipe-to-paginate-tooltip": "Deslizar na tela deve fazer com que a página seguinte ou anterior seja acionada", - "book-reader-settings-title": "Leitor de Livro", - "tap-to-paginate-label": "Toque para Paginar", - "tap-to-paginate-tooltip": "Se as laterais da tela do leitor de livros permitirem tocar nela para ir para a página anterior/próxima", - "immersive-mode-label": "Modo Imersivo", - "immersive-mode-tooltip": "Isso ocultará o menu atrás de um clique no documento do leitor e gire o toque para paginar", - "reading-direction-book-label": "Direção de Leitura", - "reading-direction-book-tooltip": "Direção para clicar para ir para a próxima página. Da direita para a esquerda significa que você clica no lado esquerdo da tela para ir para a próxima página.", - "font-family-label": "Família da Fonte", - "font-family-tooltip": "Família de fontes para carregar. Padrão carregará a fonte padrão do livro", - "writing-style-label": "Estilo de Escrita", - "writing-style-tooltip": "Altera a direção do texto. Horizontal é da esquerda para a direita, vertical é de cima para baixo.", - "layout-mode-book-label": "Modo de Layout", - "layout-mode-book-tooltip": "Como o conteúdo deve ser apresentado. Rolar é como o livro o embala. 1 ou 2 colunas se ajustam à altura do dispositivo e 1 ou 2 colunas de texto por página", - "color-theme-book-label": "Cor do Tema", - "color-theme-book-tooltip": "Qual tema de cor aplicar ao conteúdo e ao menu do leitor de livros", - "font-size-book-label": "Tamanho da Fonte", - "font-size-book-tooltip": "Porcentagem de escala a ser aplicada à fonte do livro", - "line-height-book-label": "Espaçamento da Linha", - "line-height-book-tooltip": "Quanto espaçamento entre as linhas do livro", - "margin-book-label": "Margem", - "margin-book-tooltip": "Quanto espaçamento em cada lado da tela. Isso será substituído por 0 em dispositivos móveis, independentemente dessa configuração.", - "pdf-reader-settings-title": "Leitor de PDF", - "pdf-scroll-mode-label": "Modo de Rolagem", - "pdf-scroll-mode-tooltip": "Como você rola pelas páginas. Vertical/Horizontal e toque para paginar (sem rolagem)", - "pdf-spread-mode-label": "Modo de Propagação", - "pdf-spread-mode-tooltip": "Como as páginas devem ser dispostas. Simples ou duplo (ímpar/par)", - "pdf-theme-label": "Tema", - "pdf-theme-tooltip": "Tema de cores do leitor", "clients-opds-alert": "O OPDS não está ativado neste servidor. Isso não afetará os usuários do Tachiyomi.", "clients-opds-description": "Todos os clientes de terceiros usarão a chave de API ou o URL de conexão abaixo. Estas são como senhas, mantenha-as privadas.", "clients-api-key-tooltip": "A chave API é como uma senha. Redefini-lo invalidará todos os clientes existentes.", @@ -168,9 +119,7 @@ "anilist-scrobbling-label": "AniList Scrobbling", "anilist-scrobbling-tooltip": "Permitir que Kavita faça scrobble (sincronização unidirecional) de leitura e avaliações para o AniList", "want-to-read-sync-label": "Sincronizar Quero Ler", - "want-to-read-sync-tooltip": "Permitir que Kavita adicione itens à sua lista de Quero Ler com base em séries Anilist e Mal, pendentes na lista de leitura", - "allow-auto-webtoon-reader-label": "Modo de leitor automático de Webtoon", - "allow-auto-webtoon-reader-tooltip": "Mudar para o modo Leitor Webtoon se as páginas parecerem um webtoon. Podem ocorrer alguns falsos positivos." + "want-to-read-sync-tooltip": "Permitir que Kavita adicione itens à sua lista de Quero Ler com base em séries Anilist e Mal, pendentes na lista de leitura" }, "user-holds": { "title": "Paradas do Scrobble", @@ -680,7 +629,10 @@ "incognito-mode-label": "Modo Incógnito", "next": "Seguinte", "previous": "Anterior", - "go-to-page-prompt": "Existem {{totalPages}} páginas. Para qual página você quer ir?" + "go-to-page-prompt": "Existem {{totalPages}} páginas. Para qual página você quer ir?", + "go-to-section": "Ir para seção", + "go-to-section-prompt": "Existem {{totalSections}} seções. Para qual seção você deseja ir?", + "go-to-first-page": "Ir para a primeira página" }, "personal-table-of-contents": { "no-data": "Nada Marcado ainda", @@ -728,7 +680,7 @@ "series-detail": { "page-settings-title": "Configurações da Página", "close": "{{common.close}}", - "layout-mode-label": "{{user-preferences.layout-mode-book-label}}", + "layout-mode-label": "{{manage-reading-profiles.layout-mode-book-label}}", "layout-mode-option-card": "Cartão", "layout-mode-option-list": "Lista", "continue-from": "Continuar {{title}}", @@ -851,9 +803,9 @@ "back": "Voltar", "more": "Mais", "customize": "{{settings.customize}}", - "browse-authors": "Navegar pelos Autores", "edit": "{{common.edit}}", - "cancel-edit": "Fechar Reordenar" + "cancel-edit": "Fechar Reordenar", + "browse-people": "Navegar por Pessoas" }, "library-settings-modal": { "close": "{{common.close}}", @@ -916,30 +868,30 @@ }, "reader-settings": { "general-settings-title": "Configurações Gerais", - "font-family-label": "{{user-preferences.font-family-label}}", - "font-size-label": "{{user-preferences.font-size-book-label}}", - "line-spacing-label": "{{user-preferences.line-height-book-label}}", - "margin-label": "{{user-preferences.margin-book-label}}", + "font-family-label": "{{manage-reading-profiles.font-family-label}}", + "font-size-label": "{{manage-reading-profiles.font-size-book-label}}", + "line-spacing-label": "{{manage-reading-profiles.line-height-book-label}}", + "margin-label": "{{manage-reading-profiles.margin-book-label}}", "reset-to-defaults": "Redefinir para os Padrões", "reader-settings-title": "Configurações do Leitor", - "reading-direction-label": "{{user-preferences.reading-direction-book-label}}", + "reading-direction-label": "{{manage-reading-profiles.reading-direction-book-label}}", "right-to-left": "Direita para Esquerda", "left-to-right": "Esquerda para Direita", "horizontal": "Horizontal", "vertical": "Vertical", - "writing-style-label": "{{user-preferences.writing-style-label}}", + "writing-style-label": "{{manage-reading-profiles.writing-style-label}}", "writing-style-tooltip": "Altera a direção do texto. Horizontal é da esquerda para a direita, vertical é de cima para baixo.", "tap-to-paginate-label": "Toque em Paginação", "tap-to-paginate-tooltip": "Clique nas bordas da tela para paginar", "on": "Ligado", "off": "Desligado", - "immersive-mode-label": "{{user-preferences.immersive-mode-label}}", + "immersive-mode-label": "{{manage-reading-profiles.immersive-mode-label}}", "immersive-mode-tooltip": "Isso ocultará o menu atrás de um clique no documento do leitor e gire o toque para paginar", "fullscreen-label": "Tela Cheia", "fullscreen-tooltip": "Põe o leitor no modo de tela cheia", "exit": "Sair", "enter": "Entrar", - "layout-mode-label": "{{user-preferences.layout-mode-book-label}}", + "layout-mode-label": "{{manage-reading-profiles.layout-mode-book-label}}", "layout-mode-tooltip": "Scroll: Espelha o arquivo epub (geralmente uma longa página de rolagem por capítulo).
1 Coluna: Cria uma única página virtual por vez.
2 Colunas: Cria duas páginas virtuais por vez dispostas lado a lado.", "layout-mode-option-scroll": "Rolar", "layout-mode-option-1col": "1 Coluna", @@ -948,7 +900,15 @@ "theme-dark": "Escuro", "theme-black": "Preto", "theme-white": "Branco", - "theme-paper": "Papel" + "theme-paper": "Papel", + "update-parent": "Salvar em {{name}}", + "loading": "carregando", + "create-new": "Novo perfil do implícito", + "create-new-tooltip": "Criar um novo perfil gerenciável a partir do seu perfil implícito atual", + "reading-profile-updated": "Perfil de leitura atualizado", + "reading-profile-promoted": "Perfil de leitura promovido", + "line-spacing-min-label": "1x", + "line-spacing-max-label": "2.5x" }, "table-of-contents": { "no-data": "Este livro não tem um índice definido nos metadados ou um arquivo toc" @@ -1389,7 +1349,8 @@ "admin-email-history": "Histórico do e-mail", "admin-matched-metadata": "Metadados Correspondentes", "admin-manage-tokens": "Gerenciar Tokens do Usuário", - "admin-metadata": "Gerenciar Metadados" + "admin-metadata": "Gerenciar Metadados", + "reading-profiles": "Perfis de Leitura" }, "collection-detail": { "no-data": "Não há itens. Tente adicionar uma série.", @@ -1514,7 +1475,9 @@ "all-filters": "Filtros Inteligentes", "nav-link-header": "Opções de Navegação", "close": "{{common.close}}", - "person-aka-status": "Resultados como pseudônimos" + "person-aka-status": "Resultados como pseudônimos", + "browse-tags": "Navegar pelas Etiquetas", + "browse-genres": "Navegar pelos Gêneros" }, "promoted-icon": { "promoted": "{{common.promoted}}" @@ -1625,7 +1588,6 @@ }, "manga-reader": { "back": "Voltar", - "save-globally": "Salvar Globalmente", "incognito-alt": "O modo de navegação anônima está ativado. Alterne para desligar.", "incognito-title": "Modo Incógnito:", "shortcuts-menu-alt": "Modal de Atalhos de Teclado", @@ -1647,9 +1609,9 @@ "height": "Altura", "width": "Largura", "width-override-label": "Substituição de Largura", - "off": "Desligado", + "off": "{{reader-settings.off}}", "original": "Original", - "auto-close-menu-label": "{{user-preferences.auto-close-menu-label}}", + "auto-close-menu-label": "{{manage-reading-profiles.auto-close-menu-label}}", "swipe-enabled-label": "Deslizar Ativado", "enable-comic-book-label": "Emular quadrinhos", "brightness-label": "Brilho", @@ -1660,9 +1622,14 @@ "layout-mode-switched": "Modo de layout alterado para Único devido a espaço insuficiente para renderizar layout duplo", "no-next-chapter": "Nenhum Capítulo A Seguir", "no-prev-chapter": "Nenhum Capítulo Anterior", - "user-preferences-updated": "Preferências de usuário atualizadas", - "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", - "series-progress": "Progresso das Séries: {{percentage}}" + "emulate-comic-book-label": "{{manage-reading-profiles.emulate-comic-book-label}}", + "series-progress": "Progresso das Séries: {{percentage}}", + "loading": "{{reader-settings.loading}}", + "create-new-tooltip": "{{reader-settings.create-new-tooltip}}", + "reading-profile-promoted": "Perfil de leitura promovido", + "update-parent": "{{reader-settings.update-parent}}", + "reading-profile-updated": "Perfil de leitura atualizado", + "create-new": "{{reader-settings.create-new}}" }, "metadata-filter": { "filter-title": "{{common.filter}}", @@ -1719,7 +1686,10 @@ "release-year": "Ano de Lançamento", "read-progress": "Última Leitura", "average-rating": "Avaliação Média", - "random": "Aleatório" + "random": "Aleatório", + "person-name": "Nome", + "person-chapter-count": "Número de Capítulos", + "person-series-count": "Número de Séries" }, "edit-series-modal": { "title": "{{seriesName}} Detalhes", @@ -1856,8 +1826,8 @@ "cover-image-tab": "{{tabs.cover-tab}}", "tasks-tab": "{{tabs.tasks-tab}}", "info-tab": "{{tabs.info-tab}}", - "pages-label": "{{edit-chapter-modal.pages-count}}", - "words-label": "{{edit-chapter-modal.length-title}}", + "pages-label": "{{edit-chapter-modal.pages-label}}", + "words-label": "{{edit-chapter-modal.words-label}}", "pages-count": "{{edit-chapter-modal.pages-count}}", "words-count": "{{edit-chapter-modal.words-count}}", "reading-time-label": "{{edit-chapter-modal.reading-time-label}}", @@ -2078,7 +2048,7 @@ "reading-lists": "{{side-nav.reading-lists}}", "bookmarks": "{{side-nav.bookmarks}}", "all-series": "{{side-nav.all-series}}", - "browse-authors": "{{side-nav.browse-authors}}" + "browse-authors": "{{side-nav.browse-people}}" }, "filter-field-pipe": { "age-rating": "{{metadata-fields.age-rating-title}}", @@ -2254,7 +2224,9 @@ "webtoon-override": "Mudando para o modo Webtoon devido a imagens que representam um Webtoon.", "scrobble-gen-init": "Enfileirou uma tarefa para gerar eventos scrobble a partir do histórico de leitura e avaliações anteriores, sincronizando-os com serviços conectados.", "confirm-delete-multiple-volumes": "Tem certeza de que deseja excluir {{count}} volumes? Isso não modificará os arquivos no disco.", - "series-added-want-to-read": "Série adicionada da lista Quero Ler" + "series-added-want-to-read": "Série adicionada da lista Quero Ler", + "library-bound-to-reading-profile": "Biblioteca vinculada ao Perfil de Leitura {{name}}", + "series-bound-to-reading-profile": "Série vinculada ao Perfil de Leitura {{name}}" }, "read-time-pipe": { "less-than-hour": "<1 Hora", @@ -2330,7 +2302,13 @@ "reorder": "Reordenar", "rename-tooltip": "Renomear o Filtro Inteligente", "rename": "Renomear", - "merge": "Mesclar" + "merge": "Mesclar", + "reading-profiles": "Perfis de Leitura", + "clear-reading-profile": "Limpar Perfil de Leitura", + "clear-reading-profile-tooltip": "Limpar Perfil de Leitura para esta Biblioteca", + "cleared-profile": "Perfil de Leitura Limpo", + "set-reading-profile-tooltip": "Vincular um Perfil de Leitura a esta Biblioteca", + "set-reading-profile": "Definir Perfil de Leitura" }, "preferences": { "left-to-right": "Esquerda para Direita", @@ -2444,19 +2422,21 @@ "issue-num-shorthand": "#{{num}}", "volume-num-shorthand": "Vol. {{num}}", "book-nums": "Livros", - "issue-nums": "Números", + "issue-nums": "Edições", "chapter-nums": "Capítulos", "volume-nums": "Volumes", "author-count": "{{num}} Autores", "chapter-count": "{{num}} Capítulos", - "no-data": "Sem Dados" + "no-data": "Sem Dados", + "issue-count": "{{num}} Edições" }, "confirm": { "confirm": "Confirmar", "alert": "Alerta", "info": "Informações", "cancel": "{{common.cancel}}", - "ok": "Ok" + "ok": "Ok", + "prompt": "Questão" }, "edit-person-modal": { "cover-image-tab": "{{edit-series-modal.cover-image-tab}}", @@ -2485,11 +2465,6 @@ "aliases-tab": "Pseudônimos", "aliases-tooltip": "Quando uma série é etiquetada com um pseudônimo de uma pessoa, a pessoa é atribuída em vez de ser criada uma nova pessoa. Ao eliminar um pseudônimo, terá de voltar a analisar a série para que a alteração seja detectada." }, - "browse-authors": { - "author-count": "{{num}} Pessoas", - "title": "Navegar pelos Autores & Escritores", - "cover-image-description": "{{edit-series-modal.cover-image-description}}" - }, "person-detail": { "browse-person-title": "Todas as obras de {{name}}", "browse-person-by-role-title": "Todos os trabalhos de {{name}} como {{role}}", @@ -2552,7 +2527,9 @@ "dont-match-status-label": "{{dont-match-label}}", "actions-header": "Ações", "match-alt": "Correspondente à {{seriesName}}", - "library-name-header": "Biblioteca" + "library-name-header": "Biblioteca", + "matched-state-label": "Estado da Correspondência", + "library-type": "Tipo de Biblioteca" }, "match-series-modal": { "description": "Selecione uma correspondência para religar os metadados do Kavita+ e regenerar eventos scrobble. Não Corresponde pode ser usado para impedir que Kavita corresponda a metadados e scrobbling.", @@ -2690,5 +2667,146 @@ "title": "{{personName}}", "merge-warning": "se prosseguir, a pessoa selecionada será removida. O nome da pessoa selecionada será adicionado como um pseudônimo e todas as suas funções serão transferidas.", "known-for-title": "Conhecido por" + }, + "manage-reading-profiles": { + "swipe-to-paginate-tooltip": "Ao deslizar na tela, a página seguinte ou anterior será acionada", + "no-selected": "Nenhum perfil selecionado", + "selection-tip": "Selecione um perfil da lista ou crie um novo no canto superior direito", + "reading-direction-label": "Direção da Leitura", + "scaling-option-label": "Opções de Escala", + "page-splitting-label": "Divisão de Página", + "reading-mode-label": "Modo de Leitura", + "layout-mode-tooltip": "Renderizar uma única imagem na tela ou duas imagens lado a lado", + "background-color-tooltip": "Cor de Fundo do Leitor de Imagem", + "auto-close-menu-label": "Fechar o Menu Automaticamente", + "auto-close-menu-tooltip": "O menu deve fechar automaticamente", + "show-screen-hints-tooltip": "Mostrar uma sobreposição para ajudar a entender a área de paginação e a direção", + "swipe-to-paginate-label": "Deslizar para Paginar", + "allow-auto-webtoon-reader-label": "Modo de Leitura Automática Webtoon", + "width-override-label": "{{manga-reader.width-override-label}}", + "reset": "{{common.reset}}", + "book-reader-settings-title": "Leitor de Livros", + "tap-to-paginate-label": "Toque para Paginar", + "immersive-mode-label": "Modo Imersivo", + "reading-direction-book-label": "Direção da Leitura", + "font-family-label": "Família da Fonte", + "font-family-tooltip": "Família da fonte para carregar. O padrão carregará a fonte padrão do livro", + "layout-mode-book-label": "Modo de Layout", + "color-theme-book-label": "Cor do Tema", + "font-size-book-label": "Tamanho da Fonte", + "font-size-book-tooltip": "Porcentagem de escala a ser aplicada à fonte do livro", + "line-height-book-label": "Espaçamento entre Linhas", + "line-height-book-tooltip": "Quanto espaçamento entre as linhas do livro", + "pdf-reader-settings-title": "Leitor de PDF", + "pdf-scroll-mode-label": "Modo de Rolagem", + "pdf-theme-label": "Tema", + "pdf-theme-tooltip": "Cor do tema do leitor", + "reading-profile-library-settings-title": "Biblioteca", + "delete": "{{common.delete}}", + "image-reader-settings-title": "Leitor de Imagens", + "extra-tip": "Atribua perfis de leitura por meio do menu de ações em séries e bibliotecas, ou em massa. Ao alterar as configurações em um leitor, um perfil oculto é criado para lembrar suas escolhas para aquela série (exceto para PDFs). Este perfil é removido quando você atribui um dos seus próprios perfis de leitura à série. Mais informações podem ser encontradas em", + "make-default": "Definir como padrão", + "layout-mode-label": "Modo de Layout", + "add": "{{common.add}}", + "scaling-option-tooltip": "Como dimensionar a imagem para sua tela.", + "background-color-label": "Cor do Fundo", + "default-profile": "Padrão", + "description": "Nem todas as suas séries podem ser lidas da mesma forma; crie perfis de leitura distintos por biblioteca ou série para que o retorno às suas séries seja o mais tranquilo possível.", + "show-screen-hints-label": "Mostrar Dicas na Tela", + "width-override-tooltip": "Substituir largura de imagens no leitor", + "pdf-spread-mode-tooltip": "Como as páginas devem ser dispostas. Simples ou duplas (ímpar/par)", + "immersive-mode-tooltip": "Isso ocultará o menu atrás de um clique no documento do leitor e girará o toque para paginar", + "allow-auto-webtoon-reader-tooltip": "Mude para o modo Leitor de Webtoons se as páginas parecerem um webtoon. Podem ocorrer alguns falsos positivos.", + "writing-style-tooltip": "Altera a direção do texto. Horizontal é da esquerda para a direita, vertical é de cima para baixo.", + "writing-style-label": "Estilo da Escrita", + "reading-profile-series-settings-title": "Séries", + "emulate-comic-book-label": "Emular história em quadrinhos", + "profiles-title": "Seus perfis de leitura", + "page-splitting-tooltip": "Como dividir uma imagem de largura total (por exemplo, as imagens esquerda e direita são combinadas)", + "reading-mode-tooltip": "Alterar o leitor para paginar verticalmente, horizontalmente ou ter uma rolagem infinita", + "confirm": "Tem certeza de que deseja excluir o perfil de leitura {{name}}?", + "add-tooltip": "Seu novo perfil será salvo após fazer uma alteração nele", + "reading-direction-tooltip": "Direção para clicar para ir para a próxima página. Da direita para a esquerda significa clicar no lado esquerdo da tela para ir para a próxima página.", + "tap-to-paginate-tooltip": "As laterais da tela do leitor de livros devem permitir que você toque nela para ir para a página anterior/seguinte", + "margin-book-tooltip": "Quanto espaçamento em cada lado da tela. Isso será substituído por 0 em dispositivos móveis, independentemente desta configuração.", + "layout-mode-book-tooltip": "Como o conteúdo deve ser disposto. A rolagem é como o livro o embala. 1 ou 2 colunas se ajustam à altura do dispositivo e comportam 1 ou 2 colunas de texto por página", + "color-theme-book-tooltip": "Qual tema de cor aplicar ao conteúdo e menu do leitor de livros", + "margin-book-label": "Margem", + "pdf-spread-mode-label": "Modo de Propagação", + "pdf-scroll-mode-tooltip": "Como você rola as páginas. Vertical/Horizontal e Toque para Paginar (sem rolagem)", + "emulate-comic-book-tooltip": "Aplica um efeito de sombra para emular a leitura de um livro", + "reading-direction-book-tooltip": "Direção para clicar para ir para a próxima página. Da direita para a esquerda significa clicar no lado esquerdo da tela para ir para a próxima página.", + "wiki-title": "wiki", + "disable-width-override-label": "Desativar substituição de largura", + "disable-width-override-tooltip": "Evitar que a substituição da largura entre em vigor quando a tela estiver pelo menos no ponto de interrupção configurado ou menor" + }, + "bulk-set-reading-profile-modal": { + "title": "Definir perfil de leitura", + "filter-label": "{{common.filter}}", + "clear": "{{common.clear}}", + "loading": "{{common.loading}}", + "create": "{{common.create}}", + "bound": "Vinculado", + "no-data": "Nenhuma coleção criada ainda", + "close": "{{common.close}}" + }, + "browse-people": { + "series-count": "{{common.series-count}}", + "roles-label": "Papéis", + "name-label": "Nome", + "series-count-label": "Número de Séries", + "title": "Navegar por Pessoas", + "sort-label": "Ordenar", + "issue-count-label": "Número de Edições", + "author-count": "{{num}} Pessoas", + "cover-image-description": "{{edit-series-modal.cover-image-description}}", + "issue-count": "{{common.issue-count}}" + }, + "browse-genres": { + "genre-count": "{{num}} Gêneros", + "issue-count": "{{common.issue-count}}", + "title": "Navegar por Gêneros", + "series-count": "{{common.series-count}}" + }, + "browse-tags": { + "issue-count": "{{common.issue-count}}", + "series-count": "{{common.series-count}}", + "title": "Navegar pelas Etiquetas", + "genre-count": "{{num}} Etiquetas" + }, + "browse-title-pipe": { + "age-rating": "Avaliado com {{value}}", + "user-rating": "{{value}} avaliação com estrelas", + "translator": "Traduzido por {{value}}", + "editor": "Editado por {{value}}", + "artist": "Desenhado por {{value}}", + "colorist": "Colorido por {{value}}", + "inker": "Arte-finalizado por {{value}}", + "penciller": "Desenhado por {{value}}", + "genre": "Tem o Gênero {{value}}", + "imprint": "Impressão de {{value}}", + "team": "Equipe {{value}}", + "location": "Em {{value}} localização", + "release-year": "Lançado em {{value}}", + "tag": "Tem a Etiqueta {{value}}", + "writer": "Escrito por {{value}}", + "publication-status": "{{value}} trabalhos", + "format": "Formato de {{value}}", + "letterer": "Com letra de {{value}}", + "library": "Dentro da biblioteca {{value}}", + "character": "Tem o personagem {{value}}", + "publisher": "Publicado por {{value}}" + }, + "breakpoint-pipe": { + "desktop": "Desktop", + "tablet": "Tablet", + "never": "Nunca", + "mobile": "Móvel" + }, + "generic-filter-field-pipe": { + "person-name": "Nome", + "person-role": "Papel", + "person-series-count": "Número de Séries", + "person-chapter-count": "Número de Capítulos" } } diff --git a/UI/Web/src/assets/langs/ru.json b/UI/Web/src/assets/langs/ru.json index 0789c6685..18c9a9a06 100644 --- a/UI/Web/src/assets/langs/ru.json +++ b/UI/Web/src/assets/langs/ru.json @@ -93,7 +93,7 @@ "page-layout-mode-tooltip": "Показывать элементы в виде карточек или списка на странице сведений о серии.", "locale-label": "Язык", "locale-tooltip": "Язык, который должна использовать Кавита", - "blur-unread-summaries-label": "Размытие непрочитанных сводок", + "blur-unread-summaries-label": "Размытие непрочитанного", "blur-unread-summaries-tooltip": "Размытие аннотаций в не прочитанных томах или главах (во избежание спойлеров)", "prompt-on-download-label": "Запрос на загрузку", "prompt-on-download-tooltip": "Запрашивать, когда размер загрузки превышает {{size}} МБ", @@ -103,41 +103,6 @@ "collapse-series-relationships-tooltip": "Должна ли Кавита показывать серии, которые не имеют отношения друг к другу, или это родителаякий серия/приквел", "share-series-reviews-label": "Поделится обзором серий", "share-series-reviews-tooltip": "Должна ли Kavita включать ваши отзывы о сериях для других пользователей", - "image-reader-settings-title": "Просмоторщик изображений", - "reading-direction-label": "Направление чтения", - "reading-direction-tooltip": "Направление нажатия для перехода на следующую страницу. С права на лево означает, что для перехода на следующую страницу нужно нажать в левой части экрана.", - "scaling-option-label": "Параметры масштабирования", - "scaling-option-tooltip": "Как масштабировать изображение для вашего экрана.", - "page-splitting-label": "Разделение страниц", - "page-splitting-tooltip": "Как разделить изображение во всю ширину (т.е. как левое, так и правое изображения объединяются)", - "reading-mode-label": "Режим чтения", - "layout-mode-label": "Режим макета", - "layout-mode-tooltip": "Вывод на экран одного изображения или двух рядом расположенных изображений", - "background-color-label": "Цвет фона", - "auto-close-menu-label": "Автоматическое закрытие меню", - "show-screen-hints-label": "Показывать экранные подсказки", - "emulate-comic-book-label": "Имитация комиксов", - "swipe-to-paginate-label": "Проведите пальцем по страницам", - "book-reader-settings-title": "Читалка", - "tap-to-paginate-label": "Нажмите для пагинации", - "tap-to-paginate-tooltip": "Должны ли боковые стороны экрана читалки позволять нажимать на них для перехода к предыдущей/следующей странице", - "immersive-mode-label": "Режим погружения", - "immersive-mode-tooltip": "Это позволит скрыть меню после щелчка по документу читателя и включить функцию перехода к страницам", - "reading-direction-book-label": "Направление чтения", - "reading-direction-book-tooltip": "Направление нажатия для перехода на следующую страницу. С лева на право означает, что для перехода на следующую страницу нужно щелкнуть в левой части экрана.", - "font-family-label": "Семейство шрифтов", - "font-family-tooltip": "Семейство шрифтов для загрузки. По умолчанию загрузит шрифт, используемый в книге по умолчанию", - "writing-style-label": "Стиль письма", - "writing-style-tooltip": "Изменяет направление текста. Горизонтальное направление - слева направо, вертикальное - сверху вниз.", - "layout-mode-book-label": "Режим макета", - "layout-mode-book-tooltip": "Как должен быть размещен контент. Scroll - так, как это принято в книгах. 1 или 2 колонки соответствуют высоте устройства и вмещают 1 или 2 колонки текста на страницу", - "color-theme-book-label": "Цветовая тема", - "color-theme-book-tooltip": "Какую цветовую тему применить к содержанию и меню программы чтения книг", - "font-size-book-label": "Размер шрифта", - "line-height-book-label": "Межстрочный интервал", - "line-height-book-tooltip": "Размер интервала между строками книги", - "margin-book-label": "Отступ", - "margin-book-tooltip": "Величина интервала с каждой стороны экрана. На мобильных устройствах этот параметр будет иметь значение 0 независимо от этой настройки.", "clients-opds-alert": "OPDS не включен на этом сервере. Это не повлияет на пользователей Tachiyomi.", "clients-opds-description": "Все сторонние клиенты будут использовать либо API-ключ, либо указанный ниже Url подключения. Например пароли, храните их в тайне.", "clients-api-key-tooltip": "Ключ API - это как пароль. Храните его в секрете, берегите его.", @@ -185,7 +150,7 @@ }, "manage-devices": { "title": "Диспетчер устройств", - "description": "Этот раздел предназначен для настройки устройств, которые не могут подключаться к Kavita через веб-браузер, а вместо этого имеют адрес электронной почты, принимающий файлы.", + "description": "Раздел предназначен для настройки устройств, которые не могут подключаться к Kavita через веб-браузер, но используют адрес электронной почты, принимающий файлы.", "devices-title": "Устройства", "no-devices": "Пока нет ни одного настроенного устройства", "platform-label": "Платформа: ", @@ -285,21 +250,25 @@ "open-filtered-search": "Откройте отфильтрованный поиск для {{item}}." }, "user-stats-info-cards": { - "total-pages-read-label": "Всего прочитанных страниц", + "total-pages-read-label": "Прочитанных страниц", "total-pages-read-tooltip": "{{user-stats-info-cards.total-pages-read-label}}: {{value}}", - "total-words-read-label": "Всего слов прочитано", + "total-words-read-label": "Прочитанных слов", "total-words-read-tooltip": "{{user-stats-info-cards.total-words-read-label}}: {{value}}", "time-spent-reading-label": "Время, проведенное за чтением", "time-spent-reading-tooltip": "{{user-stats-info-cards.time-spent-reading-label}}: {{value}}", - "chapters-read-label": "Читать главы", + "chapters-read-label": "Прочитанных глав", "chapters-read-tooltip": "{{user-stats-info-cards.chapters-read-label}}: {{value}}", - "avg-reading-per-week-label": "Среднее чтение / неделя", - "last-active-label": "Последний активный", - "chapters": "{{value}} глав" + "avg-reading-per-week-label": "Среднее время чтения / в неделю", + "last-active-label": "Последняя активность", + "chapters": "{{value}} глав", + "pages-count": "{{num}} страниц", + "pages-read-by-year-title": "Прочтено страниц за год", + "words-read-by-year-title": "Прочтенных слов за год", + "words-count": "{{num}} слов" }, "user-stats": { "library-read-progress-title": "Прогресс чтения в библиотеке", - "read-percentage": "% Читать" + "read-percentage": "% Прочитанного" }, "top-readers": { "title": "Лучшие читатели", @@ -510,7 +479,10 @@ "reading-direction-label": "{{user-preferences.reading-direction-book-label}}", "writing-style-label": "{{user-preferences.writing-style-label}}", "immersive-mode-label": "{{user-preferences.immersive-mode-label}}", - "layout-mode-label": "{{user-preferences.layout-mode-book-label}}" + "layout-mode-label": "{{user-preferences.layout-mode-book-label}}", + "left-to-right": "Слева направо", + "right-to-left": "Справа налево", + "writing-style-tooltip": "Изменяет направление текста. По горизонтали - слева направо, по вертикали - сверху вниз." }, "bookmarks": { "title": "{{side-nav.bookmarks}}", @@ -599,7 +571,11 @@ "reset-to-default": "{{common.reset-to-default}}", "reset": "{{common.reset}}", "save": "{{common.save}}", - "field-required": "{{validation.field-required}}" + "field-required": "{{validation.field-required}}", + "allow-stats-label": "Отправка анонимной статистики", + "port-tooltip": "Порт сервера. Для вступления в силу требуется перезагрузка. Эту настройку нельзя изменить, если вы запускаете образ Docker как пользователь без прав root.", + "port-label": "Порт", + "allow-stats-tooltip-part-1": "Отправляйте анонимные данные об использовании Kavita. Сюда входит информация о некоторых используемых функциях, количестве файлов, версии операционной системы, версии установки Kavita, процессоре и памяти. Мы будем использовать эту информацию для определения приоритетов добавления новых функций, исправления ошибок и оптимизации производительности. Более подробная информация о собираемых данных отражена на https://wiki.kavitareader.com . Для вступления в силу требуется перезагрузка. " }, "manage-tasks-settings": { "reset-to-default": "{{common.reset-to-default}}", @@ -697,7 +673,9 @@ }, "manga-reader": { "auto-close-menu-label": "{{user-preferences.auto-close-menu-label}}", - "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}" + "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", + "right-to-left-alt": "Справа налево", + "left-to-right-alt": "Слево направо" }, "metadata-filter": { "filter-title": "{{common.filter}}", @@ -814,7 +792,9 @@ "last-30-days": "{{time-periods.last-30-days}}", "last-90-days": "{{time-periods.last-90-days}}", "last-year": "{{time-periods.last-year}}", - "all-time": "{{time-periods.all-time}}" + "all-time": "{{time-periods.all-time}}", + "title": "Активность чтения", + "no-data": "Отсутствует прогресс" }, "series-preview-drawer": { "tags-label": "{{filter-field-pipe.tags}}", @@ -882,12 +862,19 @@ "writers": "{{metadata-fields.writers-title}}" }, "actionable": { - "clear": "{{common.clear}}" + "clear": "{{common.clear}}", + "download": "Скачать", + "add-to": "Добавить к", + "read": "Читать", + "new-collection": "Новая коллекция", + "rename": "Переименовать", + "refresh-covers-tooltip": "Востанавить все обложки" }, "validation": { "required-field": "Это поле обязательно для заполнения", "valid-email": "Это должен быть действительный адрес электронной почты", - "password-validation": "Длина пароля должна составлять от 6 до 32 символов" + "password-validation": "Длина пароля должна составлять от 6 до 32 символов", + "year-validation": "Значение года должно быть настоящий, цифра должна быть больше 1000 и содержать 4 цифры" }, "entity-type": { "volume": "том", @@ -925,6 +912,42 @@ "issue-hash-num": "Вопрос #", "issue-num": "Вопрос", "chapter-num": "Глава", - "volume-num": "Том" + "volume-num": "Том", + "book-num-shorthand": "Книга {{num}}", + "chapter-nums": "Главы", + "volume-nums": "Тома", + "book-nums": "Книги", + "issue-num-shorthand": "#{{num}}", + "volume-num-shorthand": "Том. {{num}}", + "chapter-num-shorthand": "Глава {{num}}", + "chapter-count": "{{num}} Главы", + "no-data": "Нет данных" + }, + "preferences": { + "original": "Оригинал", + "list": "Список", + "pdf-multiple": "По умолчанию", + "left-to-right": "Слева направо", + "right-to-left": "Справа налево" + }, + "toasts": { + "confirm-download-size-ios": "В iOS возникают проблемы с загрузкой файлов размером более 200 МБ, загрузка может завершиться некорректно.", + "confirm-library-type-change": "Смена типа библиотеки вызовет новое сканирование с другими правилами синтаксического анализа и может привести к повторному созданию серий, что может привести к потере прогресса и закладок. Перед началом разумно создать резервную копию. Вы уверены, что хотите продолжить?", + "chapter-deleted": "Удаление главы", + "collection-not-owned": "Вы не являетесь владельцем этой коллекции" + }, + "tabs": { + "devices-tab": "Устройства", + "smart-filters-tab": "Умный фильтр", + "folder-tab": "Папка" + }, + "metadata-setting-field-pipe": { + "chapter-release-date": "Дата Релиза (Главы)" + }, + "read-time-pipe": { + "less-than-hour": "<1 Часа" + }, + "day-breakdown": { + "no-data": "Отсутствует прогресс, отправляйтесь читать =)" } } diff --git a/UI/Web/src/assets/langs/sk.json b/UI/Web/src/assets/langs/sk.json index ac2f5ec6a..e88666343 100644 --- a/UI/Web/src/assets/langs/sk.json +++ b/UI/Web/src/assets/langs/sk.json @@ -72,7 +72,8 @@ "your-review": "Toto je Vaša recenzia", "external-review": "Externá recenzia", "local-review": "Lokálna Recenzia", - "rating-percentage": "Hodnotenie {{r}}%" + "rating-percentage": "Hodnotenie {{r}}%", + "critic": "kritika" }, "want-to-read": { "title": "Chcem prečítať", @@ -82,7 +83,7 @@ }, "user-preferences": { "title": "Používateľský panel", - "pref-description": "Toto sú globálne nastavenia, ktoré sú viazané na Váš účet.", + "pref-description": "Toto sú globálne nastavenia, ktoré sú prepojené s vaším účtom. Nastavenia čitateľa sa nachádzajú v časti Profilov čitateľa.", "account-tab": "{{tabs.account-tab}}", "preferences-tab": "{{tabs.preferences-tab}}", "theme-tab": "{{tabs.theme-tab}}", @@ -106,63 +107,14 @@ "collapse-series-relationships-tooltip": "Mala by Kavita ukázať seriál, ktorý nemá žiadne vzťahy alebo je nadradeným/prequel", "share-series-reviews-label": "Zdieľať recenzie série", "share-series-reviews-tooltip": "Ak má Kavita zahrnúť vaše recenzie na Série pre iných používateľov", - "image-reader-settings-title": "Čítačka obrázkov", - "reading-direction-label": "Smer čítania", - "reading-direction-tooltip": "Smer pre kliknutie na prejdenie na ďalšiu stránku. Sprava doľava znamená, že kliknutím na ľavú stranu obrazovky prejdete na ďalšiu stránku.", - "scaling-option-label": "Možnosti škálovania", - "scaling-option-tooltip": "Ako zmeniť mierku obrázka na obrazovku.", - "page-splitting-label": "Rozdelenie stránky", - "page-splitting-tooltip": "Ako rozdeliť obrázok na celú šírku (tj ľavý a pravý obrázok sa skombinujú)", - "reading-mode-label": "Režim čítania", - "layout-mode-label": "Režim rozloženia", - "layout-mode-tooltip": "Vykreslite jeden obrázok na obrazovku alebo dva obrázky vedľa seba", - "background-color-label": "Farba pozadia", - "auto-close-menu-label": "Automaticky zatvoriť menu", - "show-screen-hints-label": "Zobraziť tipy na obrazovke", - "emulate-comic-book-label": "Napodobniť komiks", - "swipe-to-paginate-label": "Potiahnuť pre Stránkovanie", - "book-reader-settings-title": "Čítačka kníh", - "tap-to-paginate-label": "Klepnutím môžete stránkovať", - "tap-to-paginate-tooltip": "Ak strany obrazovky čítačky kníh umožňujú ťuknutím na ňu prejsť na predchádzajúcu/nasledujúcu stranu", - "immersive-mode-label": "Imerzívny režim", - "immersive-mode-tooltip": "Ponuka sa skryje pri kliknutí na dokument čítačky a zapne dotyk pre listovanie strán", - "reading-direction-book-label": "Smer čítania", - "reading-direction-book-tooltip": "Smer na kliknutie pre prejdenie na ďalšiu stránku. Sprava doľava znamená, že kliknutím na ľavú stranu obrazovky prejdete na ďalšiu stránku.", - "font-family-label": "Typ Písma", - "font-family-tooltip": "Typ písma na načítanie. Predvolené načíta predvolené písmo knihy", - "writing-style-label": "Štýl písania", - "writing-style-tooltip": "Zmení smer textu. Horizontálne je zľava doprava, vertikálne je zhora nadol.", - "layout-mode-book-label": "Režim Rozloženia", - "layout-mode-book-tooltip": "Ako by mal byť obsah usporiadaný. Zvitok je taký, ako ho balí kniha. 1 alebo 2 stĺpce sa prispôsobia výške zariadenia a zmestia sa 1 alebo 2 stĺpce textu na stranu", - "color-theme-book-tooltip": "Akú farebnú tému použiť na obsah a ponuku čítačky kníh", - "font-size-book-label": "Veľkosť Písma", - "line-height-book-label": "Riadkovanie", - "line-height-book-tooltip": "Koľko medzier medzi riadkami knihy", - "margin-book-label": "Okraj stránky", - "margin-book-tooltip": "Koľko medzier na každej strane obrazovky. Na mobilných zariadeniach sa to prepíše na 0 bez ohľadu na toto nastavenie.", "clients-opds-description": "Všetci klienti tretích strán budú používať kľúč API alebo pripájaciu adresu URL nižšie. Musí sa s nimi zaobchádzať dôverne, rovnako ako s heslami.", "clients-api-key-tooltip": "Kľúč API je ako heslo. Jeho obnovenie zruší platnosť všetkých existujúcich klientov.", "reset": "{{common.reset}}", "save": "{{common.save}}", - "reading-mode-tooltip": "Zmeňte čítačku na zvislé, vodorovné stránkovanie alebo na nekonečné posúvanie", "clients-opds-alert": "OPDS nie je na tomto serveri povolené. Toto neovplyvní používateľov Tachiyomi.", "clients-opds-url-tooltip": "Pozrite si zoznam podporovaných klientov OPDS: ", - "color-theme-book-label": "Farebný motív", - "pdf-reader-settings-title": "PDF Čítačka", - "pdf-scroll-mode-label": "Režim scrollovania", - "pdf-scroll-mode-tooltip": "Ako listujete stránkami. Vertikálne/horizontálne a stránkovanie klepnutím (bez posúvania)", - "pdf-spread-mode-label": "Režim šírenia", - "swipe-to-paginate-tooltip": "Potiahnutie prstom po obrazovke by malo spôsobiť spustenie nasledujúcej alebo predchádzajúcej stránky", - "font-size-book-tooltip": "Percento mierky, ktorá sa má použiť na písmo v knihe", - "pdf-theme-tooltip": "Farebná téma čitateľa", "clients-opds-url-label": "OPDS URL", "clients-api-key-label": "API kľúč", - "background-color-tooltip": "Farba pozadia čítačky obrázkov", - "auto-close-menu-tooltip": "Malo by sa menu automaticky zatvoriť", - "show-screen-hints-tooltip": "Zobrazte prekrytie, ktoré vám pomôže pochopiť oblasť a smer stránkovania", - "emulate-comic-book-tooltip": "Aplikuje tieňový efekt na emuláciu čítania z knihy", - "pdf-spread-mode-tooltip": "Ako by mali byť stránky usporiadané. Jedno alebo dvojlôžkové (nepárne/párne)", - "pdf-theme-label": "Téma", "kavitaplus-settings-title": "Kavita+", "anilist-scrobbling-label": "AniList Scrobblovanie", "anilist-scrobbling-tooltip": "Umožniť Kavite scrobblovať (jednosmerná synchronizácia) priebeh čítania a hodnotenia do zoznamu AniList", @@ -180,7 +132,7 @@ "theme-manager": { "title": "Správca tém", "site-themes": "Témy/Motívy stránok", - "set-default": "Nastaviť ako predvolené", + "set-default": "Nastaviť predvolené", "download": "{{changelog.download}}", "apply": "{{common.apply}}", "applied": "Aplikované", @@ -677,7 +629,10 @@ "go-to-page-prompt": "Počet stránok: {{totalPages}}. Na akú stránku chceš ísť?", "toc-header": "ToC", "loading-book": "Načítava sa kniha…", - "close-reader": "Zavrieť Čítačku" + "close-reader": "Zavrieť Čítačku", + "go-to-first-page": "Prejsť na prvú stránku", + "go-to-section": "Prejsť do sekcie", + "go-to-section-prompt": "Existuje {{totalSections}} sekcií. Do ktorej sekcie chcete prejsť?" }, "personal-table-of-contents": { "no-data": "Zatiaľ nič nie je v záložkách", @@ -725,7 +680,7 @@ "series-detail": { "page-settings-title": "Nastavenie Stránky", "close": "{{common.close}}", - "layout-mode-label": "{{user-preferences.layout-mode-book-label}}", + "layout-mode-label": "{{manage-reading-profiles.layout-mode-book-label}}", "layout-mode-option-card": "Karta", "continue-from": "Pokračovať {{title}}", "read": "{{common.read}}", @@ -769,7 +724,7 @@ "volume-num": "{{common.volume-num}}", "reading-lists-title": "{{side-nav.reading-lists}}", "time-to-read-alt": "{{sort-field-pipe.time-to-read}}", - "scrobbling-tooltip": "{{settings.scrobbling}}", + "scrobbling-tooltip": "{{settings.scrobbling}}: {{value}}", "time-left-alt": "Zostávajúci čas", "layout-mode-option-list": "Zoznam", "more-alt": "Viac", @@ -778,7 +733,9 @@ "pages-count": "{{num}} Strán", "words-count": "{{num}} Slov", "weblinks-title": "Odkazy", - "release-date-title": "Vydanie" + "release-date-title": "Vydanie", + "on": "{{reader-settings.on}}", + "off": "{{reader-settings.off}}" }, "metadata-fields": { "collections-title": "{{side-nav.collections}}", @@ -806,7 +763,8 @@ "close": "{{common.close}}", "entry-label": "Pozri detaily", "kavita-tooltip": "Vaše hodnotenie + Celkové", - "kavita-rating-title": "Vaše hodnotenie" + "kavita-rating-title": "Vaše hodnotenie", + "critic": "{{review-card.critic}}" }, "badge-expander": { "more-items": "a {{count}} ďalších" @@ -841,7 +799,8 @@ "back": "Naspäť", "more": "Viac", "customize": "{{settings.customize}}", - "browse-authors": "Prezerať autorov" + "edit": "{{common.edit}}", + "cancel-edit": "Zavrieť zmenu poradia" }, "library-settings-modal": { "close": "{{common.close}}", @@ -852,7 +811,7 @@ "cover-tab": "{{tabs.cover-tab}}", "advanced-tab": "{{tabs.advanced-tab}}", "tasks-tab": "{{tabs.tasks-tab}}", - "name-label": "Názov", + "name-label": "Meno", "library-name-unique": "Názov knižnice musí byť jedinečný", "last-scanned-label": "Naposledy skenované:", "type-label": "Typ", @@ -904,27 +863,27 @@ }, "reader-settings": { "general-settings-title": "Všeobecné Nastavenia", - "font-family-label": "{{user-preferences.font-family-label}}", - "font-size-label": "{{user-preferences.font-size-book-label}}", - "line-spacing-label": "{{user-preferences.line-height-book-label}}", - "margin-label": "{{user-preferences.margin-book-label}}", + "font-family-label": "{{manage-reading-profiles.font-family-label}}", + "font-size-label": "{{manage-reading-profiles.font-size-book-label}}", + "line-spacing-label": "{{manage-reading-profiles.line-height-book-label}}", + "margin-label": "{{manage-reading-profiles.margin-book-label}}", "reset-to-defaults": "Obnoviť predvolené nastavenia", "reader-settings-title": "Nastavenia čítačky", - "reading-direction-label": "{{user-preferences.reading-direction-book-label}}", + "reading-direction-label": "{{manage-reading-profiles.reading-direction-book-label}}", "right-to-left": "Zprava doľava", "left-to-right": "Zľava doprava", "horizontal": "Horizontálne", "vertical": "Vertikálne", - "writing-style-label": "{{user-preferences.writing-style-label}}", + "writing-style-label": "{{manage-reading-profiles.writing-style-label}}", "tap-to-paginate-label": "Klepnite na Stránkovanie", "tap-to-paginate-tooltip": "Pre stránkovanie kliknite na okraje obrazovky", "on": "Zapnuté", "off": "Vypnuté", - "immersive-mode-label": "{{user-preferences.immersive-mode-label}}", + "immersive-mode-label": "{{manage-reading-profiles.immersive-mode-label}}", "immersive-mode-tooltip": "Ponuka sa skryje pri kliknutí na dokument čítačky a zapne dotyk pre listovanie strán", "fullscreen-label": "Celá obrazovka", "fullscreen-tooltip": "Prepnite čítačku do režimu celej obrazovky", - "layout-mode-label": "{{user-preferences.layout-mode-book-label}}", + "layout-mode-label": "{{manage-reading-profiles.layout-mode-book-label}}", "layout-mode-option-scroll": "Skrolovať", "layout-mode-option-1col": "1 Stĺpec", "layout-mode-option-2col": "2 Stĺpce", @@ -936,7 +895,15 @@ "writing-style-tooltip": "Zmení smer textu. Vodorovný je zľava doprava, zvislý zhora nadol.", "exit": "Ukončiť", "enter": "Enter", - "layout-mode-tooltip": "Posúvanie: Zrkadlí súbor epub (zvyčajne jedna dlhá rolovacia stránka na kapitolu).
1 stĺpec: Vytvorí jednu virtuálnu stránku naraz.
2 stĺpec: Vytvorí dve virtuálne stránky naraz rozložené vedľa seba." + "layout-mode-tooltip": "Posúvanie: Zrkadlí súbor epub (zvyčajne jedna dlhá rolovacia stránka na kapitolu).
1 stĺpec: Vytvorí jednu virtuálnu stránku naraz.
2 stĺpec: Vytvorí dve virtuálne stránky naraz rozložené vedľa seba.", + "loading": "načítavanie", + "create-new-tooltip": "Vytvorte nový spravovateľný profil z vášho súčasného implicitného profilu", + "reading-profile-updated": "Profil čítania bol aktualizovaný", + "line-spacing-max-label": "2.5x", + "create-new": "Nový profil z implicitného", + "reading-profile-promoted": "Profil čítania bol povýšený", + "update-parent": "Uložiť do {{name}}", + "line-spacing-min-label": "1x" }, "table-of-contents": { "no-data": "Táto kniha nemá v metadátach ani v súbore toc nastavený obsah" @@ -960,7 +927,8 @@ }, "card-detail-layout": { "total-items": "Celkový počet položiek: {{count}}", - "jumpkey-count": "{{count}} Série" + "jumpkey-count": "{{count}} Série", + "no-data": "{{common.no-data}}" }, "card-item": { "cannot-read": "Nedá sa čítať" @@ -986,7 +954,8 @@ "language-title": "{{edit-chapter-modal.language-label}}", "release-title": "{{sort-field-pipe.release-year}}", "format-title": "{{metadata-filter.format-label}}", - "length-title": "{{edit-chapter-modal.words-label}}" + "length-title": "{{edit-chapter-modal.words-label}}", + "age-rating-title": "{{metadata-fields.age-rating-title}}" }, "related-tab": { "reading-lists-title": "{{reading-lists.title}}", @@ -1058,7 +1027,7 @@ "email-settings-title": "Nastavenia e-mailu", "reset": "{{common.reset}}", "test": "Test", - "host-name-tooltip": "Názov domény (reverzného proxy). Vyžaduje sa pre funkčnosť e-mailu. Ak nieje reverzné proxy, použite ľubovoľnú url adresu.", + "host-name-tooltip": "Názov domény vášho reverzného proxy servera, potrebný pre fungovanie e-mailu. Ak nepoužívate reverzný proxy server, môžete použiť ľubovoľnú URL adresu vrátane http://externalip:port/", "host-name-validation": "Hostname musí začínať http(s) a nesmie končiť na /", "sender-address-label": "Adresa odosielateľa", "sender-address-tooltip": "Toto je e-mailová adresa, z ktorej príjemca uvidí, keď dostane e-mail. Zvyčajne ide o e-mailovú adresu priradenú k účtu.", @@ -1299,7 +1268,8 @@ "roles-header": "Roly", "name-header": "Meno", "sharing-header": "Zdieľanie", - "pending-tooltip": "Tento používateľ neoveril svoj e-mail" + "pending-tooltip": "Tento používateľ neoveril svoj e-mail", + "all-libraries": "Všetky knižnice" }, "edit-collection-tags": { "title": "Upraviť zbierku {{collectionName}}", @@ -1311,7 +1281,7 @@ "cover-image-tab": "{{tabs.cover-tab}}", "info-tab": "{{tabs.info-tab}}", "series-tab": "{{tabs.series-tab}}", - "name-label": "Názov", + "name-label": "Meno", "name-validation": "Názov musí byť jedinečný", "promote-label": "Povýšiť", "promote-tooltip": "Povýšenie znamená, že tag možno vidieť na celom serveri, nielen pre správcov. Všetky série, ktoré majú tento tag, budú mať stále obmedzenia prístupu používateľov.", @@ -1365,7 +1335,8 @@ "admin-email-history": "História e-mailov", "admin-matched-metadata": "Zhodné metadáta", "scrobble-holds": "Scrobble chyty", - "admin-metadata": "Spravovať metadáta" + "admin-metadata": "Spravovať metadáta", + "reading-profiles": "Čítanie profilov" }, "collection-detail": { "no-data": "Neexistujú žiadne položky. Skúste pridať sériu.", @@ -1411,7 +1382,8 @@ "provided": "Poskytnuté", "smart-filter": "Smart Filter", "library": "Knižnica", - "external-source": "Externý zdroj" + "external-source": "Externý zdroj", + "delete": "{{common.delete}}" }, "reading-list-detail": { "item-count": "{{common.item-count}}", @@ -1423,7 +1395,19 @@ "read-options-alt": "Možnosti čítania", "incognito-alt": "(Inkognito)", "no-data": "Nič nebolo pridané", - "characters-title": "{{metadata-fields.characters-title}}" + "characters-title": "{{metadata-fields.characters-title}}", + "writers-title": "{{metadata-fields.writers-title}}", + "edit-alt": "{{common.edit}}", + "storyline-tab": "{{series-detail.storyline-tab}}", + "details-tab": "{{series-detail.details-tab}}", + "more-alt": "{{series-detail.more-alt}}", + "cover-artists-title": "{{metadata-fields.cover-artists-title}}", + "publishers-title": "{{metadata-fields.publishers-title}}", + "items-title": "Položky", + "edit-label": "Režim úprav", + "reorder-alt": "Preusporiadať položky", + "date-range-title": "Rozsah dátumov", + "dnd-warning": "Funkcia „drag and drop“ nie je k dispozícii na mobilných zariadeniach alebo ak zoznam na čítanie obsahuje viac ako 100 položiek." }, "events-widget": { "title-alt": "Aktivita", @@ -1476,7 +1460,8 @@ "logout": "Odhlásiť sa", "all-filters": "Smart Filtre", "close": "{{common.close}}", - "nav-link-header": "Možnosti navigácie" + "nav-link-header": "Možnosti navigácie", + "person-aka-status": "Zhoduje sa s aliasom" }, "promoted-icon": { "promoted": "{{common.promoted}}" @@ -1563,7 +1548,6 @@ }, "manga-reader": { "back": "Naspäť", - "save-globally": "Uložiť globálne", "incognito-alt": "Režim inkognito je zapnutý. Prepínačom vypnete.", "incognito-title": "Režim inkognito:", "shortcuts-menu-alt": "Modálne klávesové skratky", @@ -1584,7 +1568,7 @@ "height": "Výška", "width": "Šírka", "original": "Originál", - "auto-close-menu-label": "{{user-preferences.auto-close-menu-label}}", + "auto-close-menu-label": "{{manage-reading-profiles.auto-close-menu-label}}", "swipe-enabled-label": "Potiahnutie prstom je povolené", "enable-comic-book-label": "Napodobniť komiks", "brightness-label": "Jas", @@ -1595,12 +1579,17 @@ "layout-mode-switched": "Režim rozloženia bol prepnutý na Single z dôvodu nedostatku miesta na vykreslenie dvojitého rozloženia", "no-next-chapter": "Žiadna ďalšia kapitola", "no-prev-chapter": "Žiadna predchádzajúca kapitola", - "user-preferences-updated": "Predvoľby používateľa boli aktualizované", - "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", + "emulate-comic-book-label": "{{manage-reading-profiles.emulate-comic-book-label}}", "series-progress": "Priebeh série: {{percentage}}", "collapse": "Zabaliť", - "off": "Vypnuté", - "width-override-label": "Prepísanie šírky" + "off": "{{reader-settings.off}}", + "width-override-label": "Prepísanie šírky", + "create-new-tooltip": "{{reader-settings.create-new-tooltip}}", + "create-new": "{{reader-settings.create-new}}", + "loading": "{{reader-settings.loading}}", + "update-parent": "{{reader-settings.update-parent}}", + "reading-profile-updated": "Profil čítania bol aktualizovaný", + "reading-profile-promoted": "Profil čítania bol povýšený" }, "metadata-filter": { "filter-title": "{{common.filter}}", @@ -1689,7 +1678,7 @@ "publication-status-label": "Stav publikácie", "required-field": "{{validation.required-field}}", "close": "{{common.close}}", - "name-label": "Názov", + "name-label": "Meno", "sort-name-label": "Triediť podľa názvu", "localized-name-label": "Lokalizovaný názov", "summary-label": "Zhrnutie", @@ -1793,8 +1782,8 @@ "cover-image-tab": "{{tabs.cover-tab}}", "tasks-tab": "{{tabs.tasks-tab}}", "info-tab": "{{tabs.info-tab}}", - "pages-label": "{{edit-chapter-modal.pages-count}}", - "words-label": "{{edit-chapter-modal.length-title}}", + "pages-label": "{{edit-chapter-modal.pages-label}}", + "words-label": "{{edit-chapter-modal.words-label}}", "pages-count": "{{edit-chapter-modal.pages-count}}", "words-count": "{{edit-chapter-modal.words-count}}", "reading-time-label": "{{edit-chapter-modal.reading-time-label}}", @@ -1942,7 +1931,8 @@ "save": "{{common.save}}", "add": "{{common.add}}", "filter": "{{common.filter}}", - "clear": "{{common.clear}}" + "clear": "{{common.clear}}", + "smart-filter-title": "{{customize-dashboard-modal.title-smart-filters}}" }, "customize-sidenav-streams": { "no-data": "Všetky Smart filtre boli pridané do bočnej navigácie alebo ešte neboli vytvorené žiadne.", @@ -1970,13 +1960,21 @@ "no-data": "Neboli vytvorené žiadne Smart filtre", "filter": "{{common.filter}}", "clear": "{{common.clear}}", - "errored": "Vo filtri je chyba kódovania. Musíte to znova vytvoriť." + "errored": "Vo filtri je chyba kódovania. Musíte to znova vytvoriť.", + "save": "{{common.save}}", + "edit-smart-filter": "Upraviť {{name}}", + "name-label": "Meno", + "required-field": "Inteligentné filtre potrebujú názov", + "close": "{{common.close}}", + "filter-name-unique": "Názvy inteligentných filtrov musia byť jedinečné", + "edit": "{{common.edit}}", + "cancel": "{{common.cancel}}" }, "edit-external-source-item": { "not-unique": "Externý zdroj už existuje s týmto host-om. Uistite sa, že nemáte duplikáty", "title": "Nový externý zdroj", "host-label": "Host", - "name-label": "Názov", + "name-label": "Meno", "api-key-label": "API kľúč", "save": "{{common.save}}", "edit": "{{common.edit}}", @@ -2168,7 +2166,12 @@ "confirm-delete-multiple-chapters": "Naozaj chcete odstrániť {{count}} kapitolu/zväzky? Nezmení súbory na disku.", "bulk-delete-libraries": "Naozaj chcete odstrániť {{count}} knižnice?", "match-success": "Séria správne priradená", - "webtoon-override": "Prepnutie do režimu Webtoon kvôli obrázkom reprezentujúcim webtoon." + "webtoon-override": "Prepnutie do režimu Webtoon kvôli obrázkom reprezentujúcim webtoon.", + "confirm-delete-multiple-volumes": "Naozaj chcete odstrániť {{count}} zväzkov? Týmto sa nezmenia súbory na disku.", + "library-bound-to-reading-profile": "Knižnica prepojená s čitateľským profilom {{name}}", + "scrobble-gen-init": "Zaradená úloha na generovanie udalostí Scrobblingu z histórie čítania a hodnotení v minulosti a ich synchronizáciu s pripojenými službami.", + "series-added-want-to-read": "Séria bola pridaná zo zoznamu Chcem si prečítať", + "series-bound-to-reading-profile": "Séria viazaná na čitateľský profil {{name}}" }, "actionable": { "scan-library": "Skenovať knižnicu", @@ -2235,7 +2238,17 @@ "unpromote-tooltip": "Obmedzte viditeľnosť iba na svoj účet", "copy-settings": "Kopírovať nastavenia z", "match": "Zhoda", - "match-tooltip": "Zoraďte sériu s Kavitou+ manuálne" + "match-tooltip": "Zoraďte sériu s Kavitou+ manuálne", + "merge": "Zlúčiť", + "clear-reading-profile": "Vymazať profil čítania", + "clear-reading-profile-tooltip": "Vymazať profil čítania pre túto knižnicu", + "cleared-profile": "Vymazaný profil čítania", + "reading-profiles": "Profily čítania", + "set-reading-profile-tooltip": "Prepojiť čitateľský profil s touto knižnicou", + "reorder": "Zmeniť poradie", + "rename": "Premenovať", + "rename-tooltip": "Premenovanie inteligentného filtra", + "set-reading-profile": "Nastaviť profil čítania" }, "preferences": { "left-to-right": "Zľava doprava", @@ -2333,17 +2346,14 @@ "browse-person-title": "Všetky diela {{name}}", "known-for-title": "Známy pre", "browse-person-by-role-title": "Všetky diela {{name}} ako {{role}}", - "anilist-url": "{{edit-person-modal.anilist-tooltip}}" + "anilist-url": "{{edit-person-modal.anilist-tooltip}}", + "aka-title": "Tiež známe ako ", + "no-info": "Žiadne informácie o tejto osobe" }, "download-button": { "download-tooltip": "Stiahnuť", "downloading-status": "Sťahuje sa…" }, - "browse-authors": { - "title": "Prezerať autorov a spisovateľov", - "author-count": "{{num}} Ľudí", - "cover-image-description": "{{edit-series-modal.cover-image-description}}" - }, "pdf-scroll-mode-pipe": { "horizontal": "Horizontálne", "page": "Klepnutím môžete stránkovať", @@ -2404,7 +2414,8 @@ "confirm": "Potvrdiť", "info": "Info", "cancel": "{{common.cancel}}", - "ok": "Ok" + "ok": "Ok", + "prompt": "Otázka" }, "collection-owner": { "collection-created-label": "Vytvorené: {{owner}}", @@ -2431,7 +2442,11 @@ "cover-image-description-extra": "Alternatívne si môžete stiahnuť obal z CoversDB, ak je k dispozícii.", "cover-image-description": "{{edit-series-modal.cover-image-description}}", "save": "{{common.save}}", - "download-coversdb": "Stiahnuť z CoversDB" + "download-coversdb": "Stiahnuť z CoversDB", + "aliases-tab": "Aliasy", + "aliases-label": "Upraviť aliasy", + "alias-overlap": "Tento alias už odkazuje na inú osobu alebo je menom tejto osoby, zvážte ich zlúčenie.", + "aliases-tooltip": "Keď je séria označená aliasom osoby, táto osoba je priradená, nie je vytvorená nová osoba. Pri odstraňovaní aliasu budete musieť znova prehľadať sériu, aby sa zmena prejavila." }, "kavitaplus-metadata-breakdown-stats": { "completed-series-label": "Dokončené série", @@ -2505,7 +2520,7 @@ "close": "{{common.close}}", "no-results": "Nedá sa nájsť zhoda. Skúste pridať adresu URL od podporovaného poskytovateľa a skúste to znova.", "query-label": "Dopyt", - "query-tooltip": "Zadajte názov série, webovú adresu AniList/MyAnimeList. Adresy URL budú používať priame vyhľadávanie.", + "query-tooltip": "Zadajte názov série, URL adresu AniList/MyAnimeList/ComicBookRoundup. URL adresy budú používať priame vyhľadávanie.", "dont-match-label": "Nezhoduj", "dont-match-tooltip": "Rozhodnite sa pre túto sériu z párovania a scrobblingu", "search": "Hľadať" @@ -2534,7 +2549,8 @@ "volume-count": "{{server-stats.volume-count}}", "releasing": "Vydanie", "details": "Zobraziť stránku", - "updating-metadata-status": "Aktualizácia metadát" + "updating-metadata-status": "Aktualizácia metadát", + "issue-count": "{{common.issue-count}}" }, "manage-metadata-settings": { "tag": "Tag/Značka", @@ -2575,7 +2591,18 @@ "remove-source-tag-label": "Odstrániť zdroj značky/tagu", "first-last-name-label": "Meno Priezvisko Pomenovanie", "person-roles-label": "Roly", - "first-last-name-tooltip": "Uistite sa, že mená osôb sú napísané najprv s krstným menom a potom s priezviskom" + "first-last-name-tooltip": "Uistite sa, že mená osôb sú napísané najprv s krstným menom a potom s priezviskom", + "enable-chapter-release-date-label": "Dátum vydania", + "enable-chapter-release-date-tooltip": "Umožniť napísanie dátumu vydania kapitoly/čísla", + "enable-chapter-publisher-label": "Vydavateľ", + "enable-chapter-publisher-tooltip": "Povoliť vydavateľovi kapitoly/čísla napísať", + "enable-chapter-cover-label": "Obálka kapitoly", + "enable-chapter-cover-tooltip": "Povoliť nastavenie obálky kapitoly/čísla", + "chapter-header": "Polia kapitoly", + "enable-chapter-title-label": "Názov", + "enable-chapter-title-tooltip": "Umožniť napísanie názvu kapitoly/vydania", + "enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}", + "enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}" }, "metadata-setting-field-pipe": { "start-date": "{{manage-metadata-settings.enable-start-date-label}}", @@ -2586,6 +2613,129 @@ "people": "{{tabs.people-tab}}", "summary": "{{filter-field-pipe.summary}}", "tags": "{{metadata-fields.tags-title}}", - "localized-name": "{{edit-series-modal.localized-name-label}}" + "localized-name": "{{edit-series-modal.localized-name-label}}", + "chapter-covers": "Obálky (kapitola)", + "chapter-title": "Názov (kapitola)", + "chapter-release-date": "Dátum vydania (kapitola)", + "chapter-summary": "Zhrnutie (kapitola)", + "chapter-publisher": "{{person-role-pipe.publisher}} (Chapter)" + }, + "log-level-pipe": { + "trace": "Trace/stopy", + "warning": "Pozor", + "critical": "Kritické", + "debug": "Debugovanie", + "information": "Informácie" + }, + "role-localized-pipe": { + "change-password": "Zmeniť heslo", + "download": "Stiahnuť", + "login": "Prihlásenie", + "change-restriction": "Zmeniť obmedzenie", + "admin": "Admin", + "bookmark": "Záložka", + "promote": "Propagovať", + "read-only": "Iba na čítanie" + }, + "merge-person-modal": { + "title": "{{personName}}", + "src": "Zlúčiť osobu", + "save": "{{common.save}}", + "close": "{{common.close}}", + "known-for-title": "Známy pre", + "merge-warning": "Ak budete pokračovať, vybraná osoba bude odstránená. Meno vybranej osoby bude pridané ako alias a všetky jej role budú prenesené.", + "alias-title": "Nové aliasy" + }, + "manage-reading-profiles": { + "no-selected": "Nie je vybratý žiadny profil", + "confirm": "Naozaj chcete odstrániť čitateľský profil {{name}}?", + "scaling-option-tooltip": "Ako prispôsobiť obrázok veľkosti obrazovky.", + "auto-close-menu-label": "Menu automatického zatvorenia", + "auto-close-menu-tooltip": "Ponuka by sa mala automaticky zatvoriť", + "show-screen-hints-label": "Zobraziť rady na obrazovke", + "show-screen-hints-tooltip": "Zobraziť prekrytie, ktoré pomôže pochopiť oblasť a smer stránkovania", + "width-override-label": "{{manga-reader.width-override-label}}", + "reset": "{{common.reset}}", + "book-reader-settings-title": "Čítačka kníh", + "tap-to-paginate-label": "Klepnutie pre stránkovanie", + "tap-to-paginate-tooltip": "Mali by bočné strany obrazovky čítačky kníh umožňovať klepnutie na prechod na predchádzajúcu/nasledujúcu stranu", + "immersive-mode-label": "Imerzívny režim", + "reading-direction-book-label": "Smer čítania", + "reading-direction-book-tooltip": "Smer kliknutia pre presun na ďalšiu stránku. Sprava doľava znamená, že kliknutím na ľavú stranu obrazovky prejdete na ďalšiu stránku.", + "font-family-label": "Skupina písma", + "writing-style-label": "Štýl písania", + "writing-style-tooltip": "Mení smer textu. Horizontálne je zľava doprava, vertikálne je zhora nadol.", + "layout-mode-book-label": "Režim rozloženia", + "font-size-book-label": "Veľkosť písma", + "margin-book-label": "Okraj", + "margin-book-tooltip": "Veľkosť rozstupu na každej strane obrazovky. Na mobilných zariadeniach sa táto hodnota prepíše na 0 bez ohľadu na toto nastavenie.", + "pdf-reader-settings-title": "PDF Čítačka", + "pdf-scroll-mode-label": "Režim scrolovania", + "pdf-scroll-mode-tooltip": "Spôsob posúvania medzi stránkami. Vertikálne/vodorovne a klepnutím pre stránkovanie (bez posúvania)", + "pdf-theme-tooltip": "Farebná téma čítačky", + "reading-profile-series-settings-title": "Série", + "profiles-title": "Vaše čitateľské profily", + "add": "{{common.add}}", + "delete": "{{common.delete}}", + "selection-tip": "Vyberte profil zo zoznamu alebo si vytvorte nový v pravom hornom rohu", + "font-size-book-tooltip": "Percentuálna zmena mierky, ktorá sa má použiť na písmo v knihe", + "line-height-book-tooltip": "Aké medzery medzi riadkami v knihe", + "extra-tip": "Priraďte čitateľské profily prostredníctvom ponuky akcií v sériách a knižniciach alebo hromadne. Pri zmene nastavení v čítačke sa vytvorí skrytý profil, ktorý si pamätá vaše voľby pre danú sériu (nie pre súbory PDF). Tento profil sa odstráni, keď k sérii priradíte alebo aktualizujete jeden z vlastných čitateľských profilov.", + "default-profile": "Predvolené", + "add-tooltip": "Váš nový profil bude uložený po vykonaní zmien", + "make-default": "Nastaviť ako predvolené", + "scaling-option-label": "Možnosti škálovania", + "description": "Nie všetky vaše série sa dajú čítať rovnakým spôsobom, preto si pre každú knižnicu alebo sériu nastavte samostatné čitateľské profily, aby bol návrat k sérii čo najplynulejší.", + "immersive-mode-tooltip": "Týmto sa ponuka skryje za kliknutím na dokument čítačky a zapne sa možnosť klepnutia pre stránkovanie", + "image-reader-settings-title": "Čítačka obrázkov", + "pdf-spread-mode-tooltip": "Ako by mali byť stránky rozložené. Jednoduché alebo dvojité (párne/nepárne)", + "page-splitting-label": "Rozdelenie stránky", + "swipe-to-paginate-tooltip": "Malo by potiahnutie prstom po obrazovke spustiť ďalšiu alebo predchádzajúcu stránku", + "layout-mode-book-tooltip": "Ako by mal byť obsah rozložený. Posúvanie je také, aké je v knihe. 1 alebo 2 stĺpce sa prispôsobia výške zariadenia a zmestia sa 1 alebo 2 stĺpce textu na stranu", + "color-theme-book-label": "Farebný motív", + "page-splitting-tooltip": "Ako rozdeliť obrázok na celú šírku (t. j. ľavý aj pravý obrázok sa spoja)", + "allow-auto-webtoon-reader-label": "Automatický režim čítačky webových karikatúr", + "reading-direction-label": "Smer čítania", + "reading-direction-tooltip": "Smer kliknutia pre presun na ďalšiu stránku. Sprava doľava znamená, že kliknutím na ľavú stranu obrazovky prejdete na ďalšiu stránku.", + "emulate-comic-book-tooltip": "Aplikuje efekt tieňa na emuláciu čítania z knihy", + "background-color-tooltip": "Farba pozadia čítačky obrázkov", + "reading-mode-tooltip": "Zmeňte čítačku na vertikálne, horizontálne stránkovanie alebo nekonečné posúvanie", + "layout-mode-label": "Režim rozloženia", + "allow-auto-webtoon-reader-tooltip": "Ak stránky vyzerajú ako webtoon, prepnite do režimu čítačky webtoonov. Môžu sa vyskytnúť falošne pozitívne výsledky.", + "pdf-spread-mode-label": "Režim šírenia", + "layout-mode-tooltip": "Zobrazenie jedného obrázka na obrazovke alebo dvoch obrázkov vedľa seba", + "emulate-comic-book-label": "Napodobniť komiks", + "swipe-to-paginate-label": "Prejdením prstom prejdete na stránkovanie", + "line-height-book-label": "Riadkovanie", + "background-color-label": "Farba pozadia", + "reading-profile-library-settings-title": "Knižnica", + "width-override-tooltip": "Prepísať šírku obrázkov v čítačke", + "font-family-tooltip": "Skupina písiem, ktoré sa majú načítať. Predvolené načíta predvolené písmo knihy", + "color-theme-book-tooltip": "Akú farebnú tému použiť na obsah a ponuku čítačky kníh", + "reading-mode-label": "Režim čítania", + "pdf-theme-label": "Téma" + }, + "bulk-set-reading-profile-modal": { + "close": "{{common.close}}", + "title": "Nastaviť profil čítania", + "filter-label": "{{common.filter}}", + "clear": "{{common.clear}}", + "no-data": "Zatiaľ nie sú vytvorené žiadne zbierky", + "loading": "{{common.loading}}", + "bound": "Väzba", + "create": "{{common.create}}" + }, + "reviews": { + "user-reviews-local": "Miestne recenzie", + "user-reviews-plus": "Externé recenzie" + }, + "review-modal": { + "title": "Upraviť recenziu", + "review-label": "Recenzia", + "close": "{{common.close}}", + "save": "{{common.save}}", + "delete": "{{common.delete}}", + "min-length": "Recenzia musí mať aspoň {{count}} znakov", + "required": "{{validation.required-field}}" } } diff --git a/UI/Web/src/assets/langs/sv.json b/UI/Web/src/assets/langs/sv.json index f18657fe4..5dfd7a1c0 100644 --- a/UI/Web/src/assets/langs/sv.json +++ b/UI/Web/src/assets/langs/sv.json @@ -79,71 +79,20 @@ "blur-unread-summaries-label": "Censurera olästa sammanfattningar", "blur-unread-summaries-tooltip": "Cencurera handlingen på volymer eller kapitel som inte har någon läsprogress (för att undvika spoilers)", "disable-animations-tooltip": "Inaktivera animationer på sidan. Användbart på e-ink-läsare.", - "reading-mode-label": "Läsläge", - "layout-mode-label": "Layoutläge", - "show-screen-hints-label": "Visa Skärmtips", - "immersive-mode-label": "Immersionsläge", - "layout-mode-book-tooltip": "Hur innehåll ska visas. Bläddra är som boken har packat det. 1 eller 2 Kolumn anpassar till höjden på enheten och passar in 1 eller 2 kolumner av text per sida", - "pdf-spread-mode-label": "Visningsläge", - "pdf-scroll-mode-tooltip": "Hur du bläddrar mellan sidorna. Vertikal/Horisontell eller Tryck för att Paginera (ingen bläddring)", "share-series-reviews-tooltip": "Ska Kavita inkludera dina recensioner av Serier för andra användare", "kavitaplus-settings-title": "Kavita+", "anilist-scrobbling-label": "AniList Scrobbling", "anilist-scrobbling-tooltip": "Tillåt Kavita att Scrobbla (envägssynk) läsförlopp och betygsättningar till AniList", "want-to-read-sync-label": "Vill Läsa Synk", "want-to-read-sync-tooltip": "Tillåt Kavita att lägga till artiklar till din Vill Läsa-lista baserat på AniList- och MAL-serier i Väntande läslista", - "reading-direction-tooltip": "Riktning att klicka för att gå till nästa sida. Höger till Vänster betyder att du klickar på vänster sida om skärmen för att gå till nästa sida.", - "scaling-option-label": "Skalningsalternativ", - "background-color-label": "Bakgrundsfärg", - "auto-close-menu-label": "Stäng Meny Automatiskt", - "show-screen-hints-tooltip": "Visa en overlay för att hjälpa till att förstå pagineringsområde och riktning", - "emulate-comic-book-label": "Emulera serietidning", - "emulate-comic-book-tooltip": "Applicerar en skuggeffekt för att efterlikna läsning från en bok", - "swipe-to-paginate-label": "Svep för att Paginera", - "swipe-to-paginate-tooltip": "Ska svepning på skärmen leda till nästa eller föregående sida", - "book-reader-settings-title": "Bokläsare", - "immersive-mode-tooltip": "Detta döljer menyn bakom ett klick på läsarens dokument och anger tryck för att paginera på", - "reading-direction-book-label": "Läsriktning", - "reading-direction-book-tooltip": "Riktning att klicka för att gå til nästa sida. Höger till Vänster betyder att du klickar på vänster sida av skärmen för att gå till nästa sida.", - "font-family-label": "Typsnittsfamilj", - "font-family-tooltip": "Typsnitt att ladda. Standard kommer ladda bokens standardtypsnitt", - "layout-mode-book-label": "Layoutläge", - "color-theme-book-label": "Färgtema", - "color-theme-book-tooltip": "Vilket färgtema ska appliceras till bokläsarinnehåll och meny", - "line-height-book-label": "Radavstånd", - "line-height-book-tooltip": "Hur stort avstånd det ska vara mellan rader i boken", - "margin-book-label": "Marginal", - "pdf-reader-settings-title": "PDF-Läsare", - "image-reader-settings-title": "Bildläsare", - "reading-direction-label": "Läsriktning", - "page-splitting-label": "Siddelning", - "scaling-option-tooltip": "Hur du skalar bilden till din skärm.", - "tap-to-paginate-label": "Tryck för att Paginera", - "tap-to-paginate-tooltip": "Ska sidorna av bokläsarskärmen tillåta tryck för att gå till föregående/nästa sida", - "writing-style-tooltip": "Ändrar riktningen av texten. Horisontal är vänster till höger, vertikal är uppifrån och ner.", - "font-size-book-label": "Typsnittsstorlek", - "margin-book-tooltip": "Hur stort avstånd det ska vara på endera sida av skärmen. Detta skrivs över av 0 på mobila enheter oavsett inställning.", - "pdf-scroll-mode-label": "Bläddringsläge", "collapse-series-relationships-label": "Dölj Serierelationer", - "background-color-tooltip": "Bakgrundsfärg för Bildläsaren", "collapse-series-relationships-tooltip": "Ska Kavita visa Serier som saknar relation eller är förälder/uppföljare", - "auto-close-menu-tooltip": "Ska menyn stängas automatiskt", - "font-size-book-tooltip": "Procent skalning att applicera på typsnitt i boken", - "pdf-spread-mode-tooltip": "Hur sidor ska visas. Enkel eller dubbel (udda/jämna)", "share-series-reviews-label": "Dela Serierecensioner", - "page-splitting-tooltip": "Hur man delar en fullbreddsbild (dvs både den vänsta och högra bilden är sammanfogade)", - "reading-mode-tooltip": "Ändra läsaren till att paginera (numrera) vertikalt, horisontellt, eller att ha oändlig bläddring", - "writing-style-label": "Skrivstil", - "layout-mode-tooltip": "Återge en enstaka bild på skärmen eller två sida-vid-sida-bilder", - "pdf-theme-label": "Utseende", "clients-opds-url-label": "OPDS-länk", "clients-api-key-label": "API-nyckel", "clients-opds-url-tooltip": "Se en lista av kompatibla OPDS klienter. ", "clients-opds-description": "Alla tredje partens klienter kommer använda antingen API-nyckeln eller Anslutnings-länken nedan. Dessa är som lösenord, håll dem hemliga.", "clients-opds-alert": "OPDS är inte aktiverat på denna server. Detta kommer inte påverka Tachiyomi användare.", - "pdf-theme-tooltip": "Läsarens färgschema", - "allow-auto-webtoon-reader-label": "Automatisk Webtoon Läsarläge", - "allow-auto-webtoon-reader-tooltip": "Byt till Webtoon Läsarläge om sidor liknar webtoon. Vissa kan visas som falskpositiv.", "clients-api-key-tooltip": "API-nyckeln är som ett lösenord. Återställs den tas alla anslutningar till klienter bort." }, "user-holds": { @@ -660,7 +609,6 @@ "home": "Hem", "all-series": "Alla Serier", "want-to-read": "Vill läsa", - "browse-authors": "Bläddra Författare", "donate": "Donera" }, "library-settings-modal": { @@ -1338,7 +1286,6 @@ "auto-close-menu-label": "{{user-preferences.auto-close-menu-label}}", "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", "image-splitting-label": "Bilduppdelning", - "user-preferences-updated": "Användarinställningar uppdaterade", "prev-page-tooltip": "Föregående Sida", "next-page-tooltip": "Nästa Sida", "unbookmark-page-tooltip": "Ta bort Bokmärning av Sida", @@ -1369,7 +1316,6 @@ "back": "Bakåt", "swipe-enabled-label": "Svep Aktiverat", "bookmark-page-tooltip": "Bokmärk Sida", - "save-globally": "Spara Globalt", "reading-mode-tooltip": "Läsläge", "off": "Av", "first-time-reading-manga": "Tryck på bilden när som helst för att öppna menyn. Du kan konfigurera olika inställningar eller gå till en sida genom att klicka på progressbaren. Tryck på sidorna av bilden för att gå till nästa eller föregående sida.", @@ -1964,11 +1910,6 @@ "downloading-status": "Laddar ner…", "download-tooltip": "Ladda ner" }, - "browse-authors": { - "cover-image-description": "{{edit-series-modal.cover-image-description}}", - "author-count": "{{num}} Personer", - "title": "Bläddra Författare" - }, "file-type-group-pipe": { "archive": "Arkiv", "epub": "Epub", diff --git a/UI/Web/src/assets/langs/ta.json b/UI/Web/src/assets/langs/ta.json index 5b61fd060..73df4f370 100644 --- a/UI/Web/src/assets/langs/ta.json +++ b/UI/Web/src/assets/langs/ta.json @@ -820,14 +820,12 @@ "unbookmark-page-tooltip": "புத்தகமன் பக்கம்", "bookmarks-title": "புக்மார்க்குகள்", "collapse": "சரிவு", - "save-globally": "உலகளவில் சேமிக்கவும்", "incognito-alt": "மறைநிலை பயன்முறை இயக்கத்தில் உள்ளது. அணைக்க மாற்று.", "back": "பின்", "first-time-reading-manga": "மெனுவைத் திறக்க எந்த நேரத்திலும் படத்தைத் தட்டவும். முன்னேற்றப் பட்டியைக் சொடுக்கு செய்வதன் மூலம் நீங்கள் வெவ்வேறு அமைப்புகளை உள்ளமைக்கலாம் அல்லது பக்கத்திற்குச் செல்லலாம். படத்தின் பக்கங்களைத் தட்டவும் அடுத்த/முந்தைய பக்கத்திற்கு நகர்த்தவும்.", "layout-mode-switched": "இரட்டை தளவமைப்பை வழங்க போதுமான இடம் இல்லாததால் தளவமைப்பு பயன்முறை ஒற்றைக்கு மாறியது", "no-next-chapter": "அடுத்த அத்தியாயம் இல்லை", "no-prev-chapter": "முந்தைய அத்தியாயம் இல்லை", - "user-preferences-updated": "பயனர் விருப்பத்தேர்வுகள் புதுப்பிக்கப்பட்டன", "emulate-comic-book-label": "{{பயனர்-முன்னுரிமைகள்.இமுலேட்-காமிக்-புத்தக-லேபிள்}}", "series-progress": "தொடர் முன்னேற்றம்: {{percentage}}}" }, @@ -1215,8 +1213,6 @@ "create": "{{common.create}}" }, "user-preferences": { - "background-color-label": "பின்னணி நிறம்", - "margin-book-label": "விளிம்பு", "title": "பயனர் டாச்போர்டு", "pref-description": "இவை உங்கள் கணக்கிற்கு கட்டுப்பட்ட உலகளாவிய அமைப்புகள்.", "account-tab": "{{tabs.account-tab}}}", @@ -1242,53 +1238,6 @@ "collapse-series-relationships-tooltip": "உறவுகள் இல்லாத அல்லது பெற்றோர்/முன்னுரை இல்லாத கவிதா நிகழ்ச்சிகளைக் காட்ட வேண்டுமா", "share-series-reviews-label": "பங்கு தொடர் மதிப்புரைகளைப் பகிரவும்", "share-series-reviews-tooltip": "கவிதா மற்ற பயனர்களுக்கான தொடரின் உங்கள் மதிப்புரைகளை சேர்க்க வேண்டுமா", - "image-reader-settings-title": "பட வாசகர்", - "reading-direction-label": "வாசிப்பு திசை", - "reading-direction-tooltip": "அடுத்த பக்கத்திற்கு செல்ல சொடுக்கு செய்வதற்கான திசை. வலதுபுறம் இடதுபுறம் என்பது அடுத்த பக்கத்திற்கு செல்ல திரையின் இடது பக்கத்தில் சொடுக்கு செய்க.", - "scaling-option-label": "அளவிடுதல் விருப்பங்கள்", - "scaling-option-tooltip": "உங்கள் திரையில் படத்தை எவ்வாறு அளவிடுவது.", - "page-splitting-label": "பக்கம் பிரித்தல்", - "page-splitting-tooltip": "ஒரு முழு அகல படத்தை எவ்வாறு பிரிப்பது (அதாவது இடது மற்றும் வலது படங்கள் இணைக்கப்படுகின்றன)", - "reading-mode-label": "படித்தல் பயன்முறை", - "reading-mode-tooltip": "வாசகரை செங்குத்தாக, கிடைமட்டமாக, அல்லது எல்லையற்ற சுருளாக மாற்றவும்", - "layout-mode-label": "தளவமைப்பு பயன்முறை", - "layout-mode-tooltip": "ஒரு படத்தை திரையில் அல்லது இரண்டு பக்கவாட்டு படங்களை வழங்கவும்", - "background-color-tooltip": "பட வாசகரின் பின்னணி நிறம்", - "auto-close-menu-label": "ஆட்டோ மூடு பட்டியல்", - "auto-close-menu-tooltip": "பட்டியல் தானாக மூட வேண்டும்", - "show-screen-hints-label": "திரை குறிப்புகளைக் காட்டு", - "show-screen-hints-tooltip": "மண்பாண்டம் மற்றும் திசையைப் புரிந்துகொள்ள உதவும் மேலடுக்கு ஒரு மேலடுக்கைக் காட்டு", - "emulate-comic-book-label": "காமிக் புத்தகத்தைப் பின்பற்றுங்கள்", - "emulate-comic-book-tooltip": "ஒரு புத்தகத்திலிருந்து வாசிப்பைப் பின்பற்ற ஒரு நிழல் விளைவைப் பயன்படுத்துகிறது", - "swipe-to-paginate-label": "புறக்கணிக்க ச்வைப் செய்யவும்", - "swipe-to-paginate-tooltip": "திரையில் ச்வைப் செய்வது அடுத்த அல்லது முந்தைய பக்கம் தூண்டப்பட வேண்டும்", - "book-reader-settings-title": "புத்தக வாசகர்", - "tap-to-paginate-label": "புறக்கணிக்க தட்டவும்", - "tap-to-paginate-tooltip": "புத்தக வாசகர் திரையின் பக்கங்கள் முந்தைய/அடுத்த பக்கத்திற்கு செல்ல அதைத் தட்ட அனுமதிக்க வேண்டும்", - "immersive-mode-label": "அதிவேக முறை", - "immersive-mode-tooltip": "இது ரீடர் ஆவணத்தில் ஒரு கிளிக்கின் பின்னால் உள்ள மெனுவை மறைத்து, தட்டுதல்", - "reading-direction-book-label": "வாசிப்பு திசை", - "reading-direction-book-tooltip": "அடுத்த பக்கத்திற்கு செல்ல சொடுக்கு செய்வதற்கான திசை. வலதுபுறம் இடதுபுறம் என்பது அடுத்த பக்கத்திற்கு செல்ல திரையின் இடது பக்கத்தில் சொடுக்கு செய்க.", - "font-family-label": "எழுத்துரு குடும்பம்", - "font-family-tooltip": "ஏற்றுவதற்கு எழுத்துரு குடும்பம். இயல்புநிலை புத்தகத்தின் இயல்புநிலை எழுத்துருவை ஏற்றும்", - "writing-style-label": "எழுதும் நடை", - "writing-style-tooltip": "உரையின் திசையை மாற்றுகிறது. கிடைமட்டமானது இடமிருந்து வலமாக உள்ளது, செங்குத்து மேலிருந்து கீழாக உள்ளது.", - "layout-mode-book-label": "தளவமைப்பு பயன்முறை", - "layout-mode-book-tooltip": "உள்ளடக்கம் எவ்வாறு அமைக்கப்பட வேண்டும். நூல் அதை பொதி செய்வது போல உருள் உள்ளது. 1 அல்லது 2 நெடுவரிசை சாதனத்தின் உயரத்திற்கு பொருந்துகிறது மற்றும் ஒரு பக்கத்திற்கு 1 அல்லது 2 நெடுவரிசைகளுக்கு பொருந்துகிறது", - "color-theme-book-label": "வண்ண கருப்பொருள்", - "color-theme-book-tooltip": "புத்தக வாசகர் உள்ளடக்கம் மற்றும் மெனுவுக்கு என்ன வண்ண கருப்பொருள் பொருந்தும்", - "font-size-book-label": "எழுத்துரு அளவு", - "font-size-book-tooltip": "புத்தகத்தில் எழுத்துருவுக்கு விண்ணப்பிக்க அளவிடுதலின் விழுக்காடு", - "line-height-book-label": "வரி இடைவெளி", - "line-height-book-tooltip": "புத்தகத்தின் வரிகளுக்கு இடையில் எவ்வளவு இடைவெளி", - "margin-book-tooltip": "திரையின் ஒவ்வொரு பக்கத்திலும் எவ்வளவு இடைவெளி. இந்த அமைப்பைப் பொருட்படுத்தாமல் இது மொபைல் சாதனங்களில் 0 ஆக மேலெழுதும்.", - "pdf-reader-settings-title": "PDF ரீடர்", - "pdf-scroll-mode-label": "உருள் பயன்முறை", - "pdf-scroll-mode-tooltip": "நீங்கள் பக்கங்கள் வழியாக எப்படி உருட்டுகிறீர்கள். செங்குத்து/கிடைமட்ட மற்றும் புறக்கணிப்பு (சுருள் இல்லை)", - "pdf-spread-mode-label": "பரவல் பயன்முறை", - "pdf-spread-mode-tooltip": "பக்கங்கள் எவ்வாறு அமைக்கப்பட வேண்டும். ஒற்றை அல்லது இரட்டை (ஒற்றைப்படை/கூட)", - "pdf-theme-label": "கருப்பொருள்", - "pdf-theme-tooltip": "வாசகரின் வண்ண கருப்பொருள்", "clients-opds-alert": "இந்த சேவையகத்தில் OPDS இயக்கப்படவில்லை. இது டச்சியோமி பயனர்களை பாதிக்காது.", "clients-opds-description": "அனைத்து 3 வது தரப்பு வாடிக்கையாளர்களும் பநிஇ விசை அல்லது கீழே உள்ள இணைப்பு முகவரி ஐப் பயன்படுத்துவார்கள். இவை கடவுச்சொற்கள் போன்றவை, அதை தனிப்பட்டதாக வைத்திருங்கள்.", "clients-api-key-tooltip": "பநிஇ விசை கடவுச்சொல் போன்றது. அதை மீட்டமைப்பது தற்போதுள்ள எந்த வாடிக்கையாளர்களையும் செல்லாது.", @@ -1898,7 +1847,6 @@ "collections": "சேகரிப்புகள்", "reading-lists": "பட்டியல்களைப் படித்தல்", "bookmarks": "புக்மார்க்குகள்", - "browse-authors": "ஆசிரியர்களை உலாவுக", "filter-label": "{{common.filter}}}", "all-series": "அனைத்து தொடர்களும்", "clear": "{{common.clear}}}", @@ -1908,11 +1856,6 @@ "more": "மேலும்", "customize": "{{settings.customize}}" }, - "browse-authors": { - "title": "ஆசிரியர்கள் மற்றும் எழுத்தாளர்களை உலாவுக", - "author-count": "{{num}} மக்கள்", - "cover-image-description": "{{edit-series-modal.cover-image-description}}" - }, "person-detail": { "known-for-title": "அறியப்படுகிறது", "individual-role-title": "ஒரு {{role}}", diff --git a/UI/Web/src/assets/langs/th.json b/UI/Web/src/assets/langs/th.json index 059c707e6..d09001c7b 100644 --- a/UI/Web/src/assets/langs/th.json +++ b/UI/Web/src/assets/langs/th.json @@ -99,42 +99,6 @@ "collapse-series-relationships-tooltip": "Kavita ควรแสดงซีรีส์ที่ไม่มีความสัมพันธ์หรือเป็นพาเรนต์/พรีเควล", "share-series-reviews-label": "แบ่งปันบทวิจารณ์ซีรีส์", "share-series-reviews-tooltip": "Kavita ควรรวมบทวิจารณ์ซีรี่ส์ของคุณสำหรับผู้ใช้รายอื่นหรือไม่", - "image-reader-settings-title": "โปรแกรมอ่านรูปภาพ", - "reading-direction-label": "ทิศทางการอ่าน", - "reading-direction-tooltip": "ทิศทางการคลิกเพื่อไปยังหน้าถัดไป ขวาไปซ้าย หมายถึงคุณคลิกที่ด้านซ้ายของหน้าจอเพื่อไปยังหน้าถัดไป", - "scaling-option-label": "ตัวเลือกการปรับขนาด", - "scaling-option-tooltip": "วิธีปรับขนาดภาพให้พอดีกับหน้าจอของคุณ", - "page-splitting-label": "การแยกหน้า", - "page-splitting-tooltip": "วิธีแยกภาพเต็มความกว้าง (เช่น รวมภาพซ้ายและขวาเข้าด้วยกัน)", - "reading-mode-label": "โหมดการอ่าน", - "layout-mode-label": "โหมดเค้าโครง", - "layout-mode-tooltip": "เรนเดอร์ภาพเดียวไปที่หน้าจอหรือสองภาพเคียงข้างกัน", - "background-color-label": "สีพื้นหลัง", - "auto-close-menu-label": "ปิดเมนูอัตโนมัติ", - "show-screen-hints-label": "แสดงคำแนะนำบนหน้าจอ", - "emulate-comic-book-label": "เลียนแบบหนังสือการ์ตูน", - "swipe-to-paginate-label": "ปัดเพื่อเปลี่ยนหน้า", - "book-reader-settings-title": "เครื่องอ่านหนังสือ", - "tap-to-paginate-label": "แตะเพื่อเปลี่ยนหน้า", - "tap-to-paginate-tooltip": "อนุญาตให้แตะมุมหนังสือเพื่อเลื่อนไปยังหน้าก่อนหน้า/ถัดไป", - "immersive-mode-label": "โหมดดื่มด่ำ", - "immersive-mode-tooltip": "วิธีนี้จะซ่อนเมนูหลังการคลิกบนเอกสารของผู้อ่านและเปิดการแตะเพื่อแบ่งหน้า", - "reading-direction-book-label": "ทิศทางการอ่าน", - "reading-direction-book-tooltip": "ทิศทางการคลิกเพื่อไปยังหน้าถัดไป ขวาไปซ้าย หมายถึงคุณคลิกที่ด้านซ้ายของหน้าจอเพื่อไปยังหน้าถัดไป", - "font-family-label": "ตระกูลฟอนต์", - "font-family-tooltip": "ตระกูลฟอนต์เริ่มต้น ค่าเริ่มต้นจะโหลดแบบอักษรเริ่มต้นของหนังสือ", - "writing-style-label": "สไตล์การเขียน", - "writing-style-tooltip": "เปลี่ยนทิศทางของข้อความ แนวนอนคือซ้ายไปขวา แนวตั้งคือบนลงล่าง", - "layout-mode-book-label": "โหมดเค้าโครง", - "layout-mode-book-tooltip": "ควรจัดวางเนื้อหาอย่างไร เลื่อนเป็นไปตามที่หนังสือบรรจุไว้ 1 หรือ 2 คอลัมน์พอดีกับความสูงของอุปกรณ์ และพอดีกับ 1 หรือ 2 คอลัมน์ของข้อความต่อหน้า", - "color-theme-book-label": "ธีมสี", - "color-theme-book-tooltip": "ธีมสีใดที่จะใช้กับเนื้อหาและเมนูของผู้อ่านหนังสือ", - "font-size-book-label": "ขนาดตัวอักษร", - "line-height-book-label": "ระยะห่างบรรทัด", - "line-height-book-tooltip": "ระยะห่างระหว่างบรรทัดของหนังสือมากน้อยเพียงใด", - "margin-book-label": "ระยะขอบ", - "margin-book-tooltip": "ระยะห่างแต่ละด้านของหน้าจอเท่าใด ค่านี้จะแทนที่เป็น 0 บนอุปกรณ์เคลื่อนที่โดยไม่คำนึงถึงการตั้งค่านี้", - "pdf-reader-settings-title": "โปรแกรมอ่าน PDF", "clients-opds-alert": "OPDS ไม่ได้เปิดใช้งานบนเซิร์ฟเวอร์นี้ สิ่งนี้จะไม่ส่งผลกระทบต่อผู้ใช้ Tachiyomi", "clients-opds-description": "ไคลแอนบุคคลที่สามทั้งหมดจะใช้คีย์ API หรือ URL การเชื่อมต่อด้านล่าง สิ่งเหล่านี้เป็นเหมือนรหัสผ่าน โปรดเก็บไว้เป็นส่วนตัว", "clients-api-key-tooltip": "คีย์ API เป็นเหมือนรหัสผ่าน เก็บเป็นความลับ เก็บไว้ให้ปลอดภัย", @@ -787,7 +751,6 @@ "close": "{{common.close}}" }, "manga-reader": { - "save-globally": "บันทึก", "auto-close-menu-label": "{{user-preferences.auto-close-menu-label}}", "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}" }, diff --git a/UI/Web/src/assets/langs/tr.json b/UI/Web/src/assets/langs/tr.json index eb08d66df..31de092a6 100644 --- a/UI/Web/src/assets/langs/tr.json +++ b/UI/Web/src/assets/langs/tr.json @@ -23,7 +23,8 @@ "cancel": "{{common.cancel}}", "saving": "Kaydediliyor…", "update": "Güncelle", - "account-detail-title": "Hesap ayrıntıları" + "account-detail-title": "Hesap ayrıntıları", + "invalid-email-warning": "Doğru olmayan bir e-posta, Kavita'nın bazı işlevlerini engelleyecektir" }, "user-scrobble-history": { "title": "Arşiv geçmişi", @@ -90,32 +91,12 @@ "disable-animations-tooltip": "Sitedeki animasyonları kapat. E-kitap okuyucular için ideal.", "share-series-reviews-label": "Seri incelemelerini paylaş", "share-series-reviews-tooltip": "Kavita, Seriler hakkındaki yorumlarınızı diğer kullanıcılar için eklemeli mi?", - "image-reader-settings-title": "Resim okuyucu", - "reading-direction-label": "Okuma yönü", - "scaling-option-label": "Ölçekleme Ayarları", - "page-splitting-label": "Sayfa bölmesi", - "reading-mode-label": "Okuma Modu", - "layout-mode-label": "Düzen modu", - "background-color-label": "Arka plan rengi", - "auto-close-menu-label": "Otomatik Kapatma Menüsü", - "show-screen-hints-label": "Ekran ipuçlarını göster", - "emulate-comic-book-label": "Çizgi romanı emüle et", - "book-reader-settings-title": "Kitap okuyucu", - "reading-direction-book-label": "Okuma yönü", - "font-family-label": "Font ailesi", - "writing-style-label": "Yazı stili", - "color-theme-book-label": "Renk teması", - "font-size-book-label": "Font Boyutu", - "line-height-book-label": "Satır Aralığı", - "line-height-book-tooltip": "Kitapta satır aralıları ne kadar olmalıdır", - "margin-book-label": "Birleştir", "clients-opds-alert": "OPDS bu sunucuda aktif değil. Bu Tachiyomi kullanıcıları etkilemeyecek.", "clients-opds-description": "Tüm 3. Parti istemciler API key ya da alttaki bağlantı linkini kullanmak zorundadır. Bunlar şifre gibidir, başkalarıyla paylaşmayın.", "clients-api-key-tooltip": "API anahtarı bir şifre gibidir. Sıfırlamak mevcut istemcileri geçersiz kılar.", "clients-opds-url-tooltip": "Desteklenen OPDS istemcilerinin listesine bakın: ", "reset": "{{common.reset}}", "save": "{{common.save}}", - "pdf-reader-settings-title": "PDF okuyucu", "kavitaplus-settings-title": "Kavita+" }, "user-holds": { @@ -147,7 +128,9 @@ "not-applicable-for-admins": "Bu yöneticiler için uygullanamaz.", "age-rating-label": "{{metadata-fields.age-rating-title}}", "no-restriction": "Hiçbir sınırlama yok", - "include-unknowns-label": "Bilinmeyenleri ekle" + "include-unknowns-label": "Bilinmeyenleri ekle", + "description": "Seçildiğinde, seçilen kısıtlamadan daha büyük olan en az bir ögeye sahip tüm seri ve okuma listeleri sonuçlardan kaldırılır.", + "include-unknowns-tooltip": "Eğer doğruysa, bilinmeyenlere yaş kısıtlaması ile izin verilecektir. Bu, yaş kısıtlamaları olan kullanıcılara atılmayan medyanın sızmasına yol açabilir." }, "site-theme-provider-pipe": { "system": "Sistem", @@ -161,7 +144,9 @@ "delete": "{{common.delete}}", "edit": "{{common.edit}}", "no-data": "{{typeahead.no-data}}", - "actions-header": "{{manage-users.actions-header}}" + "actions-header": "{{manage-users.actions-header}}", + "email-setup-alert": "Cihazlarınıza dosya göndermek ister misiniz? Önce yönetici kurulum e-posta ayarlarınızı alın!", + "email-label": "E-posta" }, "edit-device-modal": { "device-name-label": "{{manage-devices.name-label}}", @@ -171,7 +156,8 @@ "close": "{{common.close}}", "cancel": "{{common.cancel}}", "required-field": "{{validation.required-field}}", - "valid-email": "{{validation.valid-email}}" + "valid-email": "{{validation.valid-email}}", + "email-tooltip": "Bu e-posta, dosya göndermeyi kabul etmek için kullanılacaktır" }, "change-password": { "password-label": "{{common.password}}", @@ -194,13 +180,18 @@ "setup-user-account": "Kullanıcının hesabını kur", "email-title": "E-posta", "email-label": "Yeni e-posta", - "current-password-label": "Geçerli parola" + "current-password-label": "Geçerli parola", + "email-updated-title": "E-posta güncellendi", + "email-not-confirmed": "Bu e-posta onaylanmadı", + "email-confirmed": "Bu e-posta onaylandı" }, "change-age-restriction": { "reset": "{{common.reset}}", "edit": "{{common.edit}}", "cancel": "{{common.cancel}}", - "save": "{{common.save}}" + "save": "{{common.save}}", + "age-restriction-label": "Yaş kısıtlaması", + "unknowns": "Bilinmeyenler" }, "scrobbling-providers": { "instructions": "Kullanıcılar ilk kez Kavita+'nın {{service}} ile iletişimine izin vermek için aşağıdaki \"{{scrobbling-providers.generate}}\" üzerine tıklamalıdır. Uygulamaya yetki verdikten sonra, jetonu kopyalayıp aşağıdaki girişe yapıştırın. Jetonunuzu istediğiniz zaman yenileyebilirsiniz.", @@ -209,7 +200,8 @@ "save": "{{common.save}}", "loading": "{{common.loading}}", "anilist-first-later": "Sonra", - "anilist-first-now": "Şimdi" + "anilist-first-now": "Şimdi", + "generate": "Üret" }, "typeahead": { "close": "{{common.close}}", @@ -225,7 +217,9 @@ "total-pages-read-tooltip": "{{user-stats-info-cards.total-pages-read-label}}: {{value}}", "total-words-read-tooltip": "{{user-stats-info-cards.total-words-read-label}}: {{value}}", "time-spent-reading-tooltip": "{{user-stats-info-cards.time-spent-reading-label}}: {{value}}", - "chapters-read-tooltip": "{{user-stats-info-cards.chapters-read-label}}: {{value}}" + "chapters-read-tooltip": "{{user-stats-info-cards.chapters-read-label}}: {{value}}", + "total-words-read-label": "Toplam okunan sözcük", + "total-pages-read-label": "Toplam okunan sayfa" }, "top-readers": { "this-week": "{{time-periods.this-week}}", @@ -237,12 +231,17 @@ }, "role-selector": { "deselect-all": "{{common.deselect-all}}", - "select-all": "{{common.select-all}}" + "select-all": "{{common.select-all}}", + "title": "Roller" }, "directory-picker": { "close": "{{common.close}}", "cancel": "{{common.cancel}}", - "help": "{{common.help}}" + "help": "{{common.help}}", + "path-label": "Yol", + "type-header": "Tür", + "name-header": "Ad", + "share": "Paylaş" }, "library-access-modal": { "select-all": "{{common.select-all}}", @@ -310,7 +309,8 @@ "save": "{{common.save}}" }, "book-reader": { - "bookmarks-header": "{{side-nav.bookmarks}}" + "bookmarks-header": "{{side-nav.bookmarks}}", + "go-to-page-prompt": "{{totalPages}} sayfa vardır. Hangi sayfaya gitmek istiyorsunuz?" }, "confirm-email": { "username-label": "{{common.username}}", @@ -467,7 +467,8 @@ "entity-title": { "issue-num": "{{common.issue-hash-num}}", "chapter": "{{common.chapter-num}}", - "book-num": "{{common.book-num-shorthand}}" + "book-num": "{{common.book-num-shorthand}}", + "single-volume": "Tek cilt" }, "manage-media-issues": { "filter-label": "{{common.filter}}" @@ -561,7 +562,8 @@ "close": "{{common.close}}" }, "shortcuts-modal": { - "close": "{{common.close}}" + "close": "{{common.close}}", + "double-click": "çift tık" }, "grouped-typeahead": { "genres": "{{metadata-fields.genres-title}}", @@ -604,7 +606,8 @@ }, "manga-reader": { "auto-close-menu-label": "{{user-preferences.auto-close-menu-label}}", - "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}" + "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", + "layout-mode-switched": "Düzen modu, çift düzeni oluşturmak için yetersiz alan nedeniyle tek düzene geçti" }, "metadata-filter": { "filter-title": "{{common.filter}}", @@ -731,7 +734,14 @@ }, "server-stats": { "tags": "{{metadata-fields.tags-title}}", - "genres": "{{metadata-fields.genres-title}}" + "genres": "{{metadata-fields.genres-title}}", + "total-genres-tooltip": "Toplam tür: {{count}}", + "total-read-time-tooltip": "Toplam okuma süresi: {{count}}", + "total-volumes-tooltip": "Toplam cilt: {{count}}", + "total-files-tooltip": "Toplam dosya: {{count}}", + "total-tags-tooltip": "Toplam etiket: {{count}}", + "total-people-tooltip": "Toplam kişi: {{count}}", + "total-read-time-label": "Toplam okuma süresi" }, "customize-dashboard-modal": { "close": "{{common.close}}", @@ -797,7 +807,10 @@ "1-column": "1 Sütun", "2-column": "2 Sütun", "cards": "Kartlar", - "up-to-down": "Yukarıdan Aşağıya" + "up-to-down": "Yukarıdan Aşağıya", + "double": "Çift", + "single": "Tek", + "double-manga": "Çift (Manga)" }, "validation": { "required-field": "Bu alan gereklidir" @@ -828,7 +841,8 @@ "account-migration-complete": "Hesap taşıma tamamlandı", "reset-ip-address": "IP adresleri sıfırla", "password-reset": "Parola sıfırla", - "email-service-reset": "E-posta hizmeti sıfırla" + "email-service-reset": "E-posta hizmeti sıfırla", + "age-restriction-updated": "Yaş kısıtlaması güncellendi" }, "api-key": { "hide": "Gizle", @@ -841,5 +855,40 @@ }, "publication-status-pipe": { "cancelled": "İptal edildi" + }, + "day-of-week-pipe": { + "monday": "Pazartesi", + "sunday": "Pazar", + "tuesday": "Salı", + "thursday": "Perşembe", + "friday": "Cuma", + "saturday": "Cumartesi", + "wednesday": "Çarşamba" + }, + "role-localized-pipe": { + "change-restriction": "Kısıtlamayı değiştir" + }, + "file-breakdown-stats": { + "total-file-size-title": "Toplam dosya boyutu:" + }, + "cbl-import-result-pipe": { + "failure": "Başarısız", + "success": "Başarılı", + "partial": "Kısmi" + }, + "relationship-pipe": { + "other": "Diğer", + "prequel": "Önsöz", + "character": "Karakter", + "parent": "Ebeveyn" + }, + "manage-metadata-settings": { + "derive-publication-status-tooltip": "Yayın durumunun toplam bölüm/cilt sayılarından türetilmesine izin ver." + }, + "card-detail-layout": { + "total-items": "Toplam {{count}} öge" + }, + "pdf-layout-mode-pipe": { + "single": "Tek sayfa" } } diff --git a/UI/Web/src/assets/langs/uk.json b/UI/Web/src/assets/langs/uk.json index 37e2c1131..bef4f81cf 100644 --- a/UI/Web/src/assets/langs/uk.json +++ b/UI/Web/src/assets/langs/uk.json @@ -71,26 +71,11 @@ "reset": "{{common.reset}}", "save": "{{common.save}}", "locale-label": "Мова", - "margin-book-label": "Поля", - "immersive-mode-tooltip": "Ховатиме меню по клацу на документ читалки й вмикатиме тапання для перегортання сторінок", - "image-reader-settings-title": "Читач зображень", - "layout-mode-label": "Вигляд макету", - "background-color-label": "Колір тла", - "color-theme-book-tooltip": "Який набір кольорів має бути застосовний до контенту та меню читалки", "blur-unread-summaries-tooltip": "Розмиває текст описів непрочитаних томів чи глав, аби не натрапити на спойлери", "disable-animations-tooltip": "Вимкнути анімації на сайті. Доречно для електронних читалок.", "collapse-series-relationships-tooltip": "Чи має Kavita показувати серії, у яких немає взаємозвʼязків чи які є найпершими або приквелами", - "reading-direction-tooltip": "Напрям, в якому треба клацати, щоб перейти на наступну сторінку. Ліворуч – треба клацнути на ліву частину екрана, щоб перейти на наступну сторінку.", - "auto-close-menu-label": "Меню автозакриття", - "show-screen-hints-label": "Показувати підказки екрана", - "emulate-comic-book-label": "Симулювати комікс", - "reading-direction-book-label": "Напрям читання", - "font-family-label": "Сімейство шрифтів", - "reading-direction-book-tooltip": "\"Ліворуч\" – клацання по лівій частині екрана перемикатиме на наступну сторінку.", - "writing-style-tooltip": "Змінює напрям тексту. Горизонтальний – зліва направо, вертикальний – зверху вниз.", "clients-opds-description": "Всі сторонні клієнти мають використовувати API-ключ або URL для звʼязку нижче. Це як паролі, зберігайте їх у таємниці.", "title": "Панель користувача", - "pdf-scroll-mode-label": "Режим прокрутки", "share-series-reviews-tooltip": "Чи має Kavita показувати ваші огляди серій іншим користувачам", "pref-description": "Це глобальні налаштування вашого облікового запису.", "success-toast": "Налаштування користувача оновлені", @@ -103,43 +88,9 @@ "disable-animations-label": "Відключити анімації", "collapse-series-relationships-label": "Згорнути взаємозвʼязки серій", "share-series-reviews-label": "Ділитися оглядами серій", - "reading-direction-label": "Напрям читання", - "scaling-option-label": "Налаштування масштабу", - "scaling-option-tooltip": "Як збільшувати зображення на вашому екрані.", - "page-splitting-label": "Розбивка сторінки", - "page-splitting-tooltip": "Як розбивати зображення на весь розворот", - "reading-mode-label": "Режим читання", - "layout-mode-tooltip": "Показувати зображення цілим на екрані чи як два зображення поруч одне з одним", - "swipe-to-paginate-label": "Свайпати для перегортання", - "book-reader-settings-title": "Читач книжок", - "tap-to-paginate-label": "Тапати для перегортання", - "tap-to-paginate-tooltip": "Чи тапання по боках екрана з текстом має перегортати сторінки", - "immersive-mode-label": "Режим занурення", - "font-family-tooltip": "Яке сімейство шрифтів має завантажуватись. За замовчуванням завантажується шрифт книги", - "writing-style-label": "Стиль письма", - "layout-mode-book-label": "Режим макету", - "layout-mode-book-tooltip": "Як має бути розташований контент. Прокрутка – стандартна реалізація книги. 1 чи 2 колонки – текст відповідає висоті пристрою й представляється 1 чи 2 колонками тексту на кожній сторінці", - "color-theme-book-label": "Кольорова тема", "clients-opds-alert": "OPDS не ввімкнуто на цьому сервері. Це не зачепить користувачів Tachiyomi.", "clients-api-key-tooltip": "API-ключ – як пароль. Його скидання унеможливить роботу усіх чинних клієнтів.", - "pdf-spread-mode-label": "Режим розподілу", - "reading-mode-tooltip": "Змінити розділення на сторінки на вертикальне, горизонтальне, чи нескінченну прокрутку", - "background-color-tooltip": "Колір тла у читача зображень", - "auto-close-menu-tooltip": "Чи має меню автоматично закриватися", - "show-screen-hints-tooltip": "Показувати підказки про область пагінації та напрям читання", - "emulate-comic-book-tooltip": "Застосовує ефект тіні, як при читанні з книги", - "swipe-to-paginate-tooltip": "Чи має проведення пальця по сторінці перегортати книгу на наступну чи попередню сторінку", "locale-tooltip": "Мова інтерфейсу Kavita", - "font-size-book-label": "Розмір шрифта", - "font-size-book-tooltip": "Розмір шрифту в книзі", - "line-height-book-label": "Міжрядковий інтервал", - "line-height-book-tooltip": "Скільки вільного місця має бути між рядками в книзі", - "margin-book-tooltip": "Скільки вільного місця має бути з кожного боку сторінки. На мобільних пристроях значення завжди 0.", - "pdf-reader-settings-title": "PDF-читалка", - "pdf-scroll-mode-tooltip": "Як відбувається прокрутка сторінок – вертикальна, горизонтальна, або перегортання сторінку по клацу", - "pdf-spread-mode-tooltip": "Як повинні бути розміщені сторінки – як одинарні чи подвійні (парні/непарні)", - "pdf-theme-label": "Тема", - "pdf-theme-tooltip": "Кольорова тема читалки", "clients-opds-url-label": "OPDS-посилання", "clients-api-key-label": "API-ключ", "clients-opds-url-tooltip": "Список підтримуваних OPDS-клієнтів: " diff --git a/UI/Web/src/assets/langs/vi.json b/UI/Web/src/assets/langs/vi.json index 2dd4fbc96..beb2a3353 100644 --- a/UI/Web/src/assets/langs/vi.json +++ b/UI/Web/src/assets/langs/vi.json @@ -61,20 +61,7 @@ "smart-filters-tab": "{{tabs.smart-filters-tab}}", "reset": "{{common.reset}}", "save": "{{common.save}}", - "reading-mode-label": "Chế Độ Đọc", - "background-color-label": "Màu nền", - "show-screen-hints-tooltip": "Hiển thị lớp phủ để giúp hiểu khu vực phân trang và hướng", - "tap-to-paginate-label": "Chạm để Chuyển Trang", - "swipe-to-paginate-label": "Vuốt để chuyển trang", - "reading-direction-book-tooltip": "Hướng nhấp để chuyển sang trang tiếp theo. Từ phải sang trái có nghĩa là bạn nhấp vào bên trái màn hình để chuyển sang trang tiếp theo.", - "layout-mode-book-label": "Bố Cục", - "layout-mode-book-tooltip": "Nội dung nên được trình bày như thế nào. Cuộn là cách mà cuốn sách được đóng gói. 1 hoặc 2 cột phù hợp với chiều cao của thiết bị và phù hợp với 1 hoặc 2 cột văn bản trên mỗi trang", "clients-opds-description": "Tất cả các khách hàng của bên thứ 3 sẽ sử dụng khóa API hoặc URL kết nối bên dưới. Chúng giống như mật khẩu, hãy giữ bí mật.", - "pdf-theme-tooltip": "Màu chủ đề của trình đọc", - "layout-mode-label": "Bố Cục", - "margin-book-label": "Căn Lề", - "font-size-book-label": "Kích Thước Phông Chữ", - "background-color-tooltip": "Màu nền của Trình Đọc Truyện Tranh", "page-layout-mode-label": "Chế Độ Bố Trí Trang", "title": "Bảng điều khiển người dùng", "pref-description": "Đây là cài đặt chung được liên kết với tài khoản của bạn.", @@ -93,42 +80,6 @@ "collapse-series-relationships-tooltip": "Kavita có nên hiển thị mối quan hệ của các truyện không (phần phụ/phần tiền truyện)", "share-series-reviews-label": "Chia sẻ đánh giá của Truyện", "share-series-reviews-tooltip": "Kavita có nên đưa đánh giá của bạn về truyện này cho những người dùng khác không", - "reading-direction-label": "Hướng Đọc", - "reading-direction-tooltip": "Hướng nhấp để chuyển sang trang tiếp theo. Từ phải sang trái có nghĩa là bạn nhấp vào bên trái màn hình để chuyển sang trang tiếp theo.", - "scaling-option-label": "Cài Đặt Thu Phóng", - "scaling-option-tooltip": "Cách thu phóng hình ảnh theo màn hình của bạn.", - "page-splitting-label": "Phân Trang", - "page-splitting-tooltip": "Cách chia một hình ảnh có chiều rộng đầy đủ (tức là cả hình ảnh bên trái và bên phải được kết hợp)", - "reading-mode-tooltip": "Thay đổi trình đọc để phân trang theo chiều dọc, chiều ngang hoặc cuộn vô hạn", - "layout-mode-tooltip": "Hiển thị một hình ảnh duy nhất trên màn hình hoặc hai hình ảnh cạnh nhau", - "image-reader-settings-title": "Trình Đọc Truyện Tranh", - "auto-close-menu-label": "Tự Động Đóng Menu", - "auto-close-menu-tooltip": "Menu có nên tự động đóng", - "show-screen-hints-label": "Hiển Thị Gợi Ý Màn Hình", - "emulate-comic-book-label": "Giả Lập Truyện Tranh", - "emulate-comic-book-tooltip": "Áp dụng hiệu ứng đổ bóng để mô phỏng việc đọc sách", - "swipe-to-paginate-tooltip": "Vuốt trên màn hình có khiến trang tiếp theo hoặc trang trước đó được hiển thị không", - "book-reader-settings-title": "Trình Đọc Sách", - "tap-to-paginate-tooltip": "Các cạnh của màn hình đọc sách có cho phép chạm vào để chuyển sang trang trước/trang tiếp theo không", - "immersive-mode-label": "Chế Độ Sống Động", - "immersive-mode-tooltip": "Thao tác này sẽ ẩn menu sau khi nhấp vào tài liệu đọc và bật chạm để chuyển trang", - "reading-direction-book-label": "Hướng Đọc", - "font-family-label": "Phông chữ", - "font-family-tooltip": "Phông chữ để hiển thị trang sách. Mặc định sẽ tải phông chữ mặc định của sách", - "writing-style-label": "Phong Cách Viết", - "writing-style-tooltip": "Thay đổi hướng của văn bản. Ngang là từ trái sang phải, dọc là từ trên xuống dưới.", - "color-theme-book-label": "Màu Chủ Đề", - "color-theme-book-tooltip": "Áp dụng chủ đề màu nào cho nội dung và menu của trình đọc sách", - "font-size-book-tooltip": "Tỷ lệ phần trăm áp dụng cho phông chữ trong sách", - "line-height-book-label": "Khoảng Cách Dòng", - "line-height-book-tooltip": "Khoảng cách giữa các dòng trong sách là bao nhiêu", - "margin-book-tooltip": "Khoảng cách giữa mỗi bên màn hình là bao nhiêu. Cài đặt này sẽ tự động bị ghi đè bằng 0 trên thiết bị di động.", - "pdf-reader-settings-title": "Trình Đọc PDF", - "pdf-scroll-mode-label": "Chế Độ Cuộn", - "pdf-scroll-mode-tooltip": "Cách bạn cuộn qua các trang. Dọc/Ngang và Chạm để Chuyển trang (không cuộn)", - "pdf-spread-mode-tooltip": "Cách bố trí các trang. Đơn hoặc đôi (lẻ/chẵn)", - "pdf-spread-mode-label": "Chế Độ Mở Rộng", - "pdf-theme-label": "Chủ Đề", "clients-opds-alert": "OPDS không được bật trên máy chủ này. Điều này sẽ không ảnh hưởng đến người dùng Tachiyomi.", "clients-api-key-tooltip": "Khóa API giống như mật khẩu. Việc đặt lại khóa sẽ làm mất hiệu lực mọi ứng dụng khách hiện có.", "clients-opds-url-tooltip": "Xem danh sách các máy khách OPDS được hỗ trợ: ", @@ -606,7 +557,6 @@ "filter-label": "{{common.filter}}", "clear": "{{common.clear}}", "customize": "{{settings.customize}}", - "browse-authors": "Duyệt Theo Tác Giả", "reading-lists": "Danh Sách Đọc", "bookmarks": "Dấu Trang", "all-series": "Tất Cả Series", @@ -1269,7 +1219,6 @@ "auto-close-menu-label": "{{user-preferences.auto-close-menu-label}}", "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", "back": "Trở Về", - "save-globally": "Lưu Tất Cả", "reading-direction-tooltip": "Hướng Đọc: ", "reading-mode-tooltip": "Chế Độ Đọc", "fullscreen": "Toàn Màn Hình", @@ -1279,7 +1228,6 @@ "brightness-label": "Độ Sáng", "unbookmark-page-tooltip": "Bỏ Đánh Dấu Trang", "no-prev-chapter": "Không có chương trước đó", - "user-preferences-updated": "Tùy chọn người dùng đã được cập nhật", "width": "Chiều Rộng", "height": "Chiều Cao", "bookmarks-title": "Dấu Trang", @@ -1917,11 +1865,6 @@ "browse-person-title": "Tất cả các tác phẩm của {{name}}", "browse-person-by-role-title": "Tất cả các tác phẩm của {{name}} với tư cách là {{role}}" }, - "browse-authors": { - "title": "Duyệt Theo Tác Giả & Nhà Văn", - "author-count": "{{num}} Người", - "cover-image-description": "{{edit-series-modal.cover-image-description}}" - }, "download-button": { "downloading-status": "Đang Tải Xuống.…", "download-tooltip": "Tải Xuống" diff --git a/UI/Web/src/assets/langs/zh_Hans.json b/UI/Web/src/assets/langs/zh_Hans.json index af222921d..e35b8a171 100644 --- a/UI/Web/src/assets/langs/zh_Hans.json +++ b/UI/Web/src/assets/langs/zh_Hans.json @@ -83,7 +83,7 @@ }, "user-preferences": { "title": "用户面板", - "pref-description": "这些是与您的帐户绑定的全局设置。", + "pref-description": "这些是与您的帐户绑定的全局设置。阅读器设置位于“阅读配置文件”中。", "account-tab": "{{tabs.account-tab}}", "preferences-tab": "{{tabs.preferences-tab}}", "theme-tab": "{{tabs.theme-tab}}", @@ -107,55 +107,6 @@ "collapse-series-relationships-tooltip": "是否显示没有关联的系列或者前传", "share-series-reviews-label": "分享系列评论", "share-series-reviews-tooltip": "是否对其他用户显示你的评论", - "image-reader-settings-title": "图像阅读器", - "reading-direction-label": "阅读方向", - "reading-direction-tooltip": "单击方向移动到下一页。从右到左意味着您单击屏幕左侧以移至下一页。", - "scaling-option-label": "缩放选项", - "scaling-option-tooltip": "图片在屏幕上的缩放方式。", - "page-splitting-label": "页面分割", - "page-splitting-tooltip": "如何分割全宽图像(即左右图像合并)", - "reading-mode-label": "阅读模式", - "reading-mode-tooltip": "将阅读器更改为垂直、水平分页或无限滚动", - "layout-mode-label": "布局模式", - "layout-mode-tooltip": "将单个图像或两个并排图像渲染到屏幕上", - "background-color-label": "背景色", - "background-color-tooltip": "图像阅读器的背景颜色", - "auto-close-menu-label": "自动关闭菜单", - "auto-close-menu-tooltip": "菜单应该自动关闭", - "show-screen-hints-label": "显示屏幕提示", - "show-screen-hints-tooltip": "显示一个覆盖图层以帮助了解分页区域和方向", - "emulate-comic-book-label": "模仿漫画书", - "emulate-comic-book-tooltip": "应用阴影效果来模拟书籍", - "swipe-to-paginate-label": "滑动翻页", - "swipe-to-paginate-tooltip": "在屏幕上滑动是否应触发下一页或上一页", - "book-reader-settings-title": "书籍阅读器", - "tap-to-paginate-label": "点击翻页", - "tap-to-paginate-tooltip": "是否允许点击书籍阅读器屏幕的两侧翻页", - "immersive-mode-label": "沉浸模式", - "immersive-mode-tooltip": "点击阅读器的文档后隐藏菜单并打开“点击翻页”功能", - "reading-direction-book-label": "阅读方向", - "reading-direction-book-tooltip": "切换至下一页的点击位置。从右至左表示您需点击屏幕左侧以切换至下一页。", - "font-family-label": "字体", - "font-family-tooltip": "要加载的字体,默认加载书籍的默认字体", - "writing-style-label": "书籍排版", - "writing-style-tooltip": "更改文本排版方向。横向从左到右,竖向从上到下。", - "layout-mode-book-label": "布局模式", - "layout-mode-book-tooltip": "确定内容如何布局,滚屏就像把书塞满屏幕,单列或双列匹配设备屏幕的高度且每个页面容纳单列或双列文本", - "color-theme-book-label": "主题颜色", - "color-theme-book-tooltip": "书籍阅读器目录和菜单的主题颜色", - "font-size-book-label": "字体大小", - "font-size-book-tooltip": "书籍中字体的缩放百分比", - "line-height-book-label": "行间距", - "line-height-book-tooltip": "书籍中每行之间的间距", - "margin-book-label": "页边距", - "margin-book-tooltip": "屏幕两侧的间距,移动设备与此设置无关,间距固定为 0。", - "pdf-reader-settings-title": "PDF 阅读器", - "pdf-scroll-mode-label": "滚动模式", - "pdf-scroll-mode-tooltip": "您如何滚动页面。垂直/水平和点击翻页(无滚动)", - "pdf-spread-mode-label": "展开模式", - "pdf-spread-mode-tooltip": "页面应该如何布局。单页或双页(奇数/偶数)", - "pdf-theme-label": "主题", - "pdf-theme-tooltip": "阅读器的主题颜色", "clients-opds-alert": "此服务器未启用OPDS,不会影响Tachiyomi用户。", "clients-opds-description": "所有第三方客户端均使用下方的API密钥或链接。它们就像密码一样,请保密。", "clients-api-key-tooltip": "API 密钥等同于密码。重置密钥将导致所有客户端失效。", @@ -168,9 +119,7 @@ "kavitaplus-settings-title": "Kavita+", "anilist-scrobbling-label": "AniList Scrobbling", "want-to-read-sync-label": "想要阅读同步", - "want-to-read-sync-tooltip": "允许 Kavita 根据待读列表中的 AniList 和 MAL 系列将项目添加到您的想读列表中", - "allow-auto-webtoon-reader-tooltip": "如果页面看起来像网络漫画,请切换到网络漫画阅读器模式。可能会出现一些误报。", - "allow-auto-webtoon-reader-label": "自动网络漫画阅读器模式" + "want-to-read-sync-tooltip": "允许 Kavita 根据待读列表中的 AniList 和 MAL 系列将项目添加到您的想读列表中" }, "user-holds": { "title": "刮削暂停", @@ -680,7 +629,10 @@ "incognito-mode-label": "隐身模式", "next": "下一个", "previous": "上一个", - "go-to-page-prompt": "一共{{totalPages}}页,您想跳转至哪一页?" + "go-to-page-prompt": "共有 {{totalPages}} 页。您想转到哪一页?", + "go-to-section": "转到节", + "go-to-section-prompt": "共有 {{totalSections}} 节。您想转到哪一节?", + "go-to-first-page": "转到第一页" }, "personal-table-of-contents": { "no-data": "尚未添加书签", @@ -728,7 +680,7 @@ "series-detail": { "page-settings-title": "页面设置", "close": "{{common.close}}", - "layout-mode-label": "{{user-preferences.layout-mode-book-label}}", + "layout-mode-label": "{{manage-reading-profiles.layout-mode-book-label}}", "layout-mode-option-card": "卡片", "layout-mode-option-list": "列表", "continue-from": "继续{{title}}", @@ -851,9 +803,9 @@ "back": "后退", "more": "更多", "customize": "{{settings.customize}}", - "browse-authors": "浏览作者", "edit": "{{common.edit}}", - "cancel-edit": "取消重新排序" + "cancel-edit": "取消重新排序", + "browse-people": "浏览人物" }, "library-settings-modal": { "close": "{{common.close}}", @@ -916,30 +868,30 @@ }, "reader-settings": { "general-settings-title": "常规设置", - "font-family-label": "{{user-preferences.font-family-label}}", - "font-size-label": "{{user-preferences.font-size-book-label}}", - "line-spacing-label": "{{user-preferences.line-height-book-label}}", - "margin-label": "{{user-preferences.margin-book-label}}", + "font-family-label": "{{manage-reading-profiles.font-family-label}}", + "font-size-label": "{{manage-reading-profiles.font-size-book-label}}", + "line-spacing-label": "{{manage-reading-profiles.line-height-book-label}}", + "margin-label": "{{manage-reading-profiles.margin-book-label}}", "reset-to-defaults": "恢复默认设置", "reader-settings-title": "阅读器设置", - "reading-direction-label": "{{user-preferences.reading-direction-book-label}}", + "reading-direction-label": "{{manage-reading-profiles.reading-direction-book-label}}", "right-to-left": "从右到左", "left-to-right": "从左到右", "horizontal": "横向", "vertical": "竖向", - "writing-style-label": "{{user-preferences.writing-style-label}}", + "writing-style-label": "{{manage-reading-profiles.writing-style-label}}", "writing-style-tooltip": "更改文本排版方向。横向是从左到右,竖向是从上到下。", "tap-to-paginate-label": "点击翻页", "tap-to-paginate-tooltip": "点击屏幕边缘进行翻页", "on": "开", "off": "关", - "immersive-mode-label": "{{user-preferences.immersive-mode-label}}", + "immersive-mode-label": "{{manage-reading-profiles.immersive-mode-label}}", "immersive-mode-tooltip": "点击阅读器的文档后隐藏菜单并打开“点击翻页”功能", "fullscreen-label": "全屏", "fullscreen-tooltip": "将阅读器设置为全屏模式", "exit": "退出", "enter": "打开", - "layout-mode-label": "{{user-preferences.layout-mode-book-label}}", + "layout-mode-label": "{{manage-reading-profiles.layout-mode-book-label}}", "layout-mode-tooltip": "滚屏:镜像epub文件(通常每章节一个滚屏页面)
单列:每次创建一个单独的虚拟页面
双列:每次创建两个并排布置的虚拟页面。", "layout-mode-option-scroll": "滚屏", "layout-mode-option-1col": "单列", @@ -948,7 +900,15 @@ "theme-dark": "黑暗", "theme-black": "黑色", "theme-white": "白色", - "theme-paper": "纸张" + "theme-paper": "纸张", + "create-new-tooltip": "从当前隐式配置文件创建新的可管理配置文件", + "loading": "加载中", + "create-new": "从隐式配置文件中新建配置文件", + "line-spacing-min-label": "1x", + "reading-profile-updated": "阅读配置文件已更新", + "reading-profile-promoted": "推广阅读配置文件", + "line-spacing-max-label": "2.5x", + "update-parent": "保存到 {{name}}" }, "table-of-contents": { "no-data": "这本书没有在元数据或toc文件中设置目录" @@ -1323,7 +1283,7 @@ "loading": "{{common.loading}}", "actions-header": "活动", "pending-tooltip": "此用户尚未验证其电子邮件", - "all-libraries": "所有库" + "all-libraries": "所有资料库" }, "edit-collection-tags": { "title": "编辑 {{collectionName}} 收藏", @@ -1389,7 +1349,8 @@ "admin-email-history": "电子邮件历史记录", "admin-matched-metadata": "匹配的元数据", "scrobble-holds": "Scrobble 持有", - "admin-metadata": "管理元数据" + "admin-metadata": "管理元数据", + "reading-profiles": "阅读配置文件" }, "collection-detail": { "no-data": "暂无项目。请尝试添加一个系列。", @@ -1514,7 +1475,9 @@ "all-filters": "智能筛选", "nav-link-header": "导航选项", "close": "{{common.close}}", - "person-aka-status": "匹配别名" + "person-aka-status": "匹配别名", + "browse-genres": "浏览类型", + "browse-tags": "浏览标签" }, "promoted-icon": { "promoted": "{{common.promoted}}" @@ -1625,7 +1588,6 @@ }, "manga-reader": { "back": "返回", - "save-globally": "全局保存", "incognito-alt": "隐身模式已开启。切换以关闭。", "incognito-title": "隐身模式:", "shortcuts-menu-alt": "键盘快捷键模式", @@ -1647,9 +1609,9 @@ "height": "高度", "width": "宽度", "width-override-label": "宽度覆盖", - "off": "关", + "off": "{{reader-settings.off}}", "original": "原始尺寸", - "auto-close-menu-label": "{{user-preferences.auto-close-menu-label}}", + "auto-close-menu-label": "{{manage-reading-profiles.auto-close-menu-label}}", "swipe-enabled-label": "启用滑动手势", "enable-comic-book-label": "模拟漫画书", "brightness-label": "亮度", @@ -1660,9 +1622,14 @@ "layout-mode-switched": "由于空间不足以呈现双页布局,布局模式已切换为单页", "no-next-chapter": "没有下一章节", "no-prev-chapter": "没有上一章节", - "user-preferences-updated": "用户偏好已更新", - "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", - "series-progress": "系列进度: {{percentage}}" + "emulate-comic-book-label": "{{manage-reading-profiles.emulate-comic-book-label}}", + "series-progress": "系列进度: {{percentage}}", + "reading-profile-promoted": "推广阅读配置文件", + "create-new": "{{reader-settings.create-new}}", + "loading": "{{reader-settings.loading}}", + "create-new-tooltip": "{{reader-settings.create-new-tooltip}}", + "reading-profile-updated": "阅读配置文件已更新", + "update-parent": "{{reader-settings.update-parent}}" }, "metadata-filter": { "filter-title": "{{common.filter}}", @@ -1719,7 +1686,10 @@ "release-year": "发行年份", "read-progress": "上次阅读", "average-rating": "平均评分", - "random": "随机" + "random": "随机", + "person-series-count": "系列数", + "person-chapter-count": "章节数", + "person-name": "名称" }, "edit-series-modal": { "title": "{{seriesName}} 详细信息", @@ -1856,8 +1826,8 @@ "cover-image-tab": "{{tabs.cover-tab}}", "tasks-tab": "{{tabs.tasks-tab}}", "info-tab": "{{tabs.info-tab}}", - "pages-label": "{{edit-chapter-modal.pages-count}}", - "words-label": "{{edit-chapter-modal.length-title}}", + "pages-label": "{{edit-chapter-modal.pages-label}}", + "words-label": "{{edit-chapter-modal.words-label}}", "pages-count": "{{edit-chapter-modal.pages-count}}", "words-count": "{{edit-chapter-modal.words-count}}", "reading-time-label": "{{edit-chapter-modal.reading-time-label}}", @@ -2078,7 +2048,7 @@ "reading-lists": "{{side-nav.reading-lists}}", "bookmarks": "{{side-nav.bookmarks}}", "all-series": "{{side-nav.all-series}}", - "browse-authors": "{{side-nav.browse-authors}}" + "browse-authors": "{{side-nav.browse-people}}" }, "filter-field-pipe": { "age-rating": "{{metadata-fields.age-rating-title}}", @@ -2254,7 +2224,9 @@ "webtoon-override": "由于图片代表网络漫画,因此切换到网络漫画模式。", "scrobble-gen-init": "将一项任务加入队列,该任务将根据过去的阅读历史和评分生成 scrobble 事件,并将其与已连接的服务同步。", "confirm-delete-multiple-volumes": "您确定要删除这 {{count}} 个卷吗?这不会修改磁盘上的文件。", - "series-added-want-to-read": "从“想读”列表中添加的系列" + "series-added-want-to-read": "从“想读”列表中添加的系列", + "series-bound-to-reading-profile": "系列绑定到阅读配置文件 {{name}}", + "library-bound-to-reading-profile": "资料库绑定到阅读配置文件 {{name}}" }, "read-time-pipe": { "less-than-hour": "<1 小时", @@ -2330,7 +2302,13 @@ "reorder": "重新排序", "rename-tooltip": "重命名智能筛选", "rename": "重命名", - "merge": "合并" + "merge": "合并", + "reading-profiles": "阅读配置文件", + "set-reading-profile": "设置阅读配置文件", + "set-reading-profile-tooltip": "将阅读配置文件绑定到此资料库", + "clear-reading-profile": "清除阅读配置文件", + "cleared-profile": "清除阅读配置文件", + "clear-reading-profile-tooltip": "清除此资料库的阅读配置文件" }, "preferences": { "left-to-right": "从左到右", @@ -2432,8 +2410,8 @@ "promoted": "推广", "select-all": "全选", "deselect-all": "取消全选", - "series-count": "{{num}}系列", - "item-count": "{{num}}条目", + "series-count": "{{num}} 系列", + "item-count": "{{num}} 条目", "book-num": "书籍", "issue-hash-num": "期 #", "issue-num": "期", @@ -2449,14 +2427,16 @@ "volume-nums": "卷", "author-count": "{{num}} 作者", "chapter-count": "{{num}} 章", - "no-data": "无数据" + "no-data": "无数据", + "issue-count": "{{num}} 期" }, "confirm": { "confirm": "确认", "alert": "注意", "info": "信息", "cancel": "{{common.cancel}}", - "ok": "确定" + "ok": "确定", + "prompt": "问题" }, "edit-person-modal": { "mal-id-label": "MAL Id", @@ -2495,11 +2475,6 @@ "no-info": "没有关于此人的信息", "aka-title": "又名 " }, - "browse-authors": { - "author-count": "{{num}} 人", - "cover-image-description": "{{edit-series-modal.cover-image-description}}", - "title": "浏览作者和作家" - }, "match-series-modal": { "close": "{{common.close}}", "save": "{{common.save}}", @@ -2527,7 +2502,9 @@ "dont-match-status-label": "{{dont-match-label}}", "library-name-header": "资料库", "actions-header": "活动", - "match-alt": "匹配 {{seriesName}}" + "match-alt": "匹配 {{seriesName}}", + "matched-state-label": "匹配状态", + "library-type": "资料库类型" }, "changelog-update-item": { "removed": "已移除", @@ -2690,5 +2667,146 @@ "merge-warning": "如果继续,所选人员将被移除。所选人员的姓名将被添加为别名,并且其所有角色都将被转移。", "alias-title": "新别名", "save": "{{common.save}}" + }, + "manage-reading-profiles": { + "description": "并非所有系列都以相同的方式阅读,请为每个资料库或系列设置不同的阅读配置文件,以尽可能无缝地回到您的系列中。", + "tap-to-paginate-tooltip": "电子书阅读器屏幕的侧面是否允许点击以移动到上一页/下一页", + "immersive-mode-tooltip": "这将隐藏阅读器文档上的菜单,并点击翻页", + "add": "{{common.add}}", + "layout-mode-book-tooltip": "内容布局方式。滚动方式与书籍内容大小一致。1 列或 2 列适合设备高度,每页可容纳 1 列或 2 列文本", + "profiles-title": "您的阅读配置文件", + "default-profile": "默认", + "make-default": "设为默认值", + "no-selected": "未选择配置文件", + "confirm": "您确定要删除阅读配置文件 {{name}} 吗?", + "image-reader-settings-title": "图像阅读器", + "reading-direction-label": "阅读方向", + "reading-direction-tooltip": "点击方向为移至下一页。从右到左表示点击屏幕左侧即可移至下一页。", + "scaling-option-label": "缩放选项", + "scaling-option-tooltip": "如何将图像缩放到适合您的屏幕。", + "page-splitting-label": "页面拆分", + "reading-mode-label": "阅读模式", + "reading-mode-tooltip": "将阅读器更改为垂直、水平分页或无限滚动", + "layout-mode-label": "布局模式", + "layout-mode-tooltip": "将单幅图像或两幅并排图像渲染到屏幕上", + "background-color-label": "背景颜色", + "background-color-tooltip": "图像阅读器的背景颜色", + "auto-close-menu-label": "自动关闭菜单", + "auto-close-menu-tooltip": "菜单是否自动关闭", + "show-screen-hints-label": "显示屏幕提示", + "show-screen-hints-tooltip": "显示覆盖层以帮助理解分页区域和方向", + "emulate-comic-book-label": "模拟漫画书", + "emulate-comic-book-tooltip": "应用阴影效果来模拟读书", + "swipe-to-paginate-label": "滑动即可分页", + "swipe-to-paginate-tooltip": "在屏幕上滑动是否应该触发下一页或上一页", + "allow-auto-webtoon-reader-label": "自动网络漫画阅读器模式", + "allow-auto-webtoon-reader-tooltip": "如果页面看起来像网络漫画,请切换到网络漫画阅读器模式。可能会出现一些误报。", + "width-override-label": "{{manga-reader.width-override-label}}", + "width-override-tooltip": "覆盖阅读器中的图像宽度", + "book-reader-settings-title": "图书阅读器", + "immersive-mode-label": "沉浸模式", + "reading-direction-book-label": "阅读方向", + "reading-direction-book-tooltip": "点击方向为移至下一页。从右到左表示点击屏幕左侧即可移至下一页。", + "font-family-label": "字体系列", + "font-family-tooltip": "要加载的字体系列。默认将加载本书的默认字体", + "writing-style-tooltip": "更改文本的方向。水平表示从左到右,垂直表示从上到下。", + "layout-mode-book-label": "布局模式", + "writing-style-label": "文章风格", + "color-theme-book-tooltip": "图书阅读器内容和菜单应采用什么颜色主题", + "font-size-book-label": "字体大小", + "font-size-book-tooltip": "应用于书中字体的缩放百分比", + "line-height-book-label": "行距", + "line-height-book-tooltip": "书的行距是多少", + "margin-book-label": "边距", + "margin-book-tooltip": "屏幕两侧的间距。无论此设置如何,在移动设备上都会覆盖为 0。", + "pdf-spread-mode-tooltip": "页面布局。单页还是双页(奇数/偶数)", + "pdf-spread-mode-label": "展开模式", + "pdf-theme-label": "主题", + "pdf-theme-tooltip": "阅读器的颜色主题", + "reading-profile-series-settings-title": "系列", + "reading-profile-library-settings-title": "资料库", + "delete": "{{common.delete}}", + "tap-to-paginate-label": "点击分页", + "page-splitting-tooltip": "如何分割全宽图像(即合并左右图像)", + "pdf-scroll-mode-tooltip": "如何滚动页面。垂直/水平滚动以及点击翻页(无滚动)", + "pdf-reader-settings-title": "PDF 阅读器", + "color-theme-book-label": "颜色主题", + "pdf-scroll-mode-label": "滚动模式", + "extra-tip": "通过系列和资料库的操作菜单或批量设置来分配阅读配置文件。更改阅读器中的设置时,会创建一个隐藏的配置文件,用于记住您对该系列的选择(PDF 除外)。当您为该系列分配您自己的阅读配置文件时,此配置文件会被移除。更多信息,请访问", + "reset": "{{common.reset}}", + "selection-tip": "从列表中选择一个配置文件,或在右上角创建一个新的配置文件", + "add-tooltip": "您的新配置文件将在更改后保存", + "wiki-title": "维基百科", + "disable-width-override-label": "禁用宽度覆盖", + "disable-width-override-tooltip": "当屏幕尺寸至少等于或小于配置的断点时,防止宽度覆盖生效" + }, + "bulk-set-reading-profile-modal": { + "title": "设置阅读配置文件", + "filter-label": "{{common.filter}}", + "clear": "{{common.clear}}", + "no-data": "尚未创建收藏", + "loading": "{{common.loading}}", + "create": "{{common.create}}", + "bound": "边界", + "close": "{{common.close}}" + }, + "browse-people": { + "issue-count": "{{common.issue-count}}", + "title": "浏览人物", + "author-count": "{{num}} 人", + "cover-image-description": "{{edit-series-modal.cover-image-description}}", + "sort-label": "排序", + "name-label": "名称", + "issue-count-label": "期刊数", + "series-count-label": "系列数", + "series-count": "{{common.series-count}}", + "roles-label": "角色" + }, + "browse-genres": { + "series-count": "{{common.series-count}}", + "title": "浏览类型", + "genre-count": "{{num}} 类型", + "issue-count": "{{common.issue-count}}" + }, + "browse-title-pipe": { + "publication-status": "{{value}} 作品", + "age-rating": "评价 {{value}}", + "publisher": "由 {{value}} 发布", + "genre": "类型为 {{value}}", + "editor": "由 {{value}} 编辑", + "format": "{{value}} 的格式", + "imprint": "{{value}} 的压印", + "character": "有字符 {{value}}", + "artist": "由 {{value}} 绘制", + "letterer": "由 {{value}} 书写", + "colorist": "按 {{value}} 着色", + "writer": "由 {{value}} 撰写", + "library": "在 {{value}} 资料库中", + "release-year": "发布于 {{value}}", + "team": "团队 {{value}}", + "location": "在 {{value}} 位置", + "tag": "有标签 {{value}}", + "user-rating": "{{value}} 星级", + "penciller": "由 {{value}} 铅笔画", + "translator": "翻译者 {{value}}", + "inker": "由 {{value}} 执笔" + }, + "browse-tags": { + "genre-count": "{{num}} 标签", + "series-count": "{{common.series-count}}", + "title": "浏览标签", + "issue-count": "{{common.issue-count}}" + }, + "generic-filter-field-pipe": { + "person-chapter-count": "章节数", + "person-series-count": "系列数", + "person-role": "角色", + "person-name": "名称" + }, + "breakpoint-pipe": { + "mobile": "手机", + "tablet": "平板", + "desktop": "桌面", + "never": "从不" } } diff --git a/UI/Web/src/assets/langs/zh_Hant.json b/UI/Web/src/assets/langs/zh_Hant.json index 8e5841f26..b8a08eff9 100644 --- a/UI/Web/src/assets/langs/zh_Hant.json +++ b/UI/Web/src/assets/langs/zh_Hant.json @@ -25,7 +25,8 @@ "cancel": "{{common.cancel}}", "saving": "儲存…", "update": "更新", - "account-detail-title": "帳戶詳細資訊" + "account-detail-title": "帳戶詳細資訊", + "invalid-email-warning": "無效的電子郵件會使 Kavita 的某些功能無法運作" }, "user-scrobble-history": { "title": "Scrobble歷史", @@ -38,7 +39,7 @@ "series-header": "系列", "data-header": "資料", "is-processed-header": "已處理", - "no-data": "沒有資料", + "no-data": "{{common.no-data}}", "volume-and-chapter-num": "第{{v}}卷 第{{n}}章", "volume-num": "{{num}} 卷", "chapter-num": "章節 {{num}}", @@ -46,7 +47,10 @@ "not-applicable": "不適用", "processed": "處理", "not-processed": "未處理", - "special": "{{entity-title.special}}" + "special": "{{entity-title.special}}", + "scrobbling-disabled": "你的帳戶設定中已停用Scrobbling。", + "generate-scrobble-events": "回填事件", + "token-expired": "你的 AniList 權杖已過期!在你於帳戶頁面更新之前,追蹤事件將無法處理。" }, "scrobble-event-type-pipe": { "chapter-read": "閱讀進度", @@ -98,67 +102,18 @@ "prompt-on-download-tooltip": "下載大小超過{{size}}MB時提示", "disable-animations-label": "禁用動畫", "disable-animations-tooltip": "關閉網站中的動畫,對於電子書閱讀器很有用。", - "collapse-series-relationships-label": "摺疊系列", + "collapse-series-relationships-label": "摺疊系列關係", "collapse-series-relationships-tooltip": "Kavita是否展示沒有關係的系列或前傳", "share-series-reviews-label": "分享系列評論", "share-series-reviews-tooltip": "是否對其他用戶顯示您對系列的評論", - "image-reader-settings-title": "圖片閱讀器", - "reading-direction-label": "閱讀方向", - "reading-direction-tooltip": "單擊方向或滑動畫面移動到下一頁。", - "scaling-option-label": "縮放選項", - "scaling-option-tooltip": "如何將影像縮放到螢幕大小。", - "page-splitting-label": "頁面分割", - "page-splitting-tooltip": "如何分割全寬影像(擊左右影像合併)", - "reading-mode-label": "閱讀模式", - "layout-mode-label": "佈局模式", - "layout-mode-tooltip": "將單個影像或兩個並排影像顯示到螢幕上", - "background-color-label": "背景顏色", - "auto-close-menu-label": "自動關閉選單", - "show-screen-hints-label": "顯示螢幕提示", - "emulate-comic-book-label": "模擬漫畫書", - "swipe-to-paginate-label": "滑動到分頁", - "book-reader-settings-title": "書本閱讀器", - "tap-to-paginate-label": "點擊翻頁", - "tap-to-paginate-tooltip": "書本閱讀器的畫面兩側是否允許點擊以移到上一頁/下一頁", - "immersive-mode-label": "沉浸式模式", - "immersive-mode-tooltip": "這將隱藏點擊閱讀器文檔後的選單,然後打開“點擊翻頁”功能", - "reading-direction-book-label": "閱讀方向", - "reading-direction-book-tooltip": "單擊方向移動到下一頁。 從右到左意味著您單擊屏幕左側以移至下一頁。", - "font-family-label": "字體系列", - "font-family-tooltip": "要加載的字體系列。 默認將加載書籍的默認字體", - "writing-style-label": "文字排版", - "writing-style-tooltip": "更改文字方向。橫向從左到右,縱向從上到下。", - "layout-mode-book-label": "佈局模式", - "layout-mode-book-tooltip": "決定內容在設備螢幕上的呈現方式。“滾動”會按書本內容自然上下滾動,而“單欄”或“雙欄”會根據設備高度調整,一頁顯示一欄或兩欄文字", - "color-theme-book-label": "主題顏色", - "color-theme-book-tooltip": "閱讀器內容與選單的主題顏色", - "font-size-book-label": "字體大小", - "line-height-book-label": "單行間距", - "line-height-book-tooltip": "書的行距是多少", - "margin-book-label": "頁邊距", - "margin-book-tooltip": "與螢幕兩側的距離,在行動裝置上會忽略此設定,間距固定為0。", - "pdf-reader-settings-title": "PDF 閱讀器", - "pdf-scroll-mode-label": "全螢幕模式", - "pdf-scroll-mode-tooltip": "當您瀏覽頁面時,可以選擇垂直/水平滾動,或者點擊分頁(無需滾動)", - "pdf-spread-mode-label": "跨頁模式", - "pdf-spread-mode-tooltip": "頁面應該如何排列。第一頁為單頁或雙頁(奇數跨頁/偶數跨頁)", - "pdf-theme-label": "主題", "clients-opds-alert": "此伺服器上未啟用 OPDS。 這不會影響 Tachiyomi 用戶。", "clients-opds-description": "所有第三方客戶端都將使用 API 密鑰或下面的連接 URL。 這些就像密碼一樣,請保密。", "clients-api-key-tooltip": "API 金鑰就像密碼一樣。重置它會使任何現有的客戶端失效。", "clients-opds-url-tooltip": "查看支持的 OPDS 客户端列表: ", "reset": "{{common.reset}}", "save": "{{common.save}}", - "background-color-tooltip": "圖片閱讀器的背景顏色", - "auto-close-menu-tooltip": "菜單是否應自動關閉", - "show-screen-hints-tooltip": "顯示一個覆蓋層,以幫助理解翻頁區域和方向", - "swipe-to-paginate-tooltip": "在屏幕上滑動是否應移到上一頁/下一頁", - "font-size-book-tooltip": "書中字體的縮放比例", "clients-opds-url-label": "OPDS 網址", - "clients-api-key-label": "API 密鑰", - "reading-mode-tooltip": "將閱讀器更改為縱向翻頁、橫向翻頁,或設置為無限滾動", - "emulate-comic-book-tooltip": "套用陰影以模擬書籍閱讀效果", - "pdf-theme-tooltip": "閱讀器的顏色主題" + "clients-api-key-label": "API 密鑰" }, "user-holds": { "title": "記錄保留", @@ -439,20 +394,20 @@ "years-ago": "{{value}}年前" }, "relationship-pipe": { - "adaptation": "適應", - "alternative-setting": "替代設定", - "alternative-version": "替代版本", - "character": "角色", + "adaptation": "改編", + "alternative-setting": "同世界觀", + "alternative-version": "IF線", + "character": "客串角色", "contains": "包含", "doujinshi": "同人", "other": "其他", "prequel": "前傳", "sequel": "續集", "side-story": "番外", - "spin-off": "拆分", - "parent": "家長", - "edition": "編輯", - "annual": "年度" + "spin-off": "衍伸作", + "parent": "主系列", + "edition": "特別版", + "annual": "年度特刊" }, "publication-status-pipe": { "ongoing": "連載中", @@ -773,8 +728,7 @@ "all-series": "所有系列", "more": "更多", "donate-tooltip": "您可以通過訂閱 Kavita+ 來移除這個", - "donate": "捐款", - "browse-authors": "瀏覽作者" + "donate": "捐款" }, "library-settings-modal": { "close": "{{common.close}}", @@ -1428,7 +1382,6 @@ "emulate-comic-book-label": "{{user-preferences.emulate-comic-book-label}}", "series-progress": "系列進度:{{percentage}}", "back": "返回", - "save-globally": "全域儲存", "left-to-right-alt": "從左到右", "reading-direction-tooltip": "閱讀方向: ", "reading-mode-tooltip": "閱讀模式", @@ -1449,7 +1402,6 @@ "next-page-tooltip": "下一頁", "unbookmark-page-tooltip": "移除書籤", "bookmarks-title": "書籤", - "user-preferences-updated": "使用者偏好更新", "height": "高度", "shortcuts-menu-alt": "鍵盤快捷鍵視窗", "incognito-title": "無痕模式:", @@ -2388,9 +2340,12 @@ "asin-tooltip": "https://www.amazon.com/stores/J.K.-Rowling/author/{ASIN}", "cover-image-description-extra": "或者,您可以從 CoversDB 下載封面(如果有)。" }, - "browse-authors": { - "title": "瀏覽作者/作家", - "author-count": "{{num}} 人", - "cover-image-description": "{{edit-series-modal.cover-image-description}}" + "reviews": { + "user-reviews-local": "本地評論", + "user-reviews-plus": "外部評論" + }, + "review-modal": { + "title": "編輯評論", + "review-label": "評論" } } From 3107ca73e4334e22c6de50c6192894592d7907c6 Mon Sep 17 00:00:00 2001 From: Tyler Kenney Date: Fri, 20 Jun 2025 13:45:56 -0400 Subject: [PATCH 06/30] Koreader Progress Sync (#3823) Co-authored-by: Joseph Milazzo --- API.Benchmark/API.Benchmark.csproj | 5 + API.Benchmark/Data/AesopsFables.epub | Bin 0 -> 308180 bytes API.Benchmark/KoreaderHashBenchmark.cs | 41 + API.Tests/API.Tests.csproj | 6 + API.Tests/Data/AesopsFables.epub | Bin 0 -> 308180 bytes API.Tests/Helpers/KoreaderHelperTests.cs | 60 + API/Controllers/KoreaderController.cs | 118 + API/DTOs/Koreader/KoreaderBookDto.cs | 33 + .../Koreader/KoreaderProgressUpdateDto.cs | 15 + .../20250519151126_KoreaderHash.Designer.cs | 3574 +++++++++++++++++ .../Migrations/20250519151126_KoreaderHash.cs | 28 + .../Migrations/DataContextModelSnapshot.cs | 3 + API/Data/Repositories/MangaFileRepository.cs | 11 + API/Entities/MangaFile.cs | 5 + .../ApplicationServiceExtensions.cs | 1 + .../Builders/KoreaderBookDtoBuilder.cs | 46 + API/Helpers/Builders/MangaFileBuilder.cs | 13 + API/Helpers/KoreaderHelper.cs | 113 + API/Services/KoreaderService.cs | 90 + API/Services/Tasks/Scanner/ProcessSeries.cs | 3 + 20 files changed, 4165 insertions(+) create mode 100644 API.Benchmark/Data/AesopsFables.epub create mode 100644 API.Benchmark/KoreaderHashBenchmark.cs create mode 100644 API.Tests/Data/AesopsFables.epub create mode 100644 API.Tests/Helpers/KoreaderHelperTests.cs create mode 100644 API/Controllers/KoreaderController.cs create mode 100644 API/DTOs/Koreader/KoreaderBookDto.cs create mode 100644 API/DTOs/Koreader/KoreaderProgressUpdateDto.cs create mode 100644 API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs create mode 100644 API/Data/Migrations/20250519151126_KoreaderHash.cs create mode 100644 API/Helpers/Builders/KoreaderBookDtoBuilder.cs create mode 100644 API/Helpers/KoreaderHelper.cs create mode 100644 API/Services/KoreaderService.cs diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj index 38ec425fe..d6fd4eb9f 100644 --- a/API.Benchmark/API.Benchmark.csproj +++ b/API.Benchmark/API.Benchmark.csproj @@ -26,5 +26,10 @@ Always + + + PreserveNewest + + diff --git a/API.Benchmark/Data/AesopsFables.epub b/API.Benchmark/Data/AesopsFables.epub new file mode 100644 index 0000000000000000000000000000000000000000..d2ab9a8b210befa57285f12445dc36e7d95b96c9 GIT binary patch literal 308180 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
  • ozM*cR!tz-r%pk;bf>7ZHaYS?riEi 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 z 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

    41N`7$66wIz1rC5S{H~=#-Ly6iXxgabI4_w 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 M92|ZUa88H&f4_?JNdN!< literal 0 HcmV?d00001 diff --git a/API.Benchmark/KoreaderHashBenchmark.cs b/API.Benchmark/KoreaderHashBenchmark.cs new file mode 100644 index 000000000..c0abfd2ad --- /dev/null +++ b/API.Benchmark/KoreaderHashBenchmark.cs @@ -0,0 +1,41 @@ +using API.Helpers.Builders; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using System; +using API.Entities.Enums; + +namespace API.Benchmark +{ + [StopOnFirstError] + [MemoryDiagnoser] + [RankColumn] + [Orderer(SummaryOrderPolicy.FastestToSlowest)] + [SimpleJob(launchCount: 1, warmupCount: 5, invocationCount: 20)] + public class KoreaderHashBenchmark + { + private const string sourceEpub = "./Data/AesopsFables.epub"; + + [Benchmark(Baseline = true)] + public void TestBuildManga_baseline() + { + var file = new MangaFileBuilder(sourceEpub, MangaFormat.Epub) + .Build(); + if (file == null) + { + throw new Exception("Failed to build manga file"); + } + } + + [Benchmark] + public void TestBuildManga_withHash() + { + var file = new MangaFileBuilder(sourceEpub, MangaFormat.Epub) + .WithHash() + .Build(); + if (file == null) + { + throw new Exception("Failed to build manga file"); + } + } + } +} diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index 9e7fc3a02..73b886e13 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -36,4 +36,10 @@ + + + PreserveNewest + + + diff --git a/API.Tests/Data/AesopsFables.epub b/API.Tests/Data/AesopsFables.epub new file mode 100644 index 0000000000000000000000000000000000000000..d2ab9a8b210befa57285f12445dc36e7d95b96c9 GIT binary patch literal 308180 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

  • ozM*cR!tz-r%pk;bf>7ZHaYS?riEi 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 z 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

    +/// The endpoint to interface with Koreader's Progress Sync plugin. +/// +/// +/// Koreader uses a different form of authentication. It stores the username and password in headers. +/// https://github.com/koreader/koreader/blob/master/plugins/kosync.koplugin/KOSyncClient.lua +/// +[AllowAnonymous] +public class KoreaderController : BaseApiController +{ + + private readonly IUnitOfWork _unitOfWork; + private readonly ILocalizationService _localizationService; + private readonly IKoreaderService _koreaderService; + private readonly ILogger _logger; + + public KoreaderController(IUnitOfWork unitOfWork, ILocalizationService localizationService, + IKoreaderService koreaderService, ILogger logger) + { + _unitOfWork = unitOfWork; + _localizationService = localizationService; + _koreaderService = koreaderService; + _logger = logger; + } + + // We won't allow users to be created from Koreader. Rather, they + // must already have an account. + /* + [HttpPost("/users/create")] + public IActionResult CreateUser(CreateUserRequest request) + { + } + */ + + [HttpGet("{apiKey}/users/auth")] + public async Task Authenticate(string apiKey) + { + var userId = await GetUserId(apiKey); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (user == null) return Unauthorized(); + + return Ok(new { username = user.UserName }); + } + + /// + /// Syncs book progress with Kavita. Will attempt to save the underlying reader position if possible. + /// + /// + /// + /// + [HttpPut("{apiKey}/syncs/progress")] + public async Task> UpdateProgress(string apiKey, KoreaderBookDto request) + { + try + { + var userId = await GetUserId(apiKey); + await _koreaderService.SaveProgress(request, userId); + + return Ok(new KoreaderProgressUpdateDto{ Document = request.Document, Timestamp = DateTime.UtcNow }); + } + catch (KavitaException ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// Gets book progress from Kavita, if not found will return a 400 + /// + /// + /// + /// + [HttpGet("{apiKey}/syncs/progress/{ebookHash}")] + public async Task> GetProgress(string apiKey, string ebookHash) + { + try + { + var userId = await GetUserId(apiKey); + var response = await _koreaderService.GetProgress(ebookHash, userId); + _logger.LogDebug("Koreader response progress: {Progress}", response.Progress); + + return Ok(response); + } + catch (KavitaException ex) + { + return BadRequest(ex.Message); + } + } + + private async Task GetUserId(string apiKey) + { + try + { + return await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + } + catch + { + throw new KavitaException(await _localizationService.Get("en", "user-doesnt-exist")); + } + } +} diff --git a/API/DTOs/Koreader/KoreaderBookDto.cs b/API/DTOs/Koreader/KoreaderBookDto.cs new file mode 100644 index 000000000..b66b7da3a --- /dev/null +++ b/API/DTOs/Koreader/KoreaderBookDto.cs @@ -0,0 +1,33 @@ +using API.DTOs.Progress; + +namespace API.DTOs.Koreader; + +/// +/// This is the interface for receiving and sending updates to Koreader. The only fields +/// that are actually used are the Document and Progress fields. +/// +public class KoreaderBookDto +{ + /// + /// This is the Koreader hash of the book. It is used to identify the book. + /// + public string Document { get; set; } + /// + /// A randomly generated id from the koreader device. Only used to maintain the Koreader interface. + /// + public string Device_id { get; set; } + /// + /// The Koreader device name. Only used to maintain the Koreader interface. + /// + public string Device { get; set; } + /// + /// Percent progress of the book. Only used to maintain the Koreader interface. + /// + public float Percentage { get; set; } + /// + /// An XPath string read by Koreader to determine the location within the epub. + /// Essentially, it is Koreader's equivalent to ProgressDto.BookScrollId. + /// + /// + public string Progress { get; set; } +} diff --git a/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs b/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs new file mode 100644 index 000000000..52a1d6cbd --- /dev/null +++ b/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs @@ -0,0 +1,15 @@ +using System; + +namespace API.DTOs.Koreader; + +public class KoreaderProgressUpdateDto +{ + /// + /// This is the Koreader hash of the book. It is used to identify the book. + /// + public string Document { get; set; } + /// + /// UTC Timestamp to return to KOReader + /// + public DateTime Timestamp { get; set; } +} diff --git a/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs b/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs new file mode 100644 index 000000000..79f6f9504 --- /dev/null +++ b/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs @@ -0,0 +1,3574 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250519151126_KoreaderHash")] + partial class KoreaderHash + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("HasRunScrobbleEventGeneration") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("MalAccessToken") + .HasColumnType("TEXT"); + + b.Property("MalUserName") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventGenerationRan") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserChapterRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("MissingSeriesFromSource") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("SourceUrl") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TotalSourceCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserCollection"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(4); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserDashboardStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Host") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserExternalSource"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserOnDeckRemoval"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAutomaticWebtoonReaderDetection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AniListScrobblingEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BlurUnreadSummaries") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("CollapseSeriesRelationships") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("en"); + + b.Property("NoTransitions") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PdfScrollMode") + .HasColumnType("INTEGER"); + + b.Property("PdfSpreadMode") + .HasColumnType("INTEGER"); + + b.Property("PdfTheme") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.Property("WantToReadSync") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserWantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AverageExternalRating") + .HasColumnType("REAL"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("ISBNLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ReleaseDateLocked") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("REAL"); + + b.Property("SortOrderLocked") + .HasColumnType("INTEGER"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TitleNameLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DeliveryStatus") + .HasColumnType("TEXT"); + + b.Property("EmailTemplate") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SendDate") + .HasColumnType("TEXT"); + + b.Property("Sent") + .HasColumnType("INTEGER"); + + b.Property("Subject") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); + + b.ToTable("EmailHistory"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("RanAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ManualMigrationHistory"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowMetadataMatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Pattern") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryExcludePattern"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FileTypeGroup") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryFileTypeGroup"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bytes") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("KoreaderHash") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("AverageScore") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ProviderUrl") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalRating"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("CoverUrl") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("ExternalRecommendation"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("RawBody") + .HasColumnType("TEXT"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("SiteUrl") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalReview"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AverageExternalRating") + .HasColumnType("INTEGER"); + + b.Property("CbrId") + .HasColumnType("INTEGER"); + + b.Property("GoogleBooksId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("ValidUntilUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.ToTable("ExternalSeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastChecked") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("SeriesBlacklist"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DestinationType") + .HasColumnType("INTEGER"); + + b.Property("DestinationValue") + .HasColumnType("TEXT"); + + b.Property("ExcludeFromSource") + .HasColumnType("INTEGER"); + + b.Property("MetadataSettingsId") + .HasColumnType("INTEGER"); + + b.Property("SourceType") + .HasColumnType("INTEGER"); + + b.Property("SourceValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MetadataSettingsId"); + + b.ToTable("MetadataFieldMapping"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRatingMappings") + .HasColumnType("TEXT"); + + b.Property("Blacklist") + .HasColumnType("TEXT"); + + b.Property("EnableChapterCoverImage") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterPublisher") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterReleaseDate") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterTitle") + .HasColumnType("INTEGER"); + + b.Property("EnableCoverImage") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("EnableGenres") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalizedName") + .HasColumnType("INTEGER"); + + b.Property("EnablePeople") + .HasColumnType("INTEGER"); + + b.Property("EnablePublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("EnableRelationships") + .HasColumnType("INTEGER"); + + b.Property("EnableStartDate") + .HasColumnType("INTEGER"); + + b.Property("EnableSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableTags") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("FirstLastPeopleNaming") + .HasColumnType("INTEGER"); + + b.Property("Overrides") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("PersonRoles") + .HasColumnType("TEXT"); + + b.Property("Whitelist") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("ChapterPeople"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("Asin") + .HasColumnType("TEXT"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("HardcoverId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Alias") + .HasColumnType("TEXT"); + + b.Property("NormalizedAlias") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PersonId"); + + b.ToTable("PersonAlias"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.Property("SeriesMetadataId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.HasKey("SeriesMetadataId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ErrorDetails") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsErrored") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DontMatch") + .HasColumnType("INTEGER"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsBlacklisted") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("LowestFolderPath") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("CompatibleVersion") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("GitHubPath") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PreviewUrls") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ShaHash") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LookupName") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.Property("CollectionsId") + .HasColumnType("INTEGER"); + + b.Property("ItemsId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionsId", "ItemsId"); + + b.HasIndex("ItemsId"); + + b.ToTable("AppUserCollectionSeries"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.Property("ExternalRatingsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRatingExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.Property("ExternalRecommendationsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRecommendationExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.Property("ExternalReviewsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalReviewExternalSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ChapterRatings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Ratings") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Collections") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryExcludePatterns") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryFileTypes") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalRatings") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalReviews") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("ExternalSeriesMetadata") + .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") + .WithMany("FieldMappings") + .HasForeignKey("MetadataSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("People") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("ChapterPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("Aliases") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("SeriesMetadataPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") + .WithMany("People") + .HasForeignKey("SeriesMetadataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + + b.Navigation("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.HasOne("API.Entities.AppUserCollection", null) + .WithMany() + .HasForeignKey("CollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRating", null) + .WithMany() + .HasForeignKey("ExternalRatingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) + .WithMany() + .HasForeignKey("ExternalRecommendationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("ChapterRatings"); + + b.Navigation("Collections"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("ExternalRatings"); + + b.Navigation("ExternalReviews"); + + b.Navigation("Files"); + + b.Navigation("People"); + + b.Navigation("Ratings"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("LibraryExcludePatterns"); + + b.Navigation("LibraryFileTypes"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Navigation("FieldMappings"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Navigation("Aliases"); + + b.Navigation("ChapterPeople"); + + b.Navigation("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("ExternalSeriesMetadata"); + + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("RelationOf"); + + b.Navigation("Relations"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20250519151126_KoreaderHash.cs b/API/Data/Migrations/20250519151126_KoreaderHash.cs new file mode 100644 index 000000000..006070b72 --- /dev/null +++ b/API/Data/Migrations/20250519151126_KoreaderHash.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class KoreaderHash : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "KoreaderHash", + table: "MangaFile", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "KoreaderHash", + table: "MangaFile"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index e777bbf7c..c9fb953df 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -1408,6 +1408,9 @@ namespace API.Data.Migrations b.Property("Format") .HasColumnType("INTEGER"); + b.Property("KoreaderHash") + .HasColumnType("TEXT"); + b.Property("LastFileAnalysis") .HasColumnType("TEXT"); diff --git a/API/Data/Repositories/MangaFileRepository.cs b/API/Data/Repositories/MangaFileRepository.cs index debd52199..89c6bb418 100644 --- a/API/Data/Repositories/MangaFileRepository.cs +++ b/API/Data/Repositories/MangaFileRepository.cs @@ -5,11 +5,13 @@ using API.Entities; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface IMangaFileRepository { void Update(MangaFile file); Task> GetAllWithMissingExtension(); + Task GetByKoreaderHash(string hash); } public class MangaFileRepository : IMangaFileRepository @@ -32,4 +34,13 @@ public class MangaFileRepository : IMangaFileRepository .Where(f => string.IsNullOrEmpty(f.Extension)) .ToListAsync(); } + + public async Task GetByKoreaderHash(string hash) + { + if (string.IsNullOrEmpty(hash)) return null; + + return await _context.MangaFile + .FirstOrDefaultAsync(f => f.KoreaderHash != null && + f.KoreaderHash.Equals(hash.ToUpper())); + } } diff --git a/API/Entities/MangaFile.cs b/API/Entities/MangaFile.cs index f104f4c72..afcb23e97 100644 --- a/API/Entities/MangaFile.cs +++ b/API/Entities/MangaFile.cs @@ -21,6 +21,11 @@ public class MangaFile : IEntityDate ///

    41N`7$66wIz1rC5S{H~=#-Ly6iXxgabI4_w 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 M92|ZUa88H&f4_?JNdN!< literal 0 HcmV?d00001 diff --git a/API.Tests/Helpers/KoreaderHelperTests.cs b/API.Tests/Helpers/KoreaderHelperTests.cs new file mode 100644 index 000000000..66d287a5d --- /dev/null +++ b/API.Tests/Helpers/KoreaderHelperTests.cs @@ -0,0 +1,60 @@ +using API.DTOs.Koreader; +using API.DTOs.Progress; +using API.Helpers; +using System.Runtime.CompilerServices; +using Xunit; + +namespace API.Tests.Helpers; + + +public class KoreaderHelperTests +{ + + [Theory] + [InlineData("/body/DocFragment[11]/body/div/a", 10, null)] + [InlineData("/body/DocFragment[1]/body/div/p[40]", 0, 40)] + [InlineData("/body/DocFragment[8]/body/div/p[28]/text().264", 7, 28)] + public void GetEpubPositionDto(string koreaderPosition, int page, int? pNumber) + { + var expected = EmptyProgressDto(); + expected.BookScrollId = pNumber.HasValue ? $"//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/P[{pNumber}]" : null; + expected.PageNum = page; + var actual = EmptyProgressDto(); + + KoreaderHelper.UpdateProgressDto(actual, koreaderPosition); + Assert.Equal(expected.BookScrollId, actual.BookScrollId); + Assert.Equal(expected.PageNum, actual.PageNum); + } + + + [Theory] + [InlineData("//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/P[20]", 5, "/body/DocFragment[6]/body/div/p[20]")] + [InlineData(null, 10, "/body/DocFragment[11]/body/div/a")] + public void GetKoreaderPosition(string scrollId, int page, string koreaderPosition) + { + var given = EmptyProgressDto(); + given.BookScrollId = scrollId; + given.PageNum = page; + + Assert.Equal(koreaderPosition, KoreaderHelper.GetKoreaderPosition(given)); + } + + [Theory] + [InlineData("./Data/AesopsFables.epub", "8795ACA4BF264B57C1EEDF06A0CEE688")] + public void GetKoreaderHash(string filePath, string hash) + { + Assert.Equal(KoreaderHelper.HashContents(filePath), hash); + } + + private ProgressDto EmptyProgressDto() + { + return new ProgressDto + { + ChapterId = 0, + PageNum = 0, + VolumeId = 0, + SeriesId = 0, + LibraryId = 0 + }; + } +} diff --git a/API/Controllers/KoreaderController.cs b/API/Controllers/KoreaderController.cs new file mode 100644 index 000000000..1ce5e3202 --- /dev/null +++ b/API/Controllers/KoreaderController.cs @@ -0,0 +1,118 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using API.Data; +using API.Data.Repositories; +using API.DTOs.Koreader; +using API.Entities; +using API.Services; +using Kavita.Common; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using static System.Net.WebRequestMethods; + +namespace API.Controllers; +#nullable enable + +///

  • public required string FilePath { get; set; } /// + /// A hash of the document using Koreader's unique hashing algorithm + /// + /// KoreaderHash is only available for epub types + public string? KoreaderHash { get; set; } + /// /// Number of pages for the given file /// public int Pages { get; set; } diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 10f285669..0d1d7d561 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -55,6 +55,7 @@ public static class ApplicationServiceExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/API/Helpers/Builders/KoreaderBookDtoBuilder.cs b/API/Helpers/Builders/KoreaderBookDtoBuilder.cs new file mode 100644 index 000000000..debbe0347 --- /dev/null +++ b/API/Helpers/Builders/KoreaderBookDtoBuilder.cs @@ -0,0 +1,46 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using API.DTOs.Koreader; + +namespace API.Helpers.Builders; + +public class KoreaderBookDtoBuilder : IEntityBuilder +{ + private readonly KoreaderBookDto _dto; + public KoreaderBookDto Build() => _dto; + + public KoreaderBookDtoBuilder(string documentHash) + { + _dto = new KoreaderBookDto() + { + Document = documentHash, + Device = "Kavita" + }; + } + + public KoreaderBookDtoBuilder WithDocument(string documentHash) + { + _dto.Document = documentHash; + return this; + } + + public KoreaderBookDtoBuilder WithProgress(string progress) + { + _dto.Progress = progress; + return this; + } + + public KoreaderBookDtoBuilder WithPercentage(int? pageNum, int pages) + { + _dto.Percentage = (pageNum ?? 0) / (float) pages; + return this; + } + + public KoreaderBookDtoBuilder WithDeviceId(string installId, int userId) + { + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(installId + userId)); + _dto.Device_id = Convert.ToHexString(hash); + return this; + } +} diff --git a/API/Helpers/Builders/MangaFileBuilder.cs b/API/Helpers/Builders/MangaFileBuilder.cs index 5387a3349..ea3ff0c6d 100644 --- a/API/Helpers/Builders/MangaFileBuilder.cs +++ b/API/Helpers/Builders/MangaFileBuilder.cs @@ -60,4 +60,17 @@ public class MangaFileBuilder : IEntityBuilder _mangaFile.Id = Math.Max(id, 0); return this; } + + /// + /// Generate the Hash on the underlying file + /// + /// Only applicable to Epubs + public MangaFileBuilder WithHash() + { + if (_mangaFile.Format != MangaFormat.Epub) return this; + + _mangaFile.KoreaderHash = KoreaderHelper.HashContents(_mangaFile.FilePath); + + return this; + } } diff --git a/API/Helpers/KoreaderHelper.cs b/API/Helpers/KoreaderHelper.cs new file mode 100644 index 000000000..e779cd911 --- /dev/null +++ b/API/Helpers/KoreaderHelper.cs @@ -0,0 +1,113 @@ +using API.DTOs.Progress; +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using API.Services.Tasks.Scanner.Parser; + +namespace API.Helpers; + +/// +/// All things related to Koreader +/// +/// Original developer: https://github.com/MFDeAngelo +public static class KoreaderHelper +{ + /// + /// Hashes the document according to a custom Koreader hashing algorithm. + /// Look at the util.partialMD5 method in the attached link. + /// Note: Only applies to epub files + /// + /// The hashing algorithm is relatively quick as it only hashes ~10,000 bytes for the biggest of files. + /// + /// The path to the file to hash + public static string HashContents(string filePath) + { + if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath) || !Parser.IsEpub(filePath)) + { + return null; + } + + using var file = File.OpenRead(filePath); + + const int step = 1024; + const int size = 1024; + var md5 = MD5.Create(); + var buffer = new byte[size]; + + for (var i = -1; i < 10; i++) + { + file.Position = step << 2 * i; + var bytesRead = file.Read(buffer, 0, size); + if (bytesRead > 0) + { + md5.TransformBlock(buffer, 0, bytesRead, buffer, 0); + } + else + { + break; + } + } + + file.Close(); + md5.TransformFinalBlock([], 0, 0); + + return md5.Hash == null ? null : Convert.ToHexString(md5.Hash).ToUpper(); + } + + /// + /// Koreader can identify documents based on contents or title. + /// For now, we only support by contents. + /// + public static string HashTitle(string filePath) + { + var fileName = Path.GetFileName(filePath); + var fileNameBytes = Encoding.ASCII.GetBytes(fileName); + var bytes = MD5.HashData(fileNameBytes); + + return Convert.ToHexString(bytes); + } + + public static void UpdateProgressDto(ProgressDto progress, string koreaderPosition) + { + var path = koreaderPosition.Split('/'); + if (path.Length < 6) + { + return; + } + + var docNumber = path[2].Replace("DocFragment[", string.Empty).Replace("]", string.Empty); + progress.PageNum = int.Parse(docNumber) - 1; + var lastTag = path[5].ToUpper(); + + if (lastTag == "A") + { + progress.BookScrollId = null; + } + else + { + // The format that Kavita accepts as a progress string. It tells Kavita where Koreader last left off. + progress.BookScrollId = $"//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/{lastTag}"; + } + } + + + public static string GetKoreaderPosition(ProgressDto progressDto) + { + string lastTag; + var koreaderPageNumber = progressDto.PageNum + 1; + + if (string.IsNullOrEmpty(progressDto.BookScrollId)) + { + lastTag = "a"; + } + else + { + var tokens = progressDto.BookScrollId.Split('/'); + lastTag = tokens[^1].ToLower(); + } + + // The format that Koreader accepts as a progress string. It tells Koreader where Kavita last left off. + return $"/body/DocFragment[{koreaderPageNumber}]/body/div/{lastTag}"; + } +} diff --git a/API/Services/KoreaderService.cs b/API/Services/KoreaderService.cs new file mode 100644 index 000000000..69b3948ed --- /dev/null +++ b/API/Services/KoreaderService.cs @@ -0,0 +1,90 @@ +using System.Threading.Tasks; +using API.Data; +using API.DTOs.Koreader; +using API.DTOs.Progress; +using API.Helpers; +using API.Helpers.Builders; +using Kavita.Common; +using Microsoft.Extensions.Logging; + +namespace API.Services; + +#nullable enable + +public interface IKoreaderService +{ + Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId); + Task GetProgress(string bookHash, int userId); +} + +public class KoreaderService : IKoreaderService +{ + private readonly IReaderService _readerService; + private readonly IUnitOfWork _unitOfWork; + private readonly ILocalizationService _localizationService; + private readonly ILogger _logger; + + public KoreaderService(IReaderService readerService, IUnitOfWork unitOfWork, ILocalizationService localizationService, ILogger logger) + { + _readerService = readerService; + _unitOfWork = unitOfWork; + _localizationService = localizationService; + _logger = logger; + } + + /// + /// Given a Koreader hash, locate the underlying file and generate/update a progress event. + /// + /// + /// + public async Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId) + { + _logger.LogDebug("Saving Koreader progress for {UserId}: {KoreaderProgress}", userId, koreaderBookDto.Progress); + var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(koreaderBookDto.Document); + if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing")); + + var userProgressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId); + if (userProgressDto == null) + { + var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(file.ChapterId); + if (chapterDto == null) throw new KavitaException(await _localizationService.Translate(userId, "chapter-doesnt-exist")); + + var volumeDto = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapterDto.VolumeId); + if (volumeDto == null) throw new KavitaException(await _localizationService.Translate(userId, "volume-doesnt-exist")); + + userProgressDto = new ProgressDto() + { + ChapterId = file.ChapterId, + VolumeId = chapterDto.VolumeId, + SeriesId = volumeDto.SeriesId, + }; + } + // Update the bookScrollId if possible + KoreaderHelper.UpdateProgressDto(userProgressDto, koreaderBookDto.Progress); + + await _readerService.SaveReadingProgress(userProgressDto, userId); + } + + /// + /// Returns a Koreader Dto representing current book and the progress within + /// + /// + /// + /// + public async Task GetProgress(string bookHash, int userId) + { + var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + + var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(bookHash); + + if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing")); + + var progressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId); + var koreaderProgress = KoreaderHelper.GetKoreaderPosition(progressDto); + + return new KoreaderBookDtoBuilder(bookHash).WithProgress(koreaderProgress) + .WithPercentage(progressDto?.PageNum, file.Pages) + .WithDeviceId(settingsDto.InstallId, userId) + .Build(); + } +} diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 454c72733..cf3a9f3fb 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -880,6 +880,8 @@ public class ProcessSeries : IProcessSeries existingFile.FileName = Parser.Parser.RemoveExtensionIfSupported(existingFile.FilePath); existingFile.FilePath = Parser.Parser.NormalizePath(existingFile.FilePath); existingFile.Bytes = fileInfo.Length; + existingFile.KoreaderHash = KoreaderHelper.HashContents(existingFile.FilePath); + // We skip updating DB here with last modified time so that metadata refresh can do it } else @@ -888,6 +890,7 @@ public class ProcessSeries : IProcessSeries var file = new MangaFileBuilder(info.FullFilePath, info.Format, _readingItemService.GetNumberOfPages(info.FullFilePath, info.Format)) .WithExtension(fileInfo.Extension) .WithBytes(fileInfo.Length) + .WithHash() .Build(); chapter.Files.Add(file); } From 55f94602d437694f2508c003608b60820c9079d3 Mon Sep 17 00:00:00 2001 From: majora2007 Date: Fri, 20 Jun 2025 17:46:42 +0000 Subject: [PATCH 07/30] Bump versions by dotnet-bump-version. --- Kavita.Common/Kavita.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 5d612e6b7..9b590f97c 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -3,7 +3,7 @@ net9.0 kavitareader.com Kavita - 0.8.6.15 + 0.8.6.16 en true From 45e24aa3114f675737a1f92e742ee6be2cd6079d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 20 Jun 2025 17:47:47 +0000 Subject: [PATCH 08/30] Update OpenAPI documentation --- openapi.json | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 192 insertions(+), 2 deletions(-) diff --git a/openapi.json b/openapi.json index bfcb28ef8..209dfe2ef 100644 --- a/openapi.json +++ b/openapi.json @@ -2,12 +2,12 @@ "openapi": "3.0.4", "info": { "title": "Kavita", - "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.6.14", + "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.6.15", "license": { "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.8.6.14" + "version": "0.8.6.15" }, "servers": [ { @@ -2991,6 +2991,139 @@ } } }, + "/api/Koreader/{apiKey}/users/auth": { + "get": { + "tags": [ + "Koreader" + ], + "parameters": [ + { + "name": "apiKey", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/Koreader/{apiKey}/syncs/progress": { + "put": { + "tags": [ + "Koreader" + ], + "summary": "Syncs book progress with Kavita. Will attempt to save the underlying reader position if possible.", + "parameters": [ + { + "name": "apiKey", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KoreaderBookDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/KoreaderBookDto" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/KoreaderBookDto" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/KoreaderProgressUpdateDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/KoreaderProgressUpdateDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/KoreaderProgressUpdateDto" + } + } + } + } + } + } + }, + "/api/Koreader/{apiKey}/syncs/progress/{ebookHash}": { + "get": { + "tags": [ + "Koreader" + ], + "summary": "Gets book progress from Kavita, if not found will return a 400", + "parameters": [ + { + "name": "apiKey", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "ebookHash", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/KoreaderBookDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/KoreaderBookDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/KoreaderBookDto" + } + } + } + } + } + } + }, "/api/Library/create": { "post": { "tags": [ @@ -21027,6 +21160,54 @@ }, "additionalProperties": false }, + "KoreaderBookDto": { + "type": "object", + "properties": { + "document": { + "type": "string", + "description": "This is the Koreader hash of the book. It is used to identify the book.", + "nullable": true + }, + "device_id": { + "type": "string", + "description": "A randomly generated id from the koreader device. Only used to maintain the Koreader interface.", + "nullable": true + }, + "device": { + "type": "string", + "description": "The Koreader device name. Only used to maintain the Koreader interface.", + "nullable": true + }, + "percentage": { + "type": "number", + "description": "Percent progress of the book. Only used to maintain the Koreader interface.", + "format": "float" + }, + "progress": { + "type": "string", + "description": "An XPath string read by Koreader to determine the location within the epub.\nEssentially, it is Koreader's equivalent to ProgressDto.BookScrollId.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "This is the interface for receiving and sending updates to Koreader. The only fields\nthat are actually used are the Document and Progress fields." + }, + "KoreaderProgressUpdateDto": { + "type": "object", + "properties": { + "document": { + "type": "string", + "description": "This is the Koreader hash of the book. It is used to identify the book.", + "nullable": true + }, + "timestamp": { + "type": "string", + "description": "UTC Timestamp to return to KOReader", + "format": "date-time" + } + }, + "additionalProperties": false + }, "LanguageDto": { "required": [ "isoCode", @@ -21522,6 +21703,11 @@ "description": "Absolute path to the archive file", "nullable": true }, + "koreaderHash": { + "type": "string", + "description": "A hash of the document using Koreader's unique hashing algorithm", + "nullable": true + }, "pages": { "type": "integer", "description": "Number of pages for the given file", @@ -27661,6 +27847,10 @@ "name": "Image", "description": "Responsible for servicing up images stored in Kavita for entities" }, + { + "name": "Koreader", + "description": "The endpoint to interface with Koreader's Progress Sync plugin." + }, { "name": "Manage", "description": "All things centered around Managing the Kavita instance, that isn't aligned with an entity" From 14a8f5c1e53d6d963d01725f748a873b9075efdc Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Fri, 20 Jun 2025 14:09:29 -0500 Subject: [PATCH 09/30] Scrobbling Stability (#3863) Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com> --- .../Extensions/QueryableExtensionsTests.cs | 4 +- .../Services/ExternalMetadataServiceTests.cs | 2 + API.Tests/Services/ScrobblingServiceTests.cs | 415 +++- API/Controllers/ScrobblingController.cs | 16 +- API/DTOs/Scrobbling/ScrobbleEventDto.cs | 1 + API/DTOs/Scrobbling/ScrobbleResponseDto.cs | 1 + .../Repositories/ScrobbleEventRepository.cs | 35 +- API/Entities/Scrobble/ScrobbleEvent.cs | 10 + .../ApplicationServiceExtensions.cs | 1 + .../RestrictByAgeExtensions.cs | 6 + API/Services/Plus/KavitaPlusApiService.cs | 75 + API/Services/Plus/ScrobblingService.cs | 1724 +++++++++-------- .../app/_models/scrobbling/scrobble-event.ts | 1 + .../src/app/_services/scrobbling.service.ts | 6 +- .../user-scrobble-history.component.html | 21 +- .../user-scrobble-history.component.ts | 84 +- .../manage-scrobble-errors.component.html | 2 +- .../app/typeahead/_models/selection-model.ts | 22 + UI/Web/src/assets/langs/en.json | 2 + 19 files changed, 1622 insertions(+), 806 deletions(-) create mode 100644 API/Services/Plus/KavitaPlusApiService.cs diff --git a/API.Tests/Extensions/QueryableExtensionsTests.cs b/API.Tests/Extensions/QueryableExtensionsTests.cs index 866e0202c..96d74b46d 100644 --- a/API.Tests/Extensions/QueryableExtensionsTests.cs +++ b/API.Tests/Extensions/QueryableExtensionsTests.cs @@ -67,7 +67,7 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] - [InlineData(false, 1)] + [InlineData(false, 2)] public void RestrictAgainstAgeRestriction_Genre_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) { var items = new List() @@ -94,7 +94,7 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] - [InlineData(false, 1)] + [InlineData(false, 2)] public void RestrictAgainstAgeRestriction_Tag_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) { var items = new List() diff --git a/API.Tests/Services/ExternalMetadataServiceTests.cs b/API.Tests/Services/ExternalMetadataServiceTests.cs index 8310ed269..833e8fe5f 100644 --- a/API.Tests/Services/ExternalMetadataServiceTests.cs +++ b/API.Tests/Services/ExternalMetadataServiceTests.cs @@ -2935,6 +2935,8 @@ public class ExternalMetadataServiceTests : AbstractDbTest metadataSettings.EnableTags = false; metadataSettings.EnablePublicationStatus = false; metadataSettings.EnableStartDate = false; + metadataSettings.FieldMappings = []; + metadataSettings.AgeRatingMappings = new Dictionary(); Context.MetadataSettings.Update(metadataSettings); await Context.SaveChangesAsync(); diff --git a/API.Tests/Services/ScrobblingServiceTests.cs b/API.Tests/Services/ScrobblingServiceTests.cs index 50398a146..9245c8ecd 100644 --- a/API.Tests/Services/ScrobblingServiceTests.cs +++ b/API.Tests/Services/ScrobblingServiceTests.cs @@ -1,11 +1,17 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; +using API.Data.Repositories; using API.DTOs.Scrobbling; +using API.Entities; using API.Entities.Enums; +using API.Entities.Scrobble; using API.Helpers.Builders; using API.Services; using API.Services.Plus; using API.SignalR; +using Kavita.Common; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -15,11 +21,33 @@ namespace API.Tests.Services; public class ScrobblingServiceTests : AbstractDbTest { + private const int ChapterPages = 100; + + /// + /// { + /// "Issuer": "Issuer", + /// "Issued At": "2025-06-15T21:01:57.615Z", + /// "Expiration": "2200-06-15T21:01:57.615Z" + /// } + /// + /// Our UnitTests will fail in 2200 :( + private const string ValidJwtToken = + "eyJhbGciOiJIUzI1NiJ9.eyJJc3N1ZXIiOiJJc3N1ZXIiLCJleHAiOjcyNzI0NTAxMTcsImlhdCI6MTc1MDAyMTMxN30.zADmcGq_BfxbcV8vy4xw5Cbzn4COkmVINxgqpuL17Ng"; + private readonly ScrobblingService _service; private readonly ILicenseService _licenseService; private readonly ILocalizationService _localizationService; private readonly ILogger _logger; private readonly IEmailService _emailService; + private readonly IKavitaPlusApiService _kavitaPlusApiService; + /// + /// IReaderService, without the ScrobblingService injected + /// + private readonly IReaderService _readerService; + /// + /// IReaderService, with the _service injected + /// + private readonly IReaderService _hookedUpReaderService; public ScrobblingServiceTests() { @@ -27,8 +55,24 @@ public class ScrobblingServiceTests : AbstractDbTest _localizationService = Substitute.For(); _logger = Substitute.For>(); _emailService = Substitute.For(); + _kavitaPlusApiService = Substitute.For(); - _service = new ScrobblingService(UnitOfWork, Substitute.For(), _logger, _licenseService, _localizationService, _emailService); + _service = new ScrobblingService(UnitOfWork, Substitute.For(), _logger, _licenseService, + _localizationService, _emailService, _kavitaPlusApiService); + + _readerService = new ReaderService(UnitOfWork, + Substitute.For>(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For()); // Do not use the actual one + + _hookedUpReaderService = new ReaderService(UnitOfWork, + Substitute.For>(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + _service); } protected override async Task ResetDb() @@ -46,6 +90,30 @@ public class ScrobblingServiceTests : AbstractDbTest var series = new SeriesBuilder("Test Series") .WithFormat(MangaFormat.Archive) .WithMetadata(new SeriesMetadataBuilder().Build()) + .WithVolume(new VolumeBuilder("Volume 1") + .WithChapters([ + new ChapterBuilder("1") + .WithPages(ChapterPages) + .Build(), + new ChapterBuilder("2") + .WithPages(ChapterPages) + .Build(), + new ChapterBuilder("3") + .WithPages(ChapterPages) + .Build()]) + .Build()) + .WithVolume(new VolumeBuilder("Volume 2") + .WithChapters([ + new ChapterBuilder("4") + .WithPages(ChapterPages) + .Build(), + new ChapterBuilder("5") + .WithPages(ChapterPages) + .Build(), + new ChapterBuilder("6") + .WithPages(ChapterPages) + .Build()]) + .Build()) .Build(); var library = new LibraryBuilder("Test Library", LibraryType.Manga) @@ -67,6 +135,296 @@ public class ScrobblingServiceTests : AbstractDbTest await UnitOfWork.CommitAsync(); } + private async Task CreateScrobbleEvent(int? seriesId = null) + { + var evt = new ScrobbleEvent + { + ScrobbleEventType = ScrobbleEventType.ChapterRead, + Format = PlusMediaFormat.Manga, + SeriesId = seriesId ?? 0, + LibraryId = 0, + AppUserId = 0, + }; + + if (seriesId != null) + { + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value); + if (series != null) evt.Series = series; + } + + return evt; + } + + + #region K+ API Request Tests + + [Fact] + public async Task PostScrobbleUpdate_AuthErrors() + { + _kavitaPlusApiService.PostScrobbleUpdate(null!, "") + .ReturnsForAnyArgs(new ScrobbleResponseDto() + { + ErrorMessage = "Unauthorized" + }); + + var evt = await CreateScrobbleEvent(); + await Assert.ThrowsAsync(async () => + { + await _service.PostScrobbleUpdate(new ScrobbleDto(), "", evt); + }); + Assert.True(evt.IsErrored); + Assert.Equal("Kavita+ subscription no longer active", evt.ErrorDetails); + } + + [Fact] + public async Task PostScrobbleUpdate_UnknownSeriesLoggedAsError() + { + _kavitaPlusApiService.PostScrobbleUpdate(null!, "") + .ReturnsForAnyArgs(new ScrobbleResponseDto() + { + ErrorMessage = "Unknown Series" + }); + + await SeedData(); + var evt = await CreateScrobbleEvent(1); + + await _service.PostScrobbleUpdate(new ScrobbleDto(), "", evt); + await UnitOfWork.CommitAsync(); + Assert.True(evt.IsErrored); + + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); + Assert.True(series.IsBlacklisted); + + var errors = await UnitOfWork.ScrobbleRepository.GetAllScrobbleErrorsForSeries(1); + Assert.Single(errors); + Assert.Equal("Series cannot be matched for Scrobbling", errors.First().Comment); + Assert.Equal(series.Id, errors.First().SeriesId); + } + + [Fact] + public async Task PostScrobbleUpdate_InvalidAccessToken() + { + _kavitaPlusApiService.PostScrobbleUpdate(null!, "") + .ReturnsForAnyArgs(new ScrobbleResponseDto() + { + ErrorMessage = "Access token is invalid" + }); + + var evt = await CreateScrobbleEvent(); + + await Assert.ThrowsAsync(async () => + { + await _service.PostScrobbleUpdate(new ScrobbleDto(), "", evt); + }); + + Assert.True(evt.IsErrored); + Assert.Equal("Access Token needs to be rotated to continue scrobbling", evt.ErrorDetails); + } + + #endregion + + #region K+ API Request data tests + + [Fact] + public async Task ProcessReadEvents_CreatesNoEventsWhenNoProgress() + { + await ResetDb(); + await SeedData(); + + // Set Returns + _licenseService.HasActiveLicense().Returns(Task.FromResult(true)); + _kavitaPlusApiService.GetRateLimit(Arg.Any(), Arg.Any()) + .Returns(100); + + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); + Assert.NotNull(user); + + // Ensure CanProcessScrobbleEvent returns true + user.AniListAccessToken = ValidJwtToken; + UnitOfWork.UserRepository.Update(user); + await UnitOfWork.CommitAsync(); + + var chapter = await UnitOfWork.ChapterRepository.GetChapterAsync(4); + Assert.NotNull(chapter); + + var volume = await UnitOfWork.VolumeRepository.GetVolumeAsync(1, VolumeIncludes.Chapters); + Assert.NotNull(volume); + + // Call Scrobble without having any progress + await _service.ScrobbleReadingUpdate(1, 1); + var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Empty(events); + } + + [Fact] + public async Task ProcessReadEvents_UpdateVolumeAndChapterData() + { + await ResetDb(); + await SeedData(); + + // Set Returns + _licenseService.HasActiveLicense().Returns(Task.FromResult(true)); + _kavitaPlusApiService.GetRateLimit(Arg.Any(), Arg.Any()) + .Returns(100); + + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); + Assert.NotNull(user); + + // Ensure CanProcessScrobbleEvent returns true + user.AniListAccessToken = ValidJwtToken; + UnitOfWork.UserRepository.Update(user); + await UnitOfWork.CommitAsync(); + + var chapter = await UnitOfWork.ChapterRepository.GetChapterAsync(4); + Assert.NotNull(chapter); + + var volume = await UnitOfWork.VolumeRepository.GetVolumeAsync(1, VolumeIncludes.Chapters); + Assert.NotNull(volume); + + // Mark something as read to trigger event creation + await _readerService.MarkChaptersAsRead(user, 1, new List() {volume.Chapters[0]}); + await UnitOfWork.CommitAsync(); + + // Call Scrobble while having some progress + await _service.ScrobbleReadingUpdate(user.Id, 1); + var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Single(events); + + // Give it some (more) read progress + await _readerService.MarkChaptersAsRead(user, 1, volume.Chapters); + await _readerService.MarkChaptersAsRead(user, 1, [chapter]); + await UnitOfWork.CommitAsync(); + + await _service.ProcessUpdatesSinceLastSync(); + + await _kavitaPlusApiService.Received(1).PostScrobbleUpdate( + Arg.Is(data => + data.ChapterNumber == (int)chapter.MaxNumber && + data.VolumeNumber == (int)volume.MaxNumber + ), + Arg.Any()); + } + + #endregion + + #region Scrobble Reading Update Tests + + [Fact] + public async Task ScrobbleReadingUpdate_IgnoreNoLicense() + { + await ResetDb(); + await SeedData(); + + _licenseService.HasActiveLicense().Returns(false); + + await _service.ScrobbleReadingUpdate(1, 1); + var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Empty(events); + } + + [Fact] + public async Task ScrobbleReadingUpdate_RemoveWhenNoProgress() + { + await ResetDb(); + await SeedData(); + + _licenseService.HasActiveLicense().Returns(true); + + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); + Assert.NotNull(user); + + var volume = await UnitOfWork.VolumeRepository.GetVolumeAsync(1, VolumeIncludes.Chapters); + Assert.NotNull(volume); + + await _readerService.MarkChaptersAsRead(user, 1, new List() {volume.Chapters[0]}); + await UnitOfWork.CommitAsync(); + + await _service.ScrobbleReadingUpdate(1, 1); + var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Single(events); + + var readEvent = events.First(); + Assert.False(readEvent.IsProcessed); + + await _hookedUpReaderService.MarkSeriesAsUnread(user, 1); + await UnitOfWork.CommitAsync(); + + // Existing event is deleted + await _service.ScrobbleReadingUpdate(1, 1); + events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Empty(events); + + await _hookedUpReaderService.MarkSeriesAsUnread(user, 1); + await UnitOfWork.CommitAsync(); + + // No new events are added + events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Empty(events); + } + + [Fact] + public async Task ScrobbleReadingUpdate_UpdateExistingNotIsProcessed() + { + await ResetDb(); + await SeedData(); + + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); + Assert.NotNull(user); + + var chapter1 = await UnitOfWork.ChapterRepository.GetChapterAsync(1); + var chapter2 = await UnitOfWork.ChapterRepository.GetChapterAsync(2); + var chapter3 = await UnitOfWork.ChapterRepository.GetChapterAsync(3); + Assert.NotNull(chapter1); + Assert.NotNull(chapter2); + Assert.NotNull(chapter3); + + _licenseService.HasActiveLicense().Returns(true); + + var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Empty(events); + + + await _readerService.MarkChaptersAsRead(user, 1, [chapter1]); + await UnitOfWork.CommitAsync(); + + // Scrobble update + await _service.ScrobbleReadingUpdate(1, 1); + events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Single(events); + + var readEvent = events[0]; + Assert.False(readEvent.IsProcessed); + Assert.Equal(1, readEvent.ChapterNumber); + + // Mark as processed + readEvent.IsProcessed = true; + await UnitOfWork.CommitAsync(); + + await _readerService.MarkChaptersAsRead(user, 1, [chapter2]); + await UnitOfWork.CommitAsync(); + + // Scrobble update + await _service.ScrobbleReadingUpdate(1, 1); + events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Equal(2, events.Count); + Assert.Single(events.Where(e => e.IsProcessed).ToList()); + Assert.Single(events.Where(e => !e.IsProcessed).ToList()); + + // Should update the existing non processed event + await _readerService.MarkChaptersAsRead(user, 1, [chapter3]); + await UnitOfWork.CommitAsync(); + + // Scrobble update + await _service.ScrobbleReadingUpdate(1, 1); + events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Equal(2, events.Count); + Assert.Single(events.Where(e => e.IsProcessed).ToList()); + Assert.Single(events.Where(e => !e.IsProcessed).ToList()); + } + + #endregion + #region ScrobbleWantToReadUpdate Tests [Fact] @@ -203,6 +561,59 @@ public class ScrobblingServiceTests : AbstractDbTest #endregion + #region Scrobble Rating Update Test + + [Fact] + public async Task ScrobbleRatingUpdate_IgnoreNoLicense() + { + await ResetDb(); + await SeedData(); + + _licenseService.HasActiveLicense().Returns(false); + + await _service.ScrobbleRatingUpdate(1, 1, 1); + var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Empty(events); + } + + [Fact] + public async Task ScrobbleRatingUpdate_UpdateExistingNotIsProcessed() + { + await ResetDb(); + await SeedData(); + + _licenseService.HasActiveLicense().Returns(true); + + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1); + Assert.NotNull(user); + + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); + + await _service.ScrobbleRatingUpdate(user.Id, series.Id, 1); + var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Single(events); + Assert.Equal(1, events.First().Rating); + + // Mark as processed + events.First().IsProcessed = true; + await UnitOfWork.CommitAsync(); + + await _service.ScrobbleRatingUpdate(user.Id, series.Id, 5); + events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Equal(2, events.Count); + Assert.Single(events, evt => evt.IsProcessed); + Assert.Single(events, evt => !evt.IsProcessed); + + await _service.ScrobbleRatingUpdate(user.Id, series.Id, 5); + events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(1); + Assert.Single(events, evt => !evt.IsProcessed); + Assert.Equal(5, events.First(evt => !evt.IsProcessed).Rating); + + } + + #endregion + [Theory] [InlineData("https://anilist.co/manga/35851/Byeontaega-Doeja/", 35851)] [InlineData("https://anilist.co/manga/30105", 30105)] diff --git a/API/Controllers/ScrobblingController.cs b/API/Controllers/ScrobblingController.cs index 3904cb8e0..986f4f8e7 100644 --- a/API/Controllers/ScrobblingController.cs +++ b/API/Controllers/ScrobblingController.cs @@ -254,7 +254,7 @@ public class ScrobblingController : BaseApiController } /// - /// Adds a hold against the Series for user's scrobbling + /// Remove a hold against the Series for user's scrobbling /// /// /// @@ -281,4 +281,18 @@ public class ScrobblingController : BaseApiController var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId()); return Ok(user is {HasRunScrobbleEventGeneration: true}); } + + /// + /// Delete the given scrobble events if they belong to that user + /// + /// + /// + [HttpPost("bulk-remove-events")] + public async Task BulkRemoveScrobbleEvents(IList eventIds) + { + var events = await _unitOfWork.ScrobbleRepository.GetUserEvents(User.GetUserId(), eventIds); + _unitOfWork.ScrobbleRepository.Remove(events); + await _unitOfWork.CommitAsync(); + return Ok(); + } } diff --git a/API/DTOs/Scrobbling/ScrobbleEventDto.cs b/API/DTOs/Scrobbling/ScrobbleEventDto.cs index 7b1ccd75a..562d923ff 100644 --- a/API/DTOs/Scrobbling/ScrobbleEventDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleEventDto.cs @@ -5,6 +5,7 @@ namespace API.DTOs.Scrobbling; public sealed record ScrobbleEventDto { + public long Id { get; init; } public string SeriesName { get; set; } public int SeriesId { get; set; } public int LibraryId { get; set; } diff --git a/API/DTOs/Scrobbling/ScrobbleResponseDto.cs b/API/DTOs/Scrobbling/ScrobbleResponseDto.cs index 53d3a0cc9..ad66729d0 100644 --- a/API/DTOs/Scrobbling/ScrobbleResponseDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleResponseDto.cs @@ -8,5 +8,6 @@ public sealed record ScrobbleResponseDto { public bool Successful { get; set; } public string? ErrorMessage { get; set; } + public string? ExtraInformation {get; set;} public int RateLeft { get; set; } } diff --git a/API/Data/Repositories/ScrobbleEventRepository.cs b/API/Data/Repositories/ScrobbleEventRepository.cs index c5f30c2ec..144a3b88e 100644 --- a/API/Data/Repositories/ScrobbleEventRepository.cs +++ b/API/Data/Repositories/ScrobbleEventRepository.cs @@ -29,8 +29,23 @@ public interface IScrobbleRepository Task> GetAllScrobbleErrorsForSeries(int seriesId); Task ClearScrobbleErrors(); Task HasErrorForSeries(int seriesId); - Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType); + /// + /// Get all events for a specific user and type + /// + /// + /// + /// + /// If true, only returned not processed events + /// + Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false); Task> GetUserEventsForSeries(int userId, int seriesId); + /// + /// Return the events with given ids, when belonging to the passed user + /// + /// + /// + /// + Task> GetUserEvents(int userId, IList scrobbleEventIds); Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination); Task> GetAllEventsForSeries(int seriesId); Task> GetAllEventsWithSeriesIds(IEnumerable seriesIds); @@ -146,22 +161,32 @@ public class ScrobbleRepository : IScrobbleRepository return await _context.ScrobbleError.AnyAsync(n => n.SeriesId == seriesId); } - public async Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType) + public async Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false) { - return await _context.ScrobbleEvent.FirstOrDefaultAsync(e => - e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType); + return await _context.ScrobbleEvent + .Where(e => e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType) + .WhereIf(isNotProcessed, e => !e.IsProcessed) + .OrderBy(e => e.LastModifiedUtc) + .FirstOrDefaultAsync(); } public async Task> GetUserEventsForSeries(int userId, int seriesId) { return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId && !e.IsProcessed) + .Where(e => e.AppUserId == userId && !e.IsProcessed && e.SeriesId == seriesId) .Include(e => e.Series) .OrderBy(e => e.LastModifiedUtc) .AsSplitQuery() .ToListAsync(); } + public async Task> GetUserEvents(int userId, IList scrobbleEventIds) + { + return await _context.ScrobbleEvent + .Where(e => e.AppUserId == userId && scrobbleEventIds.Contains(e.Id)) + .ToListAsync(); + } + public async Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination) { var query = _context.ScrobbleEvent diff --git a/API/Entities/Scrobble/ScrobbleEvent.cs b/API/Entities/Scrobble/ScrobbleEvent.cs index b8708c115..8adfdcc2e 100644 --- a/API/Entities/Scrobble/ScrobbleEvent.cs +++ b/API/Entities/Scrobble/ScrobbleEvent.cs @@ -68,4 +68,14 @@ public class ScrobbleEvent : IEntityDate public DateTime LastModified { get; set; } public DateTime CreatedUtc { get; set; } public DateTime LastModifiedUtc { get; set; } + + /// + /// Sets the ErrorDetail and marks the event as + /// + /// + public void SetErrorMessage(string errorMessage) + { + ErrorDetails = errorMessage; + IsErrored = true; + } } diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 0d1d7d561..bd4783f25 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -76,6 +76,7 @@ public static class ApplicationServiceExtensions services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs index aef595596..350372e5b 100644 --- a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs +++ b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs @@ -56,6 +56,12 @@ public static class RestrictByAgeExtensions sm.Metadata.AgeRating <= restriction.AgeRating && sm.Metadata.AgeRating > AgeRating.Unknown)); } + /// + /// Returns all Genres where any of the linked Series/Chapters are less than or equal to restriction age rating + /// + /// + /// + /// public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) { if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; diff --git a/API/Services/Plus/KavitaPlusApiService.cs b/API/Services/Plus/KavitaPlusApiService.cs new file mode 100644 index 000000000..cdf9471f8 --- /dev/null +++ b/API/Services/Plus/KavitaPlusApiService.cs @@ -0,0 +1,75 @@ +#nullable enable +using System.Threading.Tasks; +using API.DTOs.Scrobbling; +using API.Extensions; +using Flurl.Http; +using Kavita.Common; +using Microsoft.Extensions.Logging; + +namespace API.Services.Plus; + +/// +/// All Http requests to K+ should be contained in this service, the service will not handle any errors. +/// This is expected from the caller. +/// +public interface IKavitaPlusApiService +{ + Task HasTokenExpired(string license, string token, ScrobbleProvider provider); + Task GetRateLimit(string license, string token); + Task PostScrobbleUpdate(ScrobbleDto data, string license); +} + +public class KavitaPlusApiService(ILogger logger): IKavitaPlusApiService +{ + private const string ScrobblingPath = "/api/scrobbling/"; + + public async Task HasTokenExpired(string license, string token, ScrobbleProvider provider) + { + var res = await Get(ScrobblingPath + "valid-key?provider=" + provider + "&key=" + token, license, token); + var str = await res.GetStringAsync(); + return bool.Parse(str); + } + + public async Task GetRateLimit(string license, string token) + { + var res = await Get(ScrobblingPath + "rate-limit?accessToken=" + token, license, token); + var str = await res.GetStringAsync(); + return int.Parse(str); + } + + public async Task PostScrobbleUpdate(ScrobbleDto data, string license) + { + return await PostAndReceive(ScrobblingPath + "update", data, license); + } + + /// + /// Send a GET request to K+ + /// + /// only path of the uri, the host is added + /// + /// + /// + private static async Task Get(string url, string license, string? aniListToken = null) + { + return await (Configuration.KavitaPlusApiUrl + url) + .WithKavitaPlusHeaders(license, aniListToken) + .GetAsync(); + } + + /// + /// Send a POST request to K+ + /// + /// only path of the uri, the host is added + /// + /// + /// + /// Return type + /// + private static async Task PostAndReceive(string url, object body, string license, string? aniListToken = null) + { + return await (Configuration.KavitaPlusApiUrl + url) + .WithKavitaPlusHeaders(license, aniListToken) + .PostJsonAsync(body) + .ReceiveJson(); + } +} diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index 85814dcd9..f9c3fdb09 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -38,31 +38,124 @@ public enum ScrobbleProvider Kavita = 0, AniList = 1, Mal = 2, - [Obsolete] + [Obsolete("No longer supported")] GoogleBooks = 3, Cbr = 4 } public interface IScrobblingService { + /// + /// An automated job that will run against all user's tokens and validate if they are still active + /// + /// This service can validate without license check as the task which calls will be guarded + /// Task CheckExternalAccessTokens(); + + /// + /// Checks if the token has expired with , if it has double checks with K+, + /// otherwise return false. + /// + /// + /// + /// + /// Returns true if there is no license present Task HasTokenExpired(int userId, ScrobbleProvider provider); + /// + /// Create, or update a non-processed, event, for the given series + /// + /// + /// + /// + /// Task ScrobbleRatingUpdate(int userId, int seriesId, float rating); + /// + /// NOP, until hardcover support has been worked out + /// + /// + /// + /// + /// + /// Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody); + /// + /// Create, or update a non-processed, event, for the given series + /// + /// + /// + /// Task ScrobbleReadingUpdate(int userId, int seriesId); + /// + /// Creates an or for + /// the given series + /// + /// + /// + /// + /// + /// Only the result of both WantToRead types is send to K+ Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead); + /// + /// Removed all processed events that are at least 7 days old + /// + /// [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public Task ClearProcessedEvents(); + + /// + /// Makes K+ requests for all non-processed events until rate limits are reached + /// + /// [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] Task ProcessUpdatesSinceLastSync(); + Task CreateEventsFromExistingHistory(int userId = 0); Task CreateEventsFromExistingHistoryForSeries(int seriesId); Task ClearEventsForSeries(int userId, int seriesId); } +/// +/// Context used when syncing scrobble events. Do NOT reuse between syncs +/// +public class ScrobbleSyncContext +{ + public required List ReadEvents {get; init;} + public required List RatingEvents {get; init;} + /// Do not use this as events to send to K+, use + public required List AddToWantToRead {get; init;} + /// Do not use this as events to send to K+, use + public required List RemoveWantToRead {get; init;} + /// + /// Final events list if all AddTo- and RemoveWantToRead would be processed sequentially + /// + public required List Decisions {get; init;} + /// + /// K+ license + /// + public required string License { get; init; } + /// + /// Maps userId to left over request amount + /// + public required Dictionary RateLimits { get; init; } + + /// + /// All users being scrobbled for + /// + public List Users { get; set; } = []; + /// + /// Amount of already processed events + /// + public int ProgressCounter { get; set; } + + /// + /// Sum of all events to process + /// + public int TotalCount => ReadEvents.Count + RatingEvents.Count + AddToWantToRead.Count + RemoveWantToRead.Count; +} + public class ScrobblingService : IScrobblingService { private readonly IUnitOfWork _unitOfWork; @@ -71,6 +164,7 @@ public class ScrobblingService : IScrobblingService private readonly ILicenseService _licenseService; private readonly ILocalizationService _localizationService; private readonly IEmailService _emailService; + private readonly IKavitaPlusApiService _kavitaPlusApiService; public const string AniListWeblinkWebsite = "https://anilist.co/manga/"; public const string MalWeblinkWebsite = "https://myanimelist.net/manga/"; @@ -80,7 +174,7 @@ public class ScrobblingService : IScrobblingService public const string AniListCharacterWebsite = "https://anilist.co/character/"; - private static readonly Dictionary WeblinkExtractionMap = new Dictionary() + private static readonly Dictionary WeblinkExtractionMap = new() { {AniListWeblinkWebsite, 0}, {MalWeblinkWebsite, 0}, @@ -104,10 +198,14 @@ public class ScrobblingService : IScrobblingService private const string UnknownSeriesErrorMessage = "Series cannot be matched for Scrobbling"; private const string AccessTokenErrorMessage = "Access Token needs to be rotated to continue scrobbling"; + private const string InvalidKPlusLicenseErrorMessage = "Kavita+ subscription no longer active"; + private const string ReviewFailedErrorMessage = "Review was unable to be saved due to upstream requirements"; + private const string BadPayLoadErrorMessage = "Bad payload from Scrobble Provider"; public ScrobblingService(IUnitOfWork unitOfWork, IEventHub eventHub, ILogger logger, - ILicenseService licenseService, ILocalizationService localizationService, IEmailService emailService) + ILicenseService licenseService, ILocalizationService localizationService, IEmailService emailService, + IKavitaPlusApiService kavitaPlusApiService) { _unitOfWork = unitOfWork; _eventHub = eventHub; @@ -115,10 +213,12 @@ public class ScrobblingService : IScrobblingService _licenseService = licenseService; _localizationService = localizationService; _emailService = emailService; + _kavitaPlusApiService = kavitaPlusApiService; FlurlConfiguration.ConfigureClientForUrl(Configuration.KavitaPlusApiUrl); } + #region Access token checks /// public bool EnableMetadata { get; set; } = true; + /// + /// Should Kavita remove sort articles "The" for the sort name + /// + public bool RemovePrefixForSortName { get; set; } = false; public DateTime Created { get; set; } diff --git a/API/Helpers/BookSortTitlePrefixHelper.cs b/API/Helpers/BookSortTitlePrefixHelper.cs new file mode 100644 index 000000000..c92df5d65 --- /dev/null +++ b/API/Helpers/BookSortTitlePrefixHelper.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace API.Helpers; + +/// +/// Responsible for parsing book titles "The man on the street" and removing the prefix -> "man on the street". +/// +/// This code is performance sensitive +public static class BookSortTitlePrefixHelper +{ + private static readonly Dictionary PrefixLookup; + private static readonly Dictionary> PrefixesByFirstChar; + + static BookSortTitlePrefixHelper() + { + var prefixes = new[] + { + // English + "the", "a", "an", + // Spanish + "el", "la", "los", "las", "un", "una", "unos", "unas", + // French + "le", "la", "les", "un", "une", "des", + // German + "der", "die", "das", "den", "dem", "ein", "eine", "einen", "einer", + // Italian + "il", "lo", "la", "gli", "le", "un", "uno", "una", + // Portuguese + "o", "a", "os", "as", "um", "uma", "uns", "umas", + // Russian (transliterated common ones) + "в", "на", "с", "к", "от", "для", + }; + + // Build lookup structures + PrefixLookup = new Dictionary(prefixes.Length, StringComparer.OrdinalIgnoreCase); + PrefixesByFirstChar = new Dictionary>(); + + foreach (var prefix in prefixes) + { + PrefixLookup[prefix] = 1; + + var firstChar = char.ToLowerInvariant(prefix[0]); + if (!PrefixesByFirstChar.TryGetValue(firstChar, out var list)) + { + list = []; + PrefixesByFirstChar[firstChar] = list; + } + list.Add(prefix); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan GetSortTitle(ReadOnlySpan title) + { + if (title.IsEmpty) return title; + + // Fast detection of script type by first character + var firstChar = title[0]; + + // CJK Unicode ranges - no processing needed for most cases + if ((firstChar >= 0x4E00 && firstChar <= 0x9FFF) || // CJK Unified + (firstChar >= 0x3040 && firstChar <= 0x309F) || // Hiragana + (firstChar >= 0x30A0 && firstChar <= 0x30FF)) // Katakana + { + return title; + } + + var firstSpaceIndex = title.IndexOf(' '); + if (firstSpaceIndex <= 0) return title; + + var potentialPrefix = title.Slice(0, firstSpaceIndex); + + // Fast path: check if first character could match any prefix + firstChar = char.ToLowerInvariant(potentialPrefix[0]); + if (!PrefixesByFirstChar.ContainsKey(firstChar)) + return title; + + // Only do the expensive lookup if first character matches + if (PrefixLookup.ContainsKey(potentialPrefix.ToString())) + { + var remainder = title.Slice(firstSpaceIndex + 1); + return remainder.IsEmpty ? title : remainder; + } + + return title; + } + + /// + /// Removes the sort prefix + /// + /// + /// + public static string GetSortTitle(string title) + { + var result = GetSortTitle(title.AsSpan()); + + return result.ToString(); + } +} diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index cf3a9f3fb..307408adb 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -126,13 +126,17 @@ public class ProcessSeries : IProcessSeries series.Format = firstParsedInfo.Format; } + var removePrefix = library.RemovePrefixForSortName; + var sortName = removePrefix ? BookSortTitlePrefixHelper.GetSortTitle(series.Name) : series.Name; + if (string.IsNullOrEmpty(series.SortName)) { - series.SortName = series.Name; + series.SortName = sortName; } + if (!series.SortNameLocked) { - series.SortName = series.Name; + series.SortName = sortName; if (!string.IsNullOrEmpty(firstParsedInfo.SeriesSort)) { series.SortName = firstParsedInfo.SeriesSort; diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts index 0e7d90ee2..bcbf9b447 100644 --- a/UI/Web/src/app/_models/library/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -32,6 +32,7 @@ export interface Library { allowScrobbling: boolean; allowMetadataMatching: boolean; enableMetadata: boolean; + removePrefixForSortName: boolean; collapseSeriesRelationships: boolean; libraryFileTypes: Array; excludePatterns: Array; diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html index ff97fcbb0..e8a3bafeb 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html @@ -127,6 +127,16 @@ +
    + + +
    + +
    +
    +
    +
    +
    diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts index d0fed5c81..9331376ef 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts @@ -115,6 +115,7 @@ export class LibrarySettingsModalComponent implements OnInit { allowMetadataMatching: new FormControl(true, { nonNullable: true, validators: [] }), collapseSeriesRelationships: new FormControl(false, { nonNullable: true, validators: [] }), enableMetadata: new FormControl(true, { nonNullable: true, validators: [] }), // required validator doesn't check value, just if true + removePrefixForSortName: new FormControl(false, { nonNullable: true, validators: [] }), }); selectedFolders: string[] = []; @@ -273,7 +274,8 @@ export class LibrarySettingsModalComponent implements OnInit { this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible ? this.library.allowScrobbling : false); this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsMetadataDownloadEligible ? this.library.allowMetadataMatching : false); this.libraryForm.get('excludePatterns')?.setValue(this.excludePatterns ? this.library.excludePatterns : false); - this.libraryForm.get('enableMetadata')?.setValue(this.library.enableMetadata, true); + this.libraryForm.get('enableMetadata')?.setValue(this.library.enableMetadata); + this.libraryForm.get('removePrefixForSortName')?.setValue(this.library.removePrefixForSortName); this.selectedFolders = this.library.folders; this.madeChanges = false; diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index c6b8c823f..33bde5e0e 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1131,6 +1131,8 @@ "include-in-search-tooltip": "Should series and any derived information (genres, people, files) from the library be included in search results.", "enable-metadata-label": "Enable Metadata (ComicInfo/Epub/PDF)", "enable-metadata-tooltip": "Allow Kavita to read metadata files which override filename parsing.", + "remove-prefix-for-sortname-label": "Remove common prefixes for Sort Name", + "remove-prefix-for-sortname-tooltip": "Kavita will remove common prefixes like 'The', 'A', 'An' from titles for sort name. Does not override set metadata.", "force-scan": "Force Scan", "force-scan-tooltip": "This will force a scan on the library, treating like a fresh scan", "reset": "{{common.reset}}", From 76fd7ab4ce2b474fd1ed94281e9217f932975734 Mon Sep 17 00:00:00 2001 From: majora2007 Date: Sat, 5 Jul 2025 22:18:52 +0000 Subject: [PATCH 29/30] Bump versions by dotnet-bump-version. --- Kavita.Common/Kavita.Common.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index c2ba1669d..c7dd0ab94 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -3,7 +3,7 @@ net9.0 kavitareader.com Kavita - 0.8.7.0 + 0.8.7.1 en true @@ -20,4 +20,4 @@ - + \ No newline at end of file From ef2640b5fc2e7e2836cb30b8f8bbbf9025774d57 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 5 Jul 2025 22:20:01 +0000 Subject: [PATCH 30/30] Update OpenAPI documentation --- openapi.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openapi.json b/openapi.json index e9a3620e9..3e4b797cb 100644 --- a/openapi.json +++ b/openapi.json @@ -21371,6 +21371,10 @@ "type": "boolean", "description": "Should Kavita read metadata files from the library" }, + "removePrefixForSortName": { + "type": "boolean", + "description": "Should Kavita remove sort articles \"The\" for the sort name" + }, "created": { "type": "string", "format": "date-time" @@ -21533,6 +21537,10 @@ "enableMetadata": { "type": "boolean", "description": "Allow Kavita to read metadata (ComicInfo.xml, Epub, PDF)" + }, + "removePrefixForSortName": { + "type": "boolean", + "description": "Should Kavita remove sort articles \"The\" for the sort name" } }, "additionalProperties": false @@ -26438,6 +26446,7 @@ "manageCollections", "manageReadingLists", "name", + "removePrefixForSortName", "type" ], "type": "object", @@ -26492,6 +26501,9 @@ "enableMetadata": { "type": "boolean" }, + "removePrefixForSortName": { + "type": "boolean" + }, "fileGroupTypes": { "type": "array", "items": {
    /// An automated job that will run against all user's tokens and validate if they are still active @@ -196,7 +296,6 @@ public class ScrobblingService : IScrobblingService } - public async Task HasTokenExpired(int userId, ScrobbleProvider provider) { var token = await GetTokenForProvider(userId, provider); @@ -214,19 +313,14 @@ public class ScrobblingService : IScrobblingService private async Task HasTokenExpired(string token, ScrobbleProvider provider) { - if (string.IsNullOrEmpty(token) || - !TokenService.HasTokenExpired(token)) return false; + if (string.IsNullOrEmpty(token) || !TokenService.HasTokenExpired(token)) return false; var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); if (string.IsNullOrEmpty(license.Value)) return true; try { - var response = await (Configuration.KavitaPlusApiUrl + "/api/scrobbling/valid-key?provider=" + provider + "&key=" + token) - .WithKavitaPlusHeaders(license.Value, token) - .GetStringAsync(); - - return bool.Parse(response); + return await _kavitaPlusApiService.HasTokenExpired(license.Value, token, provider); } catch (HttpRequestException e) { @@ -252,59 +346,14 @@ public class ScrobblingService : IScrobblingService } ?? string.Empty; } - public async Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody) + #endregion + + #region Scrobble ingest + + public Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody) { // Currently disabled until at least hardcover is implemented - return; - if (!await _licenseService.HasActiveLicense()) return; - - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library); - if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - - _logger.LogInformation("Processing Scrobbling review event for {AppUserId} on {SeriesName}", userId, series.Name); - if (await CheckIfCannotScrobble(userId, seriesId, series)) return; - - if (IsAniListReviewValid(reviewTitle, reviewBody)) - { - _logger.LogDebug( - "Rejecting Scrobble event for {Series}. Review is not long enough to meet requirements", series.Name); - return; - } - - var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, - ScrobbleEventType.Review); - if (existingEvt is {IsProcessed: false}) - { - _logger.LogDebug("Overriding Review scrobble event for {Series}", existingEvt.Series.Name); - existingEvt.ReviewBody = reviewBody; - existingEvt.ReviewTitle = reviewTitle; - _unitOfWork.ScrobbleRepository.Update(existingEvt); - await _unitOfWork.CommitAsync(); - return; - } - - var evt = new ScrobbleEvent() - { - SeriesId = series.Id, - LibraryId = series.LibraryId, - ScrobbleEventType = ScrobbleEventType.Review, - AniListId = ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite), - MalId = GetMalId(series), - AppUserId = userId, - Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), - ReviewBody = reviewBody, - ReviewTitle = reviewTitle - }; - _unitOfWork.ScrobbleRepository.Attach(evt); - await _unitOfWork.CommitAsync(); - _logger.LogDebug("Added Scrobbling Review update on {SeriesName} with Userid {AppUserId} ", series.Name, userId); - } - - private static bool IsAniListReviewValid(string reviewTitle, string reviewBody) - { - return string.IsNullOrEmpty(reviewTitle) || string.IsNullOrEmpty(reviewBody) || (reviewTitle.Length < 2200 || - reviewTitle.Length > 120 || - reviewTitle.Length < 20); + return Task.CompletedTask; } public async Task ScrobbleRatingUpdate(int userId, int seriesId, float rating) @@ -321,7 +370,7 @@ public class ScrobblingService : IScrobblingService if (await CheckIfCannotScrobble(userId, seriesId, series)) return; var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, - ScrobbleEventType.ScoreUpdated); + ScrobbleEventType.ScoreUpdated, true); if (existingEvt is {IsProcessed: false}) { // We need to just update Volume/Chapter number @@ -349,18 +398,6 @@ public class ScrobblingService : IScrobblingService _logger.LogDebug("Added Scrobbling Rating update on {SeriesName} with Userid {AppUserId}", series.Name, userId); } - public static long? GetMalId(Series series) - { - var malId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite); - return malId ?? series.ExternalSeriesMetadata?.MalId; - } - - public static int? GetAniListId(Series seriesWithExternalMetadata) - { - var aniListId = ExtractId(seriesWithExternalMetadata.Metadata.WebLinks, AniListWeblinkWebsite); - return aniListId ?? seriesWithExternalMetadata.ExternalSeriesMetadata?.AniListId; - } - public async Task ScrobbleReadingUpdate(int userId, int seriesId) { if (!await _licenseService.HasActiveLicense()) return; @@ -374,28 +411,49 @@ public class ScrobblingService : IScrobblingService _logger.LogInformation("Processing Scrobbling reading event for {AppUserId} on {SeriesName}", userId, series.Name); if (await CheckIfCannotScrobble(userId, seriesId, series)) return; + var isAnyProgressOnSeries = await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, userId); + + var volumeNumber = (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId); + var chapterNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId); + + // Check if there is an existing not yet processed event, if so update it var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, - ScrobbleEventType.ChapterRead); + ScrobbleEventType.ChapterRead, true); + if (existingEvt is {IsProcessed: false}) { + if (!isAnyProgressOnSeries) + { + _unitOfWork.ScrobbleRepository.Remove(existingEvt); + await _unitOfWork.CommitAsync(); + _logger.LogDebug("Removed scrobble event for {Series} as there is no reading progress", series.Name); + return; + } + // We need to just update Volume/Chapter number var prevChapter = $"{existingEvt.ChapterNumber}"; var prevVol = $"{existingEvt.VolumeNumber}"; - existingEvt.VolumeNumber = - (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId); - existingEvt.ChapterNumber = - await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId); + existingEvt.VolumeNumber = volumeNumber; + existingEvt.ChapterNumber = chapterNumber; + _unitOfWork.ScrobbleRepository.Update(existingEvt); await _unitOfWork.CommitAsync(); + _logger.LogDebug("Overriding scrobble event for {Series} from vol {PrevVol} ch {PrevChap} -> vol {UpdatedVol} ch {UpdatedChap}", existingEvt.Series.Name, prevVol, prevChapter, existingEvt.VolumeNumber, existingEvt.ChapterNumber); return; } + if (!isAnyProgressOnSeries) + { + // Do not create a new scrobble event if there is no progress + return; + } + try { - var evt = new ScrobbleEvent() + var evt = new ScrobbleEvent { SeriesId = series.Id, LibraryId = series.LibraryId, @@ -403,10 +461,8 @@ public class ScrobblingService : IScrobblingService AniListId = GetAniListId(series), MalId = GetMalId(series), AppUserId = userId, - VolumeNumber = - (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId), - ChapterNumber = - await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId), + VolumeNumber = volumeNumber, + ChapterNumber = chapterNumber, Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), }; @@ -431,7 +487,9 @@ public class ScrobblingService : IScrobblingService if (!await _licenseService.HasActiveLicense()) return; var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata); - if (series == null || !series.Library.AllowScrobbling) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); + if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); + + if (!series.Library.AllowScrobbling) return; var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; @@ -444,10 +502,7 @@ public class ScrobblingService : IScrobblingService .Where(e => new[] { ScrobbleEventType.AddWantToRead, ScrobbleEventType.RemoveWantToRead }.Contains(e.ScrobbleEventType)); // Remove all existing want-to-read events for this series/user - foreach (var existingEvent in existingEvents) - { - _unitOfWork.ScrobbleRepository.Remove(existingEvent); - } + _unitOfWork.ScrobbleRepository.Remove(existingEvents); // Create the new event var evt = new ScrobbleEvent() @@ -466,704 +521,27 @@ public class ScrobblingService : IScrobblingService _logger.LogDebug("Added Scrobbling WantToRead update on {SeriesName} with Userid {AppUserId} ", series.Name, userId); } - private async Task CheckIfCannotScrobble(int userId, int seriesId, Series series) + #endregion + + #region Scrobble provider methods + + private static bool IsAniListReviewValid(string reviewTitle, string reviewBody) { - if (series.DontMatch) return true; - if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId)) - { - _logger.LogInformation("Series {SeriesName} is on AppUserId {AppUserId}'s hold list. Not scrobbling", series.Name, - userId); - return true; - } - - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId); - if (library is not {AllowScrobbling: true}) return true; - if (!ExternalMetadataService.IsPlusEligible(library.Type)) return true; - - return false; + return string.IsNullOrEmpty(reviewTitle) || string.IsNullOrEmpty(reviewBody) || (reviewTitle.Length < 2200 || + reviewTitle.Length > 120 || + reviewTitle.Length < 20); } - private async Task GetRateLimit(string license, string aniListToken) + public static long? GetMalId(Series series) { - if (string.IsNullOrWhiteSpace(aniListToken)) return 0; - try - { - var response = await (Configuration.KavitaPlusApiUrl + "/api/scrobbling/rate-limit?accessToken=" + aniListToken) - .WithKavitaPlusHeaders(license, aniListToken) - .GetStringAsync(); - - return int.Parse(response); - } - catch (Exception e) - { - _logger.LogError(e, "An error happened trying to get rate limit from Kavita+ API"); - } - - return 0; + var malId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite); + return malId ?? series.ExternalSeriesMetadata?.MalId; } - private async Task PostScrobbleUpdate(ScrobbleDto data, string license, ScrobbleEvent evt) + public static int? GetAniListId(Series seriesWithExternalMetadata) { - try - { - var response = await (Configuration.KavitaPlusApiUrl + "/api/scrobbling/update") - .WithKavitaPlusHeaders(license) - .PostJsonAsync(data) - .ReceiveJson(); - - if (!response.Successful) - { - // Might want to log this under ScrobbleError - if (response.ErrorMessage != null && response.ErrorMessage.Contains("Too Many Requests")) - { - _logger.LogInformation("Hit Too many requests, sleeping to regain requests and retrying"); - await Task.Delay(TimeSpan.FromMinutes(10)); - return await PostScrobbleUpdate(data, license, evt); - } - if (response.ErrorMessage != null && response.ErrorMessage.Contains("Unauthorized")) - { - _logger.LogCritical("Kavita+ responded with Unauthorized. Please check your subscription"); - await _licenseService.HasActiveLicense(true); - evt.IsErrored = true; - evt.ErrorDetails = "Kavita+ subscription no longer active"; - throw new KavitaException("Kavita+ responded with Unauthorized. Please check your subscription"); - } - if (response.ErrorMessage != null && response.ErrorMessage.Contains("Access token is invalid")) - { - evt.IsErrored = true; - evt.ErrorDetails = AccessTokenErrorMessage; - throw new KavitaException("Access token is invalid"); - } - if (response.ErrorMessage != null && response.ErrorMessage.Contains("Unknown Series")) - { - // Log the Series name and Id in ScrobbleErrors - _logger.LogInformation("Kavita+ was unable to match the series: {SeriesName}", evt.Series.Name); - if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) - { - // Create a new ExternalMetadata entry to indicate that this is not matchable - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(evt.SeriesId, SeriesIncludes.ExternalMetadata); - if (series == null) return 0; - - series.ExternalSeriesMetadata ??= new ExternalSeriesMetadata() {SeriesId = evt.SeriesId}; - series.IsBlacklisted = true; - _unitOfWork.SeriesRepository.Update(series); - - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() - { - Comment = UnknownSeriesErrorMessage, - Details = data.SeriesName, - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - - } - - evt.IsErrored = true; - evt.ErrorDetails = UnknownSeriesErrorMessage; - } else if (response.ErrorMessage != null && response.ErrorMessage.StartsWith("Review")) - { - // Log the Series name and Id in ScrobbleErrors - _logger.LogInformation("Kavita+ was unable to save the review"); - if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) - { - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() - { - Comment = response.ErrorMessage, - Details = data.SeriesName, - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - } - evt.IsErrored = true; - evt.ErrorDetails = "Review was unable to be saved due to upstream requirements"; - } - } - - return response.RateLeft; - } - catch (FlurlHttpException ex) - { - var errorMessage = await ex.GetResponseStringAsync(); - // Trim quotes if the response is a JSON string - errorMessage = errorMessage.Trim('"'); - - if (errorMessage.Contains("Too Many Requests")) - { - _logger.LogInformation("Hit Too many requests, sleeping to regain requests and retrying"); - await Task.Delay(TimeSpan.FromMinutes(10)); - return await PostScrobbleUpdate(data, license, evt); - } - - _logger.LogError(ex, "Scrobbling to Kavita+ API failed due to error: {ErrorMessage}", ex.Message); - if (ex.Message.Contains("Call failed with status code 500 (Internal Server Error)")) - { - if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) - { - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() - { - Comment = UnknownSeriesErrorMessage, - Details = data.SeriesName, - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - } - evt.IsErrored = true; - evt.ErrorDetails = "Bad payload from Scrobble Provider"; - throw new KavitaException("Bad payload from Scrobble Provider"); - } - throw; - } - } - - /// - /// This will backfill events from existing progress history, ratings, and want to read for users that have a valid license - /// - /// Defaults to 0 meaning all users. Allows a userId to be set if a scrobble key is added to a user - public async Task CreateEventsFromExistingHistory(int userId = 0) - { - if (!await _licenseService.HasActiveLicense()) return; - - if (userId != 0) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user == null || string.IsNullOrEmpty(user.AniListAccessToken)) return; - if (user.HasRunScrobbleEventGeneration) - { - _logger.LogWarning("User {UserName} has already run scrobble event generation, Kavita will not generate more events", user.UserName); - return; - } - } - - - - var libAllowsScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) - .ToDictionary(lib => lib.Id, lib => lib.AllowScrobbling); - - var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()) - .Where(l => userId == 0 || userId == l.Id) - .Select(u => u.Id); - - foreach (var uId in userIds) - { - var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(uId); - foreach (var wtr in wantToRead) - { - if (!libAllowsScrobbling[wtr.LibraryId]) continue; - await ScrobbleWantToReadUpdate(uId, wtr.Id, true); - } - - var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(uId); - foreach (var rating in ratings) - { - if (!libAllowsScrobbling[rating.Series.LibraryId]) continue; - await ScrobbleRatingUpdate(uId, rating.SeriesId, rating.Rating); - } - - var seriesWithProgress = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(0, uId, - new UserParams(), new FilterDto() - { - ReadStatus = new ReadStatus() - { - Read = true, - InProgress = true, - NotRead = false - }, - Libraries = libAllowsScrobbling.Keys.Where(k => libAllowsScrobbling[k]).ToList() - }); - - foreach (var series in seriesWithProgress) - { - if (!libAllowsScrobbling[series.LibraryId]) continue; - if (series.PagesRead <= 0) continue; // Since we only scrobble when things are higher, we can - await ScrobbleReadingUpdate(uId, series.Id); - } - - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(uId); - if (user != null) - { - user.HasRunScrobbleEventGeneration = true; - user.ScrobbleEventGenerationRan = DateTime.UtcNow; - await _unitOfWork.CommitAsync(); - } - } - } - - public async Task CreateEventsFromExistingHistoryForSeries(int seriesId) - { - if (!await _licenseService.HasActiveLicense()) return; - - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); - if (series == null || !series.Library.AllowScrobbling) return; - - _logger.LogInformation("Creating Scrobbling events for Series {SeriesName}", series.Name); - - var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()) - .Select(u => u.Id); - - foreach (var uId in userIds) - { - // Handle "Want to Read" updates specific to the series - var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(uId); - foreach (var wtr in wantToRead.Where(wtr => wtr.Id == seriesId)) - { - await ScrobbleWantToReadUpdate(uId, wtr.Id, true); - } - - // Handle ratings specific to the series - var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(uId); - foreach (var rating in ratings.Where(rating => rating.SeriesId == seriesId)) - { - await ScrobbleRatingUpdate(uId, rating.SeriesId, rating.Rating); - } - - // Handle progress updates for the specific series - var seriesProgress = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync( - series.LibraryId, - uId, - new UserParams(), - new FilterDto - { - ReadStatus = new ReadStatus - { - Read = true, - InProgress = true, - NotRead = false - }, - Libraries = new List { series.LibraryId }, - SeriesNameQuery = series.Name - }); - - foreach (var progress in seriesProgress.Where(progress => progress.Id == seriesId)) - { - if (progress.PagesRead > 0) - { - await ScrobbleReadingUpdate(uId, progress.Id); - } - } - } - } - - /// - /// Removes all events (active) that are tied to a now-on hold series - /// - /// - /// - public async Task ClearEventsForSeries(int userId, int seriesId) - { - _logger.LogInformation("Clearing Pre-existing Scrobble events for Series {SeriesId} by User {AppUserId} as Series is now on hold list", seriesId, userId); - var events = await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId); - foreach (var scrobble in events) - { - _unitOfWork.ScrobbleRepository.Remove(scrobble); - } - - await _unitOfWork.CommitAsync(); - } - - /// - /// Removes all events that have been processed that are 7 days old - /// - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ClearProcessedEvents() - { - const int daysAgo = 7; - var events = await _unitOfWork.ScrobbleRepository.GetProcessedEvents(daysAgo); - _unitOfWork.ScrobbleRepository.Remove(events); - _logger.LogInformation("Removing {Count} scrobble events that have been processed {DaysAgo}+ days ago", events.Count, daysAgo); - await _unitOfWork.CommitAsync(); - } - - /// - /// This is a task that is ran on a fixed schedule (every few hours or every day) that clears out the scrobble event table - /// and offloads the data to the API server which performs the syncing to the providers. - /// - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ProcessUpdatesSinceLastSync() - { - // Check how many scrobble events we have available then only do those. - var userRateLimits = new Dictionary(); - - var progressCounter = 0; - - var librariesWithScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) - .AsEnumerable() - .Where(l => l.AllowScrobbling) - .Select(l => l.Id) - .ToImmutableHashSet(); - - var errors = (await _unitOfWork.ScrobbleRepository.GetScrobbleErrors()) - .Where(e => e.Comment == "Unknown Series" || e.Comment == UnknownSeriesErrorMessage || e.Comment == AccessTokenErrorMessage) - .Select(e => e.SeriesId) - .ToList(); - - var readEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.ChapterRead)) - .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) - .Where(e => !errors.Contains(e.SeriesId)) - .ToList(); - var addToWantToRead = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.AddWantToRead)) - .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) - .Where(e => !errors.Contains(e.SeriesId)) - .ToList(); - var removeWantToRead = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.RemoveWantToRead)) - .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) - .Where(e => !errors.Contains(e.SeriesId)) - .ToList(); - var ratingEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.ScoreUpdated)) - .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) - .Where(e => !errors.Contains(e.SeriesId)) - .ToList(); - - var decisions = CalculateNetWantToReadDecisions(addToWantToRead, removeWantToRead); - - // Clear any events that are already on error table - var erroredEvents = await _unitOfWork.ScrobbleRepository.GetAllEventsWithSeriesIds(errors); - if (erroredEvents.Count > 0) - { - _unitOfWork.ScrobbleRepository.Remove(erroredEvents); - await _unitOfWork.CommitAsync(); - } - - var totalEvents = readEvents.Count + decisions.Count + ratingEvents.Count; - if (totalEvents == 0) return; - - // Get all the applicable users to scrobble and set their rate limits - var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - var usersToScrobble = await PrepareUsersToScrobble(readEvents, addToWantToRead, removeWantToRead, ratingEvents, userRateLimits, license); - - - _logger.LogInformation("Scrobble Processing Details:" + - "\n Read Events: {ReadEventsCount}" + - "\n Want to Read Events: {WantToReadEventsCount}" + - "\n Rating Events: {RatingEventsCount}" + - "\n Users to Scrobble: {UsersToScrobbleCount}" + - "\n Total Events to Process: {TotalEvents}", - readEvents.Count, - decisions.Count, - ratingEvents.Count, - usersToScrobble.Count, - totalEvents); - - try - { - progressCounter = await ProcessReadEvents(readEvents, userRateLimits, usersToScrobble, totalEvents, progressCounter); - - progressCounter = await ProcessRatingEvents(ratingEvents, userRateLimits, usersToScrobble, totalEvents, progressCounter); - - progressCounter = await ProcessWantToReadRatingEvents(decisions, userRateLimits, usersToScrobble, totalEvents, progressCounter); - } - catch (FlurlHttpException ex) - { - _logger.LogError(ex, "Kavita+ API or a Scrobble service may be experiencing an outage. Stopping sending data"); - return; - } - - - await SaveToDb(progressCounter, true); - _logger.LogInformation("Scrobbling Events is complete"); - - // Cleanup any events that are due to bugs or legacy - try - { - var eventsWithoutAnilistToken = (await _unitOfWork.ScrobbleRepository.GetEvents()) - .Where(e => !e.IsProcessed && !e.IsErrored) - .Where(e => string.IsNullOrEmpty(e.AppUser.AniListAccessToken)); - - _unitOfWork.ScrobbleRepository.Remove(eventsWithoutAnilistToken); - await _unitOfWork.CommitAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception when trying to delete old scrobble events when the user has no active token"); - } - } - - /// - /// Calculates the net want-to-read decisions by considering all events. - /// Returns events that represent the final state for each user/series pair. - /// - /// List of events for adding to want-to-read - /// List of events for removing from want-to-read - /// List of events that represent the final state (add or remove) - private static List CalculateNetWantToReadDecisions(List addEvents, List removeEvents) - { - // Create a dictionary to track the latest event for each user/series combination - var latestEvents = new Dictionary<(int SeriesId, int AppUserId), ScrobbleEvent>(); - - // Process all add events - foreach (var addEvent in addEvents) - { - var key = (addEvent.SeriesId, addEvent.AppUserId); - - if (latestEvents.TryGetValue(key, out var value) && addEvent.CreatedUtc <= value.CreatedUtc) continue; - - value = addEvent; - latestEvents[key] = value; - } - - // Process all remove events - foreach (var removeEvent in removeEvents) - { - var key = (removeEvent.SeriesId, removeEvent.AppUserId); - - if (latestEvents.TryGetValue(key, out var value) && removeEvent.CreatedUtc <= value.CreatedUtc) continue; - - value = removeEvent; - latestEvents[key] = value; - } - - // Return all events that represent the final state - return latestEvents.Values.ToList(); - } - - private async Task ProcessWantToReadRatingEvents(List decisions, Dictionary userRateLimits, List usersToScrobble, int totalEvents, int progressCounter) - { - progressCounter = await ProcessEvents(decisions, userRateLimits, usersToScrobble.Count, progressCounter, - totalEvents, evt => Task.FromResult(new ScrobbleDto() - { - Format = evt.Format, - AniListId = evt.AniListId, - MALId = (int?) evt.MalId, - ScrobbleEventType = evt.ScrobbleEventType, - ChapterNumber = evt.ChapterNumber, - VolumeNumber = (int?) evt.VolumeNumber, - AniListToken = evt.AppUser.AniListAccessToken, - SeriesName = evt.Series.Name, - LocalizedSeriesName = evt.Series.LocalizedName, - Year = evt.Series.Metadata.ReleaseYear - })); - - // After decisions, we need to mark all the want to read and remove from want to read as completed - if (decisions.Any(d => d.IsProcessed)) - { - foreach (var scrobbleEvent in decisions.Where(d => d.IsProcessed)) - { - scrobbleEvent.IsProcessed = true; - scrobbleEvent.ProcessDateUtc = DateTime.UtcNow; - _unitOfWork.ScrobbleRepository.Update(scrobbleEvent); - } - await _unitOfWork.CommitAsync(); - } - - return progressCounter; - } - - private async Task ProcessRatingEvents(List ratingEvents, Dictionary userRateLimits, List usersToScrobble, - int totalEvents, int progressCounter) - { - return await ProcessEvents(ratingEvents, userRateLimits, usersToScrobble.Count, progressCounter, - totalEvents, evt => Task.FromResult(new ScrobbleDto() - { - Format = evt.Format, - AniListId = evt.AniListId, - MALId = (int?) evt.MalId, - ScrobbleEventType = evt.ScrobbleEventType, - AniListToken = evt.AppUser.AniListAccessToken, - SeriesName = evt.Series.Name, - LocalizedSeriesName = evt.Series.LocalizedName, - Rating = evt.Rating, - Year = evt.Series.Metadata.ReleaseYear - })); - } - - - private async Task ProcessReadEvents(List readEvents, Dictionary userRateLimits, List usersToScrobble, int totalEvents, - int progressCounter) - { - // Recalculate the highest volume/chapter - foreach (var readEvt in readEvents) - { - // Note: this causes skewing in the scrobble history because it makes it look like there are duplicate events - readEvt.VolumeNumber = - (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(readEvt.SeriesId, - readEvt.AppUser.Id); - readEvt.ChapterNumber = - await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(readEvt.SeriesId, - readEvt.AppUser.Id); - _unitOfWork.ScrobbleRepository.Update(readEvt); - } - - return await ProcessEvents(readEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalEvents, - async evt => new ScrobbleDto() - { - Format = evt.Format, - AniListId = evt.AniListId, - MALId = (int?) evt.MalId, - ScrobbleEventType = evt.ScrobbleEventType, - ChapterNumber = evt.ChapterNumber, - VolumeNumber = (int?) evt.VolumeNumber, - AniListToken = evt.AppUser.AniListAccessToken!, - SeriesName = evt.Series.Name, - LocalizedSeriesName = evt.Series.LocalizedName, - ScrobbleDateUtc = evt.LastModifiedUtc, - Year = evt.Series.Metadata.ReleaseYear, - StartedReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetFirstProgressForSeries(evt.SeriesId, evt.AppUser.Id), - LatestReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetLatestProgressForSeries(evt.SeriesId, evt.AppUser.Id), - }); - } - - - private async Task> PrepareUsersToScrobble(List readEvents, List addToWantToRead, List removeWantToRead, List ratingEvents, - Dictionary userRateLimits, ServerSetting license) - { - // For all userIds, ensure that we can connect and have access - var usersToScrobble = readEvents.Select(r => r.AppUser) - .Concat(addToWantToRead.Select(r => r.AppUser)) - .Concat(removeWantToRead.Select(r => r.AppUser)) - .Concat(ratingEvents.Select(r => r.AppUser)) - .Where(user => !string.IsNullOrEmpty(user.AniListAccessToken)) - .Where(user => user.UserPreferences.AniListScrobblingEnabled) - .DistinctBy(u => u.Id) - .ToList(); - - foreach (var user in usersToScrobble) - { - await SetAndCheckRateLimit(userRateLimits, user, license.Value); - } - - return usersToScrobble; - } - - - private async Task ProcessEvents(IEnumerable events, Dictionary userRateLimits, - int usersToScrobble, int progressCounter, int totalProgress, Func> createEvent) - { - var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - foreach (var evt in events) - { - _logger.LogDebug("Processing Reading Events: {Count} / {Total}", progressCounter, totalProgress); - progressCounter++; - - // Check if this media item can even be processed for this user - if (!CanProcessScrobbleEvent(evt)) - { - continue; - } - - if (TokenService.HasTokenExpired(evt.AppUser.AniListAccessToken)) - { - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() - { - Comment = "AniList token has expired and needs rotating. Scrobbling wont work until then", - Details = $"User: {evt.AppUser.UserName}, Expired: {TokenService.GetTokenExpiry(evt.AppUser.AniListAccessToken)}", - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - await _unitOfWork.CommitAsync(); - continue; - } - - if (evt.Series.IsBlacklisted || evt.Series.DontMatch) - { - _logger.LogInformation("Series {SeriesName} ({SeriesId}) can't be matched and thus cannot scrobble this event", evt.Series.Name, evt.SeriesId); - _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() - { - Comment = UnknownSeriesErrorMessage, - Details = $"User: {evt.AppUser.UserName} Series: {evt.Series.Name}", - LibraryId = evt.LibraryId, - SeriesId = evt.SeriesId - }); - evt.IsErrored = true; - evt.ErrorDetails = UnknownSeriesErrorMessage; - evt.ProcessDateUtc = DateTime.UtcNow; - _unitOfWork.ScrobbleRepository.Update(evt); - await _unitOfWork.CommitAsync(); - - continue; - } - - var count = await SetAndCheckRateLimit(userRateLimits, evt.AppUser, license.Value); - userRateLimits[evt.AppUserId] = count; - if (count == 0) - { - if (usersToScrobble == 1) break; - continue; - } - - try - { - var data = await createEvent(evt); - // We need to handle the encoding and changing it to the old one until we can update the API layer to handle these - // which could happen in v0.8.3 - if (data.VolumeNumber is Parser.SpecialVolumeNumber or Parser.DefaultChapterNumber) - { - data.VolumeNumber = 0; - } - - if (data.ChapterNumber is Parser.DefaultChapterNumber) - { - data.ChapterNumber = 0; - } - userRateLimits[evt.AppUserId] = await PostScrobbleUpdate(data, license.Value, evt); - evt.IsProcessed = true; - evt.ProcessDateUtc = DateTime.UtcNow; - _unitOfWork.ScrobbleRepository.Update(evt); - } - catch (FlurlHttpException) - { - // If a flurl exception occured, the API is likely down. Kill processing - throw; - } - catch (KavitaException ex) - { - if (ex.Message.Contains("Access token is invalid")) - { - _logger.LogCritical(ex, "Access Token for AppUserId: {AppUserId} needs to be regenerated/renewed to continue scrobbling", evt.AppUser.Id); - evt.IsErrored = true; - evt.ErrorDetails = AccessTokenErrorMessage; - _unitOfWork.ScrobbleRepository.Update(evt); - } - } - catch (Exception ex) - { - /* Swallow as it's already been handled in PostScrobbleUpdate */ - _logger.LogError(ex, "Error processing event {EventId}", evt.Id); - } - await SaveToDb(progressCounter); - // We can use count to determine how long to sleep based on rate gain. It might be specific to AniList, but we can model others - var delay = count > 10 ? TimeSpan.FromMilliseconds(ScrobbleSleepTime) : TimeSpan.FromSeconds(60); - await Task.Delay(delay); - } - - await SaveToDb(progressCounter, true); - return progressCounter; - } - - private async Task SaveToDb(int progressCounter, bool force = false) - { - if ((force || progressCounter % 5 == 0) && _unitOfWork.HasChanges()) - { - _logger.LogDebug("Saving Scrobbling Event Processing Progress"); - await _unitOfWork.CommitAsync(); - } - } - - - private static bool CanProcessScrobbleEvent(ScrobbleEvent readEvent) - { - var userProviders = GetUserProviders(readEvent.AppUser); - switch (readEvent.Series.Library.Type) - { - case LibraryType.Manga when MangaProviders.Intersect(userProviders).Any(): - case LibraryType.Comic when - ComicProviders.Intersect(userProviders).Any(): - case LibraryType.Book when - BookProviders.Intersect(userProviders).Any(): - case LibraryType.LightNovel when - LightNovelProviders.Intersect(userProviders).Any(): - return true; - default: - return false; - } - } - - private static List GetUserProviders(AppUser appUser) - { - var providers = new List(); - if (!string.IsNullOrEmpty(appUser.AniListAccessToken)) providers.Add(ScrobbleProvider.AniList); - - return providers; + var aniListId = ExtractId(seriesWithExternalMetadata.Metadata.WebLinks, AniListWeblinkWebsite); + return aniListId ?? seriesWithExternalMetadata.ExternalSeriesMetadata?.AniListId; } /// @@ -1178,23 +556,23 @@ public class ScrobblingService : IScrobblingService foreach (var webLink in webLinks.Split(',')) { if (!webLink.StartsWith(website)) continue; + var tokens = webLink.Split(website)[1].Split('/'); var value = tokens[index]; + if (typeof(T) == typeof(int?)) { - if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) - return (T)(object)intValue; + if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) return (T)(object)intValue; } else if (typeof(T) == typeof(int)) { - if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) - return (T)(object)intValue; + if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) return (T)(object)intValue; + return default; } else if (typeof(T) == typeof(long?)) { - if (long.TryParse(value, CultureInfo.InvariantCulture, out var longValue)) - return (T)(object)longValue; + if (long.TryParse(value, CultureInfo.InvariantCulture, out var longValue)) return (T)(object)longValue; } else if (typeof(T) == typeof(string)) { @@ -1238,6 +616,778 @@ public class ScrobblingService : IScrobblingService return id is null or 0 ? string.Empty : $"{url}{id}/"; } + #endregion + + /// + /// Returns false if, the series is on hold or Don't Match, or when the library has scrobbling disable or not eligible + /// + /// + /// + /// + /// + private async Task CheckIfCannotScrobble(int userId, int seriesId, Series series) + { + if (series.DontMatch) return true; + + if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId)) + { + _logger.LogInformation("Series {SeriesName} is on AppUserId {AppUserId}'s hold list. Not scrobbling", series.Name, userId); + return true; + } + + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId); + if (library is not {AllowScrobbling: true} || !ExternalMetadataService.IsPlusEligible(library.Type)) return true; + + return false; + } + + /// + /// Returns the rate limit from the K+ api + /// + /// + /// + /// + private async Task GetRateLimit(string license, string aniListToken) + { + if (string.IsNullOrWhiteSpace(aniListToken)) return 0; + + try + { + return await _kavitaPlusApiService.GetRateLimit(license, aniListToken); + } + catch (Exception e) + { + _logger.LogError(e, "An error happened trying to get rate limit from Kavita+ API"); + } + + return 0; + } + + #region Scrobble process (Requests to K+) + + /// + /// Retrieve all events for which the series has not errored, then delete all current errors + /// + private async Task PrepareScrobbleContext() + { + var librariesWithScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) + .AsEnumerable() + .Where(l => l.AllowScrobbling) + .Select(l => l.Id) + .ToImmutableHashSet(); + + var erroredSeries = (await _unitOfWork.ScrobbleRepository.GetScrobbleErrors()) + .Where(e => e.Comment is "Unknown Series" or UnknownSeriesErrorMessage or AccessTokenErrorMessage) + .Select(e => e.SeriesId) + .ToList(); + + var readEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.ChapterRead)) + .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) + .Where(e => !erroredSeries.Contains(e.SeriesId)) + .ToList(); + var addToWantToRead = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.AddWantToRead)) + .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) + .Where(e => !erroredSeries.Contains(e.SeriesId)) + .ToList(); + var removeWantToRead = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.RemoveWantToRead)) + .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) + .Where(e => !erroredSeries.Contains(e.SeriesId)) + .ToList(); + var ratingEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.ScoreUpdated)) + .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) + .Where(e => !erroredSeries.Contains(e.SeriesId)) + .ToList(); + + return new ScrobbleSyncContext + { + ReadEvents = readEvents, + RatingEvents = ratingEvents, + AddToWantToRead = addToWantToRead, + RemoveWantToRead = removeWantToRead, + Decisions = CalculateNetWantToReadDecisions(addToWantToRead, removeWantToRead), + RateLimits = [], + License = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value, + }; + } + + /// + /// Filters users who can scrobble, sets their rate limit and updates the + /// + /// + /// + private async Task PrepareUsersToScrobble(ScrobbleSyncContext ctx) + { + // For all userIds, ensure that we can connect and have access + var usersToScrobble = ctx.ReadEvents.Select(r => r.AppUser) + .Concat(ctx.AddToWantToRead.Select(r => r.AppUser)) + .Concat(ctx.RemoveWantToRead.Select(r => r.AppUser)) + .Concat(ctx.RatingEvents.Select(r => r.AppUser)) + .Where(user => !string.IsNullOrEmpty(user.AniListAccessToken)) + .Where(user => user.UserPreferences.AniListScrobblingEnabled) + .DistinctBy(u => u.Id) + .ToList(); + + foreach (var user in usersToScrobble) + { + await SetAndCheckRateLimit(ctx.RateLimits, user, ctx.License); + } + + ctx.Users = usersToScrobble; + } + + /// + /// Cleans up any events that are due to bugs or legacy + /// + private async Task CleanupOldOrBuggedEvents() + { + try + { + var eventsWithoutAnilistToken = (await _unitOfWork.ScrobbleRepository.GetEvents()) + .Where(e => e is { IsProcessed: false, IsErrored: false }) + .Where(e => string.IsNullOrEmpty(e.AppUser.AniListAccessToken)); + + _unitOfWork.ScrobbleRepository.Remove(eventsWithoutAnilistToken); + await _unitOfWork.CommitAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an exception when trying to delete old scrobble events when the user has no active token"); + } + } + + /// + /// This is a task that is run on a fixed schedule (every few hours or every day) that clears out the scrobble event table + /// and offloads the data to the API server which performs the syncing to the providers. + /// + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + public async Task ProcessUpdatesSinceLastSync() + { + var ctx = await PrepareScrobbleContext(); + if (ctx.TotalCount == 0) return; + + // Get all the applicable users to scrobble and set their rate limits + await PrepareUsersToScrobble(ctx); + + _logger.LogInformation("Scrobble Processing Details:" + + "\n Read Events: {ReadEventsCount}" + + "\n Want to Read Events: {WantToReadEventsCount}" + + "\n Rating Events: {RatingEventsCount}" + + "\n Users to Scrobble: {UsersToScrobbleCount}" + + "\n Total Events to Process: {TotalEvents}", + ctx.ReadEvents.Count, + ctx.Decisions.Count, + ctx.RatingEvents.Count, + ctx.Users.Count, + ctx.TotalCount); + + try + { + await ProcessReadEvents(ctx); + await ProcessRatingEvents(ctx); + await ProcessWantToReadRatingEvents(ctx); + } + catch (FlurlHttpException ex) + { + _logger.LogError(ex, "Kavita+ API or a Scrobble service may be experiencing an outage. Stopping sending data"); + return; + } + + + await SaveToDb(ctx.ProgressCounter, true); + _logger.LogInformation("Scrobbling Events is complete"); + + await CleanupOldOrBuggedEvents(); + } + + /// + /// Calculates the net want-to-read decisions by considering all events. + /// Returns events that represent the final state for each user/series pair. + /// + /// List of events for adding to want-to-read + /// List of events for removing from want-to-read + /// List of events that represent the final state (add or remove) + private static List CalculateNetWantToReadDecisions(List addEvents, List removeEvents) + { + // Create a dictionary to track the latest event for each user/series combination + var latestEvents = new Dictionary<(int SeriesId, int AppUserId), ScrobbleEvent>(); + + // Process all add events + foreach (var addEvent in addEvents) + { + var key = (addEvent.SeriesId, addEvent.AppUserId); + + if (latestEvents.TryGetValue(key, out var value) && addEvent.CreatedUtc <= value.CreatedUtc) continue; + + value = addEvent; + latestEvents[key] = value; + } + + // Process all remove events + foreach (var removeEvent in removeEvents) + { + var key = (removeEvent.SeriesId, removeEvent.AppUserId); + + if (latestEvents.TryGetValue(key, out var value) && removeEvent.CreatedUtc <= value.CreatedUtc) continue; + + value = removeEvent; + latestEvents[key] = value; + } + + // Return all events that represent the final state + return latestEvents.Values.ToList(); + } + + private async Task ProcessWantToReadRatingEvents(ScrobbleSyncContext ctx) + { + await ProcessEvents(ctx.Decisions, ctx, evt => Task.FromResult(new ScrobbleDto + { + Format = evt.Format, + AniListId = evt.AniListId, + MALId = (int?) evt.MalId, + ScrobbleEventType = evt.ScrobbleEventType, + ChapterNumber = evt.ChapterNumber, + VolumeNumber = (int?) evt.VolumeNumber, + AniListToken = evt.AppUser.AniListAccessToken ?? string.Empty, + SeriesName = evt.Series.Name, + LocalizedSeriesName = evt.Series.LocalizedName, + Year = evt.Series.Metadata.ReleaseYear + })); + + // After decisions, we need to mark all the want to read and remove from want to read as completed + var processedDecisions = ctx.Decisions.Where(d => d.IsProcessed).ToList(); + if (processedDecisions.Count > 0) + { + foreach (var scrobbleEvent in processedDecisions) + { + scrobbleEvent.IsProcessed = true; + scrobbleEvent.ProcessDateUtc = DateTime.UtcNow; + _unitOfWork.ScrobbleRepository.Update(scrobbleEvent); + } + await _unitOfWork.CommitAsync(); + } + } + + private async Task ProcessRatingEvents(ScrobbleSyncContext ctx) + { + await ProcessEvents(ctx.RatingEvents, ctx, evt => Task.FromResult(new ScrobbleDto + { + Format = evt.Format, + AniListId = evt.AniListId, + MALId = (int?) evt.MalId, + ScrobbleEventType = evt.ScrobbleEventType, + AniListToken = evt.AppUser.AniListAccessToken ?? string.Empty, + SeriesName = evt.Series.Name, + LocalizedSeriesName = evt.Series.LocalizedName, + Rating = evt.Rating, + Year = evt.Series.Metadata.ReleaseYear + })); + } + + private async Task ProcessReadEvents(ScrobbleSyncContext ctx) + { + // Recalculate the highest volume/chapter + foreach (var readEvt in ctx.ReadEvents) + { + // Note: this causes skewing in the scrobble history because it makes it look like there are duplicate events + readEvt.VolumeNumber = + (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(readEvt.SeriesId, + readEvt.AppUser.Id); + readEvt.ChapterNumber = + await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(readEvt.SeriesId, + readEvt.AppUser.Id); + _unitOfWork.ScrobbleRepository.Update(readEvt); + } + + await ProcessEvents(ctx.ReadEvents, ctx, async evt => new ScrobbleDto + { + Format = evt.Format, + AniListId = evt.AniListId, + MALId = (int?) evt.MalId, + ScrobbleEventType = evt.ScrobbleEventType, + ChapterNumber = evt.ChapterNumber, + VolumeNumber = (int?) evt.VolumeNumber, + AniListToken = evt.AppUser.AniListAccessToken ?? string.Empty, + SeriesName = evt.Series.Name, + LocalizedSeriesName = evt.Series.LocalizedName, + ScrobbleDateUtc = evt.LastModifiedUtc, + Year = evt.Series.Metadata.ReleaseYear, + StartedReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetFirstProgressForSeries(evt.SeriesId, evt.AppUser.Id), + LatestReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetLatestProgressForSeries(evt.SeriesId, evt.AppUser.Id), + }); + } + + /// + /// Returns true if the user token is valid + /// + /// + /// + /// If the token is not, adds a scrobble error + private async Task ValidateUserToken(ScrobbleEvent evt) + { + if (!TokenService.HasTokenExpired(evt.AppUser.AniListAccessToken)) + return true; + + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError + { + Comment = "AniList token has expired and needs rotating. Scrobbling wont work until then", + Details = $"User: {evt.AppUser.UserName}, Expired: {TokenService.GetTokenExpiry(evt.AppUser.AniListAccessToken)}", + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + await _unitOfWork.CommitAsync(); + return false; + } + + /// + /// Returns true if the series can be scrobbled + /// + /// + /// + /// If the series cannot be scrobbled, adds a scrobble error + private async Task ValidateSeriesCanBeScrobbled(ScrobbleEvent evt) + { + if (evt.Series is { IsBlacklisted: false, DontMatch: false }) + return true; + + _logger.LogInformation("Series {SeriesName} ({SeriesId}) can't be matched and thus cannot scrobble this event", + evt.Series.Name, evt.SeriesId); + + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError + { + Comment = UnknownSeriesErrorMessage, + Details = $"User: {evt.AppUser.UserName} Series: {evt.Series.Name}", + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + + evt.SetErrorMessage(UnknownSeriesErrorMessage); + evt.ProcessDateUtc = DateTime.UtcNow; + _unitOfWork.ScrobbleRepository.Update(evt); + await _unitOfWork.CommitAsync(); + return false; + } + + /// + /// Removed Special parses numbers from chatter and volume numbers + /// + /// + /// + private static ScrobbleDto NormalizeScrobbleData(ScrobbleDto data) + { + // We need to handle the encoding and changing it to the old one until we can update the API layer to handle these + // which could happen in v0.8.3 + if (data.VolumeNumber is Parser.SpecialVolumeNumber or Parser.DefaultChapterNumber) + { + data.VolumeNumber = 0; + } + + + if (data.ChapterNumber is Parser.DefaultChapterNumber) + { + data.ChapterNumber = 0; + } + + + return data; + } + + /// + /// Loops through all events, and post them to K+ + /// + /// + /// + /// + private async Task ProcessEvents(IEnumerable events, ScrobbleSyncContext ctx, Func> createEvent) + { + foreach (var evt in events.Where(CanProcessScrobbleEvent)) + { + _logger.LogDebug("Processing Scrobble Events: {Count} / {Total}", ctx.ProgressCounter, ctx.TotalCount); + ctx.ProgressCounter++; + + if (!await ValidateUserToken(evt)) continue; + if (!await ValidateSeriesCanBeScrobbled(evt)) continue; + + var count = await SetAndCheckRateLimit(ctx.RateLimits, evt.AppUser, ctx.License); + if (count == 0) + { + if (ctx.Users.Count == 1) break; + continue; + } + + try + { + var data = NormalizeScrobbleData(await createEvent(evt)); + + ctx.RateLimits[evt.AppUserId] = await PostScrobbleUpdate(data, ctx.License, evt); + + evt.IsProcessed = true; + evt.ProcessDateUtc = DateTime.UtcNow; + _unitOfWork.ScrobbleRepository.Update(evt); + } + catch (FlurlHttpException) + { + // If a flurl exception occured, the API is likely down. Kill processing + throw; + } + catch (KavitaException ex) + { + if (ex.Message.Contains("Access token is invalid")) + { + _logger.LogCritical(ex, "Access Token for AppUserId: {AppUserId} needs to be regenerated/renewed to continue scrobbling", evt.AppUser.Id); + evt.SetErrorMessage(AccessTokenErrorMessage); + _unitOfWork.ScrobbleRepository.Update(evt); + + // Ensure series with this error do not get re-processed next sync + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError + { + Comment = AccessTokenErrorMessage, + Details = $"{evt.AppUser.UserName} has an invalid access token (K+ Error)", + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId, + }); + } + } + catch (Exception ex) + { + /* Swallow as it's already been handled in PostScrobbleUpdate */ + _logger.LogError(ex, "Error processing event {EventId}", evt.Id); + } + + await SaveToDb(ctx.ProgressCounter); + + // We can use count to determine how long to sleep based on rate gain. It might be specific to AniList, but we can model others + var delay = count > 10 ? TimeSpan.FromMilliseconds(ScrobbleSleepTime) : TimeSpan.FromSeconds(60); + await Task.Delay(delay); + } + + await SaveToDb(ctx.ProgressCounter, true); + } + + /// + /// Save changes every five updates + /// + /// + /// Ignore update count check + private async Task SaveToDb(int progressCounter, bool force = false) + { + if ((force || progressCounter % 5 == 0) && _unitOfWork.HasChanges()) + { + _logger.LogDebug("Saving Scrobbling Event Processing Progress"); + await _unitOfWork.CommitAsync(); + } + } + + /// + /// If no errors have been logged for the given series, creates a new Unknown series error, and blacklists the series + /// + /// + /// + private async Task MarkSeriesAsUnknown(ScrobbleDto data, ScrobbleEvent evt) + { + if (await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) return; + + // Create a new ExternalMetadata entry to indicate that this is not matchable + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(evt.SeriesId, SeriesIncludes.ExternalMetadata); + if (series == null) return; + + series.ExternalSeriesMetadata ??= new ExternalSeriesMetadata {SeriesId = evt.SeriesId}; + series.IsBlacklisted = true; + _unitOfWork.SeriesRepository.Update(series); + + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError + { + Comment = UnknownSeriesErrorMessage, + Details = data.SeriesName, + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + } + + /// + /// Makes the K+ request, and handles any exceptions that occur + /// + /// Data to send to K+ + /// K+ license key + /// Related scrobble event + /// + /// Exceptions may be rethrown as a KavitaException + /// Some FlurlHttpException are also rethrown + public async Task PostScrobbleUpdate(ScrobbleDto data, string license, ScrobbleEvent evt) + { + try + { + var response = await _kavitaPlusApiService.PostScrobbleUpdate(data, license); + + _logger.LogDebug("K+ API Scrobble response for series {SeriesName}: Successful {Successful}, ErrorMessage {ErrorMessage}, ExtraInformation: {ExtraInformation}, RateLeft: {RateLeft}", + data.SeriesName, response.Successful, response.ErrorMessage, response.ExtraInformation, response.RateLeft); + + if (response.Successful || response.ErrorMessage == null) return response.RateLeft; + + // Might want to log this under ScrobbleError + if (response.ErrorMessage.Contains("Too Many Requests")) + { + _logger.LogInformation("Hit Too many requests while posting scrobble updates, sleeping to regain requests and retrying"); + await Task.Delay(TimeSpan.FromMinutes(10)); + return await PostScrobbleUpdate(data, license, evt); + } + + if (response.ErrorMessage.Contains("Unauthorized")) + { + _logger.LogCritical("Kavita+ responded with Unauthorized. Please check your subscription"); + await _licenseService.HasActiveLicense(true); + evt.SetErrorMessage(InvalidKPlusLicenseErrorMessage); + throw new KavitaException("Kavita+ responded with Unauthorized. Please check your subscription"); + } + + if (response.ErrorMessage.Contains("Access token is invalid")) + { + evt.SetErrorMessage(AccessTokenErrorMessage); + throw new KavitaException("Access token is invalid"); + } + + if (response.ErrorMessage.Contains("Unknown Series")) + { + // Log the Series name and Id in ScrobbleErrors + _logger.LogInformation("Kavita+ was unable to match the series: {SeriesName}", evt.Series.Name); + await MarkSeriesAsUnknown(data, evt); + evt.SetErrorMessage(UnknownSeriesErrorMessage); + } else if (response.ErrorMessage.StartsWith("Review")) + { + // Log the Series name and Id in ScrobbleErrors + _logger.LogInformation("Kavita+ was unable to save the review"); + if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) + { + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() + { + Comment = response.ErrorMessage, + Details = data.SeriesName, + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + } + evt.SetErrorMessage(ReviewFailedErrorMessage); + } + + return response.RateLeft; + } + catch (FlurlHttpException ex) + { + var errorMessage = await ex.GetResponseStringAsync(); + // Trim quotes if the response is a JSON string + errorMessage = errorMessage.Trim('"'); + + if (errorMessage.Contains("Too Many Requests")) + { + _logger.LogInformation("Hit Too many requests while posting scrobble updates, sleeping to regain requests and retrying"); + await Task.Delay(TimeSpan.FromMinutes(10)); + return await PostScrobbleUpdate(data, license, evt); + } + + _logger.LogError(ex, "Scrobbling to Kavita+ API failed due to error: {ErrorMessage}", ex.Message); + if (ex.StatusCode == 500 || ex.Message.Contains("Call failed with status code 500 (Internal Server Error)")) + { + if (!await _unitOfWork.ScrobbleRepository.HasErrorForSeries(evt.SeriesId)) + { + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() + { + Comment = UnknownSeriesErrorMessage, + Details = data.SeriesName, + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + } + evt.SetErrorMessage(BadPayLoadErrorMessage); + throw new KavitaException(BadPayLoadErrorMessage); + } + throw; + } + } + + #endregion + + #region BackFill + + + /// + /// This will backfill events from existing progress history, ratings, and want to read for users that have a valid license + /// + /// Defaults to 0 meaning all users. Allows a userId to be set if a scrobble key is added to a user + public async Task CreateEventsFromExistingHistory(int userId = 0) + { + if (!await _licenseService.HasActiveLicense()) return; + + if (userId != 0) + { + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (user == null || string.IsNullOrEmpty(user.AniListAccessToken)) return; + if (user.HasRunScrobbleEventGeneration) + { + _logger.LogWarning("User {UserName} has already run scrobble event generation, Kavita will not generate more events", user.UserName); + return; + } + } + + var libAllowsScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) + .ToDictionary(lib => lib.Id, lib => lib.AllowScrobbling); + + var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()) + .Where(l => userId == 0 || userId == l.Id) + .Where(u => !u.HasRunScrobbleEventGeneration) + .Select(u => u.Id); + + foreach (var uId in userIds) + { + await CreateEventsFromExistingHistoryForUser(uId, libAllowsScrobbling); + } + } + + /// + /// Creates wantToRead, rating, reviews, and series progress events for the suer + /// + /// + /// + private async Task CreateEventsFromExistingHistoryForUser(int userId, Dictionary libAllowsScrobbling) + { + var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(userId); + foreach (var wtr in wantToRead) + { + if (!libAllowsScrobbling[wtr.LibraryId]) continue; + await ScrobbleWantToReadUpdate(userId, wtr.Id, true); + } + + var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(userId); + foreach (var rating in ratings) + { + if (!libAllowsScrobbling[rating.Series.LibraryId]) continue; + await ScrobbleRatingUpdate(userId, rating.SeriesId, rating.Rating); + } + + var reviews = await _unitOfWork.UserRepository.GetSeriesWithReviews(userId); + foreach (var review in reviews.Where(r => !string.IsNullOrEmpty(r.Review))) + { + if (!libAllowsScrobbling[review.Series.LibraryId]) continue; + await ScrobbleReviewUpdate(userId, review.SeriesId, string.Empty, review.Review!); + } + + var seriesWithProgress = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(0, userId, + new UserParams(), new FilterDto + { + ReadStatus = new ReadStatus + { + Read = true, + InProgress = true, + NotRead = false + }, + Libraries = libAllowsScrobbling.Keys.Where(k => libAllowsScrobbling[k]).ToList() + }); + + foreach (var series in seriesWithProgress.Where(series => series.PagesRead > 0)) + { + if (!libAllowsScrobbling[series.LibraryId]) continue; + await ScrobbleReadingUpdate(userId, series.Id); + } + + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (user != null) + { + user.HasRunScrobbleEventGeneration = true; + user.ScrobbleEventGenerationRan = DateTime.UtcNow; + await _unitOfWork.CommitAsync(); + } + } + + public async Task CreateEventsFromExistingHistoryForSeries(int seriesId) + { + if (!await _licenseService.HasActiveLicense()) return; + + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); + if (series == null || !series.Library.AllowScrobbling) return; + + _logger.LogInformation("Creating Scrobbling events for Series {SeriesName}", series.Name); + + var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(u => u.Id); + + foreach (var uId in userIds) + { + // Handle "Want to Read" updates specific to the series + var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(uId); + foreach (var wtr in wantToRead.Where(wtr => wtr.Id == seriesId)) + { + await ScrobbleWantToReadUpdate(uId, wtr.Id, true); + } + + // Handle ratings specific to the series + var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(uId); + foreach (var rating in ratings.Where(rating => rating.SeriesId == seriesId)) + { + await ScrobbleRatingUpdate(uId, rating.SeriesId, rating.Rating); + } + + // Handle review specific to the series + var reviews = await _unitOfWork.UserRepository.GetSeriesWithReviews(uId); + foreach (var review in reviews.Where(r => r.SeriesId == seriesId && !string.IsNullOrEmpty(r.Review))) + { + await ScrobbleReviewUpdate(uId, review.SeriesId, string.Empty, review.Review!); + } + + // Handle progress updates for the specific series + await ScrobbleReadingUpdate(uId, seriesId); + } + } + + #endregion + + /// + /// Removes all events (active) that are tied to a now-on hold series + /// + /// + /// + public async Task ClearEventsForSeries(int userId, int seriesId) + { + _logger.LogInformation("Clearing Pre-existing Scrobble events for Series {SeriesId} by User {AppUserId} as Series is now on hold list", seriesId, userId); + + var events = await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId); + _unitOfWork.ScrobbleRepository.Remove(events); + await _unitOfWork.CommitAsync(); + } + + /// + /// Removes all events that have been processed that are 7 days old + /// + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + public async Task ClearProcessedEvents() + { + const int daysAgo = 7; + var events = await _unitOfWork.ScrobbleRepository.GetProcessedEvents(daysAgo); + _unitOfWork.ScrobbleRepository.Remove(events); + _logger.LogInformation("Removing {Count} scrobble events that have been processed {DaysAgo}+ days ago", events.Count, daysAgo); + await _unitOfWork.CommitAsync(); + } + + private static bool CanProcessScrobbleEvent(ScrobbleEvent readEvent) + { + var userProviders = GetUserProviders(readEvent.AppUser); + switch (readEvent.Series.Library.Type) + { + case LibraryType.Manga when MangaProviders.Intersect(userProviders).Any(): + case LibraryType.Comic when ComicProviders.Intersect(userProviders).Any(): + case LibraryType.Book when BookProviders.Intersect(userProviders).Any(): + case LibraryType.LightNovel when LightNovelProviders.Intersect(userProviders).Any(): + return true; + default: + return false; + } + } + + private static List GetUserProviders(AppUser appUser) + { + var providers = new List(); + if (!string.IsNullOrEmpty(appUser.AniListAccessToken)) providers.Add(ScrobbleProvider.AniList); + + return providers; + } private async Task SetAndCheckRateLimit(IDictionary userRateLimits, AppUser user, string license) { diff --git a/UI/Web/src/app/_models/scrobbling/scrobble-event.ts b/UI/Web/src/app/_models/scrobbling/scrobble-event.ts index 48a75afda..7db1ceeaa 100644 --- a/UI/Web/src/app/_models/scrobbling/scrobble-event.ts +++ b/UI/Web/src/app/_models/scrobbling/scrobble-event.ts @@ -7,6 +7,7 @@ export enum ScrobbleEventType { } export interface ScrobbleEvent { + id: number; seriesName: string; seriesId: number; libraryId: number; diff --git a/UI/Web/src/app/_services/scrobbling.service.ts b/UI/Web/src/app/_services/scrobbling.service.ts index 76b9212f4..cfc7b34ac 100644 --- a/UI/Web/src/app/_services/scrobbling.service.ts +++ b/UI/Web/src/app/_services/scrobbling.service.ts @@ -104,6 +104,10 @@ export class ScrobblingService { triggerScrobbleEventGeneration() { return this.httpClient.post(this.baseUrl + 'scrobbling/generate-scrobble-events', TextResonse); - } + + bulkRemoveEvents(eventIds: number[]) { + return this.httpClient.post(this.baseUrl + "scrobbling/bulk-remove-events", eventIds) + } + } diff --git a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html index 51caae2f3..96fd71b95 100644 --- a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html +++ b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html @@ -20,7 +20,10 @@
    - +
    + + +
    @@ -40,6 +43,20 @@ [sorts]="[{prop: 'createdUtc', dir: 'desc'}]" > + + +
    + + +
    +
    + + + +
    + {{t('created-header')}} @@ -101,7 +118,7 @@ - + {{t('is-processed-header')}} diff --git a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts index c0306c4cf..ac48b6add 100644 --- a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts +++ b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts @@ -1,4 +1,12 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef, + HostListener, + inject, + OnInit +} from '@angular/core'; import {ScrobbleProvider, ScrobblingService} from "../../_services/scrobbling.service"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; @@ -9,7 +17,7 @@ import {ScrobbleEventSortField} from "../../_models/scrobbling/scrobble-event-fi import {debounceTime, take} from "rxjs/operators"; import {PaginatedResult} from "../../_models/pagination"; import {SortEvent} from "../table/_directives/sortable-header.directive"; -import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; +import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular/forms"; import {translate, TranslocoModule} from "@jsverse/transloco"; import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; import {TranslocoLocaleModule} from "@jsverse/transloco-locale"; @@ -19,6 +27,7 @@ import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable"; import {AsyncPipe} from "@angular/common"; import {AccountService} from "../../_services/account.service"; import {ToastrService} from "ngx-toastr"; +import {SelectionModel} from "../../typeahead/_models/selection-model"; export interface DataTablePage { pageNumber: number, @@ -30,7 +39,7 @@ export interface DataTablePage { @Component({ selector: 'app-user-scrobble-history', imports: [ScrobbleEventTypePipe, ReactiveFormsModule, TranslocoModule, - DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe, NgbTooltip, NgxDatatableModule, AsyncPipe], + DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe, NgbTooltip, NgxDatatableModule, AsyncPipe, FormsModule], templateUrl: './user-scrobble-history.component.html', styleUrls: ['./user-scrobble-history.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -48,8 +57,6 @@ export class UserScrobbleHistoryComponent implements OnInit { private readonly toastr = inject(ToastrService); protected readonly accountService = inject(AccountService); - - tokenExpired = false; formGroup: FormGroup = new FormGroup({ 'filter': new FormControl('', []) @@ -68,6 +75,21 @@ export class UserScrobbleHistoryComponent implements OnInit { }; hasRunScrobbleGen: boolean = false; + selections: SelectionModel = new SelectionModel(); + selectAll: boolean = false; + isShiftDown: boolean = false; + lastSelectedIndex: number | null = null; + + @HostListener('document:keydown.shift', ['$event']) + handleKeypress(_: KeyboardEvent) { + this.isShiftDown = true; + } + + @HostListener('document:keyup.shift', ['$event']) + handleKeyUp(_: KeyboardEvent) { + this.isShiftDown = false; + } + ngOnInit() { this.pageInfo.pageNumber = 0; @@ -118,6 +140,7 @@ export class UserScrobbleHistoryComponent implements OnInit { .pipe(take(1)) .subscribe((result: PaginatedResult) => { this.events = result.result; + this.selections = new SelectionModel(false, this.events); this.pageInfo.totalPages = result.pagination.totalPages - 1; // ngx-datatable is 0 based, Kavita is 1 based this.pageInfo.size = result.pagination.itemsPerPage; @@ -143,4 +166,55 @@ export class UserScrobbleHistoryComponent implements OnInit { this.toastr.info(translate('toasts.scrobble-gen-init')) }); } + + bulkDelete() { + if (!this.selections.hasAnySelected()) { + return; + } + + const eventIds = this.selections.selected().map(e => e.id); + + this.scrobblingService.bulkRemoveEvents(eventIds).subscribe({ + next: () => { + this.events = this.events.filter(e => !eventIds.includes(e.id)); + this.selectAll = false; + this.selections.clearSelected(); + this.pageInfo.totalElements -= eventIds.length; + this.cdRef.markForCheck(); + }, + error: err => { + console.error(err); + } + }); + } + + toggleAll() { + this.selectAll = !this.selectAll; + this.events.forEach(e => this.selections.toggle(e, this.selectAll)); + this.cdRef.markForCheck(); + } + + handleSelection(item: ScrobbleEvent, index: number) { + if (this.isShiftDown && this.lastSelectedIndex !== null) { + // Bulk select items between the last selected item and the current one + const start = Math.min(this.lastSelectedIndex, index); + const end = Math.max(this.lastSelectedIndex, index); + + for (let i = start; i <= end; i++) { + const event = this.events[i]; + if (!this.selections.isSelected(event, (e1, e2) => e1.id == e2.id)) { + this.selections.toggle(event, true); + } + } + } else { + this.selections.toggle(item); + } + + this.lastSelectedIndex = index; + + + const numberOfSelected = this.selections.selected().length; + this.selectAll = numberOfSelected === this.events.length; + this.cdRef.markForCheck(); + } } diff --git a/UI/Web/src/app/admin/manage-scrobble-errors/manage-scrobble-errors.component.html b/UI/Web/src/app/admin/manage-scrobble-errors/manage-scrobble-errors.component.html index 78724272c..59a45873e 100644 --- a/UI/Web/src/app/admin/manage-scrobble-errors/manage-scrobble-errors.component.html +++ b/UI/Web/src/app/admin/manage-scrobble-errors/manage-scrobble-errors.component.html @@ -8,7 +8,7 @@
    - +
    diff --git a/UI/Web/src/app/typeahead/_models/selection-model.ts b/UI/Web/src/app/typeahead/_models/selection-model.ts index c4b2ab18a..8493a4eed 100644 --- a/UI/Web/src/app/typeahead/_models/selection-model.ts +++ b/UI/Web/src/app/typeahead/_models/selection-model.ts @@ -70,6 +70,28 @@ export class SelectionModel { return (selectedCount !== this._data.length && selectedCount !== 0) } + /** + * @return If at least one item is selected + */ + hasAnySelected(): boolean { + for (const d of this._data) { + if (d.selected) { + return true; + } + } + return false; + } + + /** + * Marks every data entry has not selected + */ + clearSelected() { + this._data = this._data.map(d => { + d.selected = false; + return d; + }); + } + /** * * @returns All Selected items diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 2a2d40c4f..91a3dac9e 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -42,6 +42,8 @@ "series-header": "Series", "data-header": "Data", "is-processed-header": "Is Processed", + "select-all-label": "Select all", + "delete-selected-label": "Delete selected", "no-data": "{{common.no-data}}", "volume-and-chapter-num": "Volume {{v}} Chapter {{n}}", "volume-num": "Volume {{num}}", From 225572732f44aadbe05b65d14be0c4e6b2cc88a1 Mon Sep 17 00:00:00 2001 From: majora2007 Date: Fri, 20 Jun 2025 19:10:12 +0000 Subject: [PATCH 10/30] Bump versions by dotnet-bump-version. --- Kavita.Common/Kavita.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 9b590f97c..081ab80ca 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -3,7 +3,7 @@ net9.0 kavitareader.com Kavita - 0.8.6.16 + 0.8.6.17 en true From fa8d778c8da2e9bad77336c5ac4c03eae43a3575 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 20 Jun 2025 19:11:24 +0000 Subject: [PATCH 11/30] Update OpenAPI documentation --- openapi.json | 55 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/openapi.json b/openapi.json index 209dfe2ef..5f50b88f7 100644 --- a/openapi.json +++ b/openapi.json @@ -2,12 +2,12 @@ "openapi": "3.0.4", "info": { "title": "Kavita", - "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.6.15", + "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.6.16", "license": { "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.8.6.15" + "version": "0.8.6.16" }, "servers": [ { @@ -10522,7 +10522,7 @@ "tags": [ "Scrobbling" ], - "summary": "Adds a hold against the Series for user's scrobbling", + "summary": "Remove a hold against the Series for user's scrobbling", "parameters": [ { "name": "seriesId", @@ -10571,6 +10571,51 @@ } } }, + "/api/Scrobbling/bulk-remove-events": { + "post": { + "tags": [ + "Scrobbling" + ], + "summary": "Delete the given scrobble events if they belong to that user", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + }, + "application/*+json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/Search/series-for-mangafile": { "get": { "tags": [ @@ -23505,6 +23550,10 @@ "ScrobbleEventDto": { "type": "object", "properties": { + "id": { + "type": "integer", + "format": "int64" + }, "seriesName": { "type": "string", "nullable": true From 36aa5f5c85cd49b13200c15f933f52548082a6e7 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Mon, 23 Jun 2025 18:57:14 -0500 Subject: [PATCH 12/30] Ability to turn off Metadata Parsing (#3872) --- API.Benchmark/API.Benchmark.csproj | 4 +- API.Tests/API.Tests.csproj | 6 +- API.Tests/Parsers/ComicVineParserTests.cs | 8 +- API.Tests/Parsers/DefaultParserTests.cs | 20 +- API.Tests/Parsers/ImageParserTests.cs | 6 +- API.Tests/Parsers/PdfParserTests.cs | 2 +- API.Tests/Parsing/ImageParsingTests.cs | 6 +- API.Tests/Parsing/MangaParsingTests.cs | 2 - API.Tests/Services/BookServiceTests.cs | 2 +- API.Tests/Services/CacheServiceTests.cs | 4 +- .../Services/ExternalMetadataServiceTests.cs | 2 +- API.Tests/Services/ParseScannedFilesTests.cs | 16 +- API.Tests/Services/ScannerServiceTests.cs | 38 +- ...es with Localized No Metadata - Manga.json | 5 + API/API.csproj | 38 +- API/Controllers/LibraryController.cs | 1 + .../ExternalMetadataIdsDto.cs | 2 +- .../ExternalMetadata/MatchSeriesRequestDto.cs | 2 +- .../SeriesDetailPlusApiDto.cs | 2 +- .../KavitaPlus/Metadata/ExternalChapterDto.cs | 1 + API/DTOs/LibraryDto.cs | 4 + API/DTOs/UpdateLibraryDto.cs | 2 + API/Data/DataContext.cs | 3 + ...20215058_EnableMetadataLibrary.Designer.cs | 3709 +++++++++++++++++ .../20250620215058_EnableMetadataLibrary.cs | 29 + .../Migrations/DataContextModelSnapshot.cs | 7 +- API/Entities/Library.cs | 4 + .../RestrictByLibraryExtensions.cs | 0 API/Helpers/Builders/LibraryBuilder.cs | 6 + API/Services/Plus/ExternalMetadataService.cs | 47 +- API/Services/Plus/KavitaPlusApiService.cs | 53 +- API/Services/ReadingItemService.cs | 20 +- .../Tasks/Scanner/ParseScannedFiles.cs | 4 +- .../Tasks/Scanner/Parser/BasicParser.cs | 11 +- .../Tasks/Scanner/Parser/BookParser.cs | 4 +- .../Tasks/Scanner/Parser/ComicVineParser.cs | 7 +- .../Tasks/Scanner/Parser/DefaultParser.cs | 5 +- .../Tasks/Scanner/Parser/ImageParser.cs | 2 +- API/Services/Tasks/Scanner/Parser/Parser.cs | 4 +- .../Tasks/Scanner/Parser/PdfParser.cs | 16 +- API/Services/Tasks/ScannerService.cs | 5 + API/SignalR/MessageFactory.cs | 16 + Kavita.Common/Configuration.cs | 2 +- Kavita.Common/Kavita.Common.csproj | 8 +- UI/Web/src/_tag-card-common.scss | 5 + UI/Web/src/app/_helpers/form-debug.ts | 120 + .../external-match-rate-limit-error-event.ts | 4 + UI/Web/src/app/_models/library/library.ts | 1 + .../src/app/_services/message-hub.service.ts | 30 +- UI/Web/src/app/_services/reader.service.ts | 8 +- .../manage-matched-metadata.component.ts | 22 +- .../browse-genres.component.html | 2 +- .../browse-genres/browse-genres.component.ts | 10 +- .../browse-tags/browse-tags.component.html | 2 +- .../browse-tags/browse-tags.component.ts | 10 +- .../reading-list-detail.component.html | 9 +- .../reading-list-detail.component.ts | 10 +- .../reading-list-item.component.html | 8 +- .../reading-list-item.component.ts | 10 +- .../external-rating.component.ts | 5 +- .../library-settings-modal.component.html | 10 + .../library-settings-modal.component.ts | 37 +- UI/Web/src/assets/langs/en.json | 5 +- 63 files changed, 4257 insertions(+), 186 deletions(-) create mode 100644 API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json create mode 100644 API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs create mode 100644 API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs create mode 100644 API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs create mode 100644 UI/Web/src/app/_helpers/form-debug.ts create mode 100644 UI/Web/src/app/_models/events/external-match-rate-limit-error-event.ts diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj index d6fd4eb9f..ec9c1884f 100644 --- a/API.Benchmark/API.Benchmark.csproj +++ b/API.Benchmark/API.Benchmark.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index 73b886e13..a571a6e72 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -6,13 +6,13 @@ - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/API.Tests/Parsers/ComicVineParserTests.cs b/API.Tests/Parsers/ComicVineParserTests.cs index f01e98afd..2f4fd568e 100644 --- a/API.Tests/Parsers/ComicVineParserTests.cs +++ b/API.Tests/Parsers/ComicVineParserTests.cs @@ -36,7 +36,7 @@ public class ComicVineParserTests public void Parse_SeriesWithComicInfo() { var actual = _parser.Parse("C:/Comics/Birds of Prey (2002)/Birds of Prey 001 (2002).cbz", "C:/Comics/Birds of Prey (2002)/", - RootDirectory, LibraryType.ComicVine, new ComicInfo() + RootDirectory, LibraryType.ComicVine, true, new ComicInfo() { Series = "Birds of Prey", Volume = "2002" @@ -54,7 +54,7 @@ public class ComicVineParserTests public void Parse_SeriesWithDirectoryNameAsSeriesYear() { var actual = _parser.Parse("C:/Comics/Birds of Prey (2002)/Birds of Prey 001 (2002).cbz", "C:/Comics/Birds of Prey (2002)/", - RootDirectory, LibraryType.ComicVine, null); + RootDirectory, LibraryType.ComicVine, true, null); Assert.NotNull(actual); Assert.Equal("Birds of Prey (2002)", actual.Series); @@ -69,7 +69,7 @@ public class ComicVineParserTests public void Parse_SeriesWithADirectoryNameAsSeriesYear() { var actual = _parser.Parse("C:/Comics/DC Comics/Birds of Prey (1999)/Birds of Prey 001 (1999).cbz", "C:/Comics/DC Comics/", - RootDirectory, LibraryType.ComicVine, null); + RootDirectory, LibraryType.ComicVine, true, null); Assert.NotNull(actual); Assert.Equal("Birds of Prey (1999)", actual.Series); @@ -84,7 +84,7 @@ public class ComicVineParserTests public void Parse_FallbackToDirectoryNameOnly() { var actual = _parser.Parse("C:/Comics/DC Comics/Blood Syndicate/Blood Syndicate 001 (1999).cbz", "C:/Comics/DC Comics/", - RootDirectory, LibraryType.ComicVine, null); + RootDirectory, LibraryType.ComicVine, true, null); Assert.NotNull(actual); Assert.Equal("Blood Syndicate", actual.Series); diff --git a/API.Tests/Parsers/DefaultParserTests.cs b/API.Tests/Parsers/DefaultParserTests.cs index 733b55d62..244c08b97 100644 --- a/API.Tests/Parsers/DefaultParserTests.cs +++ b/API.Tests/Parsers/DefaultParserTests.cs @@ -33,7 +33,7 @@ public class DefaultParserTests [InlineData("C:/", "C:/Something Random/Mujaki no Rakuen SP01.cbz", "Something Random")] public void ParseFromFallbackFolders_FallbackShouldParseSeries(string rootDir, string inputPath, string expectedSeries) { - var actual = _defaultParser.Parse(inputPath, rootDir, rootDir, LibraryType.Manga, null); + var actual = _defaultParser.Parse(inputPath, rootDir, rootDir, LibraryType.Manga, true, null); if (actual == null) { Assert.NotNull(actual); @@ -74,7 +74,7 @@ public class DefaultParserTests fs.AddFile(inputFile, new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), fs); var parser = new BasicParser(ds, new ImageParser(ds)); - var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, null); + var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, true, null); _defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual); Assert.Equal(expectedParseInfo, actual.Series); } @@ -90,7 +90,7 @@ public class DefaultParserTests fs.AddFile(inputFile, new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), fs); var parser = new BasicParser(ds, new ImageParser(ds)); - var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, null); + var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, true, null); _defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual); Assert.Equal(expectedParseInfo, actual.Series); } @@ -251,7 +251,7 @@ public class DefaultParserTests foreach (var file in expected.Keys) { var expectedInfo = expected[file]; - var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Manga, null); + var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Manga, true, null); if (expectedInfo == null) { Assert.Null(actual); @@ -289,7 +289,7 @@ public class DefaultParserTests Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image, FullFilePath = filepath, IsSpecial = false }; - var actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Monster #8", "E:/Manga", LibraryType.Manga, null); + var actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Monster #8", "E:/Manga", LibraryType.Manga, true, null); Assert.NotNull(actual2); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expectedInfo2.Format, actual2.Format); @@ -315,7 +315,7 @@ public class DefaultParserTests FullFilePath = filepath, IsSpecial = false }; - actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga",LibraryType.Manga, null); + actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga",LibraryType.Manga, true, null); Assert.NotNull(actual2); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expectedInfo2.Format, actual2.Format); @@ -341,7 +341,7 @@ public class DefaultParserTests FullFilePath = filepath, IsSpecial = false }; - actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga", LibraryType.Manga, null); + actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga", LibraryType.Manga, true, null); Assert.NotNull(actual2); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expectedInfo2.Format, actual2.Format); @@ -383,7 +383,7 @@ public class DefaultParserTests FullFilePath = filepath }; - var actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, null); + var actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, true, null); Assert.NotNull(actual); _testOutputHelper.WriteLine($"Validating {filepath}"); @@ -412,7 +412,7 @@ public class DefaultParserTests FullFilePath = filepath }; - actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, null); + actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, true, null); Assert.NotNull(actual); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expected.Format, actual.Format); @@ -475,7 +475,7 @@ public class DefaultParserTests foreach (var file in expected.Keys) { var expectedInfo = expected[file]; - var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Comic, null); + var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Comic, true, null); if (expectedInfo == null) { Assert.Null(actual); diff --git a/API.Tests/Parsers/ImageParserTests.cs b/API.Tests/Parsers/ImageParserTests.cs index f95c98ddf..63df1926e 100644 --- a/API.Tests/Parsers/ImageParserTests.cs +++ b/API.Tests/Parsers/ImageParserTests.cs @@ -34,7 +34,7 @@ public class ImageParserTests public void Parse_SeriesWithDirectoryName() { var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01/01.jpg", "C:/Comics/Birds of Prey/", - RootDirectory, LibraryType.Image, null); + RootDirectory, LibraryType.Image, true, null); Assert.NotNull(actual); Assert.Equal("Birds of Prey", actual.Series); @@ -48,7 +48,7 @@ public class ImageParserTests public void Parse_SeriesWithNoNestedChapter() { var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01 page 01.jpg", "C:/Comics/", - RootDirectory, LibraryType.Image, null); + RootDirectory, LibraryType.Image, true, null); Assert.NotNull(actual); Assert.Equal("Birds of Prey", actual.Series); @@ -62,7 +62,7 @@ public class ImageParserTests public void Parse_SeriesWithLooseImages() { var actual = _parser.Parse("C:/Comics/Birds of Prey/page 01.jpg", "C:/Comics/", - RootDirectory, LibraryType.Image, null); + RootDirectory, LibraryType.Image, true, null); Assert.NotNull(actual); Assert.Equal("Birds of Prey", actual.Series); diff --git a/API.Tests/Parsers/PdfParserTests.cs b/API.Tests/Parsers/PdfParserTests.cs index 72088526d..08bf9f25d 100644 --- a/API.Tests/Parsers/PdfParserTests.cs +++ b/API.Tests/Parsers/PdfParserTests.cs @@ -35,7 +35,7 @@ public class PdfParserTests { var actual = _parser.Parse("C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/A Dictionary of Japanese Food - Ingredients and Culture.pdf", "C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/", - RootDirectory, LibraryType.Book, null); + RootDirectory, LibraryType.Book, true, null); Assert.NotNull(actual); Assert.Equal("A Dictionary of Japanese Food - Ingredients and Culture", actual.Series); diff --git a/API.Tests/Parsing/ImageParsingTests.cs b/API.Tests/Parsing/ImageParsingTests.cs index 3d78d9372..362b4b08c 100644 --- a/API.Tests/Parsing/ImageParsingTests.cs +++ b/API.Tests/Parsing/ImageParsingTests.cs @@ -34,7 +34,7 @@ public class ImageParsingTests Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image, FullFilePath = filepath, IsSpecial = false }; - var actual2 = _parser.Parse(filepath, @"E:\Manga\Monster #8", "E:/Manga", LibraryType.Image, null); + var actual2 = _parser.Parse(filepath, @"E:\Manga\Monster #8", "E:/Manga", LibraryType.Image, true, null); Assert.NotNull(actual2); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expectedInfo2.Format, actual2.Format); @@ -60,7 +60,7 @@ public class ImageParsingTests FullFilePath = filepath, IsSpecial = false }; - actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, null); + actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, true, null); Assert.NotNull(actual2); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expectedInfo2.Format, actual2.Format); @@ -86,7 +86,7 @@ public class ImageParsingTests FullFilePath = filepath, IsSpecial = false }; - actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, null); + actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, true, null); Assert.NotNull(actual2); _testOutputHelper.WriteLine($"Validating {filepath}"); Assert.Equal(expectedInfo2.Format, actual2.Format); diff --git a/API.Tests/Parsing/MangaParsingTests.cs b/API.Tests/Parsing/MangaParsingTests.cs index 8b93c5f90..53f2bc4c9 100644 --- a/API.Tests/Parsing/MangaParsingTests.cs +++ b/API.Tests/Parsing/MangaParsingTests.cs @@ -68,10 +68,8 @@ public class MangaParsingTests [InlineData("Манга Тома 1-4", "1-4")] [InlineData("Манга Том 1-4", "1-4")] [InlineData("조선왕조실톡 106화", "106")] - [InlineData("죽음 13회", "13")] [InlineData("동의보감 13장", "13")] [InlineData("몰?루 아카이브 7.5권", "7.5")] - [InlineData("주술회전 1.5권", "1.5")] [InlineData("63권#200", "63")] [InlineData("시즌34삽화2", "34")] [InlineData("Accel World Chapter 001 Volume 002", "2")] diff --git a/API.Tests/Services/BookServiceTests.cs b/API.Tests/Services/BookServiceTests.cs index a80c1ca01..5848c74ba 100644 --- a/API.Tests/Services/BookServiceTests.cs +++ b/API.Tests/Services/BookServiceTests.cs @@ -137,7 +137,7 @@ public class BookServiceTests var comicInfo = _bookService.GetComicInfo(filePath); Assert.NotNull(comicInfo); - var parserInfo = pdfParser.Parse(filePath, testDirectory, ds.GetParentDirectoryName(testDirectory), LibraryType.Book, comicInfo); + var parserInfo = pdfParser.Parse(filePath, testDirectory, ds.GetParentDirectoryName(testDirectory), LibraryType.Book, true, comicInfo); Assert.NotNull(parserInfo); Assert.Equal(parserInfo.Title, comicInfo.Title); Assert.Equal(parserInfo.Series, comicInfo.Title); diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index 5c1752cd8..caf1ae393 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -50,12 +50,12 @@ internal class MockReadingItemServiceForCacheService : IReadingItemService throw new System.NotImplementedException(); } - public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type) + public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true) { throw new System.NotImplementedException(); } - public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type) + public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true) { throw new System.NotImplementedException(); } diff --git a/API.Tests/Services/ExternalMetadataServiceTests.cs b/API.Tests/Services/ExternalMetadataServiceTests.cs index 833e8fe5f..8278f3b1a 100644 --- a/API.Tests/Services/ExternalMetadataServiceTests.cs +++ b/API.Tests/Services/ExternalMetadataServiceTests.cs @@ -42,7 +42,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest _externalMetadataService = new ExternalMetadataService(UnitOfWork, Substitute.For>(), Mapper, Substitute.For(), Substitute.For(), Substitute.For(), - Substitute.For()); + Substitute.For(), Substitute.For()); } #region Gloabl diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/API.Tests/Services/ParseScannedFilesTests.cs index f8714f69a..a732b2526 100644 --- a/API.Tests/Services/ParseScannedFilesTests.cs +++ b/API.Tests/Services/ParseScannedFilesTests.cs @@ -58,35 +58,35 @@ public class MockReadingItemService : IReadingItemService throw new NotImplementedException(); } - public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type) + public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) { if (_comicVineParser.IsApplicable(path, type)) { - return _comicVineParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _comicVineParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_imageParser.IsApplicable(path, type)) { - return _imageParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _imageParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_bookParser.IsApplicable(path, type)) { - return _bookParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _bookParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_pdfParser.IsApplicable(path, type)) { - return _pdfParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _pdfParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_basicParser.IsApplicable(path, type)) { - return _basicParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _basicParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } return null; } - public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type) + public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) { - return Parse(path, rootPath, libraryRoot, type); + return Parse(path, rootPath, libraryRoot, type, enableMetadata); } } diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 2e812647b..acc0345b1 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -483,7 +483,7 @@ public class ScannerServiceTests : AbstractDbTest var infos = new Dictionary(); var library = await _scannerHelper.GenerateScannerData(testcase, infos); - library.LibraryExcludePatterns = [new LibraryExcludePattern() {Pattern = "**/Extra/*"}]; + library.LibraryExcludePatterns = [new LibraryExcludePattern() { Pattern = "**/Extra/*" }]; UnitOfWork.LibraryRepository.Update(library); await UnitOfWork.CommitAsync(); @@ -507,7 +507,7 @@ public class ScannerServiceTests : AbstractDbTest var infos = new Dictionary(); var library = await _scannerHelper.GenerateScannerData(testcase, infos); - library.LibraryExcludePatterns = [new LibraryExcludePattern() {Pattern = "**\\Extra\\*"}]; + library.LibraryExcludePatterns = [new LibraryExcludePattern() { Pattern = "**\\Extra\\*" }]; UnitOfWork.LibraryRepository.Update(library); await UnitOfWork.CommitAsync(); @@ -938,4 +938,38 @@ public class ScannerServiceTests : AbstractDbTest Assert.True(sortedChapters[1].SortOrder.Is(4f)); Assert.True(sortedChapters[2].SortOrder.Is(5f)); } + + + [Fact] + public async Task ScanLibrary_MetadataDisabled_NoOverrides() + { + const string testcase = "Series with Localized No Metadata - Manga.json"; + + // Get the first file and generate a ComicInfo + var infos = new Dictionary(); + infos.Add("Immoral Guild v01.cbz", new ComicInfo() + { + Series = "Immoral Guild", + LocalizedSeries = "Futoku no Guild" // Filename has a capital N and localizedSeries has lowercase + }); + + var library = await _scannerHelper.GenerateScannerData(testcase, infos); + + // Disable metadata + library.EnableMetadata = false; + UnitOfWork.LibraryRepository.Update(library); + await UnitOfWork.CommitAsync(); + + var scanner = _scannerHelper.CreateServices(); + await scanner.ScanLibrary(library.Id); + + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + + // Validate that there are 2 series + Assert.NotNull(postLib); + Assert.Equal(2, postLib.Series.Count); + + Assert.Contains(postLib.Series, x => x.Name == "Immoral Guild"); + Assert.Contains(postLib.Series, x => x.Name == "Futoku No Guild"); + } } diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json new file mode 100644 index 000000000..d6e91183b --- /dev/null +++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json @@ -0,0 +1,5 @@ +[ + "Immoral Guild/Immoral Guild v01.cbz", + "Immoral Guild/Immoral Guild v02.cbz", + "Immoral Guild/Futoku No Guild - Vol. 12 Ch. 67 - Take Responsibility.cbz" +] diff --git a/API/API.csproj b/API/API.csproj index 4eed66f22..a7d1177dc 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -50,9 +50,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -62,25 +62,25 @@ - + - + - - - - - + + + + + - - - + + + @@ -89,16 +89,16 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 2f12aa1fe..c09011b77 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -623,6 +623,7 @@ public class LibraryController : BaseApiController library.ManageReadingLists = dto.ManageReadingLists; library.AllowScrobbling = dto.AllowScrobbling; library.AllowMetadataMatching = dto.AllowMetadataMatching; + library.EnableMetadata = dto.EnableMetadata; library.LibraryFileTypes = dto.FileGroupTypes .Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id}) .Distinct() diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs index 2b7dea8e6..c05ff0567 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs @@ -6,7 +6,7 @@ namespace API.DTOs.KavitaPlus.ExternalMetadata; /// /// Used for matching and fetching metadata on a series /// -internal sealed record ExternalMetadataIdsDto +public sealed record ExternalMetadataIdsDto { public long? MalId { get; set; } public int? AniListId { get; set; } diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs index fae674ded..a7359d69b 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs @@ -7,7 +7,7 @@ namespace API.DTOs.KavitaPlus.ExternalMetadata; /// /// Represents a request to match some series from Kavita to an external id which K+ uses. /// -internal sealed record MatchSeriesRequestDto +public sealed record MatchSeriesRequestDto { public required string SeriesName { get; set; } public ICollection AlternativeNames { get; set; } = []; diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs index d0cbb7bd3..84e9bbf3e 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs @@ -6,7 +6,7 @@ using API.DTOs.SeriesDetail; namespace API.DTOs.KavitaPlus.ExternalMetadata; -internal sealed record SeriesDetailPlusApiDto +public sealed record SeriesDetailPlusApiDto { public IEnumerable Recommendations { get; set; } public IEnumerable Reviews { get; set; } diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs b/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs index 1dcd8494c..add9ca723 100644 --- a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs +++ b/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using API.DTOs.SeriesDetail; namespace API.DTOs.KavitaPlus.Metadata; +#nullable enable /// /// Information about an individual issue/chapter/book from Kavita+ diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs index 8ba687346..7b38379c9 100644 --- a/API/DTOs/LibraryDto.cs +++ b/API/DTOs/LibraryDto.cs @@ -66,4 +66,8 @@ public sealed record LibraryDto /// This does not exclude the library from being linked to wrt Series Relationships /// Requires a valid LicenseKey public bool AllowMetadataMatching { get; set; } = true; + /// + /// Allow Kavita to read metadata (ComicInfo.xml, Epub, PDF) + /// + public bool EnableMetadata { get; set; } = true; } diff --git a/API/DTOs/UpdateLibraryDto.cs b/API/DTOs/UpdateLibraryDto.cs index 9bd47fd39..68d2417ec 100644 --- a/API/DTOs/UpdateLibraryDto.cs +++ b/API/DTOs/UpdateLibraryDto.cs @@ -28,6 +28,8 @@ public sealed record UpdateLibraryDto public bool AllowScrobbling { get; init; } [Required] public bool AllowMetadataMatching { get; init; } + [Required] + public bool EnableMetadata { get; init; } /// /// What types of files to allow the scanner to pickup /// diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 3bbf45e23..aa8b67283 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -147,6 +147,9 @@ public sealed class DataContext : IdentityDbContext() .Property(b => b.AllowMetadataMatching) .HasDefaultValue(true); + builder.Entity() + .Property(b => b.EnableMetadata) + .HasDefaultValue(true); builder.Entity() .Property(b => b.WebLinks) diff --git a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs b/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs new file mode 100644 index 000000000..c15f9f77b --- /dev/null +++ b/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs @@ -0,0 +1,3709 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250620215058_EnableMetadataLibrary")] + partial class EnableMetadataLibrary + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("HasRunScrobbleEventGeneration") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("MalAccessToken") + .HasColumnType("TEXT"); + + b.Property("MalUserName") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventGenerationRan") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserChapterRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("MissingSeriesFromSource") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("SourceUrl") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TotalSourceCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserCollection"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(4); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserDashboardStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Host") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserExternalSource"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserOnDeckRemoval"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAutomaticWebtoonReaderDetection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AniListScrobblingEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BlurUnreadSummaries") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("CollapseSeriesRelationships") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("en"); + + b.Property("NoTransitions") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PdfScrollMode") + .HasColumnType("INTEGER"); + + b.Property("PdfSpreadMode") + .HasColumnType("INTEGER"); + + b.Property("PdfTheme") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.Property("WantToReadSync") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAutomaticWebtoonReaderDetection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("DisableWidthOverride") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("LibraryIds") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PdfScrollMode") + .HasColumnType("INTEGER"); + + b.Property("PdfSpreadMode") + .HasColumnType("INTEGER"); + + b.Property("PdfTheme") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("SeriesIds") + .HasColumnType("TEXT"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("WidthOverride") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserReadingProfiles"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserWantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AverageExternalRating") + .HasColumnType("REAL"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("ISBNLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ReleaseDateLocked") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("REAL"); + + b.Property("SortOrderLocked") + .HasColumnType("INTEGER"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TitleNameLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DeliveryStatus") + .HasColumnType("TEXT"); + + b.Property("EmailTemplate") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SendDate") + .HasColumnType("TEXT"); + + b.Property("Sent") + .HasColumnType("INTEGER"); + + b.Property("Subject") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); + + b.ToTable("EmailHistory"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("RanAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ManualMigrationHistory"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowMetadataMatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EnableMetadata") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Pattern") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryExcludePattern"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FileTypeGroup") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryFileTypeGroup"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bytes") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("KoreaderHash") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("AverageScore") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ProviderUrl") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalRating"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("CoverUrl") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("ExternalRecommendation"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("RawBody") + .HasColumnType("TEXT"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("SiteUrl") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalReview"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AverageExternalRating") + .HasColumnType("INTEGER"); + + b.Property("CbrId") + .HasColumnType("INTEGER"); + + b.Property("GoogleBooksId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("ValidUntilUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.ToTable("ExternalSeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastChecked") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("SeriesBlacklist"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DestinationType") + .HasColumnType("INTEGER"); + + b.Property("DestinationValue") + .HasColumnType("TEXT"); + + b.Property("ExcludeFromSource") + .HasColumnType("INTEGER"); + + b.Property("MetadataSettingsId") + .HasColumnType("INTEGER"); + + b.Property("SourceType") + .HasColumnType("INTEGER"); + + b.Property("SourceValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MetadataSettingsId"); + + b.ToTable("MetadataFieldMapping"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRatingMappings") + .HasColumnType("TEXT"); + + b.Property("Blacklist") + .HasColumnType("TEXT"); + + b.Property("EnableChapterCoverImage") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterPublisher") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterReleaseDate") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterTitle") + .HasColumnType("INTEGER"); + + b.Property("EnableCoverImage") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("EnableGenres") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalizedName") + .HasColumnType("INTEGER"); + + b.Property("EnablePeople") + .HasColumnType("INTEGER"); + + b.Property("EnablePublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("EnableRelationships") + .HasColumnType("INTEGER"); + + b.Property("EnableStartDate") + .HasColumnType("INTEGER"); + + b.Property("EnableSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableTags") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("FirstLastPeopleNaming") + .HasColumnType("INTEGER"); + + b.Property("Overrides") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("PersonRoles") + .HasColumnType("TEXT"); + + b.Property("Whitelist") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("ChapterPeople"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("Asin") + .HasColumnType("TEXT"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("HardcoverId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Alias") + .HasColumnType("TEXT"); + + b.Property("NormalizedAlias") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PersonId"); + + b.ToTable("PersonAlias"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.Property("SeriesMetadataId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.HasKey("SeriesMetadataId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ErrorDetails") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsErrored") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DontMatch") + .HasColumnType("INTEGER"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsBlacklisted") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("LowestFolderPath") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("CompatibleVersion") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("GitHubPath") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PreviewUrls") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ShaHash") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LookupName") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.Property("CollectionsId") + .HasColumnType("INTEGER"); + + b.Property("ItemsId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionsId", "ItemsId"); + + b.HasIndex("ItemsId"); + + b.ToTable("AppUserCollectionSeries"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.Property("ExternalRatingsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRatingExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.Property("ExternalRecommendationsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRecommendationExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.Property("ExternalReviewsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalReviewExternalSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ChapterRatings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Ratings") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Collections") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingProfiles") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryExcludePatterns") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryFileTypes") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalRatings") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalReviews") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("ExternalSeriesMetadata") + .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") + .WithMany("FieldMappings") + .HasForeignKey("MetadataSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("People") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("ChapterPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("Aliases") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("SeriesMetadataPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") + .WithMany("People") + .HasForeignKey("SeriesMetadataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + + b.Navigation("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.HasOne("API.Entities.AppUserCollection", null) + .WithMany() + .HasForeignKey("CollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRating", null) + .WithMany() + .HasForeignKey("ExternalRatingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) + .WithMany() + .HasForeignKey("ExternalRecommendationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("ChapterRatings"); + + b.Navigation("Collections"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ReadingProfiles"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("ExternalRatings"); + + b.Navigation("ExternalReviews"); + + b.Navigation("Files"); + + b.Navigation("People"); + + b.Navigation("Ratings"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("LibraryExcludePatterns"); + + b.Navigation("LibraryFileTypes"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Navigation("FieldMappings"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Navigation("Aliases"); + + b.Navigation("ChapterPeople"); + + b.Navigation("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("ExternalSeriesMetadata"); + + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("RelationOf"); + + b.Navigation("Relations"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs b/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs new file mode 100644 index 000000000..f9e38c01d --- /dev/null +++ b/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class EnableMetadataLibrary : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EnableMetadata", + table: "Library", + type: "INTEGER", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EnableMetadata", + table: "Library"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index c9fb953df..eb786865b 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace API.Data.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); modelBuilder.Entity("API.Entities.AppRole", b => { @@ -1296,6 +1296,11 @@ namespace API.Data.Migrations b.Property("CreatedUtc") .HasColumnType("TEXT"); + b.Property("EnableMetadata") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + b.Property("FolderWatching") .HasColumnType("INTEGER"); diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index abab81378..8dc386298 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -48,6 +48,10 @@ public class Library : IEntityDate, IHasCoverImage /// This does not exclude the library from being linked to wrt Series Relationships /// Requires a valid LicenseKey public bool AllowMetadataMatching { get; set; } = true; + /// + /// Should Kavita read metadata files from the library + /// + public bool EnableMetadata { get; set; } = true; public DateTime Created { get; set; } diff --git a/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs b/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs new file mode 100644 index 000000000..e69de29bb diff --git a/API/Helpers/Builders/LibraryBuilder.cs b/API/Helpers/Builders/LibraryBuilder.cs index 30e6136a5..950c5d3d2 100644 --- a/API/Helpers/Builders/LibraryBuilder.cs +++ b/API/Helpers/Builders/LibraryBuilder.cs @@ -110,6 +110,12 @@ public class LibraryBuilder : IEntityBuilder return this; } + public LibraryBuilder WithEnableMetadata(bool enable) + { + _library.EnableMetadata = enable; + return this; + } + public LibraryBuilder WithAllowScrobbling(bool allowScrobbling) { _library.AllowScrobbling = allowScrobbling; diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 435727bda..1db334b91 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -67,6 +67,7 @@ public class ExternalMetadataService : IExternalMetadataService private readonly IScrobblingService _scrobblingService; private readonly IEventHub _eventHub; private readonly ICoverDbService _coverDbService; + private readonly IKavitaPlusApiService _kavitaPlusApiService; private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(30); public static readonly HashSet NonEligibleLibraryTypes = [LibraryType.Comic, LibraryType.Book, LibraryType.Image]; @@ -82,7 +83,8 @@ public class ExternalMetadataService : IExternalMetadataService private static bool IsRomanCharacters(string input) => Regex.IsMatch(input, @"^[\p{IsBasicLatin}\p{IsLatin-1Supplement}]+$"); public ExternalMetadataService(IUnitOfWork unitOfWork, ILogger logger, IMapper mapper, - ILicenseService licenseService, IScrobblingService scrobblingService, IEventHub eventHub, ICoverDbService coverDbService) + ILicenseService licenseService, IScrobblingService scrobblingService, IEventHub eventHub, ICoverDbService coverDbService, + IKavitaPlusApiService kavitaPlusApiService) { _unitOfWork = unitOfWork; _logger = logger; @@ -91,6 +93,7 @@ public class ExternalMetadataService : IExternalMetadataService _scrobblingService = scrobblingService; _eventHub = eventHub; _coverDbService = coverDbService; + _kavitaPlusApiService = kavitaPlusApiService; FlurlConfiguration.ConfigureClientForUrl(Configuration.KavitaPlusApiUrl); } @@ -179,9 +182,7 @@ public class ExternalMetadataService : IExternalMetadataService _logger.LogDebug("Fetching Kavita+ for MAL Stacks for user {UserName}", user.MalUserName); var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var result = await ($"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/stacks?username={user.MalUserName}") - .WithKavitaPlusHeaders(license) - .GetJsonAsync>(); + var result = await _kavitaPlusApiService.GetMalStacks(user.MalUserName, license); if (result == null) { @@ -207,7 +208,7 @@ public class ExternalMetadataService : IExternalMetadataService /// public async Task> MatchSeries(MatchSeriesDto dto) { - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata | SeriesIncludes.Library); if (series == null) return []; @@ -239,14 +240,9 @@ public class ExternalMetadataService : IExternalMetadataService MalId = potentialMalId ?? ScrobblingService.GetMalId(series) }; - var token = (await _unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; - try { - var results = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/match-series") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(matchRequest) - .ReceiveJson>(); + var results = await _kavitaPlusApiService.MatchSeries(matchRequest); // Some summaries can contain multiple
    s, we need to ensure it's only 1 foreach (var result in results) @@ -287,9 +283,7 @@ public class ExternalMetadataService : IExternalMetadataService } // This is for the Series drawer. We can get this extra information during the initial SeriesDetail call so it's all coming from the DB - - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var details = await GetSeriesDetail(license, aniListId, malId, seriesId); + var details = await GetSeriesDetail(aniListId, malId, seriesId); return details; @@ -392,6 +386,9 @@ public class ExternalMetadataService : IExternalMetadataService { // We can't rethrow because Fix match is done in a background thread and Hangfire will requeue multiple times _logger.LogInformation(ex, "Rate limit hit for matching {SeriesName} with Kavita+", series.Name); + // Fire SignalR event about this + await _eventHub.SendMessageAsync(MessageFactory.ExternalMatchRateLimitError, + MessageFactory.ExternalMatchRateLimitErrorEvent(series.Id, series.Name)); } } @@ -442,16 +439,12 @@ public class ExternalMetadataService : IExternalMetadataService try { _logger.LogDebug("Fetching Kavita+ Series Detail data for {SeriesName}", string.IsNullOrEmpty(data.SeriesName) ? data.AniListId : data.SeriesName); - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var token = (await _unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; SeriesDetailPlusApiDto? result = null; try { - result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(data) - .ReceiveJson(); // This returns an AniListSeries and Match returns ExternalSeriesDto + // This returns an AniListSeries and Match returns ExternalSeriesDto + result = await _kavitaPlusApiService.GetSeriesDetail(data); } catch (FlurlHttpException ex) { @@ -466,11 +459,7 @@ public class ExternalMetadataService : IExternalMetadataService _logger.LogDebug("Hit rate limit, will retry in 3 seconds"); await Task.Delay(3000); - result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(data) - .ReceiveJson< - SeriesDetailPlusApiDto>(); + result = await _kavitaPlusApiService.GetSeriesDetail(data); } else if (errorMessage.Contains("Unknown Series")) { @@ -1777,7 +1766,7 @@ public class ExternalMetadataService : IExternalMetadataService /// /// /// - private async Task GetSeriesDetail(string license, int? aniListId, long? malId, int? seriesId) + private async Task GetSeriesDetail(int? aniListId, long? malId, int? seriesId) { var payload = new ExternalMetadataIdsDto() { @@ -1809,11 +1798,7 @@ public class ExternalMetadataService : IExternalMetadataService } try { - var token = (await _unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; - var ret = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids") - .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(payload) - .ReceiveJson(); + var ret = await _kavitaPlusApiService.GetSeriesDetailById(payload); ret.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(ret.Summary)); diff --git a/API/Services/Plus/KavitaPlusApiService.cs b/API/Services/Plus/KavitaPlusApiService.cs index cdf9471f8..ec4f414c3 100644 --- a/API/Services/Plus/KavitaPlusApiService.cs +++ b/API/Services/Plus/KavitaPlusApiService.cs @@ -1,6 +1,13 @@ #nullable enable +using System.Collections.Generic; using System.Threading.Tasks; +using API.Data; +using API.DTOs.Collection; +using API.DTOs.KavitaPlus.ExternalMetadata; +using API.DTOs.KavitaPlus.Metadata; +using API.DTOs.Metadata.Matching; using API.DTOs.Scrobbling; +using API.Entities.Enums; using API.Extensions; using Flurl.Http; using Kavita.Common; @@ -17,9 +24,13 @@ public interface IKavitaPlusApiService Task HasTokenExpired(string license, string token, ScrobbleProvider provider); Task GetRateLimit(string license, string token); Task PostScrobbleUpdate(ScrobbleDto data, string license); + Task> GetMalStacks(string malUsername, string license); + Task> MatchSeries(MatchSeriesRequestDto request); + Task GetSeriesDetail(PlusSeriesRequestDto request); + Task GetSeriesDetailById(ExternalMetadataIdsDto request); } -public class KavitaPlusApiService(ILogger logger): IKavitaPlusApiService +public class KavitaPlusApiService(ILogger logger, IUnitOfWork unitOfWork): IKavitaPlusApiService { private const string ScrobblingPath = "/api/scrobbling/"; @@ -42,6 +53,46 @@ public class KavitaPlusApiService(ILogger logger): IKavita return await PostAndReceive(ScrobblingPath + "update", data, license); } + public async Task> GetMalStacks(string malUsername, string license) + { + return await $"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/stacks?username={malUsername}" + .WithKavitaPlusHeaders(license) + .GetJsonAsync>(); + } + + public async Task> MatchSeries(MatchSeriesRequestDto request) + { + var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; + var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; + + return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/match-series") + .WithKavitaPlusHeaders(license, token) + .PostJsonAsync(request) + .ReceiveJson>(); + } + + public async Task GetSeriesDetail(PlusSeriesRequestDto request) + { + var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; + var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; + + return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") + .WithKavitaPlusHeaders(license, token) + .PostJsonAsync(request) + .ReceiveJson(); + } + + public async Task GetSeriesDetailById(ExternalMetadataIdsDto request) + { + var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; + var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; + + return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids") + .WithKavitaPlusHeaders(license, token) + .PostJsonAsync(request) + .ReceiveJson(); + } + /// /// Send a GET request to K+ /// diff --git a/API/Services/ReadingItemService.cs b/API/Services/ReadingItemService.cs index efdaec8ff..6ff8d19de 100644 --- a/API/Services/ReadingItemService.cs +++ b/API/Services/ReadingItemService.cs @@ -12,7 +12,7 @@ public interface IReadingItemService int GetNumberOfPages(string filePath, MangaFormat format); string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1); - ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type); + ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata); } public class ReadingItemService : IReadingItemService @@ -71,11 +71,12 @@ public class ReadingItemService : IReadingItemService /// Path of a file /// /// Library type to determine parsing to perform - public ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type) + /// Enable Metadata parsing overriding filename parsing + public ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) { try { - var info = Parse(path, rootPath, libraryRoot, type); + var info = Parse(path, rootPath, libraryRoot, type, enableMetadata); if (info == null) { _logger.LogError("Unable to parse any meaningful information out of file {FilePath}", path); @@ -174,28 +175,29 @@ public class ReadingItemService : IReadingItemService /// /// /// + /// /// - private ParserInfo? Parse(string path, string rootPath, string libraryRoot, LibraryType type) + private ParserInfo? Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata) { if (_comicVineParser.IsApplicable(path, type)) { - return _comicVineParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _comicVineParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_imageParser.IsApplicable(path, type)) { - return _imageParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _imageParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_bookParser.IsApplicable(path, type)) { - return _bookParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _bookParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_pdfParser.IsApplicable(path, type)) { - return _pdfParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _pdfParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } if (_basicParser.IsApplicable(path, type)) { - return _basicParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); + return _basicParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path)); } return null; diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index c3f36ef2e..83558eaa0 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -804,7 +804,7 @@ public class ParseScannedFiles { // Process files sequentially result.ParserInfos = files - .Select(file => _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type)) + .Select(file => _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type, library.EnableMetadata)) .Where(info => info != null) .ToList()!; } @@ -812,7 +812,7 @@ public class ParseScannedFiles { // Process files in parallel var tasks = files.Select(file => Task.Run(() => - _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type))); + _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type, library.EnableMetadata))); var infos = await Task.WhenAll(tasks); result.ParserInfos = infos.Where(info => info != null).ToList()!; diff --git a/API/Services/Tasks/Scanner/Parser/BasicParser.cs b/API/Services/Tasks/Scanner/Parser/BasicParser.cs index 1462ab3d3..168ca7f01 100644 --- a/API/Services/Tasks/Scanner/Parser/BasicParser.cs +++ b/API/Services/Tasks/Scanner/Parser/BasicParser.cs @@ -12,7 +12,7 @@ namespace API.Services.Tasks.Scanner.Parser; ///
    public class BasicParser(IDirectoryService directoryService, IDefaultParser imageParser) : DefaultParser(directoryService) { - public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null) + public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null) { var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); // TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this. @@ -20,7 +20,7 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag if (Parser.IsImage(filePath)) { - return imageParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Image, comicInfo); + return imageParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Image, enableMetadata, comicInfo); } var ret = new ParserInfo() @@ -101,7 +101,12 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag } // Patch in other information from ComicInfo - UpdateFromComicInfo(ret); + if (enableMetadata) + { + UpdateFromComicInfo(ret); + } + + if (ret.Volumes == Parser.LooseLeafVolume && ret.Chapters == Parser.DefaultChapter) { diff --git a/API/Services/Tasks/Scanner/Parser/BookParser.cs b/API/Services/Tasks/Scanner/Parser/BookParser.cs index 499e554ef..14f42c989 100644 --- a/API/Services/Tasks/Scanner/Parser/BookParser.cs +++ b/API/Services/Tasks/Scanner/Parser/BookParser.cs @@ -5,7 +5,7 @@ namespace API.Services.Tasks.Scanner.Parser; public class BookParser(IDirectoryService directoryService, IBookService bookService, BasicParser basicParser) : DefaultParser(directoryService) { - public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo comicInfo = null) + public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo comicInfo = null) { var info = bookService.ParseInfo(filePath); if (info == null) return null; @@ -35,7 +35,7 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer } else { - var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, comicInfo); + var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, enableMetadata, comicInfo); info.Merge(info2); if (hasVolumeInSeries && info2 != null && Parser.ParseVolume(info2.Series, type) .Equals(Parser.LooseLeafVolume)) diff --git a/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs b/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs index b68596245..b60f28aee 100644 --- a/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs +++ b/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs @@ -19,7 +19,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser /// /// /// - public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null) + public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null) { if (type != LibraryType.ComicVine) return null; @@ -81,7 +81,10 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser info.IsSpecial = Parser.IsSpecial(info.Filename, type) || Parser.IsSpecial(info.ComicInfo?.Format, type); // Patch in other information from ComicInfo - UpdateFromComicInfo(info); + if (enableMetadata) + { + UpdateFromComicInfo(info); + } if (string.IsNullOrEmpty(info.Series)) { diff --git a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs index 679d6a031..687617fd7 100644 --- a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs +++ b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs @@ -8,7 +8,7 @@ namespace API.Services.Tasks.Scanner.Parser; public interface IDefaultParser { - ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null); + ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null); void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret); bool IsApplicable(string filePath, LibraryType type); } @@ -26,8 +26,9 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau /// /// Root folder /// Allows different Regex to be used for parsing. + /// Allows overriding data from metadata (ComicInfo/pdf/epub) /// or null if Series was empty - public abstract ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null); + public abstract ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null); /// /// Fills out by trying to parse volume, chapters, and series from folders diff --git a/API/Services/Tasks/Scanner/Parser/ImageParser.cs b/API/Services/Tasks/Scanner/Parser/ImageParser.cs index 415533631..12f9f4d50 100644 --- a/API/Services/Tasks/Scanner/Parser/ImageParser.cs +++ b/API/Services/Tasks/Scanner/Parser/ImageParser.cs @@ -7,7 +7,7 @@ namespace API.Services.Tasks.Scanner.Parser; public class ImageParser(IDirectoryService directoryService) : DefaultParser(directoryService) { - public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null) + public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null) { if (!IsApplicable(filePath, type)) return null; diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index c8eb010b3..c0b130f91 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -165,9 +165,9 @@ public static partial class Parser new Regex( @"(卷|册)(?\d+)", MatchOptions, RegexTimeout), - // Korean Volume: 제n화|권|회|장 -> Volume n, n화|권|회|장 -> Volume n, 63권#200.zip -> Volume 63 (no chapter, #200 is just files inside) + // Korean Volume: 제n화|회|장 -> Volume n, n화|권|장 -> Volume n, 63권#200.zip -> Volume 63 (no chapter, #200 is just files inside) new Regex( - @"제?(?\d+(\.\d+)?)(권|회|화|장)", + @"제?(?\d+(\.\d+)?)(권|화|장)", MatchOptions, RegexTimeout), // Korean Season: 시즌n -> Season n, new Regex( diff --git a/API/Services/Tasks/Scanner/Parser/PdfParser.cs b/API/Services/Tasks/Scanner/Parser/PdfParser.cs index bc12e2c77..80bfa9a48 100644 --- a/API/Services/Tasks/Scanner/Parser/PdfParser.cs +++ b/API/Services/Tasks/Scanner/Parser/PdfParser.cs @@ -6,7 +6,7 @@ namespace API.Services.Tasks.Scanner.Parser; public class PdfParser(IDirectoryService directoryService) : DefaultParser(directoryService) { - public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo comicInfo = null) + public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo comicInfo = null) { var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); var ret = new ParserInfo @@ -68,14 +68,18 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc ParseFromFallbackFolders(filePath, tempRootPath, type, ref ret); } - // Patch in other information from ComicInfo - UpdateFromComicInfo(ret); - - if (comicInfo != null && !string.IsNullOrEmpty(comicInfo.Title)) + if (enableMetadata) { - ret.Title = comicInfo.Title.Trim(); + // Patch in other information from ComicInfo + UpdateFromComicInfo(ret); + + if (comicInfo != null && !string.IsNullOrEmpty(comicInfo.Title)) + { + ret.Title = comicInfo.Title.Trim(); + } } + if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && type == LibraryType.Book) { ret.IsSpecial = true; diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index e22ee4bb6..cb5f4302f 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -521,6 +521,11 @@ public class ScannerService : IScannerService // Validations are done, now we can start actual scan _logger.LogInformation("[ScannerService] Beginning file scan on {LibraryName}", library.Name); + if (!library.EnableMetadata) + { + _logger.LogInformation("[ScannerService] Warning! {LibraryName} has metadata turned off", library.Name); + } + // This doesn't work for something like M:/Manga/ and a series has library folder as root var shouldUseLibraryScan = !(await _unitOfWork.LibraryRepository.DoAnySeriesFoldersMatch(libraryFolderPaths)); if (!shouldUseLibraryScan) diff --git a/API/SignalR/MessageFactory.cs b/API/SignalR/MessageFactory.cs index ba967d8a6..87a464e6a 100644 --- a/API/SignalR/MessageFactory.cs +++ b/API/SignalR/MessageFactory.cs @@ -152,6 +152,10 @@ public static class MessageFactory /// A Person merged has been merged into another /// public const string PersonMerged = "PersonMerged"; + /// + /// A Rate limit error was hit when matching a series with Kavita+ + /// + public const string ExternalMatchRateLimitError = "ExternalMatchRateLimitError"; public static SignalRMessage DashboardUpdateEvent(int userId) { @@ -679,4 +683,16 @@ public static class MessageFactory }, }; } + public static SignalRMessage ExternalMatchRateLimitErrorEvent(int seriesId, string seriesName) + { + return new SignalRMessage() + { + Name = ExternalMatchRateLimitError, + Body = new + { + seriesId = seriesId, + seriesName = seriesName, + }, + }; + } } diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index f2d64cde6..ba4fd09b7 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -17,7 +17,7 @@ public static class Configuration private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename()); public static readonly string KavitaPlusApiUrl = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development - ? "http://localhost:5020" : "https://plus.kavitareader.com"; + ? "https://plus.kavitareader.com" : "https://plus.kavitareader.com"; // http://localhost:5020 public static readonly string StatsApiUrl = "https://stats.kavitareader.com"; public static int Port diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 081ab80ca..b920416bb 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -9,12 +9,12 @@ - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UI/Web/src/_tag-card-common.scss b/UI/Web/src/_tag-card-common.scss index 07f37c2a0..39a1e87fd 100644 --- a/UI/Web/src/_tag-card-common.scss +++ b/UI/Web/src/_tag-card-common.scss @@ -5,6 +5,11 @@ box-shadow: 0 2px 5px rgba(0,0,0,0.2); transition: transform 0.2s ease, background 0.3s ease; cursor: pointer; + + &.not-selectable:hover { + cursor: not-allowed; + background-color: var(--bs-card-color, #2c2c2c) !important; + } } .tag-card:hover { diff --git a/UI/Web/src/app/_helpers/form-debug.ts b/UI/Web/src/app/_helpers/form-debug.ts new file mode 100644 index 000000000..4ad70ac87 --- /dev/null +++ b/UI/Web/src/app/_helpers/form-debug.ts @@ -0,0 +1,120 @@ +import {AbstractControl, FormArray, FormControl, FormGroup} from '@angular/forms'; + +interface ValidationIssue { + path: string; + controlType: string; + value: any; + errors: { [key: string]: any } | null; + status: string; + disabled: boolean; +} + +export function analyzeFormGroupValidation(formGroup: FormGroup, basePath: string = ''): ValidationIssue[] { + const issues: ValidationIssue[] = []; + + function analyzeControl(control: AbstractControl, path: string): void { + // Determine control type for better debugging + let controlType = 'AbstractControl'; + if (control instanceof FormGroup) { + controlType = 'FormGroup'; + } else if (control instanceof FormArray) { + controlType = 'FormArray'; + } else if (control instanceof FormControl) { + controlType = 'FormControl'; + } + + // Add issue if control has validation errors or is invalid + if (control.invalid || control.errors || control.disabled) { + issues.push({ + path: path || 'root', + controlType, + value: control.value, + errors: control.errors, + status: control.status, + disabled: control.disabled + }); + } + + // Recursively check nested controls + if (control instanceof FormGroup) { + Object.keys(control.controls).forEach(key => { + const childPath = path ? `${path}.${key}` : key; + analyzeControl(control.controls[key], childPath); + }); + } else if (control instanceof FormArray) { + control.controls.forEach((childControl, index) => { + const childPath = path ? `${path}[${index}]` : `[${index}]`; + analyzeControl(childControl, childPath); + }); + } + } + + analyzeControl(formGroup, basePath); + return issues; +} + +export function printFormGroupValidation(formGroup: FormGroup, basePath: string = ''): void { + const issues = analyzeFormGroupValidation(formGroup, basePath); + + console.group(`🔍 FormGroup Validation Analysis (${basePath || 'root'})`); + console.log(`Overall Status: ${formGroup.status}`); + console.log(`Overall Valid: ${formGroup.valid}`); + console.log(`Total Issues Found: ${issues.length}`); + + if (issues.length === 0) { + console.log('✅ No validation issues found!'); + } else { + console.log('\n📋 Detailed Issues:'); + issues.forEach((issue, index) => { + console.group(`${index + 1}. ${issue.path} (${issue.controlType})`); + console.log(`Status: ${issue.status}`); + console.log(`Value:`, issue.value); + console.log(`Disabled: ${issue.disabled}`); + + if (issue.errors) { + console.log('Validation Errors:'); + Object.entries(issue.errors).forEach(([errorKey, errorValue]) => { + console.log(` • ${errorKey}:`, errorValue); + }); + } else { + console.log('No specific validation errors (but control is invalid)'); + } + console.groupEnd(); + }); + } + + console.groupEnd(); +} + +// Alternative function that returns a formatted string instead of console logging +export function getFormGroupValidationReport(formGroup: FormGroup, basePath: string = ''): string { + const issues = analyzeFormGroupValidation(formGroup, basePath); + + let report = `FormGroup Validation Report (${basePath || 'root'})\n`; + report += `Overall Status: ${formGroup.status}\n`; + report += `Overall Valid: ${formGroup.valid}\n`; + report += `Total Issues Found: ${issues.length}\n\n`; + + if (issues.length === 0) { + report += '✅ No validation issues found!'; + } else { + report += 'Detailed Issues:\n'; + issues.forEach((issue, index) => { + report += `\n${index + 1}. ${issue.path} (${issue.controlType})\n`; + report += ` Status: ${issue.status}\n`; + report += ` Value: ${JSON.stringify(issue.value)}\n`; + report += ` Disabled: ${issue.disabled}\n`; + + if (issue.errors) { + report += ' Validation Errors:\n'; + Object.entries(issue.errors).forEach(([errorKey, errorValue]) => { + report += ` • ${errorKey}: ${JSON.stringify(errorValue)}\n`; + }); + } else { + report += ' No specific validation errors (but control is invalid)\n'; + } + }); + } + + return report; +} diff --git a/UI/Web/src/app/_models/events/external-match-rate-limit-error-event.ts b/UI/Web/src/app/_models/events/external-match-rate-limit-error-event.ts new file mode 100644 index 000000000..3695651d6 --- /dev/null +++ b/UI/Web/src/app/_models/events/external-match-rate-limit-error-event.ts @@ -0,0 +1,4 @@ +export interface ExternalMatchRateLimitErrorEvent { + seriesId: number; + seriesName: string; +} diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts index bad83f54b..0e7d90ee2 100644 --- a/UI/Web/src/app/_models/library/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -31,6 +31,7 @@ export interface Library { manageReadingLists: boolean; allowScrobbling: boolean; allowMetadataMatching: boolean; + enableMetadata: boolean; collapseSeriesRelationships: boolean; libraryFileTypes: Array; excludePatterns: Array; diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index 67f07f32e..f870d1449 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -1,15 +1,16 @@ -import { Injectable } from '@angular/core'; -import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr'; -import { BehaviorSubject, ReplaySubject } from 'rxjs'; -import { environment } from 'src/environments/environment'; -import { LibraryModifiedEvent } from '../_models/events/library-modified-event'; -import { NotificationProgressEvent } from '../_models/events/notification-progress-event'; -import { ThemeProgressEvent } from '../_models/events/theme-progress-event'; -import { UserUpdateEvent } from '../_models/events/user-update-event'; -import { User } from '../_models/user'; +import {Injectable} from '@angular/core'; +import {HubConnection, HubConnectionBuilder} from '@microsoft/signalr'; +import {BehaviorSubject, ReplaySubject} from 'rxjs'; +import {environment} from 'src/environments/environment'; +import {LibraryModifiedEvent} from '../_models/events/library-modified-event'; +import {NotificationProgressEvent} from '../_models/events/notification-progress-event'; +import {ThemeProgressEvent} from '../_models/events/theme-progress-event'; +import {UserUpdateEvent} from '../_models/events/user-update-event'; +import {User} from '../_models/user'; import {DashboardUpdateEvent} from "../_models/events/dashboard-update-event"; import {SideNavUpdateEvent} from "../_models/events/sidenav-update-event"; import {SiteThemeUpdatedEvent} from "../_models/events/site-theme-updated-event"; +import {ExternalMatchRateLimitErrorEvent} from "../_models/events/external-match-rate-limit-error-event"; export enum EVENTS { UpdateAvailable = 'UpdateAvailable', @@ -114,6 +115,10 @@ export enum EVENTS { * A Person merged has been merged into another */ PersonMerged = 'PersonMerged', + /** + * A Rate limit error was hit when matching a series with Kavita+ + */ + ExternalMatchRateLimitError = 'ExternalMatchRateLimitError' } export interface Message { @@ -236,6 +241,13 @@ export class MessageHubService { }); }); + this.hubConnection.on(EVENTS.ExternalMatchRateLimitError, resp => { + this.messagesSource.next({ + event: EVENTS.ExternalMatchRateLimitError, + payload: resp.body as ExternalMatchRateLimitErrorEvent + }); + }); + this.hubConnection.on(EVENTS.NotificationProgress, (resp: NotificationProgressEvent) => { this.messagesSource.next({ event: EVENTS.NotificationProgress, diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 05958ee61..52aef2a4a 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -266,13 +266,13 @@ export class ReaderService { getQueryParamsObject(incognitoMode: boolean = false, readingListMode: boolean = false, readingListId: number = -1) { - let params: {[key: string]: any} = {}; - if (incognitoMode) { - params['incognitoMode'] = true; - } + const params: {[key: string]: any} = {}; + params['incognitoMode'] = incognitoMode; + if (readingListMode) { params['readingListId'] = readingListId; } + return params; } diff --git a/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts b/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts index 223b309da..2a5582145 100644 --- a/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts +++ b/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts @@ -1,7 +1,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; import {LicenseService} from "../../_services/license.service"; import {Router} from "@angular/router"; -import {TranslocoDirective} from "@jsverse/transloco"; +import {translate, TranslocoDirective} from "@jsverse/transloco"; import {ImageComponent} from "../../shared/image/image.component"; import {ImageService} from "../../_services/image.service"; import {Series} from "../../_models/series"; @@ -23,6 +23,8 @@ import {EVENTS, MessageHubService} from "../../_services/message-hub.service"; import {ScanSeriesEvent} from "../../_models/events/scan-series-event"; import {LibraryTypePipe} from "../../_pipes/library-type.pipe"; import {allKavitaPlusMetadataApplicableTypes} from "../../_models/library/library"; +import {ExternalMatchRateLimitErrorEvent} from "../../_models/events/external-match-rate-limit-error-event"; +import {ToastrService} from "ngx-toastr"; @Component({ selector: 'app-manage-matched-metadata', @@ -55,6 +57,7 @@ export class ManageMatchedMetadataComponent implements OnInit { private readonly manageService = inject(ManageService); private readonly messageHub = inject(MessageHubService); private readonly cdRef = inject(ChangeDetectorRef); + private readonly toastr = inject(ToastrService); protected readonly imageService = inject(ImageService); @@ -74,12 +77,19 @@ export class ManageMatchedMetadataComponent implements OnInit { } this.messageHub.messages$.subscribe(message => { - if (message.event !== EVENTS.ScanSeries) return; - - const evt = message.payload as ScanSeriesEvent; - if (this.data.filter(d => d.series.id === evt.seriesId).length > 0) { - this.loadData(); + if (message.event == EVENTS.ScanSeries) { + const evt = message.payload as ScanSeriesEvent; + if (this.data.filter(d => d.series.id === evt.seriesId).length > 0) { + this.loadData(); + } } + + if (message.event == EVENTS.ExternalMatchRateLimitError) { + const evt = message.payload as ExternalMatchRateLimitErrorEvent; + this.toastr.error(translate('toasts.external-match-rate-error', {seriesName: evt.seriesName})) + } + + }); this.filterGroup.valueChanges.pipe( diff --git a/UI/Web/src/app/browse/browse-genres/browse-genres.component.html b/UI/Web/src/app/browse/browse-genres/browse-genres.component.html index 5eef2c91f..8166ef12c 100644 --- a/UI/Web/src/app/browse/browse-genres/browse-genres.component.html +++ b/UI/Web/src/app/browse/browse-genres/browse-genres.component.html @@ -19,7 +19,7 @@ > -
    +
    {{ item.title }}
    {{t('series-count', {num: item.seriesCount | compactNumber})}} diff --git a/UI/Web/src/app/browse/browse-genres/browse-genres.component.ts b/UI/Web/src/app/browse/browse-genres/browse-genres.component.ts index 02c2a8ead..c46795e25 100644 --- a/UI/Web/src/app/browse/browse-genres/browse-genres.component.ts +++ b/UI/Web/src/app/browse/browse-genres/browse-genres.component.ts @@ -1,6 +1,6 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, inject, OnInit} from '@angular/core'; import {CardDetailLayoutComponent} from "../../cards/card-detail-layout/card-detail-layout.component"; -import {DecimalPipe} from "@angular/common"; +import {DecimalPipe, NgClass} from "@angular/common"; import { SideNavCompanionBarComponent } from "../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component"; @@ -24,7 +24,8 @@ import {Title} from "@angular/platform-browser"; DecimalPipe, SideNavCompanionBarComponent, TranslocoDirective, - CompactNumberPipe + CompactNumberPipe, + NgClass ], templateUrl: './browse-genres.component.html', styleUrl: './browse-genres.component.scss', @@ -62,7 +63,8 @@ export class BrowseGenresComponent implements OnInit { }); } - openFilter(field: FilterField, value: string | number) { - this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${value}`).subscribe(); + openFilter(field: FilterField, genre: BrowseGenre) { + if (genre.seriesCount === 0) return; // We don't yet have an issue page + this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${genre.id}`).subscribe(); } } diff --git a/UI/Web/src/app/browse/browse-tags/browse-tags.component.html b/UI/Web/src/app/browse/browse-tags/browse-tags.component.html index dcd59bb1f..627e05584 100644 --- a/UI/Web/src/app/browse/browse-tags/browse-tags.component.html +++ b/UI/Web/src/app/browse/browse-tags/browse-tags.component.html @@ -19,7 +19,7 @@ > -
    +
    {{ item.title }}
    {{t('series-count', {num: item.seriesCount | compactNumber})}} diff --git a/UI/Web/src/app/browse/browse-tags/browse-tags.component.ts b/UI/Web/src/app/browse/browse-tags/browse-tags.component.ts index 92910b0b9..05abb6300 100644 --- a/UI/Web/src/app/browse/browse-tags/browse-tags.component.ts +++ b/UI/Web/src/app/browse/browse-tags/browse-tags.component.ts @@ -1,6 +1,6 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, inject, OnInit} from '@angular/core'; import {CardDetailLayoutComponent} from "../../cards/card-detail-layout/card-detail-layout.component"; -import {DecimalPipe} from "@angular/common"; +import {DecimalPipe, NgClass} from "@angular/common"; import { SideNavCompanionBarComponent } from "../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component"; @@ -25,7 +25,8 @@ import {Title} from "@angular/platform-browser"; DecimalPipe, SideNavCompanionBarComponent, TranslocoDirective, - CompactNumberPipe + CompactNumberPipe, + NgClass ], templateUrl: './browse-tags.component.html', styleUrl: './browse-tags.component.scss', @@ -61,7 +62,8 @@ export class BrowseTagsComponent implements OnInit { }); } - openFilter(field: FilterField, value: string | number) { - this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${value}`).subscribe(); + openFilter(field: FilterField, tag: BrowseTag) { + if (tag.seriesCount === 0) return; // We don't yet have an issue page + this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${tag.id}`).subscribe(); } } diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html index 1d1ce4c7e..7433c26c3 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html @@ -229,14 +229,17 @@
    } - + diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts index 6e8e3b22a..3056d7eb5 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts @@ -25,7 +25,8 @@ import {ImageService} from 'src/app/_services/image.service'; import {ReadingListService} from 'src/app/_services/reading-list.service'; import { DraggableOrderedListComponent, - IndexUpdateEvent + IndexUpdateEvent, + ItemRemoveEvent } from '../draggable-ordered-list/draggable-ordered-list.component'; import {forkJoin, startWith, tap} from 'rxjs'; import {ReaderService} from 'src/app/_services/reader.service'; @@ -321,6 +322,7 @@ export class ReadingListDetailComponent implements OnInit { } editReadingList(readingList: ReadingList) { + if (!readingList) return; this.actionService.editReadingList(readingList, (readingList: ReadingList) => { // Reload information around list this.readingListService.getReadingList(this.listId).subscribe(rl => { @@ -347,10 +349,10 @@ export class ReadingListDetailComponent implements OnInit { }); } - itemRemoved(item: ReadingListItem, position: number) { + removeItem(removeEvent: ItemRemoveEvent) { if (!this.readingList) return; - this.readingListService.deleteItem(this.readingList.id, item.id).subscribe(() => { - this.items.splice(position, 1); + this.readingListService.deleteItem(this.readingList.id, removeEvent.item.id).subscribe(() => { + this.items.splice(removeEvent.position, 1); this.items = [...this.items]; this.cdRef.markForCheck(); this.toastr.success(translate('toasts.item-removed')); diff --git a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html index 901ee270a..6421205ab 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.html @@ -18,10 +18,10 @@ {{item.title}}
    @if (showRemove) { - } diff --git a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts index acde50022..7ce6f6790 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts @@ -9,6 +9,7 @@ import {ImageComponent} from '../../../shared/image/image.component'; import {TranslocoDirective} from "@jsverse/transloco"; import {SeriesFormatComponent} from "../../../shared/series-format/series-format.component"; import {ReadMoreComponent} from "../../../shared/read-more/read-more.component"; +import {ItemRemoveEvent} from "../draggable-ordered-list/draggable-ordered-list.component"; @Component({ selector: 'app-reading-list-item', @@ -33,9 +34,16 @@ export class ReadingListItemComponent { @Input() promoted: boolean = false; @Output() read: EventEmitter = new EventEmitter(); - @Output() remove: EventEmitter = new EventEmitter(); + @Output() remove: EventEmitter = new EventEmitter(); readChapter(item: ReadingListItem) { this.read.emit(item); } + + removeItem(item: ReadingListItem) { + this.remove.emit({ + item: item, + position: item.order + }); + } } diff --git a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts index 8685adc48..d18939c4e 100644 --- a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts +++ b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts @@ -61,7 +61,8 @@ export class ExternalRatingComponent implements OnInit { ngOnInit() { this.reviewService.overallRating(this.seriesId, this.chapterId).subscribe(r => { this.overallRating = r.averageScore; - }); + this.cdRef.markForCheck(); + }); } updateRating(rating: number) { @@ -92,6 +93,4 @@ export class ExternalRatingComponent implements OnInit { return ''; } - - protected readonly RatingAuthority = RatingAuthority; } diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html index 8cbac271a..ff97fcbb0 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html @@ -127,6 +127,16 @@
    +
    + + +
    + +
    +
    +
    +
    +
    diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts index 797124c4f..d0fed5c81 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts @@ -105,15 +105,16 @@ export class LibrarySettingsModalComponent implements OnInit { libraryForm: FormGroup = new FormGroup({ name: new FormControl('', { nonNullable: true, validators: [Validators.required] }), type: new FormControl(LibraryType.Manga, { nonNullable: true, validators: [Validators.required] }), - folderWatching: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - includeInDashboard: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - includeInRecommended: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - includeInSearch: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - manageCollections: new FormControl(false, { nonNullable: true, validators: [Validators.required] }), - manageReadingLists: new FormControl(false, { nonNullable: true, validators: [Validators.required] }), - allowScrobbling: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - allowMetadataMatching: new FormControl(true, { nonNullable: true, validators: [Validators.required] }), - collapseSeriesRelationships: new FormControl(false, { nonNullable: true, validators: [Validators.required] }), + folderWatching: new FormControl(true, { nonNullable: true, validators: [] }), + includeInDashboard: new FormControl(true, { nonNullable: true, validators: [] }), + includeInRecommended: new FormControl(true, { nonNullable: true, validators: [] }), + includeInSearch: new FormControl(true, { nonNullable: true, validators: [] }), + manageCollections: new FormControl(false, { nonNullable: true, validators: [] }), + manageReadingLists: new FormControl(false, { nonNullable: true, validators: [] }), + allowScrobbling: new FormControl(true, { nonNullable: true, validators: [] }), + allowMetadataMatching: new FormControl(true, { nonNullable: true, validators: [] }), + collapseSeriesRelationships: new FormControl(false, { nonNullable: true, validators: [] }), + enableMetadata: new FormControl(true, { nonNullable: true, validators: [] }), // required validator doesn't check value, just if true }); selectedFolders: string[] = []; @@ -155,7 +156,7 @@ export class LibrarySettingsModalComponent implements OnInit { this.libraryForm.get('allowScrobbling')?.disable(); if (this.IsMetadataDownloadEligible) { - this.libraryForm.get('allowMetadataMatching')?.setValue(this.library.allowMetadataMatching); + this.libraryForm.get('allowMetadataMatching')?.setValue(this.library.allowMetadataMatching ?? true); this.libraryForm.get('allowMetadataMatching')?.enable(); } else { this.libraryForm.get('allowMetadataMatching')?.setValue(false); @@ -184,6 +185,20 @@ export class LibrarySettingsModalComponent implements OnInit { this.setValues(); + // Turn on/off manage collections/rl + this.libraryForm.get('enableMetadata')?.valueChanges.pipe( + tap(enabled => { + const manageCollectionsFc = this.libraryForm.get('manageCollections'); + const manageReadingListsFc = this.libraryForm.get('manageReadingLists'); + + manageCollectionsFc?.setValue(enabled); + manageReadingListsFc?.setValue(enabled); + + this.cdRef.markForCheck(); + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(); + // This needs to only apply after first render this.libraryForm.get('type')?.valueChanges.pipe( tap((type: LibraryType) => { @@ -257,6 +272,8 @@ export class LibrarySettingsModalComponent implements OnInit { this.libraryForm.get('collapseSeriesRelationships')?.setValue(this.library.collapseSeriesRelationships); this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible ? this.library.allowScrobbling : false); this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsMetadataDownloadEligible ? this.library.allowMetadataMatching : false); + this.libraryForm.get('excludePatterns')?.setValue(this.excludePatterns ? this.library.excludePatterns : false); + this.libraryForm.get('enableMetadata')?.setValue(this.library.enableMetadata, true); this.selectedFolders = this.library.folders; this.madeChanges = false; diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 91a3dac9e..c6b8c823f 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1129,6 +1129,8 @@ "include-in-dashboard-tooltip": "Should series from the library be included on the Dashboard. This affects all streams, like On Deck, Recently Updated, Recently Added, or any custom additions.", "include-in-search-label": "Include in Search", "include-in-search-tooltip": "Should series and any derived information (genres, people, files) from the library be included in search results.", + "enable-metadata-label": "Enable Metadata (ComicInfo/Epub/PDF)", + "enable-metadata-tooltip": "Allow Kavita to read metadata files which override filename parsing.", "force-scan": "Force Scan", "force-scan-tooltip": "This will force a scan on the library, treating like a fresh scan", "reset": "{{common.reset}}", @@ -2743,7 +2745,8 @@ "webtoon-override": "Switching to Webtoon mode due to images representing a webtoon.", "scrobble-gen-init": "Enqueued a job to generate scrobble events from past reading history and ratings, syncing them with connected services.", "series-bound-to-reading-profile": "Series bound to Reading Profile {{name}}", - "library-bound-to-reading-profile": "Library bound to Reading Profile {{name}}" + "library-bound-to-reading-profile": "Library bound to Reading Profile {{name}}", + "external-match-rate-error": "Kavita ran out of rate looking up {{seriesName}}. Try again in 5 minutes." }, "read-time-pipe": { From d536cc7f6a3f6b9e7fb00bdc6d460c2bfad86104 Mon Sep 17 00:00:00 2001 From: majora2007 Date: Mon, 23 Jun 2025 23:57:53 +0000 Subject: [PATCH 13/30] Bump versions by dotnet-bump-version. --- Kavita.Common/Kavita.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index b920416bb..fb13c5605 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -3,7 +3,7 @@ net9.0 kavitareader.com Kavita - 0.8.6.17 + 0.8.6.18 en true From 62231d3c4e0b8b3f33ef3f91c7912cd422512cf9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 23 Jun 2025 23:58:56 +0000 Subject: [PATCH 14/30] Update OpenAPI documentation --- openapi.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/openapi.json b/openapi.json index 5f50b88f7..0ee2657d0 100644 --- a/openapi.json +++ b/openapi.json @@ -2,12 +2,12 @@ "openapi": "3.0.4", "info": { "title": "Kavita", - "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.6.16", + "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.6.17", "license": { "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.8.6.16" + "version": "0.8.6.17" }, "servers": [ { @@ -21341,6 +21341,10 @@ "type": "boolean", "description": "Allow any series within this Library to download metadata." }, + "enableMetadata": { + "type": "boolean", + "description": "Should Kavita read metadata files from the library" + }, "created": { "type": "string", "format": "date-time" @@ -21499,6 +21503,10 @@ "allowMetadataMatching": { "type": "boolean", "description": "Allow any series within this Library to download metadata." + }, + "enableMetadata": { + "type": "boolean", + "description": "Allow Kavita to read metadata (ComicInfo.xml, Epub, PDF)" } }, "additionalProperties": false @@ -26367,6 +26375,7 @@ "required": [ "allowMetadataMatching", "allowScrobbling", + "enableMetadata", "excludePatterns", "fileGroupTypes", "folders", @@ -26428,6 +26437,9 @@ "allowMetadataMatching": { "type": "boolean" }, + "enableMetadata": { + "type": "boolean" + }, "fileGroupTypes": { "type": "array", "items": { From 6fa1cf994efe23096f05289dcb7cad91e9430ef3 Mon Sep 17 00:00:00 2001 From: Fesaa <77553571+Fesaa@users.noreply.github.com> Date: Wed, 25 Jun 2025 17:04:26 +0200 Subject: [PATCH 15/30] A bunch of bug fixes and some enhancements (#3871) Co-authored-by: Joseph Milazzo --- API.Tests/Repository/GenreRepositoryTests.cs | 280 ++++++++++++++ API.Tests/Repository/PersonRepositoryTests.cs | 342 ++++++++++++++++++ API.Tests/Repository/TagRepositoryTests.cs | 278 ++++++++++++++ .../Services/ExternalMetadataServiceTests.cs | 212 +++++++++++ API/Controllers/PersonController.cs | 3 +- .../Metadata/ExternalSeriesDetailDto.cs | 2 + API/Data/Repositories/GenreRepository.cs | 16 +- API/Data/Repositories/PersonRepository.cs | 68 ++-- API/Data/Repositories/TagRepository.cs | 14 +- API/Extensions/EnumerableExtensions.cs | 13 + .../RestrictByAgeExtensions.cs | 26 ++ .../RestrictByLibraryExtensions.cs | 31 ++ API/Helpers/Builders/ChapterBuilder.cs | 20 + API/Helpers/Builders/SeriesMetadataBuilder.cs | 19 + API/Services/Plus/ExternalMetadataService.cs | 101 +++++- API/Services/TaskScheduler.cs | 4 +- UI/Web/src/app/_services/nav.service.ts | 52 +++ .../user-scrobble-history.component.html | 2 +- .../manage-metadata-settings.component.ts | 11 +- .../nav-header/nav-header.component.html | 15 +- .../nav-header/nav-header.component.ts | 8 - .../nav-link-modal.component.html | 25 +- .../nav-link-modal.component.ts | 8 +- .../person-detail/person-detail.component.ts | 5 +- 24 files changed, 1464 insertions(+), 91 deletions(-) create mode 100644 API.Tests/Repository/GenreRepositoryTests.cs create mode 100644 API.Tests/Repository/PersonRepositoryTests.cs create mode 100644 API.Tests/Repository/TagRepositoryTests.cs diff --git a/API.Tests/Repository/GenreRepositoryTests.cs b/API.Tests/Repository/GenreRepositoryTests.cs new file mode 100644 index 000000000..d197a91ba --- /dev/null +++ b/API.Tests/Repository/GenreRepositoryTests.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.DTOs.Metadata.Browse; +using API.Entities; +using API.Entities.Enums; +using API.Entities.Metadata; +using API.Helpers; +using API.Helpers.Builders; +using Xunit; + +namespace API.Tests.Repository; + +public class GenreRepositoryTests : AbstractDbTest +{ + private AppUser _fullAccess; + private AppUser _restrictedAccess; + private AppUser _restrictedAgeAccess; + + protected override async Task ResetDb() + { + Context.Genre.RemoveRange(Context.Genre); + Context.Library.RemoveRange(Context.Library); + await Context.SaveChangesAsync(); + } + + private TestGenreSet CreateTestGenres() + { + return new TestGenreSet + { + SharedSeriesChaptersGenre = new GenreBuilder("Shared Series Chapter Genre").Build(), + SharedSeriesGenre = new GenreBuilder("Shared Series Genre").Build(), + SharedChaptersGenre = new GenreBuilder("Shared Chapters Genre").Build(), + Lib0SeriesChaptersGenre = new GenreBuilder("Lib0 Series Chapter Genre").Build(), + Lib0SeriesGenre = new GenreBuilder("Lib0 Series Genre").Build(), + Lib0ChaptersGenre = new GenreBuilder("Lib0 Chapters Genre").Build(), + Lib1SeriesChaptersGenre = new GenreBuilder("Lib1 Series Chapter Genre").Build(), + Lib1SeriesGenre = new GenreBuilder("Lib1 Series Genre").Build(), + Lib1ChaptersGenre = new GenreBuilder("Lib1 Chapters Genre").Build(), + Lib1ChapterAgeGenre = new GenreBuilder("Lib1 Chapter Age Genre").Build() + }; + } + + private async Task SeedDbWithGenres(TestGenreSet genres) + { + await CreateTestUsers(); + await AddGenresToContext(genres); + await CreateLibrariesWithGenres(genres); + await AssignLibrariesToUsers(); + } + + private async Task CreateTestUsers() + { + _fullAccess = new AppUserBuilder("amelia", "amelia@example.com").Build(); + _restrictedAccess = new AppUserBuilder("mila", "mila@example.com").Build(); + _restrictedAgeAccess = new AppUserBuilder("eva", "eva@example.com").Build(); + _restrictedAgeAccess.AgeRestriction = AgeRating.Teen; + _restrictedAgeAccess.AgeRestrictionIncludeUnknowns = true; + + Context.Users.Add(_fullAccess); + Context.Users.Add(_restrictedAccess); + Context.Users.Add(_restrictedAgeAccess); + await Context.SaveChangesAsync(); + } + + private async Task AddGenresToContext(TestGenreSet genres) + { + var allGenres = genres.GetAllGenres(); + Context.Genre.AddRange(allGenres); + await Context.SaveChangesAsync(); + } + + private async Task CreateLibrariesWithGenres(TestGenreSet genres) + { + var lib0 = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("lib0-s0") + .WithMetadata(new SeriesMetadataBuilder() + .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedSeriesGenre, genres.Lib0SeriesChaptersGenre, genres.Lib0SeriesGenre]) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1") + .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib0SeriesChaptersGenre, genres.Lib0ChaptersGenre]) + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre]) + .Build()) + .Build()) + .Build()) + .Build(); + + var lib1 = new LibraryBuilder("lib1") + .WithSeries(new SeriesBuilder("lib1-s0") + .WithMetadata(new SeriesMetadataBuilder() + .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedSeriesGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1SeriesGenre]) + .WithAgeRating(AgeRating.Mature17Plus) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1") + .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre]) + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre, genres.Lib1ChapterAgeGenre]) + .WithAgeRating(AgeRating.Mature17Plus) + .Build()) + .Build()) + .Build()) + .WithSeries(new SeriesBuilder("lib1-s1") + .WithMetadata(new SeriesMetadataBuilder() + .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedSeriesGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1SeriesGenre]) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1") + .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre]) + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithGenres([genres.SharedSeriesChaptersGenre, genres.SharedChaptersGenre, genres.Lib1SeriesChaptersGenre, genres.Lib1ChaptersGenre]) + .Build()) + .Build()) + .Build()) + .Build(); + + Context.Library.Add(lib0); + Context.Library.Add(lib1); + await Context.SaveChangesAsync(); + } + + private async Task AssignLibrariesToUsers() + { + var lib0 = Context.Library.First(l => l.Name == "lib0"); + var lib1 = Context.Library.First(l => l.Name == "lib1"); + + _fullAccess.Libraries.Add(lib0); + _fullAccess.Libraries.Add(lib1); + _restrictedAccess.Libraries.Add(lib1); + _restrictedAgeAccess.Libraries.Add(lib1); + + await Context.SaveChangesAsync(); + } + + private static Predicate ContainsGenreCheck(Genre genre) + { + return g => g.Id == genre.Id; + } + + private static void AssertGenrePresent(IEnumerable genres, Genre expectedGenre) + { + Assert.Contains(genres, ContainsGenreCheck(expectedGenre)); + } + + private static void AssertGenreNotPresent(IEnumerable genres, Genre expectedGenre) + { + Assert.DoesNotContain(genres, ContainsGenreCheck(expectedGenre)); + } + + private static BrowseGenreDto GetGenreDto(IEnumerable genres, Genre genre) + { + return genres.First(dto => dto.Id == genre.Id); + } + + [Fact] + public async Task GetBrowseableGenre_FullAccess_ReturnsAllGenresWithCorrectCounts() + { + // Arrange + await ResetDb(); + var genres = CreateTestGenres(); + await SeedDbWithGenres(genres); + + // Act + var fullAccessGenres = await UnitOfWork.GenreRepository.GetBrowseableGenre(_fullAccess.Id, new UserParams()); + + // Assert + Assert.Equal(genres.GetAllGenres().Count, fullAccessGenres.TotalCount); + + foreach (var genre in genres.GetAllGenres()) + { + AssertGenrePresent(fullAccessGenres, genre); + } + + // Verify counts - 1 lib0 series, 2 lib1 series = 3 total series + Assert.Equal(3, GetGenreDto(fullAccessGenres, genres.SharedSeriesChaptersGenre).SeriesCount); + Assert.Equal(6, GetGenreDto(fullAccessGenres, genres.SharedSeriesChaptersGenre).ChapterCount); + Assert.Equal(1, GetGenreDto(fullAccessGenres, genres.Lib0SeriesGenre).SeriesCount); + } + + [Fact] + public async Task GetBrowseableGenre_RestrictedAccess_ReturnsOnlyAccessibleGenres() + { + // Arrange + await ResetDb(); + var genres = CreateTestGenres(); + await SeedDbWithGenres(genres); + + // Act + var restrictedAccessGenres = await UnitOfWork.GenreRepository.GetBrowseableGenre(_restrictedAccess.Id, new UserParams()); + + // Assert - Should see: 3 shared + 4 library 1 specific = 7 genres + Assert.Equal(7, restrictedAccessGenres.TotalCount); + + // Verify shared and Library 1 genres are present + AssertGenrePresent(restrictedAccessGenres, genres.SharedSeriesChaptersGenre); + AssertGenrePresent(restrictedAccessGenres, genres.SharedSeriesGenre); + AssertGenrePresent(restrictedAccessGenres, genres.SharedChaptersGenre); + AssertGenrePresent(restrictedAccessGenres, genres.Lib1SeriesChaptersGenre); + AssertGenrePresent(restrictedAccessGenres, genres.Lib1SeriesGenre); + AssertGenrePresent(restrictedAccessGenres, genres.Lib1ChaptersGenre); + AssertGenrePresent(restrictedAccessGenres, genres.Lib1ChapterAgeGenre); + + // Verify Library 0 specific genres are not present + AssertGenreNotPresent(restrictedAccessGenres, genres.Lib0SeriesChaptersGenre); + AssertGenreNotPresent(restrictedAccessGenres, genres.Lib0SeriesGenre); + AssertGenreNotPresent(restrictedAccessGenres, genres.Lib0ChaptersGenre); + + // Verify counts - 2 lib1 series + Assert.Equal(2, GetGenreDto(restrictedAccessGenres, genres.SharedSeriesChaptersGenre).SeriesCount); + Assert.Equal(4, GetGenreDto(restrictedAccessGenres, genres.SharedSeriesChaptersGenre).ChapterCount); + Assert.Equal(2, GetGenreDto(restrictedAccessGenres, genres.Lib1SeriesGenre).SeriesCount); + Assert.Equal(4, GetGenreDto(restrictedAccessGenres, genres.Lib1ChaptersGenre).ChapterCount); + Assert.Equal(1, GetGenreDto(restrictedAccessGenres, genres.Lib1ChapterAgeGenre).ChapterCount); + } + + [Fact] + public async Task GetBrowseableGenre_RestrictedAgeAccess_FiltersAgeRestrictedContent() + { + // Arrange + await ResetDb(); + var genres = CreateTestGenres(); + await SeedDbWithGenres(genres); + + // Act + var restrictedAgeAccessGenres = await UnitOfWork.GenreRepository.GetBrowseableGenre(_restrictedAgeAccess.Id, new UserParams()); + + // Assert - Should see: 3 shared + 3 lib1 specific = 6 genres (age-restricted genre filtered out) + Assert.Equal(6, restrictedAgeAccessGenres.TotalCount); + + // Verify accessible genres are present + AssertGenrePresent(restrictedAgeAccessGenres, genres.SharedSeriesChaptersGenre); + AssertGenrePresent(restrictedAgeAccessGenres, genres.SharedSeriesGenre); + AssertGenrePresent(restrictedAgeAccessGenres, genres.SharedChaptersGenre); + AssertGenrePresent(restrictedAgeAccessGenres, genres.Lib1SeriesChaptersGenre); + AssertGenrePresent(restrictedAgeAccessGenres, genres.Lib1SeriesGenre); + AssertGenrePresent(restrictedAgeAccessGenres, genres.Lib1ChaptersGenre); + + // Verify age-restricted genre is filtered out + AssertGenreNotPresent(restrictedAgeAccessGenres, genres.Lib1ChapterAgeGenre); + + // Verify counts - 1 series lib1 (age-restricted series filtered out) + Assert.Equal(1, GetGenreDto(restrictedAgeAccessGenres, genres.SharedSeriesChaptersGenre).SeriesCount); + Assert.Equal(1, GetGenreDto(restrictedAgeAccessGenres, genres.Lib1SeriesGenre).SeriesCount); + + // These values represent a bug - chapters are not properly filtered when their series is age-restricted + // Should be 2, but currently returns 3 due to the filtering issue + Assert.Equal(3, GetGenreDto(restrictedAgeAccessGenres, genres.SharedSeriesChaptersGenre).ChapterCount); + Assert.Equal(3, GetGenreDto(restrictedAgeAccessGenres, genres.Lib1ChaptersGenre).ChapterCount); + } + + private class TestGenreSet + { + public Genre SharedSeriesChaptersGenre { get; set; } + public Genre SharedSeriesGenre { get; set; } + public Genre SharedChaptersGenre { get; set; } + public Genre Lib0SeriesChaptersGenre { get; set; } + public Genre Lib0SeriesGenre { get; set; } + public Genre Lib0ChaptersGenre { get; set; } + public Genre Lib1SeriesChaptersGenre { get; set; } + public Genre Lib1SeriesGenre { get; set; } + public Genre Lib1ChaptersGenre { get; set; } + public Genre Lib1ChapterAgeGenre { get; set; } + + public List GetAllGenres() + { + return + [ + SharedSeriesChaptersGenre, SharedSeriesGenre, SharedChaptersGenre, + Lib0SeriesChaptersGenre, Lib0SeriesGenre, Lib0ChaptersGenre, + Lib1SeriesChaptersGenre, Lib1SeriesGenre, Lib1ChaptersGenre, Lib1ChapterAgeGenre + ]; + } + } +} diff --git a/API.Tests/Repository/PersonRepositoryTests.cs b/API.Tests/Repository/PersonRepositoryTests.cs new file mode 100644 index 000000000..a2b19cc0c --- /dev/null +++ b/API.Tests/Repository/PersonRepositoryTests.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.DTOs.Metadata.Browse; +using API.DTOs.Metadata.Browse.Requests; +using API.Entities; +using API.Entities.Enums; +using API.Entities.Person; +using API.Helpers; +using API.Helpers.Builders; +using Xunit; + +namespace API.Tests.Repository; + +public class PersonRepositoryTests : AbstractDbTest +{ + private AppUser _fullAccess; + private AppUser _restrictedAccess; + private AppUser _restrictedAgeAccess; + + protected override async Task ResetDb() + { + Context.Person.RemoveRange(Context.Person.ToList()); + Context.Library.RemoveRange(Context.Library.ToList()); + Context.AppUser.RemoveRange(Context.AppUser.ToList()); + await UnitOfWork.CommitAsync(); + } + + private async Task SeedDb() + { + _fullAccess = new AppUserBuilder("amelia", "amelia@example.com").Build(); + _restrictedAccess = new AppUserBuilder("mila", "mila@example.com").Build(); + _restrictedAgeAccess = new AppUserBuilder("eva", "eva@example.com").Build(); + _restrictedAgeAccess.AgeRestriction = AgeRating.Teen; + _restrictedAgeAccess.AgeRestrictionIncludeUnknowns = true; + + Context.AppUser.Add(_fullAccess); + Context.AppUser.Add(_restrictedAccess); + Context.AppUser.Add(_restrictedAgeAccess); + await Context.SaveChangesAsync(); + + var people = CreateTestPeople(); + Context.Person.AddRange(people); + await Context.SaveChangesAsync(); + + var libraries = CreateTestLibraries(people); + Context.Library.AddRange(libraries); + await Context.SaveChangesAsync(); + + _fullAccess.Libraries.Add(libraries[0]); // lib0 + _fullAccess.Libraries.Add(libraries[1]); // lib1 + _restrictedAccess.Libraries.Add(libraries[1]); // lib1 only + _restrictedAgeAccess.Libraries.Add(libraries[1]); // lib1 only + + await Context.SaveChangesAsync(); + } + + private static List CreateTestPeople() + { + return new List + { + new PersonBuilder("Shared Series Chapter Person").Build(), + new PersonBuilder("Shared Series Person").Build(), + new PersonBuilder("Shared Chapters Person").Build(), + new PersonBuilder("Lib0 Series Chapter Person").Build(), + new PersonBuilder("Lib0 Series Person").Build(), + new PersonBuilder("Lib0 Chapters Person").Build(), + new PersonBuilder("Lib1 Series Chapter Person").Build(), + new PersonBuilder("Lib1 Series Person").Build(), + new PersonBuilder("Lib1 Chapters Person").Build(), + new PersonBuilder("Lib1 Chapter Age Person").Build() + }; + } + + private static List CreateTestLibraries(List people) + { + var lib0 = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("lib0-s0") + .WithMetadata(new SeriesMetadataBuilder() + .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Writer) + .WithPerson(GetPersonByName(people, "Shared Series Person"), PersonRole.Writer) + .WithPerson(GetPersonByName(people, "Lib0 Series Chapter Person"), PersonRole.Writer) + .WithPerson(GetPersonByName(people, "Lib0 Series Person"), PersonRole.Writer) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1") + .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Colorist) + .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Colorist) + .WithPerson(GetPersonByName(people, "Lib0 Series Chapter Person"), PersonRole.Colorist) + .WithPerson(GetPersonByName(people, "Lib0 Chapters Person"), PersonRole.Colorist) + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Editor) + .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Editor) + .WithPerson(GetPersonByName(people, "Lib0 Series Chapter Person"), PersonRole.Editor) + .WithPerson(GetPersonByName(people, "Lib0 Chapters Person"), PersonRole.Editor) + .Build()) + .Build()) + .Build()) + .Build(); + + var lib1 = new LibraryBuilder("lib1") + .WithSeries(new SeriesBuilder("lib1-s0") + .WithMetadata(new SeriesMetadataBuilder() + .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Letterer) + .WithPerson(GetPersonByName(people, "Shared Series Person"), PersonRole.Letterer) + .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Letterer) + .WithPerson(GetPersonByName(people, "Lib1 Series Person"), PersonRole.Letterer) + .WithAgeRating(AgeRating.Mature17Plus) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1") + .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Imprint) + .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Imprint) + .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Imprint) + .WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.Imprint) + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.CoverArtist) + .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.CoverArtist) + .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.CoverArtist) + .WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.CoverArtist) + .WithPerson(GetPersonByName(people, "Lib1 Chapter Age Person"), PersonRole.CoverArtist) + .WithAgeRating(AgeRating.Mature17Plus) + .Build()) + .Build()) + .Build()) + .WithSeries(new SeriesBuilder("lib1-s1") + .WithMetadata(new SeriesMetadataBuilder() + .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Inker) + .WithPerson(GetPersonByName(people, "Shared Series Person"), PersonRole.Inker) + .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Inker) + .WithPerson(GetPersonByName(people, "Lib1 Series Person"), PersonRole.Inker) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1") + .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Team) + .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Team) + .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Team) + .WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.Team) + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithPerson(GetPersonByName(people, "Shared Series Chapter Person"), PersonRole.Translator) + .WithPerson(GetPersonByName(people, "Shared Chapters Person"), PersonRole.Translator) + .WithPerson(GetPersonByName(people, "Lib1 Series Chapter Person"), PersonRole.Translator) + .WithPerson(GetPersonByName(people, "Lib1 Chapters Person"), PersonRole.Translator) + .Build()) + .Build()) + .Build()) + .Build(); + + return new List { lib0, lib1 }; + } + + private static Person GetPersonByName(List people, string name) + { + return people.First(p => p.Name == name); + } + + private Person GetPersonByName(string name) + { + return Context.Person.First(p => p.Name == name); + } + + private static Predicate ContainsPersonCheck(Person person) + { + return p => p.Id == person.Id; + } + + [Fact] + public async Task GetBrowsePersonDtos() + { + await ResetDb(); + await SeedDb(); + + // Get people from database for assertions + var sharedSeriesChaptersPerson = GetPersonByName("Shared Series Chapter Person"); + var lib0SeriesPerson = GetPersonByName("Lib0 Series Person"); + var lib1SeriesPerson = GetPersonByName("Lib1 Series Person"); + var lib1ChapterAgePerson = GetPersonByName("Lib1 Chapter Age Person"); + var allPeople = Context.Person.ToList(); + + var fullAccessPeople = + await UnitOfWork.PersonRepository.GetBrowsePersonDtos(_fullAccess.Id, new BrowsePersonFilterDto(), + new UserParams()); + Assert.Equal(allPeople.Count, fullAccessPeople.TotalCount); + + foreach (var person in allPeople) + Assert.Contains(fullAccessPeople, ContainsPersonCheck(person)); + + // 1 series in lib0, 2 series in lib1 + Assert.Equal(3, fullAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).SeriesCount); + // 3 series with each 2 chapters + Assert.Equal(6, fullAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).ChapterCount); + // 1 series in lib0 + Assert.Equal(1, fullAccessPeople.First(dto => dto.Id == lib0SeriesPerson.Id).SeriesCount); + // 2 series in lib1 + Assert.Equal(2, fullAccessPeople.First(dto => dto.Id == lib1SeriesPerson.Id).SeriesCount); + + var restrictedAccessPeople = + await UnitOfWork.PersonRepository.GetBrowsePersonDtos(_restrictedAccess.Id, new BrowsePersonFilterDto(), + new UserParams()); + + Assert.Equal(7, restrictedAccessPeople.TotalCount); + + Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Shared Series Chapter Person"))); + Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Shared Series Person"))); + Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Shared Chapters Person"))); + Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Series Chapter Person"))); + Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Series Person"))); + Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Chapters Person"))); + Assert.Contains(restrictedAccessPeople, ContainsPersonCheck(GetPersonByName("Lib1 Chapter Age Person"))); + + // 2 series in lib1, no series in lib0 + Assert.Equal(2, restrictedAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).SeriesCount); + // 2 series with each 2 chapters + Assert.Equal(4, restrictedAccessPeople.First(dto => dto.Id == sharedSeriesChaptersPerson.Id).ChapterCount); + // 2 series in lib1 + Assert.Equal(2, restrictedAccessPeople.First(dto => dto.Id == lib1SeriesPerson.Id).SeriesCount); + + var restrictedAgeAccessPeople = await UnitOfWork.PersonRepository.GetBrowsePersonDtos(_restrictedAgeAccess.Id, + new BrowsePersonFilterDto(), new UserParams()); + + // Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up + Assert.Equal(6, restrictedAgeAccessPeople.TotalCount); + + // No access to the age restricted chapter + Assert.DoesNotContain(restrictedAgeAccessPeople, ContainsPersonCheck(lib1ChapterAgePerson)); + } + + [Fact] + public async Task GetRolesForPersonByName() + { + await ResetDb(); + await SeedDb(); + + var sharedSeriesPerson = GetPersonByName("Shared Series Person"); + var sharedChaptersPerson = GetPersonByName("Shared Chapters Person"); + var lib1ChapterAgePerson = GetPersonByName("Lib1 Chapter Age Person"); + + var sharedSeriesRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, _fullAccess.Id); + var chapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, _fullAccess.Id); + var ageChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, _fullAccess.Id); + Assert.Equal(3, sharedSeriesRoles.Count()); + Assert.Equal(6, chapterRoles.Count()); + Assert.Single(ageChapterRoles); + + var restrictedRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, _restrictedAccess.Id); + var restrictedChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, _restrictedAccess.Id); + var restrictedAgePersonChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, _restrictedAccess.Id); + Assert.Equal(2, restrictedRoles.Count()); + Assert.Equal(4, restrictedChapterRoles.Count()); + Assert.Single(restrictedAgePersonChapterRoles); + + var restrictedAgeRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, _restrictedAgeAccess.Id); + var restrictedAgeChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, _restrictedAgeAccess.Id); + var restrictedAgeAgePersonChapterRoles = await UnitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, _restrictedAgeAccess.Id); + Assert.Single(restrictedAgeRoles); + Assert.Equal(2, restrictedAgeChapterRoles.Count()); + // Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up + Assert.Empty(restrictedAgeAgePersonChapterRoles); + } + + [Fact] + public async Task GetPersonDtoByName() + { + await ResetDb(); + await SeedDb(); + + var allPeople = Context.Person.ToList(); + + foreach (var person in allPeople) + { + Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName(person.Name, _fullAccess.Id)); + } + + Assert.Null(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib0 Chapters Person", _restrictedAccess.Id)); + Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName("Shared Series Person", _restrictedAccess.Id)); + Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib1 Series Person", _restrictedAccess.Id)); + + Assert.Null(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib0 Chapters Person", _restrictedAgeAccess.Id)); + Assert.NotNull(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib1 Series Person", _restrictedAgeAccess.Id)); + // Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up + Assert.Null(await UnitOfWork.PersonRepository.GetPersonDtoByName("Lib1 Chapter Age Person", _restrictedAgeAccess.Id)); + } + + [Fact] + public async Task GetSeriesKnownFor() + { + await ResetDb(); + await SeedDb(); + + var sharedSeriesPerson = GetPersonByName("Shared Series Person"); + var lib1SeriesPerson = GetPersonByName("Lib1 Series Person"); + + var series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, _fullAccess.Id); + Assert.Equal(3, series.Count()); + + series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, _restrictedAccess.Id); + Assert.Equal(2, series.Count()); + + series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, _restrictedAgeAccess.Id); + Assert.Single(series); + + series = await UnitOfWork.PersonRepository.GetSeriesKnownFor(lib1SeriesPerson.Id, _restrictedAgeAccess.Id); + Assert.Single(series); + } + + [Fact] + public async Task GetChaptersForPersonByRole() + { + await ResetDb(); + await SeedDb(); + + var sharedChaptersPerson = GetPersonByName("Shared Chapters Person"); + + // Lib0 + var chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _fullAccess.Id, PersonRole.Colorist); + var restrictedChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAccess.Id, PersonRole.Colorist); + var restrictedAgeChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAgeAccess.Id, PersonRole.Colorist); + Assert.Single(chapters); + Assert.Empty(restrictedChapters); + Assert.Empty(restrictedAgeChapters); + + // Lib1 - age restricted series + chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _fullAccess.Id, PersonRole.Imprint); + restrictedChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAccess.Id, PersonRole.Imprint); + restrictedAgeChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAgeAccess.Id, PersonRole.Imprint); + Assert.Single(chapters); + Assert.Single(restrictedChapters); + Assert.Empty(restrictedAgeChapters); + + // Lib1 - not age restricted series + chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _fullAccess.Id, PersonRole.Team); + restrictedChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAccess.Id, PersonRole.Team); + restrictedAgeChapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, _restrictedAgeAccess.Id, PersonRole.Team); + Assert.Single(chapters); + Assert.Single(restrictedChapters); + Assert.Single(restrictedAgeChapters); + } +} diff --git a/API.Tests/Repository/TagRepositoryTests.cs b/API.Tests/Repository/TagRepositoryTests.cs new file mode 100644 index 000000000..229082eb6 --- /dev/null +++ b/API.Tests/Repository/TagRepositoryTests.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.DTOs.Metadata.Browse; +using API.Entities; +using API.Entities.Enums; +using API.Entities.Metadata; +using API.Helpers; +using API.Helpers.Builders; +using Xunit; + +namespace API.Tests.Repository; + +public class TagRepositoryTests : AbstractDbTest +{ + private AppUser _fullAccess; + private AppUser _restrictedAccess; + private AppUser _restrictedAgeAccess; + + protected override async Task ResetDb() + { + Context.Tag.RemoveRange(Context.Tag); + Context.Library.RemoveRange(Context.Library); + await Context.SaveChangesAsync(); + } + + private TestTagSet CreateTestTags() + { + return new TestTagSet + { + SharedSeriesChaptersTag = new TagBuilder("Shared Series Chapter Tag").Build(), + SharedSeriesTag = new TagBuilder("Shared Series Tag").Build(), + SharedChaptersTag = new TagBuilder("Shared Chapters Tag").Build(), + Lib0SeriesChaptersTag = new TagBuilder("Lib0 Series Chapter Tag").Build(), + Lib0SeriesTag = new TagBuilder("Lib0 Series Tag").Build(), + Lib0ChaptersTag = new TagBuilder("Lib0 Chapters Tag").Build(), + Lib1SeriesChaptersTag = new TagBuilder("Lib1 Series Chapter Tag").Build(), + Lib1SeriesTag = new TagBuilder("Lib1 Series Tag").Build(), + Lib1ChaptersTag = new TagBuilder("Lib1 Chapters Tag").Build(), + Lib1ChapterAgeTag = new TagBuilder("Lib1 Chapter Age Tag").Build() + }; + } + + private async Task SeedDbWithTags(TestTagSet tags) + { + await CreateTestUsers(); + await AddTagsToContext(tags); + await CreateLibrariesWithTags(tags); + await AssignLibrariesToUsers(); + } + + private async Task CreateTestUsers() + { + _fullAccess = new AppUserBuilder("amelia", "amelia@example.com").Build(); + _restrictedAccess = new AppUserBuilder("mila", "mila@example.com").Build(); + _restrictedAgeAccess = new AppUserBuilder("eva", "eva@example.com").Build(); + _restrictedAgeAccess.AgeRestriction = AgeRating.Teen; + _restrictedAgeAccess.AgeRestrictionIncludeUnknowns = true; + + Context.Users.Add(_fullAccess); + Context.Users.Add(_restrictedAccess); + Context.Users.Add(_restrictedAgeAccess); + await Context.SaveChangesAsync(); + } + + private async Task AddTagsToContext(TestTagSet tags) + { + var allTags = tags.GetAllTags(); + Context.Tag.AddRange(allTags); + await Context.SaveChangesAsync(); + } + + private async Task CreateLibrariesWithTags(TestTagSet tags) + { + var lib0 = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("lib0-s0") + .WithMetadata(new SeriesMetadata + { + Tags = [tags.SharedSeriesChaptersTag, tags.SharedSeriesTag, tags.Lib0SeriesChaptersTag, tags.Lib0SeriesTag] + }) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1") + .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib0SeriesChaptersTag, tags.Lib0ChaptersTag]) + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag]) + .Build()) + .Build()) + .Build()) + .Build(); + + var lib1 = new LibraryBuilder("lib1") + .WithSeries(new SeriesBuilder("lib1-s0") + .WithMetadata(new SeriesMetadataBuilder() + .WithTags([tags.SharedSeriesChaptersTag, tags.SharedSeriesTag, tags.Lib1SeriesChaptersTag, tags.Lib1SeriesTag]) + .WithAgeRating(AgeRating.Mature17Plus) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1") + .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag]) + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag, tags.Lib1ChapterAgeTag]) + .WithAgeRating(AgeRating.Mature17Plus) + .Build()) + .Build()) + .Build()) + .WithSeries(new SeriesBuilder("lib1-s1") + .WithMetadata(new SeriesMetadataBuilder() + .WithTags([tags.SharedSeriesChaptersTag, tags.SharedSeriesTag, tags.Lib1SeriesChaptersTag, tags.Lib1SeriesTag]) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1") + .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag]) + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithTags([tags.SharedSeriesChaptersTag, tags.SharedChaptersTag, tags.Lib1SeriesChaptersTag, tags.Lib1ChaptersTag]) + .WithAgeRating(AgeRating.Mature17Plus) + .Build()) + .Build()) + .Build()) + .Build(); + + Context.Library.Add(lib0); + Context.Library.Add(lib1); + await Context.SaveChangesAsync(); + } + + private async Task AssignLibrariesToUsers() + { + var lib0 = Context.Library.First(l => l.Name == "lib0"); + var lib1 = Context.Library.First(l => l.Name == "lib1"); + + _fullAccess.Libraries.Add(lib0); + _fullAccess.Libraries.Add(lib1); + _restrictedAccess.Libraries.Add(lib1); + _restrictedAgeAccess.Libraries.Add(lib1); + + await Context.SaveChangesAsync(); + } + + private static Predicate ContainsTagCheck(Tag tag) + { + return t => t.Id == tag.Id; + } + + private static void AssertTagPresent(IEnumerable tags, Tag expectedTag) + { + Assert.Contains(tags, ContainsTagCheck(expectedTag)); + } + + private static void AssertTagNotPresent(IEnumerable tags, Tag expectedTag) + { + Assert.DoesNotContain(tags, ContainsTagCheck(expectedTag)); + } + + private static BrowseTagDto GetTagDto(IEnumerable tags, Tag tag) + { + return tags.First(dto => dto.Id == tag.Id); + } + + [Fact] + public async Task GetBrowseableTag_FullAccess_ReturnsAllTagsWithCorrectCounts() + { + // Arrange + await ResetDb(); + var tags = CreateTestTags(); + await SeedDbWithTags(tags); + + // Act + var fullAccessTags = await UnitOfWork.TagRepository.GetBrowseableTag(_fullAccess.Id, new UserParams()); + + // Assert + Assert.Equal(tags.GetAllTags().Count, fullAccessTags.TotalCount); + + foreach (var tag in tags.GetAllTags()) + { + AssertTagPresent(fullAccessTags, tag); + } + + // Verify counts - 1 series lib0, 2 series lib1 = 3 total series + Assert.Equal(3, GetTagDto(fullAccessTags, tags.SharedSeriesChaptersTag).SeriesCount); + Assert.Equal(6, GetTagDto(fullAccessTags, tags.SharedSeriesChaptersTag).ChapterCount); + Assert.Equal(1, GetTagDto(fullAccessTags, tags.Lib0SeriesTag).SeriesCount); + } + + [Fact] + public async Task GetBrowseableTag_RestrictedAccess_ReturnsOnlyAccessibleTags() + { + // Arrange + await ResetDb(); + var tags = CreateTestTags(); + await SeedDbWithTags(tags); + + // Act + var restrictedAccessTags = await UnitOfWork.TagRepository.GetBrowseableTag(_restrictedAccess.Id, new UserParams()); + + // Assert - Should see: 3 shared + 4 library 1 specific = 7 tags + Assert.Equal(7, restrictedAccessTags.TotalCount); + + // Verify shared and Library 1 tags are present + AssertTagPresent(restrictedAccessTags, tags.SharedSeriesChaptersTag); + AssertTagPresent(restrictedAccessTags, tags.SharedSeriesTag); + AssertTagPresent(restrictedAccessTags, tags.SharedChaptersTag); + AssertTagPresent(restrictedAccessTags, tags.Lib1SeriesChaptersTag); + AssertTagPresent(restrictedAccessTags, tags.Lib1SeriesTag); + AssertTagPresent(restrictedAccessTags, tags.Lib1ChaptersTag); + AssertTagPresent(restrictedAccessTags, tags.Lib1ChapterAgeTag); + + // Verify Library 0 specific tags are not present + AssertTagNotPresent(restrictedAccessTags, tags.Lib0SeriesChaptersTag); + AssertTagNotPresent(restrictedAccessTags, tags.Lib0SeriesTag); + AssertTagNotPresent(restrictedAccessTags, tags.Lib0ChaptersTag); + + // Verify counts - 2 series lib1 + Assert.Equal(2, GetTagDto(restrictedAccessTags, tags.SharedSeriesChaptersTag).SeriesCount); + Assert.Equal(4, GetTagDto(restrictedAccessTags, tags.SharedSeriesChaptersTag).ChapterCount); + Assert.Equal(2, GetTagDto(restrictedAccessTags, tags.Lib1SeriesTag).SeriesCount); + Assert.Equal(4, GetTagDto(restrictedAccessTags, tags.Lib1ChaptersTag).ChapterCount); + } + + [Fact] + public async Task GetBrowseableTag_RestrictedAgeAccess_FiltersAgeRestrictedContent() + { + // Arrange + await ResetDb(); + var tags = CreateTestTags(); + await SeedDbWithTags(tags); + + // Act + var restrictedAgeAccessTags = await UnitOfWork.TagRepository.GetBrowseableTag(_restrictedAgeAccess.Id, new UserParams()); + + // Assert - Should see: 3 shared + 3 lib1 specific = 6 tags (age-restricted tag filtered out) + Assert.Equal(6, restrictedAgeAccessTags.TotalCount); + + // Verify accessible tags are present + AssertTagPresent(restrictedAgeAccessTags, tags.SharedSeriesChaptersTag); + AssertTagPresent(restrictedAgeAccessTags, tags.SharedSeriesTag); + AssertTagPresent(restrictedAgeAccessTags, tags.SharedChaptersTag); + AssertTagPresent(restrictedAgeAccessTags, tags.Lib1SeriesChaptersTag); + AssertTagPresent(restrictedAgeAccessTags, tags.Lib1SeriesTag); + AssertTagPresent(restrictedAgeAccessTags, tags.Lib1ChaptersTag); + + // Verify age-restricted tag is filtered out + AssertTagNotPresent(restrictedAgeAccessTags, tags.Lib1ChapterAgeTag); + + // Verify counts - 1 series lib1 (age-restricted series filtered out) + Assert.Equal(1, GetTagDto(restrictedAgeAccessTags, tags.SharedSeriesChaptersTag).SeriesCount); + Assert.Equal(2, GetTagDto(restrictedAgeAccessTags, tags.SharedSeriesChaptersTag).ChapterCount); + Assert.Equal(1, GetTagDto(restrictedAgeAccessTags, tags.Lib1SeriesTag).SeriesCount); + Assert.Equal(2, GetTagDto(restrictedAgeAccessTags, tags.Lib1ChaptersTag).ChapterCount); + } + + private class TestTagSet + { + public Tag SharedSeriesChaptersTag { get; set; } + public Tag SharedSeriesTag { get; set; } + public Tag SharedChaptersTag { get; set; } + public Tag Lib0SeriesChaptersTag { get; set; } + public Tag Lib0SeriesTag { get; set; } + public Tag Lib0ChaptersTag { get; set; } + public Tag Lib1SeriesChaptersTag { get; set; } + public Tag Lib1SeriesTag { get; set; } + public Tag Lib1ChaptersTag { get; set; } + public Tag Lib1ChapterAgeTag { get; set; } + + public List GetAllTags() + { + return + [ + SharedSeriesChaptersTag, SharedSeriesTag, SharedChaptersTag, + Lib0SeriesChaptersTag, Lib0SeriesTag, Lib0ChaptersTag, + Lib1SeriesChaptersTag, Lib1SeriesTag, Lib1ChaptersTag, Lib1ChapterAgeTag + ]; + } + } +} diff --git a/API.Tests/Services/ExternalMetadataServiceTests.cs b/API.Tests/Services/ExternalMetadataServiceTests.cs index 8278f3b1a..973b7c6df 100644 --- a/API.Tests/Services/ExternalMetadataServiceTests.cs +++ b/API.Tests/Services/ExternalMetadataServiceTests.cs @@ -15,6 +15,7 @@ using API.Entities.Person; using API.Helpers.Builders; using API.Services.Plus; using API.Services.Tasks.Metadata; +using API.Services.Tasks.Scanner.Parser; using API.SignalR; using Hangfire; using Microsoft.EntityFrameworkCore; @@ -881,6 +882,217 @@ public class ExternalMetadataServiceTests : AbstractDbTest } + [Fact] + public void IsSeriesCompleted_ExactMatch() + { + const string seriesName = "Test - Exact Match"; + var series = new SeriesBuilder(seriesName) + .WithLibraryId(1) + .WithMetadata(new SeriesMetadataBuilder() + .WithMaxCount(5) + .WithTotalCount(5) + .Build()) + .Build(); + + var chapters = new List(); + var externalMetadata = new ExternalSeriesDetailDto { Chapters = 5, Volumes = 0 }; + + var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber); + + Assert.True(result); + } + + [Fact] + public void IsSeriesCompleted_Volumes_DecimalVolumes() + { + const string seriesName = "Test - Volume Complete"; + var series = new SeriesBuilder(seriesName) + .WithLibraryId(1) + .WithMetadata(new SeriesMetadataBuilder() + .WithMaxCount(2) + .WithTotalCount(3) + .Build()) + .WithVolume(new VolumeBuilder("1").WithNumber(1).Build()) + .WithVolume(new VolumeBuilder("2").WithNumber(2).Build()) + .WithVolume(new VolumeBuilder("2.5").WithNumber(2.5f).Build()) + .Build(); + + var chapters = new List(); + // External metadata includes decimal volume 2.5 + var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 3 }; + + var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2); + + Assert.True(result); + Assert.Equal(3, series.Metadata.MaxCount); + Assert.Equal(3, series.Metadata.TotalCount); + } + + /// + /// This is validating that we get a completed even though we have a special chapter and AL doesn't count it + /// + [Fact] + public void IsSeriesCompleted_Volumes_HasSpecialAndDecimal_ExternalNoSpecial() + { + const string seriesName = "Test - Volume Complete"; + var series = new SeriesBuilder(seriesName) + .WithLibraryId(1) + .WithMetadata(new SeriesMetadataBuilder() + .WithMaxCount(2) + .WithTotalCount(3) + .Build()) + .WithVolume(new VolumeBuilder("1").WithNumber(1).Build()) + .WithVolume(new VolumeBuilder("1.5").WithNumber(1.5f).Build()) + .WithVolume(new VolumeBuilder("2").WithNumber(2).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume).Build()) + .Build(); + + var chapters = new List(); + // External metadata includes volume 1.5, but not the special + var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 3 }; + + var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2); + + Assert.True(result); + Assert.Equal(3, series.Metadata.MaxCount); + Assert.Equal(3, series.Metadata.TotalCount); + } + + /// + /// This unit test also illustrates the bug where you may get a false positive if you had Volumes 1,2, and 2.1. While + /// missing volume 3. With the external metadata expecting non-decimal volumes. + /// i.e. it would fail if we only had one decimal volume + /// + [Fact] + public void IsSeriesCompleted_Volumes_TooManyDecimalVolumes() + { + const string seriesName = "Test - Volume Complete"; + var series = new SeriesBuilder(seriesName) + .WithLibraryId(1) + .WithMetadata(new SeriesMetadataBuilder() + .WithMaxCount(2) + .WithTotalCount(3) + .Build()) + .WithVolume(new VolumeBuilder("1").WithNumber(1).Build()) + .WithVolume(new VolumeBuilder("2").WithNumber(2).Build()) + .WithVolume(new VolumeBuilder("2.1").WithNumber(2.1f).Build()) + .WithVolume(new VolumeBuilder("2.2").WithNumber(2.2f).Build()) + .Build(); + + var chapters = new List(); + // External metadata includes no special or decimals. There are 3 volumes. And we're missing volume 3 + var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 3 }; + + var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2); + + Assert.False(result); + } + + [Fact] + public void IsSeriesCompleted_NoVolumes_GEQChapterCheck() + { + // We own 11 chapters, the external metadata expects 10 + const string seriesName = "Test - Chapter MaxCount, no volumes"; + var series = new SeriesBuilder(seriesName) + .WithLibraryId(1) + .WithMetadata(new SeriesMetadataBuilder() + .WithMaxCount(11) + .WithTotalCount(10) + .Build()) + .Build(); + + var chapters = new List(); + var externalMetadata = new ExternalSeriesDetailDto { Chapters = 10, Volumes = 0 }; + + var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber); + + Assert.True(result); + Assert.Equal(11, series.Metadata.TotalCount); + Assert.Equal(11, series.Metadata.MaxCount); + } + + [Fact] + public void IsSeriesCompleted_NoVolumes_IncludeAllChaptersCheck() + { + const string seriesName = "Test - Chapter Count"; + var series = new SeriesBuilder(seriesName) + .WithLibraryId(1) + .WithMetadata(new SeriesMetadataBuilder() + .WithMaxCount(7) + .WithTotalCount(10) + .Build()) + .Build(); + + var chapters = new List + { + new ChapterBuilder("0").Build(), + new ChapterBuilder("2").Build(), + new ChapterBuilder("3").Build(), + new ChapterBuilder("4").Build(), + new ChapterBuilder("5").Build(), + new ChapterBuilder("6").Build(), + new ChapterBuilder("7").Build(), + new ChapterBuilder("7.1").Build(), + new ChapterBuilder("7.2").Build(), + new ChapterBuilder("7.3").Build() + }; + // External metadata includes prologues (0) and extra's (7.X) + var externalMetadata = new ExternalSeriesDetailDto { Chapters = 10, Volumes = 0 }; + + var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber); + + Assert.True(result); + Assert.Equal(10, series.Metadata.TotalCount); + Assert.Equal(10, series.Metadata.MaxCount); + } + + [Fact] + public void IsSeriesCompleted_NotEnoughVolumes() + { + const string seriesName = "Test - Incomplete Volume"; + var series = new SeriesBuilder(seriesName) + .WithLibraryId(1) + .WithMetadata(new SeriesMetadataBuilder() + .WithMaxCount(2) + .WithTotalCount(5) + .Build()) + .WithVolume(new VolumeBuilder("1").WithNumber(1).Build()) + .WithVolume(new VolumeBuilder("2").WithNumber(2).Build()) + .Build(); + + var chapters = new List(); + var externalMetadata = new ExternalSeriesDetailDto { Chapters = 0, Volumes = 5 }; + + var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, 2); + + Assert.False(result); + } + + [Fact] + public void IsSeriesCompleted_NoVolumes_NotEnoughChapters() + { + const string seriesName = "Test - Incomplete Chapter"; + var series = new SeriesBuilder(seriesName) + .WithLibraryId(1) + .WithMetadata(new SeriesMetadataBuilder() + .WithMaxCount(5) + .WithTotalCount(8) + .Build()) + .Build(); + + var chapters = new List + { + new ChapterBuilder("1").Build(), + new ChapterBuilder("2").Build(), + new ChapterBuilder("3").Build() + }; + var externalMetadata = new ExternalSeriesDetailDto { Chapters = 10, Volumes = 0 }; + + var result = ExternalMetadataService.IsSeriesCompleted(series, chapters, externalMetadata, Parser.DefaultChapterNumber); + + Assert.False(result); + } + #endregion diff --git a/API/Controllers/PersonController.cs b/API/Controllers/PersonController.cs index bf3cc1814..7328ff954 100644 --- a/API/Controllers/PersonController.cs +++ b/API/Controllers/PersonController.cs @@ -185,7 +185,7 @@ public class PersonController : BaseApiController [HttpGet("series-known-for")] public async Task>> GetKnownSeries(int personId) { - return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId)); + return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId, User.GetUserId())); } /// @@ -206,6 +206,7 @@ public class PersonController : BaseApiController /// /// [HttpPost("merge")] + [Authorize("RequireAdminRole")] public async Task> MergePeople(PersonMergeDto dto) { var dst = await _unitOfWork.PersonRepository.GetPersonById(dto.DestId, PersonIncludes.All); diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs b/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs index a3cd378b2..6704bf697 100644 --- a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs +++ b/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs @@ -29,7 +29,9 @@ public sealed record ExternalSeriesDetailDto public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } public int AverageScore { get; set; } + /// AniList returns the total count of unique chapters, includes 1.1 for example public int Chapters { get; set; } + /// AniList returns the total count of unique volumes, includes 1.1 for example public int Volumes { get; set; } public IList? Relations { get; set; } = []; public IList? Characters { get; set; } = []; diff --git a/API/Data/Repositories/GenreRepository.cs b/API/Data/Repositories/GenreRepository.cs index 3e645cb2e..d3baa4de6 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/API/Data/Repositories/GenreRepository.cs @@ -173,20 +173,30 @@ public class GenreRepository : IGenreRepository { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var allLibrariesCount = await _context.Library.CountAsync(); + var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + + var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync(); + var query = _context.Genre .RestrictAgainstAgeRestriction(ageRating) + .WhereIf(allLibrariesCount != userLibs.Count, + genre => genre.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) || + genre.SeriesMetadatas.Any(sm => seriesIds.Contains(sm.SeriesId))) .Select(g => new BrowseGenreDto { Id = g.Id, Title = g.Title, SeriesCount = g.SeriesMetadatas - .Select(sm => sm.Id) + .Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId)) + .RestrictAgainstAgeRestriction(ageRating) .Distinct() .Count(), ChapterCount = g.Chapters - .Select(ch => ch.Id) + .Where(cp => allLibrariesCount == userLibs.Count || seriesIds.Contains(cp.Volume.SeriesId)) + .RestrictAgainstAgeRestriction(ageRating) .Distinct() - .Count() + .Count(), }) .OrderBy(g => g.Title); diff --git a/API/Data/Repositories/PersonRepository.cs b/API/Data/Repositories/PersonRepository.cs index 6954ccf03..26045c74c 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/API/Data/Repositories/PersonRepository.cs @@ -63,7 +63,7 @@ public interface IPersonRepository Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases); Task IsNameUnique(string name); - Task> GetSeriesKnownFor(int personId); + Task> GetSeriesKnownFor(int personId, int userId); Task> GetChaptersForPersonByRole(int personId, int userId, PersonRole role); /// /// Returns all people with a matching name, or alias @@ -179,20 +179,25 @@ public class PersonRepository : IPersonRepository public async Task> GetRolesForPersonByName(int personId, int userId) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userLibs = _context.Library.GetUserLibraries(userId); // Query roles from ChapterPeople var chapterRoles = await _context.Person .Where(p => p.Id == personId) + .SelectMany(p => p.ChapterPeople) .RestrictAgainstAgeRestriction(ageRating) - .SelectMany(p => p.ChapterPeople.Select(cp => cp.Role)) + .RestrictByLibrary(userLibs) + .Select(cp => cp.Role) .Distinct() .ToListAsync(); // Query roles from SeriesMetadataPeople var seriesRoles = await _context.Person .Where(p => p.Id == personId) + .SelectMany(p => p.SeriesMetadataPeople) .RestrictAgainstAgeRestriction(ageRating) - .SelectMany(p => p.SeriesMetadataPeople.Select(smp => smp.Role)) + .RestrictByLibrary(userLibs) + .Select(smp => smp.Role) .Distinct() .ToListAsync(); @@ -204,44 +209,53 @@ public class PersonRepository : IPersonRepository { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var query = CreateFilteredPersonQueryable(userId, filter, ageRating); + var query = await CreateFilteredPersonQueryable(userId, filter, ageRating); return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } - private IQueryable CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating) + private async Task> CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating) { + var allLibrariesCount = await _context.Library.CountAsync(); + var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + + var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync(); + var query = _context.Person.AsNoTracking(); // Apply filtering based on statements query = BuildPersonFilterQuery(userId, filter, query); - // Apply age restriction - query = query.RestrictAgainstAgeRestriction(ageRating); + // Apply restrictions + query = query.RestrictAgainstAgeRestriction(ageRating) + .WhereIf(allLibrariesCount != userLibs.Count, + person => person.ChapterPeople.Any(cp => seriesIds.Contains(cp.Chapter.Volume.SeriesId)) || + person.SeriesMetadataPeople.Any(smp => seriesIds.Contains(smp.SeriesMetadata.SeriesId))); // Apply sorting and limiting var sortedQuery = query.SortBy(filter.SortOptions); var limitedQuery = ApplyPersonLimit(sortedQuery, filter.LimitTo); - // Project to DTO - var projectedQuery = limitedQuery.Select(p => new BrowsePersonDto + return limitedQuery.Select(p => new BrowsePersonDto { Id = p.Id, Name = p.Name, Description = p.Description, CoverImage = p.CoverImage, SeriesCount = p.SeriesMetadataPeople - .Select(smp => smp.SeriesMetadata.SeriesId) + .Select(smp => smp.SeriesMetadata) + .Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId)) + .RestrictAgainstAgeRestriction(ageRating) .Distinct() .Count(), ChapterCount = p.ChapterPeople - .Select(cp => cp.Chapter.Id) + .Select(chp => chp.Chapter) + .Where(ch => allLibrariesCount == userLibs.Count || seriesIds.Contains(ch.Volume.SeriesId)) + .RestrictAgainstAgeRestriction(ageRating) .Distinct() - .Count() + .Count(), }); - - return projectedQuery; } private static IQueryable BuildPersonFilterQuery(int userId, BrowsePersonFilterDto filterDto, IQueryable query) @@ -287,11 +301,13 @@ public class PersonRepository : IPersonRepository { var normalized = name.ToNormalized(); var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userLibs = _context.Library.GetUserLibraries(userId); return await _context.Person .Where(p => p.NormalizedName == normalized) .Includes(includes) .RestrictAgainstAgeRestriction(ageRating) + .RestrictByLibrary(userLibs) .ProjectTo(_mapper.ConfigurationProvider) .FirstOrDefaultAsync(); } @@ -313,14 +329,18 @@ public class PersonRepository : IPersonRepository .AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name))); } - public async Task> GetSeriesKnownFor(int personId) + public async Task> GetSeriesKnownFor(int personId, int userId) { - List notValidRoles = [PersonRole.Location, PersonRole.Team, PersonRole.Other, PersonRole.Publisher, PersonRole.Translator]; + var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + return await _context.Person .Where(p => p.Id == personId) - .SelectMany(p => p.SeriesMetadataPeople.Where(smp => !notValidRoles.Contains(smp.Role))) + .SelectMany(p => p.SeriesMetadataPeople) .Select(smp => smp.SeriesMetadata) .Select(sm => sm.Series) + .RestrictAgainstAgeRestriction(ageRating) + .Where(s => userLibs.Contains(s.LibraryId)) .Distinct() .OrderByDescending(s => s.ExternalSeriesMetadata.AverageExternalRating) .Take(20) @@ -331,11 +351,13 @@ public class PersonRepository : IPersonRepository public async Task> GetChaptersForPersonByRole(int personId, int userId, PersonRole role) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userLibs = _context.Library.GetUserLibraries(userId); return await _context.ChapterPeople .Where(cp => cp.PersonId == personId && cp.Role == role) .Select(cp => cp.Chapter) .RestrictAgainstAgeRestriction(ageRating) + .RestrictByLibrary(userLibs) .OrderBy(ch => ch.SortOrder) .Take(20) .ProjectTo(_mapper.ConfigurationProvider) @@ -386,27 +408,31 @@ public class PersonRepository : IPersonRepository .ToListAsync(); } - public async Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.Aliases) + public async Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userLibs = _context.Library.GetUserLibraries(userId); return await _context.Person .Includes(includes) - .OrderBy(p => p.Name) .RestrictAgainstAgeRestriction(ageRating) + .RestrictByLibrary(userLibs) + .OrderBy(p => p.Name) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } - public async Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.Aliases) + public async Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userLibs = _context.Library.GetUserLibraries(userId); return await _context.Person .Where(p => p.SeriesMetadataPeople.Any(smp => smp.Role == role) || p.ChapterPeople.Any(cp => cp.Role == role)) // Filter by role in both series and chapters .Includes(includes) - .OrderBy(p => p.Name) .RestrictAgainstAgeRestriction(ageRating) + .RestrictByLibrary(userLibs) + .OrderBy(p => p.Name) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } diff --git a/API/Data/Repositories/TagRepository.cs b/API/Data/Repositories/TagRepository.cs index ea39d2b0d..40d40a675 100644 --- a/API/Data/Repositories/TagRepository.cs +++ b/API/Data/Repositories/TagRepository.cs @@ -111,18 +111,28 @@ public class TagRepository : ITagRepository { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var allLibrariesCount = await _context.Library.CountAsync(); + var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + + var seriesIds = _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id); + var query = _context.Tag .RestrictAgainstAgeRestriction(ageRating) + .WhereIf(userLibs.Count != allLibrariesCount, + tag => tag.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) || + tag.SeriesMetadatas.Any(sm => seriesIds.Contains(sm.SeriesId))) .Select(g => new BrowseTagDto { Id = g.Id, Title = g.Title, SeriesCount = g.SeriesMetadatas - .Select(sm => sm.Id) + .Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId)) + .RestrictAgainstAgeRestriction(ageRating) .Distinct() .Count(), ChapterCount = g.Chapters - .Select(ch => ch.Id) + .Where(ch => allLibrariesCount == userLibs.Count || seriesIds.Contains(ch.Volume.SeriesId)) + .RestrictAgainstAgeRestriction(ageRating) .Distinct() .Count() }) diff --git a/API/Extensions/EnumerableExtensions.cs b/API/Extensions/EnumerableExtensions.cs index 8beec88ca..9bc06bab4 100644 --- a/API/Extensions/EnumerableExtensions.cs +++ b/API/Extensions/EnumerableExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using API.Data.Misc; +using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; @@ -55,4 +56,16 @@ public static class EnumerableExtensions return q; } + + public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) + { + if (restriction.AgeRating == AgeRating.NotApplicable) return items; + var q = items.Where(s => s.AgeRating <= restriction.AgeRating); + if (!restriction.IncludeUnknowns) + { + return q.Where(s => s.AgeRating != AgeRating.Unknown); + } + + return q; + } } diff --git a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs index 350372e5b..e0738bdf3 100644 --- a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs +++ b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs @@ -27,6 +27,19 @@ public static class RestrictByAgeExtensions return q; } + public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) + { + if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; + var q = queryable.Where(s => s.SeriesMetadata.AgeRating <= restriction.AgeRating); + + if (!restriction.IncludeUnknowns) + { + return q.Where(s => s.SeriesMetadata.AgeRating != AgeRating.Unknown); + } + + return q; + } + public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) { @@ -41,6 +54,19 @@ public static class RestrictByAgeExtensions return q; } + public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) + { + if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; + var q = queryable.Where(cp => cp.Chapter.Volume.Series.Metadata.AgeRating <= restriction.AgeRating); + + if (!restriction.IncludeUnknowns) + { + return q.Where(cp => cp.Chapter.Volume.Series.Metadata.AgeRating != AgeRating.Unknown); + } + + return q; + } + public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) { diff --git a/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs b/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs index e69de29bb..9ec1b8621 100644 --- a/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs +++ b/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs @@ -0,0 +1,31 @@ +using System.Linq; +using API.Entities; +using API.Entities.Person; + +namespace API.Extensions.QueryExtensions; + +public static class RestrictByLibraryExtensions +{ + + public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) + { + return query.Where(p => + p.ChapterPeople.Any(cp => userLibs.Contains(cp.Chapter.Volume.Series.LibraryId)) || + p.SeriesMetadataPeople.Any(sm => userLibs.Contains(sm.SeriesMetadata.Series.LibraryId))); + } + + public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) + { + return query.Where(cp => userLibs.Contains(cp.Volume.Series.LibraryId)); + } + + public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) + { + return query.Where(sm => userLibs.Contains(sm.SeriesMetadata.Series.LibraryId)); + } + + public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) + { + return query.Where(cp => userLibs.Contains(cp.Chapter.Volume.Series.LibraryId)); + } +} diff --git a/API/Helpers/Builders/ChapterBuilder.cs b/API/Helpers/Builders/ChapterBuilder.cs index f85c21595..d9976d92a 100644 --- a/API/Helpers/Builders/ChapterBuilder.cs +++ b/API/Helpers/Builders/ChapterBuilder.cs @@ -156,4 +156,24 @@ public class ChapterBuilder : IEntityBuilder return this; } + + public ChapterBuilder WithTags(IList tags) + { + _chapter.Tags ??= []; + foreach (var tag in tags) + { + _chapter.Tags.Add(tag); + } + return this; + } + + public ChapterBuilder WithGenres(IList genres) + { + _chapter.Genres ??= []; + foreach (var genre in genres) + { + _chapter.Genres.Add(genre); + } + return this; + } } diff --git a/API/Helpers/Builders/SeriesMetadataBuilder.cs b/API/Helpers/Builders/SeriesMetadataBuilder.cs index 8ceb16d95..462bc4455 100644 --- a/API/Helpers/Builders/SeriesMetadataBuilder.cs +++ b/API/Helpers/Builders/SeriesMetadataBuilder.cs @@ -108,4 +108,23 @@ public class SeriesMetadataBuilder : IEntityBuilder _seriesMetadata.TagsLocked = lockStatus; return this; } + + public SeriesMetadataBuilder WithTags(List tags, bool lockStatus = false) + { + _seriesMetadata.Tags = tags; + _seriesMetadata.TagsLocked = lockStatus; + return this; + } + + public SeriesMetadataBuilder WithMaxCount(int count) + { + _seriesMetadata.MaxCount = count; + return this; + } + + public SeriesMetadataBuilder WithTotalCount(int count) + { + _seriesMetadata.TotalCount = count; + return this; + } } diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 1db334b91..3c8023671 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -1057,6 +1057,7 @@ public class ExternalMetadataService : IExternalMetadataService var status = DeterminePublicationStatus(series, chapters, externalMetadata); series.Metadata.PublicationStatus = status; + series.Metadata.PublicationStatusLocked = true; return true; } catch (Exception ex) @@ -1188,32 +1189,39 @@ public class ExternalMetadataService : IExternalMetadataService #region Rating - var averageCriticRating = metadata.CriticReviews.Average(r => r.Rating); - var averageUserRating = metadata.UserReviews.Average(r => r.Rating); + // C# can't make the implicit conversation here + float? averageCriticRating = metadata.CriticReviews.Count > 0 ? metadata.CriticReviews.Average(r => r.Rating) : null; + float? averageUserRating = metadata.UserReviews.Count > 0 ? metadata.UserReviews.Average(r => r.Rating) : null; var existingRatings = await _unitOfWork.ChapterRepository.GetExternalChapterRatings(chapter.Id); _unitOfWork.ExternalSeriesMetadataRepository.Remove(existingRatings); - chapter.ExternalRatings = - [ - new ExternalRating + chapter.ExternalRatings = []; + + if (averageUserRating != null) + { + chapter.ExternalRatings.Add(new ExternalRating { AverageScore = (int) averageUserRating, Provider = ScrobbleProvider.Cbr, Authority = RatingAuthority.User, ProviderUrl = metadata.IssueUrl, - }, - new ExternalRating + + }); + chapter.AverageExternalRating = averageUserRating.Value; + } + + if (averageCriticRating != null) + { + chapter.ExternalRatings.Add(new ExternalRating { AverageScore = (int) averageCriticRating, Provider = ScrobbleProvider.Cbr, Authority = RatingAuthority.Critic, ProviderUrl = metadata.IssueUrl, - }, - ]; - - chapter.AverageExternalRating = averageUserRating; + }); + } madeModification = averageUserRating > 0f || averageCriticRating > 0f || madeModification; @@ -1563,16 +1571,16 @@ public class ExternalMetadataService : IExternalMetadataService var maxVolume = (int)(nonSpecialVolumes.Count != 0 ? nonSpecialVolumes.Max(v => v.MaxNumber) : 0); var maxChapter = (int)chapters.Max(c => c.MaxNumber); - if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1) + if (series.Format is MangaFormat.Epub or MangaFormat.Pdf && chapters.Count == 1) { series.Metadata.MaxCount = 1; } - else if (series.Metadata.TotalCount <= 1 && chapters.Count == 1 && chapters[0].IsSpecial) + else if (series.Metadata.TotalCount <= 1 && chapters is [{ IsSpecial: true }]) { series.Metadata.MaxCount = series.Metadata.TotalCount; } else if ((maxChapter == Parser.DefaultChapterNumber || maxChapter > series.Metadata.TotalCount) && - maxVolume <= series.Metadata.TotalCount) + maxVolume <= series.Metadata.TotalCount && maxVolume != Parser.DefaultChapterNumber) { series.Metadata.MaxCount = maxVolume; } @@ -1593,8 +1601,7 @@ public class ExternalMetadataService : IExternalMetadataService { status = PublicationStatus.Ended; - // Check if all volumes/chapters match the total count - if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0) + if (IsSeriesCompleted(series, chapters, externalMetadata, maxVolume)) { status = PublicationStatus.Completed; } @@ -1610,6 +1617,68 @@ public class ExternalMetadataService : IExternalMetadataService return PublicationStatus.OnGoing; } + /// + /// Returns true if the series should be marked as completed, checks loosey with chapter and series numbers. + /// Respects Specials to reach the required amount. + /// + /// + /// + /// + /// + /// + /// Updates MaxCount and TotalCount if a loosey check is used to set as completed + public static bool IsSeriesCompleted(Series series, List chapters, ExternalSeriesDetailDto externalMetadata, int maxVolumes) + { + // A series is completed if exactly the amount is found + if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0) + { + return true; + } + + // If volumes are collected, check if we reach the required volumes by including specials, and decimal volumes + // + // TODO BUG: If the series has specials, that are not included in the external count. But you do own them + // This may mark the series as completed pre-maturely + // Note: I've currently opted to keep this an equals to prevent the above bug from happening + // We *could* change this to >= in the future in case this is reported by users + // If we do; test IsSeriesCompleted_Volumes_TooManySpecials needs to be updated + if (maxVolumes != Parser.DefaultChapterNumber && externalMetadata.Volumes == series.Volumes.Count) + { + series.Metadata.MaxCount = series.Volumes.Count; + series.Metadata.TotalCount = series.Volumes.Count; + return true; + } + + // Note: If Kavita has specials, we should be lenient and ignore for the volume check + var volumeModifier = series.Volumes.Any(v => v.Name == Parser.SpecialVolume) ? 1 : 0; + var modifiedMinVolumeCount = series.Volumes.Count - volumeModifier; + if (maxVolumes != Parser.DefaultChapterNumber && externalMetadata.Volumes == modifiedMinVolumeCount) + { + series.Metadata.MaxCount = modifiedMinVolumeCount; + series.Metadata.TotalCount = modifiedMinVolumeCount; + return true; + } + + // If no volumes are collected, the series is completed if we reach or exceed the external chapters + if (maxVolumes == Parser.DefaultChapterNumber && series.Metadata.MaxCount >= externalMetadata.Chapters) + { + series.Metadata.TotalCount = series.Metadata.MaxCount; + return true; + } + + // If no volumes are collected, the series is complete if we reach or exceed the external chapters while including + // prologues, and extra chapters + if (maxVolumes == Parser.DefaultChapterNumber && chapters.Count >= externalMetadata.Chapters) + { + series.Metadata.TotalCount = chapters.Count; + series.Metadata.MaxCount = chapters.Count; + return true; + } + + + return false; + } + private static Dictionary> ApplyFieldMappings(IEnumerable values, MetadataFieldType sourceType, List mappings) { var result = new Dictionary>(); diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index e73d82b1f..575f89b3b 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -215,9 +215,9 @@ public class TaskScheduler : ITaskScheduler RecurringJob.AddOrUpdate(LicenseCheckId, () => _licenseService.GetLicenseInfo(false), LicenseService.Cron, RecurringJobOptions); - // KavitaPlus Scrobbling (every hour) + // KavitaPlus Scrobbling (every hour) - randomise minutes to spread requests out for K+ RecurringJob.AddOrUpdate(ProcessScrobblingEventsId, () => _scrobblingService.ProcessUpdatesSinceLastSync(), - "0 */1 * * *", RecurringJobOptions); + Cron.Hourly(Rnd.Next(0, 60)), RecurringJobOptions); RecurringJob.AddOrUpdate(ProcessProcessedScrobblingEventsId, () => _scrobblingService.ClearProcessedEvents(), Cron.Daily, RecurringJobOptions); diff --git a/UI/Web/src/app/_services/nav.service.ts b/UI/Web/src/app/_services/nav.service.ts index 65d9fca17..0aad76ef7 100644 --- a/UI/Web/src/app/_services/nav.service.ts +++ b/UI/Web/src/app/_services/nav.service.ts @@ -9,6 +9,24 @@ import {AccountService} from "./account.service"; import {map} from "rxjs/operators"; import {NavigationEnd, Router} from "@angular/router"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {SettingsTabId} from "../sidenav/preference-nav/preference-nav.component"; +import {WikiLink} from "../_models/wiki"; + +/** + * NavItem used to construct the dropdown or NavLinkModal on mobile + * Priority construction + * @param routerLink A link to a page on the web app, takes priority + * @param fragment Optional fragment for routerLink + * @param href A link to an external page, must set noopener noreferrer + * @param click Callback, lowest priority. Should only be used if routerLink and href or not set + */ +interface NavItem { + transLocoKey: string; + href?: string; + fragment?: string; + routerLink?: string; + click?: () => void; +} @Injectable({ providedIn: 'root' @@ -21,6 +39,33 @@ export class NavService { public localStorageSideNavKey = 'kavita--sidenav--expanded'; + public navItems: NavItem[] = [ + { + transLocoKey: 'all-filters', + routerLink: '/all-filters/', + }, + { + transLocoKey: 'browse-genres', + routerLink: '/browse/genres', + }, + { + transLocoKey: 'browse-tags', + routerLink: '/browse/tags', + }, + { + transLocoKey: 'announcements', + routerLink: '/announcements/', + }, + { + transLocoKey: 'help', + href: WikiLink.Guides, + }, + { + transLocoKey: 'logout', + click: () => this.logout(), + } + ] + private navbarVisibleSource = new ReplaySubject(1); /** * If the top Nav bar is rendered or not @@ -127,6 +172,13 @@ export class NavService { }, 10); } + logout() { + this.accountService.logout(); + this.hideNavBar(); + this.hideSideNav(); + this.router.navigateByUrl('/login'); + } + /** * Shows the side nav. When being visible, the side nav will automatically return to previous collapsed state. */ diff --git a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html index 96fd71b95..f5f4e1e26 100644 --- a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html +++ b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html @@ -43,7 +43,7 @@ [sorts]="[{prop: 'createdUtc', dir: 'desc'}]" > - + } diff --git a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts index fd4af01f0..11b1f3307 100644 --- a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts +++ b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts @@ -134,13 +134,6 @@ export class NavHeaderComponent implements OnInit { this.cdRef.markForCheck(); } - logout() { - this.accountService.logout(); - this.navService.hideNavBar(); - this.navService.hideSideNav(); - this.router.navigateByUrl('/login'); - } - moveFocus() { this.document.getElementById('content')?.focus(); } @@ -253,7 +246,6 @@ export class NavHeaderComponent implements OnInit { openLinkSelectionMenu() { const ref = this.modalService.open(NavLinkModalComponent, {fullscreen: 'sm'}); - ref.componentInstance.logoutFn = this.logout.bind(this); } } diff --git a/UI/Web/src/app/nav/_components/nav-link-modal/nav-link-modal.component.html b/UI/Web/src/app/nav/_components/nav-link-modal/nav-link-modal.component.html index 6d94f0ed5..48c93f410 100644 --- a/UI/Web/src/app/nav/_components/nav-link-modal/nav-link-modal.component.html +++ b/UI/Web/src/app/nav/_components/nav-link-modal/nav-link-modal.component.html @@ -6,21 +6,22 @@
    public bool EnableMetadata { get; set; } = true; + /// + /// Should Kavita remove sort articles "The" for the sort name + /// + public bool RemovePrefixForSortName { get; set; } = false; } diff --git a/API/DTOs/UpdateLibraryDto.cs b/API/DTOs/UpdateLibraryDto.cs index 68d2417ec..d7f314208 100644 --- a/API/DTOs/UpdateLibraryDto.cs +++ b/API/DTOs/UpdateLibraryDto.cs @@ -30,6 +30,8 @@ public sealed record UpdateLibraryDto public bool AllowMetadataMatching { get; init; } [Required] public bool EnableMetadata { get; init; } + [Required] + public bool RemovePrefixForSortName { get; init; } /// /// What types of files to allow the scanner to pickup /// diff --git a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs b/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs new file mode 100644 index 000000000..165663f3d --- /dev/null +++ b/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs @@ -0,0 +1,3724 @@ +// +using System; +using System.Collections.Generic; +using API.Data; +using API.Entities.MetadataMatching; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250629153840_LibraryRemoveSortPrefix")] + partial class LibraryRemoveSortPrefix + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("HasRunScrobbleEventGeneration") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("MalAccessToken") + .HasColumnType("TEXT"); + + b.Property("MalUserName") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventGenerationRan") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserChapterRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("MissingSeriesFromSource") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("SourceUrl") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TotalSourceCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserCollection"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(4); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserDashboardStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Host") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserExternalSource"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserOnDeckRemoval"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAutomaticWebtoonReaderDetection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AniListScrobblingEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BlurUnreadSummaries") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("CollapseSeriesRelationships") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("en"); + + b.Property("NoTransitions") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PdfScrollMode") + .HasColumnType("INTEGER"); + + b.Property("PdfSpreadMode") + .HasColumnType("INTEGER"); + + b.Property("PdfTheme") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.Property("WantToReadSync") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAutomaticWebtoonReaderDetection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("DisableWidthOverride") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("LibraryIds") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PdfScrollMode") + .HasColumnType("INTEGER"); + + b.Property("PdfSpreadMode") + .HasColumnType("INTEGER"); + + b.Property("PdfTheme") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("SeriesIds") + .HasColumnType("TEXT"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("WidthOverride") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserReadingProfiles"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserWantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AverageExternalRating") + .HasColumnType("REAL"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("ISBNLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("KPlusOverrides") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ReleaseDateLocked") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("REAL"); + + b.Property("SortOrderLocked") + .HasColumnType("INTEGER"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TitleNameLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DeliveryStatus") + .HasColumnType("TEXT"); + + b.Property("EmailTemplate") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SendDate") + .HasColumnType("TEXT"); + + b.Property("Sent") + .HasColumnType("INTEGER"); + + b.Property("Subject") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); + + b.ToTable("EmailHistory"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("RanAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ManualMigrationHistory"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowMetadataMatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EnableMetadata") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("RemovePrefixForSortName") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Pattern") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryExcludePattern"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FileTypeGroup") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryFileTypeGroup"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bytes") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("KoreaderHash") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("AverageScore") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ProviderUrl") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalRating"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("CoverUrl") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("ExternalRecommendation"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("RawBody") + .HasColumnType("TEXT"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("SiteUrl") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalReview"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AverageExternalRating") + .HasColumnType("INTEGER"); + + b.Property("CbrId") + .HasColumnType("INTEGER"); + + b.Property("GoogleBooksId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("ValidUntilUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.ToTable("ExternalSeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastChecked") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("SeriesBlacklist"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("KPlusOverrides") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DestinationType") + .HasColumnType("INTEGER"); + + b.Property("DestinationValue") + .HasColumnType("TEXT"); + + b.Property("ExcludeFromSource") + .HasColumnType("INTEGER"); + + b.Property("MetadataSettingsId") + .HasColumnType("INTEGER"); + + b.Property("SourceType") + .HasColumnType("INTEGER"); + + b.Property("SourceValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MetadataSettingsId"); + + b.ToTable("MetadataFieldMapping"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRatingMappings") + .HasColumnType("TEXT"); + + b.Property("Blacklist") + .HasColumnType("TEXT"); + + b.Property("EnableChapterCoverImage") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterPublisher") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterReleaseDate") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterTitle") + .HasColumnType("INTEGER"); + + b.Property("EnableCoverImage") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("EnableGenres") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalizedName") + .HasColumnType("INTEGER"); + + b.Property("EnablePeople") + .HasColumnType("INTEGER"); + + b.Property("EnablePublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("EnableRelationships") + .HasColumnType("INTEGER"); + + b.Property("EnableStartDate") + .HasColumnType("INTEGER"); + + b.Property("EnableSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableTags") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("FirstLastPeopleNaming") + .HasColumnType("INTEGER"); + + b.Property("Overrides") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("PersonRoles") + .HasColumnType("TEXT"); + + b.Property("Whitelist") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("ChapterPeople"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("Asin") + .HasColumnType("TEXT"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("HardcoverId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Alias") + .HasColumnType("TEXT"); + + b.Property("NormalizedAlias") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PersonId"); + + b.ToTable("PersonAlias"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.Property("SeriesMetadataId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.HasKey("SeriesMetadataId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ErrorDetails") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsErrored") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DontMatch") + .HasColumnType("INTEGER"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsBlacklisted") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("LowestFolderPath") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("CompatibleVersion") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("GitHubPath") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PreviewUrls") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ShaHash") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LookupName") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.Property("CollectionsId") + .HasColumnType("INTEGER"); + + b.Property("ItemsId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionsId", "ItemsId"); + + b.HasIndex("ItemsId"); + + b.ToTable("AppUserCollectionSeries"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.Property("ExternalRatingsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRatingExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.Property("ExternalRecommendationsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRecommendationExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.Property("ExternalReviewsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalReviewExternalSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ChapterRatings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Ratings") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Collections") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserReadingProfile", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingProfiles") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryExcludePatterns") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryFileTypes") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalRatings") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalReviews") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("ExternalSeriesMetadata") + .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") + .WithMany("FieldMappings") + .HasForeignKey("MetadataSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("People") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("ChapterPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("Aliases") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("SeriesMetadataPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") + .WithMany("People") + .HasForeignKey("SeriesMetadataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + + b.Navigation("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.HasOne("API.Entities.AppUserCollection", null) + .WithMany() + .HasForeignKey("CollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRating", null) + .WithMany() + .HasForeignKey("ExternalRatingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) + .WithMany() + .HasForeignKey("ExternalRecommendationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("ChapterRatings"); + + b.Navigation("Collections"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ReadingProfiles"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("ExternalRatings"); + + b.Navigation("ExternalReviews"); + + b.Navigation("Files"); + + b.Navigation("People"); + + b.Navigation("Ratings"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("LibraryExcludePatterns"); + + b.Navigation("LibraryFileTypes"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Navigation("FieldMappings"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Navigation("Aliases"); + + b.Navigation("ChapterPeople"); + + b.Navigation("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("ExternalSeriesMetadata"); + + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("RelationOf"); + + b.Navigation("Relations"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs b/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs new file mode 100644 index 000000000..4800cf3fa --- /dev/null +++ b/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class LibraryRemoveSortPrefix : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RemovePrefixForSortName", + table: "Library", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RemovePrefixForSortName", + table: "Library"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 106a86b4a..62d1fb1ef 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -1341,6 +1341,9 @@ namespace API.Data.Migrations b.Property("PrimaryColor") .HasColumnType("TEXT"); + b.Property("RemovePrefixForSortName") + .HasColumnType("INTEGER"); + b.Property("SecondaryColor") .HasColumnType("TEXT"); diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index 8dc386298..4a48fed99 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -52,6 +52,10 @@ public class Library : IEntityDate, IHasCoverImage /// Should Kavita read metadata files from the library ///