From fefe35a2a59b4ac3017abe8993d0f6ea8dd03a3e Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Sun, 15 Mar 2026 08:39:11 -0500 Subject: [PATCH] CBL Export and External Metadata Ids (#4532) --- .../Services/Plus/IScrobblingService.cs | 82 +- .../ReadingLists/ICblImportService.cs | 26 + .../IReadingListService.cs | 6 +- .../Helpers/WeblinkParserTests.cs | 29 + Kavita.Common/Helpers/WeblinkParser.cs | 152 + ...ExternalMetadataIdsForEntities.Designer.cs | 4535 ++++++++++++ ...13194040_ExternalMetadataIdsForEntities.cs | 213 + .../Migrations/DataContextModelSnapshot.cs | 6162 +++++++++-------- .../Repositories/SeriesRepository.cs | 10 +- Kavita.Models/DTOs/ChapterDto.cs | 11 +- .../DTOs/Common/IUpdateExternalMetadataIds.cs | 15 + .../DTOs/ReadingLists/CBL/CblBook.cs | 35 - .../ReadingLists/CBL/CblExternalDbProvider.cs | 24 + .../DTOs/ReadingLists/CBL/CblExternalId.cs | 21 + .../ReadingLists/CBL/CblImportDecisions.cs | 9 + .../DTOs/ReadingLists/CBL/CblImportOptions.cs | 16 + .../DTOs/ReadingLists/CBL/CblImportSummary.cs | 1 + .../DTOs/ReadingLists/CBL/CblIssueType.cs | 28 + .../DTOs/ReadingLists/CBL/CblListType.cs | 36 + .../DTOs/ReadingLists/CBL/CblRelationship.cs | 21 + .../DTOs/ReadingLists/CBL/CblSource.cs | 17 + .../DTOs/ReadingLists/CBL/ParsedCblItem.cs | 50 + .../ReadingLists/CBL/ParsedCblReadingList.cs | 88 + .../DTOs/ReadingLists/CBL/V1/CblBook.cs | 66 + .../CBL/{ => V1}/CblReadingList.cs | 7 +- .../ReadingLists/CBL/V2/CblV2ExternalId.cs | 26 + .../ReadingLists/CBL/V2/CblV2FileDetails.cs | 20 + .../DTOs/ReadingLists/CBL/V2/CblV2Issue.cs | 42 + .../ReadingLists/CBL/V2/CblV2ListDetails.cs | 67 + .../ReadingLists/CBL/V2/CblV2Relationship.cs | 25 + .../DTOs/ReadingLists/CBL/V2/CblV2Root.cs | 32 + .../DTOs/ReadingLists/CBL/V2/CblV2Source.cs | 20 + Kavita.Models/DTOs/SeriesDto.cs | 11 +- Kavita.Models/DTOs/UpdateChapterDto.cs | 12 +- Kavita.Models/DTOs/UpdateSeriesDto.cs | 15 +- Kavita.Models/DTOs/UpdateSeriesMetadataDto.cs | 4 +- Kavita.Models/DTOs/UpdateVolumeDto.cs | 15 + Kavita.Models/DTOs/VolumeDto.cs | 11 +- Kavita.Models/Entities/Chapter.cs | 21 +- .../Entities/Interfaces/IHasMetadataIds.cs | 18 + Kavita.Models/Entities/Person/Person.cs | 1 - Kavita.Models/Entities/Series.cs | 15 +- Kavita.Models/Entities/Volume.cs | 11 +- Kavita.Models/Parser/ParserInfo.cs | 31 +- Kavita.Server/Controllers/CBLController.cs | 7 +- .../Controllers/ChapterController.cs | 6 + Kavita.Server/Controllers/ImageController.cs | 1 + .../Controllers/ReadingListController.cs | 23 +- Kavita.Server/Controllers/SeriesController.cs | 3 + Kavita.Server/Controllers/UploadController.cs | 1 + Kavita.Server/Controllers/VolumeController.cs | 33 + .../Helpers/ExternalMetadataIdHelper.cs | 41 + Kavita.Server/I18N/en.json | 1 + Kavita.Server/Kavita.Server.csproj | 7 + Kavita.Services.Tests/ArchiveServiceTests.cs | 12 + .../Helpers/ParserInfoFactory.cs | 1 + .../Helpers/ScannerHelper.cs | 10 +- Kavita.Services.Tests/OpdsServiceTests.cs | 1 + .../ReadingListServiceTests.cs | 2 + .../ReadingLists/CblExportServiceTests.cs | 440 ++ .../ReadingLists/CblParserTests.cs | 202 + .../ScrobblingServiceTests.cs | 16 +- Kavita.Services.Tests/SeriesServiceTests.cs | 1 + .../ComicInfos/metadata_from_notes.cbz | Bin 0 -> 860 bytes .../2018-2021 Part 16.1 Reborn Again.json | 1052 +++ .../BOOM! Power Rangers Simplified 1a.cbl | 500 ++ ... Aquaman- Death of a Prince (WEB-CBRO).cbl | 83 + .../ApplicationServiceExtensions.cs | 3 + Kavita.Services/Helpers/CblParser.cs | 231 + Kavita.Services/Kavita.Services.csproj | 4 - Kavita.Services/LocalizationService.cs | 14 +- Kavita.Services/OpdsService.cs | 1 + .../Plus/ExternalMetadataService.cs | 47 +- .../ReadingLists/CblExportService.cs | 231 + .../ReadingLists/CblImporterService.cs | 24 + .../ReadingListService.cs | 2 + Kavita.Services/Scanner/BasicParser.cs | 48 +- Kavita.Services/Scanner/BookParser.cs | 26 +- Kavita.Services/Scanner/ComicVineParser.cs | 32 +- Kavita.Services/Scanner/DefaultParser.cs | 63 +- Kavita.Services/Scanner/ImageParser.cs | 12 +- Kavita.Services/Scanner/ParseScannedFiles.cs | 16 +- Kavita.Services/Scanner/Parser.cs | 38 +- Kavita.Services/Scanner/PdfParser.cs | 44 +- Kavita.Services/Scanner/ProcessSeries.cs | 23 +- Kavita.Services/Scanner/ScannerService.cs | 6 +- Kavita.Services/SeriesService.cs | 1 + UI/Web/src/app/_models/actionables/action.ts | 4 + UI/Web/src/app/_models/chapter.ts | 10 +- .../app/_models/common/i-has-metadata-ids.ts | 8 + .../app/_models/metadata/series-metadata.ts | 98 +- UI/Web/src/app/_models/series.ts | 3 +- UI/Web/src/app/_models/tabs.ts | 48 + UI/Web/src/app/_models/update-volume.ts | 5 + UI/Web/src/app/_models/volume.ts | 10 +- UI/Web/src/app/_pipes/tab-title.pipe.ts | 17 + .../app/_services/action-factory.service.ts | 34 +- UI/Web/src/app/_services/action.service.ts | 10 + .../_services/card-config-factory.service.ts | 4 +- .../src/app/_services/reading-list.service.ts | 1 - UI/Web/src/app/_services/volume.service.ts | 7 +- .../details-tab/details-tab.component.html | 10 + .../details-tab/details-tab.component.ts | 9 + .../edit-chapter-modal.component.html | 33 +- .../edit-chapter-modal.component.ts | 24 +- .../edit-volume-modal.component.html | 23 +- .../edit-volume-modal.component.ts | 41 +- .../review-modal/review-modal.component.ts | 2 +- .../view-bookmark-drawer.component.html | 8 +- .../view-bookmark-drawer.component.ts | 16 +- .../edit-collection-tags-modal.component.html | 16 +- .../edit-collection-tags-modal.component.ts | 15 +- .../edit-series-modal.component.html | 43 +- .../edit-series-modal.component.ts | 40 +- .../chapter-detail.component.html | 25 +- .../chapter-detail.component.ts | 28 +- .../download-queue-item.component.html | 1 + .../edit-person-modal.component.html | 12 +- .../edit-person-modal.component.ts | 15 +- .../profile/profile.component.html | 24 +- .../_components/profile/profile.component.ts | 19 +- .../reading-list-detail.component.html | 12 +- .../reading-list-detail.component.ts | 19 +- .../edit-reading-list-modal.component.html | 8 +- .../edit-reading-list-modal.component.ts | 16 +- .../series-detail.component.html | 65 +- .../series-detail/series-detail.component.ts | 65 +- ...edit-external-metadata-form.component.html | 24 + ...edit-external-metadata-form.component.scss | 0 .../edit-external-metadata-form.component.ts | 53 + .../external-metadata-detail.component.html | 15 + .../external-metadata-detail.component.scss | 0 .../external-metadata-detail.component.ts | 22 + .../app/shared/_services/download.service.ts | 74 +- .../manage-customization.component.html | 16 +- .../manage-customization.component.ts | 37 +- .../library-settings-modal.component.html | 20 +- .../library-settings-modal.component.ts | 24 +- .../server-stats/server-stats.component.html | 8 +- .../server-stats/server-stats.component.ts | 13 +- .../manage-reading-profiles.component.html | 18 +- .../manage-reading-profiles.component.ts | 14 +- .../volume-detail.component.html | 29 +- .../volume-detail/volume-detail.component.ts | 25 +- UI/Web/src/assets/langs/en.json | 96 +- 145 files changed, 12820 insertions(+), 3882 deletions(-) create mode 100644 Kavita.API/Services/ReadingLists/ICblImportService.cs rename Kavita.API/Services/{Reading => ReadingLists}/IReadingListService.cs (95%) create mode 100644 Kavita.Common.Tests/Helpers/WeblinkParserTests.cs create mode 100644 Kavita.Common/Helpers/WeblinkParser.cs create mode 100644 Kavita.Database/Migrations/20260313194040_ExternalMetadataIdsForEntities.Designer.cs create mode 100644 Kavita.Database/Migrations/20260313194040_ExternalMetadataIdsForEntities.cs create mode 100644 Kavita.Models/DTOs/Common/IUpdateExternalMetadataIds.cs delete mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/CblBook.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/CblExternalDbProvider.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/CblExternalId.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/CblImportDecisions.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/CblImportOptions.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/CblIssueType.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/CblListType.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/CblRelationship.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/CblSource.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/ParsedCblItem.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/ParsedCblReadingList.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/V1/CblBook.cs rename Kavita.Models/DTOs/ReadingLists/CBL/{ => V1}/CblReadingList.cs (91%) create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2ExternalId.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2FileDetails.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Issue.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2ListDetails.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Relationship.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Root.cs create mode 100644 Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Source.cs create mode 100644 Kavita.Models/DTOs/UpdateVolumeDto.cs create mode 100644 Kavita.Models/Entities/Interfaces/IHasMetadataIds.cs create mode 100644 Kavita.Server/Helpers/ExternalMetadataIdHelper.cs create mode 100644 Kavita.Services.Tests/ReadingLists/CblExportServiceTests.cs create mode 100644 Kavita.Services.Tests/ReadingLists/CblParserTests.cs create mode 100644 Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/metadata_from_notes.cbz create mode 100644 Kavita.Services.Tests/Test Data/CblParserTests/Test Cases/2018-2021 Part 16.1 Reborn Again.json create mode 100644 Kavita.Services.Tests/Test Data/CblParserTests/Test Cases/BOOM! Power Rangers Simplified 1a.cbl create mode 100644 Kavita.Services.Tests/Test Data/CblParserTests/Test Cases/[DC Comics] Aquaman- Death of a Prince (WEB-CBRO).cbl create mode 100644 Kavita.Services/Helpers/CblParser.cs create mode 100644 Kavita.Services/ReadingLists/CblExportService.cs create mode 100644 Kavita.Services/ReadingLists/CblImporterService.cs rename Kavita.Services/{Reading => ReadingLists}/ReadingListService.cs (99%) create mode 100644 UI/Web/src/app/_models/common/i-has-metadata-ids.ts create mode 100644 UI/Web/src/app/_models/tabs.ts create mode 100644 UI/Web/src/app/_models/update-volume.ts create mode 100644 UI/Web/src/app/_pipes/tab-title.pipe.ts create mode 100644 UI/Web/src/app/shared/_components/edit-external-metadata-form/edit-external-metadata-form.component.html create mode 100644 UI/Web/src/app/shared/_components/edit-external-metadata-form/edit-external-metadata-form.component.scss create mode 100644 UI/Web/src/app/shared/_components/edit-external-metadata-form/edit-external-metadata-form.component.ts create mode 100644 UI/Web/src/app/shared/_components/external-metadata-detail/external-metadata-detail.component.html create mode 100644 UI/Web/src/app/shared/_components/external-metadata-detail/external-metadata-detail.component.scss create mode 100644 UI/Web/src/app/shared/_components/external-metadata-detail/external-metadata-detail.component.ts diff --git a/Kavita.API/Services/Plus/IScrobblingService.cs b/Kavita.API/Services/Plus/IScrobblingService.cs index 71867312a..d0ac3e6eb 100644 --- a/Kavita.API/Services/Plus/IScrobblingService.cs +++ b/Kavita.API/Services/Plus/IScrobblingService.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.Threading; using System.Threading.Tasks; using Hangfire; +using Kavita.API.Services.Helpers; +using Kavita.Common.Helpers; using Kavita.Models.DTOs.Scrobbling; using Kavita.Models.Entities; using Kavita.Models.Entities.Enums; @@ -127,92 +129,16 @@ public static class ScrobblingHelper public static long? GetMalId(Series series) { - var malId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite); - return malId ?? series.ExternalSeriesMetadata?.MalId; + return WeblinkParser.GetMalId(series.Metadata.WebLinks) ?? series.ExternalSeriesMetadata?.MalId; } - public static long? GetMalId(string weblinks) - { - return ExtractId(weblinks, MalWeblinkWebsite); - } public static int? GetAniListId(Series seriesWithExternalMetadata) { - var aniListId = ExtractId(seriesWithExternalMetadata.Metadata.WebLinks, AniListWeblinkWebsite); + var aniListId = WeblinkParser.GetAniListId(seriesWithExternalMetadata.Metadata.WebLinks); return aniListId ?? seriesWithExternalMetadata.ExternalSeriesMetadata?.AniListId; } - public static int? GetAniListId(string weblinks) - { - return ExtractId(weblinks, AniListWeblinkWebsite); - } - - /// - /// Extract an Id from a given weblink - /// - /// - /// - /// - public static T? ExtractId(string webLinks, string website) - { - var index = WeblinkExtractionMap[website]; - 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; - } - else if (typeof(T) == typeof(int)) - { - 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; - } - else if (typeof(T) == typeof(string)) - { - return (T)(object)value; - } - } - - return default; - } - - /// - /// Generate a URL from a given ID and website - /// - /// Type of the ID (e.g., int, long, string) - /// The ID to embed in the URL - /// The base website URL - /// The generated URL or null if the website is not supported - public static string? GenerateUrl(T id, string website) - { - if (!WeblinkExtractionMap.ContainsKey(website)) - { - return null; // Unsupported website - } - - if (Equals(id, default(T))) - { - throw new ArgumentNullException(nameof(id), "ID cannot be null."); - } - - // Ensure the type of the ID matches supported types - if (typeof(T) == typeof(int) || typeof(T) == typeof(long) || typeof(T) == typeof(string)) - { - return $"{website}{id}"; - } - - throw new ArgumentException("Unsupported ID type. Supported types are int, long, and string.", nameof(id)); - } public static string CreateUrl(string url, long? id) { diff --git a/Kavita.API/Services/ReadingLists/ICblImportService.cs b/Kavita.API/Services/ReadingLists/ICblImportService.cs new file mode 100644 index 000000000..332785e36 --- /dev/null +++ b/Kavita.API/Services/ReadingLists/ICblImportService.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Kavita.Models.DTOs.ReadingLists.CBL; + +namespace Kavita.API.Services.ReadingLists; + + +public interface ICblImportService +{ + Task ValidateList(int userId, string filePath, CblImportOptions options); + /// + /// Creates a new RL or updates an existing + /// + /// + /// + /// + /// + Task UpsertReadingList(int userId, string filePath, CblImportOptions options, CblImportDecisions decisions); + /// + /// Checks for updates against upstream ReadingList files and attempts to Update reading list. + /// + /// Does not prompt for validation, makes best guess + /// + /// + /// + Task SyncReadingList(int userId, int readingListId); +} diff --git a/Kavita.API/Services/Reading/IReadingListService.cs b/Kavita.API/Services/ReadingLists/IReadingListService.cs similarity index 95% rename from Kavita.API/Services/Reading/IReadingListService.cs rename to Kavita.API/Services/ReadingLists/IReadingListService.cs index 32a4864c4..93d960228 100644 --- a/Kavita.API/Services/Reading/IReadingListService.cs +++ b/Kavita.API/Services/ReadingLists/IReadingListService.cs @@ -3,11 +3,12 @@ using System.Threading.Tasks; using Kavita.Common.Helpers; using Kavita.Models.DTOs.ReadingLists; using Kavita.Models.DTOs.ReadingLists.CBL; +using Kavita.Models.DTOs.ReadingLists.CBL.V1; using Kavita.Models.Entities; using Kavita.Models.Entities.Enums; using Kavita.Models.Entities.User; -namespace Kavita.API.Services.Reading; +namespace Kavita.API.Services.ReadingLists; public interface IReadingListService { @@ -19,8 +20,7 @@ public interface IReadingListService Task UserHasReadingListAccess(int readingListId, string username); Task DeleteReadingList(int readingListId, AppUser user); Task CalculateReadingListAgeRating(ReadingList readingList); - Task AddChaptersToReadingList(int seriesId, IList chapterIds, - ReadingList readingList); + Task AddChaptersToReadingList(int seriesId, IList chapterIds, ReadingList readingList); Task ValidateCblFile(int userId, CblReadingList cblReading, bool useComicLibraryMatching = false); Task CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false, bool useComicLibraryMatching = false); diff --git a/Kavita.Common.Tests/Helpers/WeblinkParserTests.cs b/Kavita.Common.Tests/Helpers/WeblinkParserTests.cs new file mode 100644 index 000000000..675a0df95 --- /dev/null +++ b/Kavita.Common.Tests/Helpers/WeblinkParserTests.cs @@ -0,0 +1,29 @@ +using Kavita.Common.Helpers; + +namespace Kavita.Common.Tests.Helpers; + +public class WeblinkParserTests +{ + [Theory] + [InlineData("https://anilist.co/manga/35851/Byeontaega-Doeja/", 35851)] + [InlineData("https://anilist.co/manga/30105", 30105)] + [InlineData("https://anilist.co/manga/30105/Kekkaishi/", 30105)] + public void CanParseWeblink_AniList(string link, int? expectedId) + { + Assert.Equal(WeblinkParser.GetAniListId(link), expectedId); + } + + [Theory] + [InlineData("https://mangadex.org/title/316d3d09-bb83-49da-9d90-11dc7ce40967/honzuki-no-gekokujou-shisho-ni-naru-tame-ni-wa-shudan-wo-erandeiraremasen-dai-3-bu-ryouchi-ni-hon-o", "316d3d09-bb83-49da-9d90-11dc7ce40967")] + public void CanParseWeblink_MangaDex(string link, string expectedId) + { + Assert.Equal(WeblinkParser.GetMangaDexId(link), expectedId); + } + + [Theory] + [InlineData("https://comicvine.gamespot.com/chew-1-taster-s-choice-part-1-of-5/4000-159233/", "159233")] + public void CanParseWeblink_ComicVine(string link, string expectedId) + { + Assert.Equal(WeblinkParser.GetComicVineId(link).Item1, expectedId); + } +} diff --git a/Kavita.Common/Helpers/WeblinkParser.cs b/Kavita.Common/Helpers/WeblinkParser.cs new file mode 100644 index 000000000..c65500aad --- /dev/null +++ b/Kavita.Common/Helpers/WeblinkParser.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Kavita.Common.Helpers; +#nullable enable + +public static class WeblinkParser +{ + private const string AniListWeblinkWebsite = "https://anilist.co/manga/"; + private const string MalWeblinkWebsite = "https://myanimelist.net/manga/"; + private const string MalStaffWebsite = "https://myanimelist.net/people/"; + private const string MalCharacterWebsite = "https://myanimelist.net/character/"; + private const string GoogleBooksWeblinkWebsite = "https://books.google.com/books?id="; + private const string MangaDexWeblinkWebsite = "https://mangadex.org/title/"; + private const string AniListStaffWebsite = "https://anilist.co/staff/"; + private const string AniListCharacterWebsite = "https://anilist.co/character/"; + private const string HardcoverStaffWebsite = "https://hardcover.app/authors/"; + /// + /// ComicVine has a unique structure: + // https://comicvine.gamespot.com/batman-the-caped-crusader/4050-112794/ + // https://comicvine.gamespot.com/batman-the-caped-crusader-6-volume-6/4000-907546/ + // The 4050 implies this is a Series (TPB/Series) and 4000 implies single issue + /// + private const string ComicVineWeblinkWebsite = "https://comicvine.gamespot.com/"; + + private static readonly Dictionary WeblinkExtractionMap = new() + { + {AniListWeblinkWebsite, 0}, + {MalWeblinkWebsite, 0}, + {GoogleBooksWeblinkWebsite, 0}, + {MangaDexWeblinkWebsite, 0}, + {AniListStaffWebsite, 0}, + {AniListCharacterWebsite, 0}, + {ComicVineWeblinkWebsite, 1}, + }; + + public static long? GetMalId(string? weblinks) + { + return ExtractId(weblinks, MalWeblinkWebsite); + } + + /// + /// Attempts to parse ComicVine Id from the weblinks. Returns id and true if Series/Volume Id. + /// + /// + /// + public static Tuple GetComicVineId(string? weblinks) + { + var extractedId = ExtractId(weblinks, ComicVineWeblinkWebsite); + if (string.IsNullOrEmpty(extractedId)) return Tuple.Create(null, false); + return Tuple.Create(extractedId.Split('-')[1], extractedId.StartsWith("4050")); + } + + public static int? GetAniListId(string? weblinks) + { + return ExtractId(weblinks, AniListWeblinkWebsite); + } + + public static int GetAniListCharacterId(string? url) + { + return ExtractId(url, AniListCharacterWebsite) ?? 0; + } + + public static int GetAniListStaffId(string? url) + { + return ExtractId(url, AniListStaffWebsite) ?? 0; + } + + public static string? GetGoogleBooksId(string? weblinks) + { + return ExtractId(weblinks, GoogleBooksWeblinkWebsite); + } + + public static string? GetMangaDexId(string? weblinks) + { + return ExtractId(weblinks, MangaDexWeblinkWebsite); + } + + + + /// + /// Extract an ID from a given weblink + /// + /// + /// + /// + private static T? ExtractId(string? webLinks, string website) + { + if (string.IsNullOrEmpty(webLinks)) return default; + + var index = WeblinkExtractionMap[website]; + foreach (var webLink in webLinks.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + 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; + } + else if (typeof(T) == typeof(int)) + { + 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; + } + else if (typeof(T) == typeof(string)) + { + return (T)(object)value; + } + } + + return default; + } + + + /// + /// Generate a URL from a given ID and website + /// + /// Type of the ID (e.g., int, long, string) + /// The ID to embed in the URL + /// The base website URL + /// The generated URL or null if the website is not supported + public static string? GenerateUrl(T id, string website) + { + if (!WeblinkExtractionMap.ContainsKey(website)) + { + return null; // Unsupported website + } + + if (Equals(id, default(T))) + { + throw new ArgumentNullException(nameof(id), "ID cannot be null."); + } + + // Ensure the type of the ID matches supported types + if (typeof(T) == typeof(int) || typeof(T) == typeof(long) || typeof(T) == typeof(string)) + { + return $"{website}{id}"; + } + + throw new ArgumentException("Unsupported ID type. Supported types are int, long, and string.", nameof(id)); + } +} diff --git a/Kavita.Database/Migrations/20260313194040_ExternalMetadataIdsForEntities.Designer.cs b/Kavita.Database/Migrations/20260313194040_ExternalMetadataIdsForEntities.Designer.cs new file mode 100644 index 000000000..071b3bffe --- /dev/null +++ b/Kavita.Database/Migrations/20260313194040_ExternalMetadataIdsForEntities.Designer.cs @@ -0,0 +1,4535 @@ +// +using System; +using System.Collections.Generic; +using Kavita.Database; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Progress; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Kavita.Database.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20260313194040_ExternalMetadataIdsForEntities")] + partial class ExternalMetadataIdsForEntities + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + 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("Kavita.Models.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("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AverageExternalRating") + .HasColumnType("REAL"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("ComicVineId") + .HasColumnType("TEXT"); + + 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("HardcoverId") + .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("MalId") + .HasColumnType("INTEGER"); + + b.Property("MangaBakaId") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MetronId") + .HasColumnType("INTEGER"); + + 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("TitleName") + .HasDatabaseName("IX_Chapter_TitleName"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.Entities.EpubFont", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .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("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("EpubFont"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("DefaultLanguage") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + 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("InheritWebLinksFromFirstChapter") + .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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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.HasIndex("FilePath") + .HasDatabaseName("IX_MangaFile_FilePath"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.Entities.Metadata.GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("AgeRating") + .HasDatabaseName("IX_SeriesMetadata_AgeRating"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.HasIndex("SeriesId", "AgeRating") + .HasDatabaseName("IX_SeriesMetadata_SeriesId_AgeRating"); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Metadata.SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("EnableExtendedMetadataProcessing") + .HasColumnType("INTEGER"); + + 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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.Entities.Progress.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("TotalReads") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ClientInfoUsed") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("Data") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("{\"TotalMinutesRead\":0,\"TotalPagesRead\":0,\"TotalWordsRead\":0,\"LongestSessionMinutes\":0,\"Activities\":[],\"SeriesIds\":null,\"ChapterIds\":null}"); + + b.Property("DateUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("DateUtc") + .IsUnique(); + + b.ToTable("AppUserReadingHistory"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("EndTimeUtc") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("StartTimeUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("AppUserId", "IsActive") + .HasDatabaseName("IX_AppUserReadingSession_AppUserId_IsActive"); + + b.HasIndex("IsActive", "LastModifiedUtc") + .HasDatabaseName("IX_AppUserReadingSession_IsActive_LastModifiedUtc"); + + b.ToTable("AppUserReadingSession"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingSessionActivityData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserReadingSessionId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("DeviceIds") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EndBookScrollId") + .HasColumnType("TEXT"); + + b.Property("EndPage") + .HasColumnType("INTEGER"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("EndTimeUtc") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("StartBookScrollId") + .HasColumnType("TEXT"); + + b.Property("StartPage") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("StartTimeUtc") + .HasColumnType("TEXT"); + + b.Property("TotalPages") + .HasColumnType("INTEGER"); + + b.Property("TotalWords") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WordsRead") + .HasColumnType("INTEGER"); + + b.ComplexProperty(typeof(Dictionary), "ClientInfo", "Kavita.Models.Entities.Progress.AppUserReadingSessionActivityData.ClientInfo#ClientInfoData", b1 => + { + b1.Property("AppVersion"); + + b1.Property("AuthType"); + + b1.Property("Browser"); + + b1.Property("BrowserVersion"); + + b1.Property("CapturedAt"); + + b1.Property("ClientType"); + + b1.Property("DeviceType"); + + b1.Property("IpAddress") + .IsRequired(); + + b1.Property("Orientation"); + + b1.Property("Platform"); + + b1.Property("ScreenHeight"); + + b1.Property("ScreenWidth"); + + b1.Property("UserAgent") + .IsRequired(); + + b1 + .ToJson("ClientInfo") + .HasColumnType("TEXT"); + }); + + b.HasKey("Id"); + + b.HasIndex("AppUserReadingSessionId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.HasIndex("StartTimeUtc", "LibraryId") + .HasDatabaseName("IX_ActivityData_StartTimeUtc_LibraryId"); + + b.ToTable("AppUserReadingSessionActivityData"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("ComicVineId") + .HasColumnType("TEXT"); + + 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("HardcoverId") + .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("MalId") + .HasColumnType("INTEGER"); + + b.Property("MangaBakaId") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MetronId") + .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") + .HasDatabaseName("IX_Series_LibraryId"); + + b.HasIndex("NormalizedName") + .HasDatabaseName("IX_Series_NormalizedName"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.Entities.User.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("Kavita.Models.Entities.User.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("CoverImage") + .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("IdentityProvider") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + 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("OidcId") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventGenerationRan") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .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("Kavita.Models.Entities.User.AppUserAnnotation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("CommentHtml") + .HasColumnType("TEXT"); + + b.Property("CommentPlainText") + .HasColumnType("TEXT"); + + b.Property("ContainsSpoiler") + .HasColumnType("INTEGER"); + + b.Property("Context") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingXPath") + .HasColumnType("TEXT"); + + b.Property("HighlightCount") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("Likes") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SelectedSlotIndex") + .HasColumnType("INTEGER"); + + b.Property("SelectedText") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("XPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserAnnotation"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserAuthKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAtUtc") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtUtc") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastAccessedAtUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Provider") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ExpiresAtUtc"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("AppUserAuthKey"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("ImageOffset") + .HasColumnType("INTEGER"); + + 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.Property("XPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.HasIndex("AppUserId", "SeriesId") + .HasDatabaseName("IX_AppUserBookmark_AppUserId_SeriesId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserChapterRating", 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("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + 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("Kavita.Models.Entities.User.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("Kavita.Models.Entities.User.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("Kavita.Models.Entities.User.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("Kavita.Models.Entities.User.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("Kavita.Models.Entities.User.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("BookReaderHighlightSlots") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + 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("ColorScapeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CustomKeyBinds") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("{}"); + + b.Property("DataSaver") + .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("OpdsPreferences") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("{\"EmbedProgressIndicator\":true,\"IncludeContinueFrom\":true}"); + + 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("PromptForRereadsAfter") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(30); + + 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("SocialPreferences") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("{\"ShareReviews\":false,\"ShareAnnotations\":false,\"ViewOtherAnnotations\":false,\"SocialLibraries\":[],\"SocialMaxAgeRating\":-1,\"SocialIncludeUnknowns\":true,\"ShareProfile\":false}"); + + 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("Kavita.Models.Entities.User.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.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.PrimitiveCollection("DeviceIds") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("DisableWidthOverride") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("LibraryIds") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + 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.PrimitiveCollection("SeriesIds") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + 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("Kavita.Models.Entities.User.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("Kavita.Models.Entities.User.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("Kavita.Models.Entities.User.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("Kavita.Models.Entities.User.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("ChapterTitle") + .HasColumnType("TEXT"); + + 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("SelectedText") + .HasColumnType("TEXT"); + + 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("Kavita.Models.Entities.User.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("Kavita.Models.Entities.User.ClientDevice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CurrentClientInfo") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("{\"UserAgent\":\"\",\"IpAddress\":\"\",\"AuthType\":0,\"ClientType\":0,\"AppVersion\":null,\"Browser\":null,\"BrowserVersion\":null,\"Platform\":0,\"DeviceType\":null,\"ScreenWidth\":null,\"ScreenHeight\":null,\"Orientation\":null,\"CapturedAt\":\"0001-01-01T00:00:00\"}"); + + b.Property("DeviceFingerprint") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstSeenUtc") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("LastSeenUtc") + .HasColumnType("TEXT"); + + b.Property("UiFingerprint") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ClientDevice"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.ClientDeviceHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CapturedAtUtc") + .HasColumnType("TEXT"); + + b.Property("ClientInfo") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("{\"UserAgent\":\"\",\"IpAddress\":\"\",\"AuthType\":0,\"ClientType\":0,\"AppVersion\":null,\"Browser\":null,\"BrowserVersion\":null,\"Platform\":0,\"DeviceType\":null,\"ScreenWidth\":null,\"ScreenHeight\":null,\"Orientation\":null,\"CapturedAt\":\"0001-01-01T00:00:00\"}"); + + b.Property("DeviceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.ToTable("ClientDeviceHistory"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("ComicVineId") + .HasColumnType("TEXT"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("HardcoverId") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LookupName") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("MangaBakaId") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MetronId") + .HasColumnType("INTEGER"); + + 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("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FriendlyName") + .HasColumnType("TEXT"); + + b.Property("Xml") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys"); + }); + + 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("AppUserCollectionSeries", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUserCollection", null) + .WithMany() + .HasForeignKey("CollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("Kavita.Models.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("Kavita.Models.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("Kavita.Models.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.HasOne("Kavita.Models.Entities.Metadata.ExternalRating", null) + .WithMany() + .HasForeignKey("ExternalRatingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.HasOne("Kavita.Models.Entities.Metadata.ExternalRecommendation", null) + .WithMany() + .HasForeignKey("ExternalRecommendationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.HasOne("Kavita.Models.Entities.Metadata.ExternalReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Chapter", b => + { + b.HasOne("Kavita.Models.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Device", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.EmailHistory", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.FolderPath", b => + { + b.HasOne("Kavita.Models.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.LibraryExcludePattern", b => + { + b.HasOne("Kavita.Models.Entities.Library", "Library") + .WithMany("LibraryExcludePatterns") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.LibraryFileTypeGroup", b => + { + b.HasOne("Kavita.Models.Entities.Library", "Library") + .WithMany("LibraryFileTypes") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.MangaFile", b => + { + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Metadata.ExternalRating", b => + { + b.HasOne("Kavita.Models.Entities.Chapter", null) + .WithMany("ExternalRatings") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Metadata.ExternalReview", b => + { + b.HasOne("Kavita.Models.Entities.Chapter", null) + .WithMany("ExternalReviews") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithOne("ExternalSeriesMetadata") + .HasForeignKey("Kavita.Models.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Metadata.GenreSeriesMetadata", b => + { + b.HasOne("Kavita.Models.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Metadata.SeriesBlacklist", b => + { + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("Kavita.Models.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Metadata.SeriesMetadataTag", b => + { + b.HasOne("Kavita.Models.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.MetadataFieldMapping", b => + { + b.HasOne("Kavita.Models.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") + .WithMany("FieldMappings") + .HasForeignKey("MetadataSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MetadataSettings"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Person.ChapterPeople", b => + { + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") + .WithMany("People") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Person.Person", "Person") + .WithMany("ChapterPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Person.PersonAlias", b => + { + b.HasOne("Kavita.Models.Entities.Person.Person", "Person") + .WithMany("Aliases") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Person.SeriesMetadataPeople", b => + { + b.HasOne("Kavita.Models.Entities.Person.Person", "Person") + .WithMany("SeriesMetadataPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Metadata.SeriesMetadata", "SeriesMetadata") + .WithMany("People") + .HasForeignKey("SeriesMetadataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + + b.Navigation("SeriesMetadata"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserProgress", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingHistory", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("ReadingHistory") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingSession", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("ReadingSessions") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingSessionActivityData", b => + { + b.HasOne("Kavita.Models.Entities.Progress.AppUserReadingSession", "ReadingSession") + .WithMany("ActivityData") + .HasForeignKey("AppUserReadingSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("Library"); + + b.Navigation("ReadingSession"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.ReadingList", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.ReadingListItem", b => + { + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("Kavita.Models.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Series", b => + { + b.HasOne("Kavita.Models.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserAnnotation", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("Annotations") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserAuthKey", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("AuthKeys") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserBookmark", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserChapterRating", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("ChapterRatings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") + .WithMany("Ratings") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserCollection", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("Collections") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserDashboardStream", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.User.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserExternalSource", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserOnDeckRemoval", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserPreferences", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("Kavita.Models.Entities.User.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserRating", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserReadingProfile", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("ReadingProfiles") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserRole", b => + { + b.HasOne("Kavita.Models.Entities.User.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.User.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserSideNavStream", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.User.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserSmartFilter", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserTableOfContent", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserWantToRead", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.ClientDevice", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("ClientDevices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.ClientDeviceHistory", b => + { + b.HasOne("Kavita.Models.Entities.User.ClientDevice", "Device") + .WithMany("History") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Volume", b => + { + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Kavita.Models.Entities.User.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Chapter", b => + { + b.Navigation("ExternalRatings"); + + b.Navigation("ExternalReviews"); + + b.Navigation("Files"); + + b.Navigation("People"); + + b.Navigation("Ratings"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("LibraryExcludePatterns"); + + b.Navigation("LibraryFileTypes"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Metadata.SeriesMetadata", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.MetadataMatching.MetadataSettings", b => + { + b.Navigation("FieldMappings"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Person.Person", b => + { + b.Navigation("Aliases"); + + b.Navigation("ChapterPeople"); + + b.Navigation("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingSession", b => + { + b.Navigation("ActivityData"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.Entities.User.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUser", b => + { + b.Navigation("Annotations"); + + b.Navigation("AuthKeys"); + + b.Navigation("Bookmarks"); + + b.Navigation("ChapterRatings"); + + b.Navigation("ClientDevices"); + + b.Navigation("Collections"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingHistory"); + + b.Navigation("ReadingLists"); + + b.Navigation("ReadingProfiles"); + + b.Navigation("ReadingSessions"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences") + .IsRequired(); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.ClientDevice", b => + { + b.Navigation("History"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Kavita.Database/Migrations/20260313194040_ExternalMetadataIdsForEntities.cs b/Kavita.Database/Migrations/20260313194040_ExternalMetadataIdsForEntities.cs new file mode 100644 index 000000000..570a6abfe --- /dev/null +++ b/Kavita.Database/Migrations/20260313194040_ExternalMetadataIdsForEntities.cs @@ -0,0 +1,213 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Kavita.Database.Migrations +{ + /// + public partial class ExternalMetadataIdsForEntities : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AniListId", + table: "Volume", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ComicVineId", + table: "Volume", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "HardcoverId", + table: "Volume", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "MalId", + table: "Volume", + type: "INTEGER", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "MangaBakaId", + table: "Volume", + type: "INTEGER", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "MetronId", + table: "Volume", + type: "INTEGER", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "AniListId", + table: "Series", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ComicVineId", + table: "Series", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "HardcoverId", + table: "Series", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "MalId", + table: "Series", + type: "INTEGER", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "MangaBakaId", + table: "Series", + type: "INTEGER", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "MetronId", + table: "Series", + type: "INTEGER", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "AniListId", + table: "Chapter", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ComicVineId", + table: "Chapter", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "HardcoverId", + table: "Chapter", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "MalId", + table: "Chapter", + type: "INTEGER", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "MangaBakaId", + table: "Chapter", + type: "INTEGER", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "MetronId", + table: "Chapter", + type: "INTEGER", + nullable: false, + defaultValue: 0L); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AniListId", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "ComicVineId", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "HardcoverId", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "MalId", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "MangaBakaId", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "MetronId", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "AniListId", + table: "Series"); + + migrationBuilder.DropColumn( + name: "ComicVineId", + table: "Series"); + + migrationBuilder.DropColumn( + name: "HardcoverId", + table: "Series"); + + migrationBuilder.DropColumn( + name: "MalId", + table: "Series"); + + migrationBuilder.DropColumn( + name: "MangaBakaId", + table: "Series"); + + migrationBuilder.DropColumn( + name: "MetronId", + table: "Series"); + + migrationBuilder.DropColumn( + name: "AniListId", + table: "Chapter"); + + migrationBuilder.DropColumn( + name: "ComicVineId", + table: "Chapter"); + + migrationBuilder.DropColumn( + name: "HardcoverId", + table: "Chapter"); + + migrationBuilder.DropColumn( + name: "MalId", + table: "Chapter"); + + migrationBuilder.DropColumn( + name: "MangaBakaId", + table: "Chapter"); + + migrationBuilder.DropColumn( + name: "MetronId", + table: "Chapter"); + } + } +} diff --git a/Kavita.Database/Migrations/DataContextModelSnapshot.cs b/Kavita.Database/Migrations/DataContextModelSnapshot.cs index f8049cfb0..782fe2e3d 100644 --- a/Kavita.Database/Migrations/DataContextModelSnapshot.cs +++ b/Kavita.Database/Migrations/DataContextModelSnapshot.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using Kavita.Database; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Progress; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -16,9 +18,2144 @@ namespace Kavita.Database.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); - modelBuilder.Entity("API.Entities.AppRole", b => + 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("Kavita.Models.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("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AverageExternalRating") + .HasColumnType("REAL"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("ComicVineId") + .HasColumnType("TEXT"); + + 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("HardcoverId") + .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("MalId") + .HasColumnType("INTEGER"); + + b.Property("MangaBakaId") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MetronId") + .HasColumnType("INTEGER"); + + 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("TitleName") + .HasDatabaseName("IX_Chapter_TitleName"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.Entities.EpubFont", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .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("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("EpubFont"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("DefaultLanguage") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + 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("InheritWebLinksFromFirstChapter") + .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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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.HasIndex("FilePath") + .HasDatabaseName("IX_MangaFile_FilePath"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.Entities.Metadata.GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("AgeRating") + .HasDatabaseName("IX_SeriesMetadata_AgeRating"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.HasIndex("SeriesId", "AgeRating") + .HasDatabaseName("IX_SeriesMetadata_SeriesId_AgeRating"); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Metadata.SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("EnableExtendedMetadataProcessing") + .HasColumnType("INTEGER"); + + 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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.Entities.Progress.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("TotalReads") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ClientInfoUsed") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("Data") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("{\"TotalMinutesRead\":0,\"TotalPagesRead\":0,\"TotalWordsRead\":0,\"LongestSessionMinutes\":0,\"Activities\":[],\"SeriesIds\":null,\"ChapterIds\":null}"); + + b.Property("DateUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("DateUtc") + .IsUnique(); + + b.ToTable("AppUserReadingHistory"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("EndTimeUtc") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("StartTimeUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("AppUserId", "IsActive") + .HasDatabaseName("IX_AppUserReadingSession_AppUserId_IsActive"); + + b.HasIndex("IsActive", "LastModifiedUtc") + .HasDatabaseName("IX_AppUserReadingSession_IsActive_LastModifiedUtc"); + + b.ToTable("AppUserReadingSession"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingSessionActivityData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserReadingSessionId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("DeviceIds") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EndBookScrollId") + .HasColumnType("TEXT"); + + b.Property("EndPage") + .HasColumnType("INTEGER"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("EndTimeUtc") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("StartBookScrollId") + .HasColumnType("TEXT"); + + b.Property("StartPage") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("StartTimeUtc") + .HasColumnType("TEXT"); + + b.Property("TotalPages") + .HasColumnType("INTEGER"); + + b.Property("TotalWords") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WordsRead") + .HasColumnType("INTEGER"); + + b.ComplexProperty(typeof(Dictionary), "ClientInfo", "Kavita.Models.Entities.Progress.AppUserReadingSessionActivityData.ClientInfo#ClientInfoData", b1 => + { + b1.Property("AppVersion"); + + b1.Property("AuthType"); + + b1.Property("Browser"); + + b1.Property("BrowserVersion"); + + b1.Property("CapturedAt"); + + b1.Property("ClientType"); + + b1.Property("DeviceType"); + + b1.Property("IpAddress") + .IsRequired(); + + b1.Property("Orientation"); + + b1.Property("Platform"); + + b1.Property("ScreenHeight"); + + b1.Property("ScreenWidth"); + + b1.Property("UserAgent") + .IsRequired(); + + b1 + .ToJson("ClientInfo") + .HasColumnType("TEXT"); + }); + + b.HasKey("Id"); + + b.HasIndex("AppUserReadingSessionId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.HasIndex("StartTimeUtc", "LibraryId") + .HasDatabaseName("IX_ActivityData_StartTimeUtc_LibraryId"); + + b.ToTable("AppUserReadingSessionActivityData"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("ComicVineId") + .HasColumnType("TEXT"); + + 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("HardcoverId") + .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("MalId") + .HasColumnType("INTEGER"); + + b.Property("MangaBakaId") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MetronId") + .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") + .HasDatabaseName("IX_Series_LibraryId"); + + b.HasIndex("NormalizedName") + .HasDatabaseName("IX_Series_NormalizedName"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.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("Kavita.Models.Entities.User.AppRole", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -45,7 +2182,7 @@ namespace Kavita.Database.Migrations b.ToTable("AspNetRoles", (string)null); }); - modelBuilder.Entity("API.Entities.AppUser", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUser", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -170,7 +2307,7 @@ namespace Kavita.Database.Migrations b.ToTable("AspNetUsers", (string)null); }); - modelBuilder.Entity("API.Entities.AppUserAnnotation", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserAnnotation", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -257,7 +2394,48 @@ namespace Kavita.Database.Migrations b.ToTable("AppUserAnnotation"); }); - modelBuilder.Entity("API.Entities.AppUserBookmark", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserAuthKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAtUtc") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtUtc") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastAccessedAtUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Provider") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ExpiresAtUtc"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("AppUserAuthKey"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserBookmark", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -316,7 +2494,54 @@ namespace Kavita.Database.Migrations b.ToTable("AppUserBookmark"); }); - modelBuilder.Entity("API.Entities.AppUserCollection", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserChapterRating", 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("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + 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("Kavita.Models.Entities.User.AppUserCollection", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -388,7 +2613,7 @@ namespace Kavita.Database.Migrations b.ToTable("AppUserCollection"); }); - modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserDashboardStream", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -428,7 +2653,7 @@ namespace Kavita.Database.Migrations b.ToTable("AppUserDashboardStream"); }); - modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserExternalSource", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -453,7 +2678,7 @@ namespace Kavita.Database.Migrations b.ToTable("AppUserExternalSource"); }); - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserOnDeckRemoval", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -474,2427 +2699,7 @@ namespace Kavita.Database.Migrations b.ToTable("AppUserOnDeckRemoval"); }); - modelBuilder.Entity("API.Entities.AppUserRating", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("Review") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - 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.PrimitiveCollection("DeviceIds") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - - b.Property("DisableWidthOverride") - .HasColumnType("INTEGER"); - - b.Property("EmulateBook") - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("LayoutMode") - .HasColumnType("INTEGER"); - - b.PrimitiveCollection("LibraryIds") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - - 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.PrimitiveCollection("SeriesIds") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - - 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("ChapterTitle") - .HasColumnType("TEXT"); - - 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("SelectedText") - .HasColumnType("TEXT"); - - 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("TitleName") - .HasDatabaseName("IX_Chapter_TitleName"); - - b.HasIndex("VolumeId"); - - b.ToTable("Chapter"); - }); - - modelBuilder.Entity("API.Entities.ClientDeviceHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CapturedAtUtc") - .HasColumnType("TEXT"); - - b.Property("ClientInfo") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("{\"UserAgent\":\"\",\"IpAddress\":\"\",\"AuthType\":0,\"ClientType\":0,\"AppVersion\":null,\"Browser\":null,\"BrowserVersion\":null,\"Platform\":0,\"DeviceType\":null,\"ScreenWidth\":null,\"ScreenHeight\":null,\"Orientation\":null,\"CapturedAt\":\"0001-01-01T00:00:00\"}"); - - b.Property("DeviceId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId"); - - b.ToTable("ClientDeviceHistory"); - }); - - 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.EpubFont", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .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("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasColumnType("TEXT"); - - b.Property("Provider") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("EpubFont"); - }); - - 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("DefaultLanguage") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue(""); - - 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("InheritWebLinksFromFirstChapter") - .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.HasIndex("FilePath") - .HasDatabaseName("IX_MangaFile_FilePath"); - - 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.GenreSeriesMetadata", b => - { - b.Property("GenresId") - .HasColumnType("INTEGER"); - - b.Property("SeriesMetadatasId") - .HasColumnType("INTEGER"); - - b.HasKey("GenresId", "SeriesMetadatasId"); - - b.HasIndex("SeriesMetadatasId"); - - b.ToTable("GenreSeriesMetadata"); - }); - - 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("AgeRating") - .HasDatabaseName("IX_SeriesMetadata_AgeRating"); - - b.HasIndex("SeriesId") - .IsUnique(); - - b.HasIndex("Id", "SeriesId") - .IsUnique(); - - b.HasIndex("SeriesId", "AgeRating") - .HasDatabaseName("IX_SeriesMetadata_SeriesId_AgeRating"); - - b.ToTable("SeriesMetadata"); - }); - - modelBuilder.Entity("API.Entities.Metadata.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.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("EnableExtendedMetadataProcessing") - .HasColumnType("INTEGER"); - - 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.Progress.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("TotalReads") - .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.Progress.AppUserReadingHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("ClientInfoUsed") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("[]"); - - b.Property("Data") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT") - .HasDefaultValue("{\"TotalMinutesRead\":0,\"TotalPagesRead\":0,\"TotalWordsRead\":0,\"LongestSessionMinutes\":0,\"Activities\":[],\"SeriesIds\":null,\"ChapterIds\":null}"); - - b.Property("DateUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("DateUtc") - .IsUnique(); - - b.ToTable("AppUserReadingHistory"); - }); - - modelBuilder.Entity("API.Entities.Progress.AppUserReadingSession", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("Created") - .HasColumnType("TEXT"); - - b.Property("CreatedUtc") - .HasColumnType("TEXT"); - - b.Property("EndTime") - .HasColumnType("TEXT"); - - b.Property("EndTimeUtc") - .HasColumnType("TEXT"); - - b.Property("IsActive") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(true); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("StartTime") - .HasColumnType("TEXT"); - - b.Property("StartTimeUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("IsActive"); - - b.HasIndex("AppUserId", "IsActive") - .HasDatabaseName("IX_AppUserReadingSession_AppUserId_IsActive"); - - b.HasIndex("IsActive", "LastModifiedUtc") - .HasDatabaseName("IX_AppUserReadingSession_IsActive_LastModifiedUtc"); - - b.ToTable("AppUserReadingSession"); - }); - - modelBuilder.Entity("API.Entities.Progress.AppUserReadingSessionActivityData", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserReadingSessionId") - .HasColumnType("INTEGER"); - - b.Property("ChapterId") - .HasColumnType("INTEGER"); - - b.PrimitiveCollection("DeviceIds") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("EndBookScrollId") - .HasColumnType("TEXT"); - - b.Property("EndPage") - .HasColumnType("INTEGER"); - - b.Property("EndTime") - .HasColumnType("TEXT"); - - b.Property("EndTimeUtc") - .HasColumnType("TEXT"); - - b.Property("Format") - .HasColumnType("INTEGER"); - - b.Property("LibraryId") - .HasColumnType("INTEGER"); - - b.Property("PagesRead") - .HasColumnType("INTEGER"); - - b.Property("SeriesId") - .HasColumnType("INTEGER"); - - b.Property("StartBookScrollId") - .HasColumnType("TEXT"); - - b.Property("StartPage") - .HasColumnType("INTEGER"); - - b.Property("StartTime") - .HasColumnType("TEXT"); - - b.Property("StartTimeUtc") - .HasColumnType("TEXT"); - - b.Property("TotalPages") - .HasColumnType("INTEGER"); - - b.Property("TotalWords") - .HasColumnType("INTEGER"); - - b.Property("VolumeId") - .HasColumnType("INTEGER"); - - b.Property("WordsRead") - .HasColumnType("INTEGER"); - - b.ComplexProperty(typeof(Dictionary), "ClientInfo", "API.Entities.Progress.AppUserReadingSessionActivityData.ClientInfo#ClientInfoData", b1 => - { - b1.Property("AppVersion"); - - b1.Property("AuthType"); - - b1.Property("Browser"); - - b1.Property("BrowserVersion"); - - b1.Property("CapturedAt"); - - b1.Property("ClientType"); - - b1.Property("DeviceType"); - - b1.Property("IpAddress") - .IsRequired(); - - b1.Property("Orientation"); - - b1.Property("Platform"); - - b1.Property("ScreenHeight"); - - b1.Property("ScreenWidth"); - - b1.Property("UserAgent") - .IsRequired(); - - b1.ToJson("ClientInfo"); - }); - - b.HasKey("Id"); - - b.HasIndex("AppUserReadingSessionId"); - - b.HasIndex("ChapterId"); - - b.HasIndex("LibraryId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("VolumeId"); - - b.HasIndex("StartTimeUtc", "LibraryId") - .HasDatabaseName("IX_ActivityData_StartTimeUtc_LibraryId"); - - b.ToTable("AppUserReadingSessionActivityData"); - }); - - 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") - .HasDatabaseName("IX_Series_LibraryId"); - - b.HasIndex("NormalizedName") - .HasDatabaseName("IX_Series_NormalizedName"); - - 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.User.AppUserAuthKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AppUserId") - .HasColumnType("INTEGER"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("ExpiresAtUtc") - .HasColumnType("TEXT"); - - b.Property("Key") - .HasColumnType("TEXT"); - - b.Property("LastAccessedAtUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Provider") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("Id"); - - b.HasIndex("AppUserId"); - - b.HasIndex("ExpiresAtUtc"); - - b.HasIndex("Key") - .IsUnique(); - - b.ToTable("AppUserAuthKey"); - }); - - modelBuilder.Entity("API.Entities.User.AppUserChapterRating", 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("HasBeenRated") - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - 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.User.AppUserPreferences", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserPreferences", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -3065,7 +2870,338 @@ namespace Kavita.Database.Migrations b.ToTable("AppUserPreferences"); }); - modelBuilder.Entity("API.Entities.User.ClientDevice", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.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.PrimitiveCollection("DeviceIds") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + b.Property("DisableWidthOverride") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("LibraryIds") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + 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.PrimitiveCollection("SeriesIds") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("[]"); + + 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("Kavita.Models.Entities.User.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("Kavita.Models.Entities.User.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("Kavita.Models.Entities.User.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("Kavita.Models.Entities.User.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("ChapterTitle") + .HasColumnType("TEXT"); + + 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("SelectedText") + .HasColumnType("TEXT"); + + 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("Kavita.Models.Entities.User.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("Kavita.Models.Entities.User.ClientDevice", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -3107,15 +3243,45 @@ namespace Kavita.Database.Migrations b.ToTable("ClientDevice"); }); - modelBuilder.Entity("API.Entities.Volume", b => + modelBuilder.Entity("Kavita.Models.Entities.User.ClientDeviceHistory", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("CapturedAtUtc") + .HasColumnType("TEXT"); + + b.Property("ClientInfo") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("{\"UserAgent\":\"\",\"IpAddress\":\"\",\"AuthType\":0,\"ClientType\":0,\"AppVersion\":null,\"Browser\":null,\"BrowserVersion\":null,\"Platform\":0,\"DeviceType\":null,\"ScreenWidth\":null,\"ScreenHeight\":null,\"Orientation\":null,\"CapturedAt\":\"0001-01-01T00:00:00\"}"); + + b.Property("DeviceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.ToTable("ClientDeviceHistory"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + b.Property("AvgHoursToRead") .HasColumnType("REAL"); + b.Property("ComicVineId") + .HasColumnType("TEXT"); + b.Property("CoverImage") .HasColumnType("TEXT"); @@ -3128,6 +3294,9 @@ namespace Kavita.Database.Migrations b.Property("CreatedUtc") .HasColumnType("TEXT"); + b.Property("HardcoverId") + .HasColumnType("INTEGER"); + b.Property("LastModified") .HasColumnType("TEXT"); @@ -3137,12 +3306,21 @@ namespace Kavita.Database.Migrations b.Property("LookupName") .HasColumnType("TEXT"); + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("MangaBakaId") + .HasColumnType("INTEGER"); + b.Property("MaxHoursToRead") .HasColumnType("INTEGER"); b.Property("MaxNumber") .HasColumnType("REAL"); + b.Property("MetronId") + .HasColumnType("INTEGER"); + b.Property("MinHoursToRead") .HasColumnType("INTEGER"); @@ -3177,126 +3355,6 @@ namespace Kavita.Database.Migrations 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("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => { b.Property("Id") @@ -3398,260 +3456,129 @@ namespace Kavita.Database.Migrations b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("API.Entities.AppUserAnnotation", b => + modelBuilder.Entity("AppUserCollectionSeries", b => { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Annotations") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") + b.HasOne("Kavita.Models.Entities.User.AppUserCollection", null) .WithMany() - .HasForeignKey("ChapterId") + .HasForeignKey("CollectionsId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Library", "Library") + b.HasOne("Kavita.Models.Entities.Series", null) .WithMany() - .HasForeignKey("LibraryId") + .HasForeignKey("ItemsId") .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("Library"); - - b.Navigation("Series"); }); - modelBuilder.Entity("API.Entities.AppUserBookmark", b => + modelBuilder.Entity("AppUserLibrary", b => { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Bookmarks") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Chapter", "Chapter") + b.HasOne("Kavita.Models.Entities.User.AppUser", null) .WithMany() - .HasForeignKey("ChapterId") + .HasForeignKey("AppUsersId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.Library", null) .WithMany() - .HasForeignKey("SeriesId") + .HasForeignKey("LibrariesId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + }); - b.HasOne("API.Entities.Volume", "Volume") + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("Kavita.Models.Entities.Chapter", null) .WithMany() - .HasForeignKey("VolumeId") + .HasForeignKey("ChaptersId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("AppUser"); - - b.Navigation("Chapter"); - - b.Navigation("Series"); - - b.Navigation("Volume"); - }); - - 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") + b.HasOne("Kavita.Models.Entities.Genre", null) .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") + .HasForeignKey("GenresId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - - b.Navigation("AppUser"); }); - modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + modelBuilder.Entity("ChapterTag", b => { - b.HasOne("API.Entities.AppUser", "AppUser") + b.HasOne("Kavita.Models.Entities.Chapter", null) .WithMany() - .HasForeignKey("AppUserId") + .HasForeignKey("ChaptersId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.Tag", null) .WithMany() - .HasForeignKey("SeriesId") + .HasForeignKey("TagsId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - - b.Navigation("AppUser"); - - b.Navigation("Series"); }); - modelBuilder.Entity("API.Entities.AppUserRating", b => + modelBuilder.Entity("CollectionTagSeriesMetadata", 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") + b.HasOne("Kavita.Models.Entities.CollectionTag", null) .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") + .HasForeignKey("CollectionTagsId") .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") + b.HasOne("Kavita.Models.Entities.Metadata.SeriesMetadata", null) .WithMany() - .HasForeignKey("ChapterId") + .HasForeignKey("SeriesMetadatasId") .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 => + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("WantToRead") - .HasForeignKey("AppUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.Metadata.ExternalRating", null) .WithMany() - .HasForeignKey("SeriesId") + .HasForeignKey("ExternalRatingsId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("AppUser"); - - b.Navigation("Series"); + b.HasOne("Kavita.Models.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); - modelBuilder.Entity("API.Entities.Chapter", b => + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => { - b.HasOne("API.Entities.Volume", "Volume") + b.HasOne("Kavita.Models.Entities.Metadata.ExternalRecommendation", null) + .WithMany() + .HasForeignKey("ExternalRecommendationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.HasOne("Kavita.Models.Entities.Metadata.ExternalReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Chapter", b => + { + b.HasOne("Kavita.Models.Entities.Volume", "Volume") .WithMany("Chapters") .HasForeignKey("VolumeId") .OnDelete(DeleteBehavior.Cascade) @@ -3660,20 +3587,9 @@ namespace Kavita.Database.Migrations b.Navigation("Volume"); }); - modelBuilder.Entity("API.Entities.ClientDeviceHistory", b => + modelBuilder.Entity("Kavita.Models.Entities.Device", b => { - b.HasOne("API.Entities.User.ClientDevice", "Device") - .WithMany("History") - .HasForeignKey("DeviceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Device"); - }); - - modelBuilder.Entity("API.Entities.Device", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") .WithMany("Devices") .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) @@ -3682,9 +3598,9 @@ namespace Kavita.Database.Migrations b.Navigation("AppUser"); }); - modelBuilder.Entity("API.Entities.EmailHistory", b => + modelBuilder.Entity("Kavita.Models.Entities.EmailHistory", b => { - b.HasOne("API.Entities.AppUser", "AppUser") + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") .WithMany() .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) @@ -3693,9 +3609,9 @@ namespace Kavita.Database.Migrations b.Navigation("AppUser"); }); - modelBuilder.Entity("API.Entities.FolderPath", b => + modelBuilder.Entity("Kavita.Models.Entities.FolderPath", b => { - b.HasOne("API.Entities.Library", "Library") + b.HasOne("Kavita.Models.Entities.Library", "Library") .WithMany("Folders") .HasForeignKey("LibraryId") .OnDelete(DeleteBehavior.Cascade) @@ -3704,9 +3620,9 @@ namespace Kavita.Database.Migrations b.Navigation("Library"); }); - modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + modelBuilder.Entity("Kavita.Models.Entities.LibraryExcludePattern", b => { - b.HasOne("API.Entities.Library", "Library") + b.HasOne("Kavita.Models.Entities.Library", "Library") .WithMany("LibraryExcludePatterns") .HasForeignKey("LibraryId") .OnDelete(DeleteBehavior.Cascade) @@ -3715,9 +3631,9 @@ namespace Kavita.Database.Migrations b.Navigation("Library"); }); - modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + modelBuilder.Entity("Kavita.Models.Entities.LibraryFileTypeGroup", b => { - b.HasOne("API.Entities.Library", "Library") + b.HasOne("Kavita.Models.Entities.Library", "Library") .WithMany("LibraryFileTypes") .HasForeignKey("LibraryId") .OnDelete(DeleteBehavior.Cascade) @@ -3726,9 +3642,9 @@ namespace Kavita.Database.Migrations b.Navigation("Library"); }); - modelBuilder.Entity("API.Entities.MangaFile", b => + modelBuilder.Entity("Kavita.Models.Entities.MangaFile", b => { - b.HasOne("API.Entities.Chapter", "Chapter") + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") .WithMany("Files") .HasForeignKey("ChapterId") .OnDelete(DeleteBehavior.Cascade) @@ -3737,49 +3653,49 @@ namespace Kavita.Database.Migrations b.Navigation("Chapter"); }); - modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + modelBuilder.Entity("Kavita.Models.Entities.Metadata.ExternalRating", b => { - b.HasOne("API.Entities.Chapter", null) + b.HasOne("Kavita.Models.Entities.Chapter", null) .WithMany("ExternalRatings") .HasForeignKey("ChapterId"); }); - modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + modelBuilder.Entity("Kavita.Models.Entities.Metadata.ExternalReview", b => { - b.HasOne("API.Entities.Chapter", null) + b.HasOne("Kavita.Models.Entities.Chapter", null) .WithMany("ExternalReviews") .HasForeignKey("ChapterId"); }); - modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + modelBuilder.Entity("Kavita.Models.Entities.Metadata.ExternalSeriesMetadata", b => { - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.Series", "Series") .WithOne("ExternalSeriesMetadata") - .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") + .HasForeignKey("Kavita.Models.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Series"); }); - modelBuilder.Entity("API.Entities.Metadata.GenreSeriesMetadata", b => + modelBuilder.Entity("Kavita.Models.Entities.Metadata.GenreSeriesMetadata", b => { - b.HasOne("API.Entities.Genre", null) + b.HasOne("Kavita.Models.Entities.Genre", null) .WithMany() .HasForeignKey("GenresId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + b.HasOne("Kavita.Models.Entities.Metadata.SeriesMetadata", null) .WithMany() .HasForeignKey("SeriesMetadatasId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + modelBuilder.Entity("Kavita.Models.Entities.Metadata.SeriesBlacklist", b => { - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.Series", "Series") .WithMany() .HasForeignKey("SeriesId") .OnDelete(DeleteBehavior.Cascade) @@ -3788,41 +3704,41 @@ namespace Kavita.Database.Migrations b.Navigation("Series"); }); - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + modelBuilder.Entity("Kavita.Models.Entities.Metadata.SeriesMetadata", b => { - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.Series", "Series") .WithOne("Metadata") - .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .HasForeignKey("Kavita.Models.Entities.Metadata.SeriesMetadata", "SeriesId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Series"); }); - modelBuilder.Entity("API.Entities.Metadata.SeriesMetadataTag", b => + modelBuilder.Entity("Kavita.Models.Entities.Metadata.SeriesMetadataTag", b => { - b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + b.HasOne("Kavita.Models.Entities.Metadata.SeriesMetadata", null) .WithMany() .HasForeignKey("SeriesMetadatasId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Tag", null) + b.HasOne("Kavita.Models.Entities.Tag", null) .WithMany() .HasForeignKey("TagsId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + modelBuilder.Entity("Kavita.Models.Entities.Metadata.SeriesRelation", b => { - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.Series", "Series") .WithMany("Relations") .HasForeignKey("SeriesId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Series", "TargetSeries") + b.HasOne("Kavita.Models.Entities.Series", "TargetSeries") .WithMany("RelationOf") .HasForeignKey("TargetSeriesId") .OnDelete(DeleteBehavior.Cascade) @@ -3833,9 +3749,9 @@ namespace Kavita.Database.Migrations b.Navigation("TargetSeries"); }); - modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + modelBuilder.Entity("Kavita.Models.Entities.MetadataFieldMapping", b => { - b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") + b.HasOne("Kavita.Models.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") .WithMany("FieldMappings") .HasForeignKey("MetadataSettingsId") .OnDelete(DeleteBehavior.Cascade) @@ -3844,15 +3760,15 @@ namespace Kavita.Database.Migrations b.Navigation("MetadataSettings"); }); - modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + modelBuilder.Entity("Kavita.Models.Entities.Person.ChapterPeople", b => { - b.HasOne("API.Entities.Chapter", "Chapter") + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") .WithMany("People") .HasForeignKey("ChapterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Person.Person", "Person") + b.HasOne("Kavita.Models.Entities.Person.Person", "Person") .WithMany("ChapterPeople") .HasForeignKey("PersonId") .OnDelete(DeleteBehavior.Cascade) @@ -3863,9 +3779,9 @@ namespace Kavita.Database.Migrations b.Navigation("Person"); }); - modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + modelBuilder.Entity("Kavita.Models.Entities.Person.PersonAlias", b => { - b.HasOne("API.Entities.Person.Person", "Person") + b.HasOne("Kavita.Models.Entities.Person.Person", "Person") .WithMany("Aliases") .HasForeignKey("PersonId") .OnDelete(DeleteBehavior.Cascade) @@ -3874,15 +3790,15 @@ namespace Kavita.Database.Migrations b.Navigation("Person"); }); - modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + modelBuilder.Entity("Kavita.Models.Entities.Person.SeriesMetadataPeople", b => { - b.HasOne("API.Entities.Person.Person", "Person") + b.HasOne("Kavita.Models.Entities.Person.Person", "Person") .WithMany("SeriesMetadataPeople") .HasForeignKey("PersonId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") + b.HasOne("Kavita.Models.Entities.Metadata.SeriesMetadata", "SeriesMetadata") .WithMany("People") .HasForeignKey("SeriesMetadataId") .OnDelete(DeleteBehavior.Cascade) @@ -3893,21 +3809,21 @@ namespace Kavita.Database.Migrations b.Navigation("SeriesMetadata"); }); - modelBuilder.Entity("API.Entities.Progress.AppUserProgress", b => + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserProgress", b => { - b.HasOne("API.Entities.AppUser", "AppUser") + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") .WithMany("Progresses") .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Chapter", null) + b.HasOne("Kavita.Models.Entities.Chapter", null) .WithMany("UserProgress") .HasForeignKey("ChapterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Series", null) + b.HasOne("Kavita.Models.Entities.Series", null) .WithMany("Progress") .HasForeignKey("SeriesId") .OnDelete(DeleteBehavior.Cascade) @@ -3916,9 +3832,9 @@ namespace Kavita.Database.Migrations b.Navigation("AppUser"); }); - modelBuilder.Entity("API.Entities.Progress.AppUserReadingHistory", b => + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingHistory", b => { - b.HasOne("API.Entities.AppUser", "AppUser") + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") .WithMany("ReadingHistory") .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) @@ -3927,9 +3843,9 @@ namespace Kavita.Database.Migrations b.Navigation("AppUser"); }); - modelBuilder.Entity("API.Entities.Progress.AppUserReadingSession", b => + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingSession", b => { - b.HasOne("API.Entities.AppUser", "AppUser") + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") .WithMany("ReadingSessions") .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) @@ -3938,33 +3854,33 @@ namespace Kavita.Database.Migrations b.Navigation("AppUser"); }); - modelBuilder.Entity("API.Entities.Progress.AppUserReadingSessionActivityData", b => + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingSessionActivityData", b => { - b.HasOne("API.Entities.Progress.AppUserReadingSession", "ReadingSession") + b.HasOne("Kavita.Models.Entities.Progress.AppUserReadingSession", "ReadingSession") .WithMany("ActivityData") .HasForeignKey("AppUserReadingSessionId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Chapter", "Chapter") + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") .WithMany() .HasForeignKey("ChapterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Library", "Library") + b.HasOne("Kavita.Models.Entities.Library", "Library") .WithMany() .HasForeignKey("LibraryId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.Series", "Series") .WithMany() .HasForeignKey("SeriesId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Volume", "Volume") + b.HasOne("Kavita.Models.Entities.Volume", "Volume") .WithMany() .HasForeignKey("VolumeId") .OnDelete(DeleteBehavior.Cascade) @@ -3981,9 +3897,9 @@ namespace Kavita.Database.Migrations b.Navigation("Volume"); }); - modelBuilder.Entity("API.Entities.ReadingList", b => + modelBuilder.Entity("Kavita.Models.Entities.ReadingList", b => { - b.HasOne("API.Entities.AppUser", "AppUser") + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") .WithMany("ReadingLists") .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) @@ -3992,27 +3908,27 @@ namespace Kavita.Database.Migrations b.Navigation("AppUser"); }); - modelBuilder.Entity("API.Entities.ReadingListItem", b => + modelBuilder.Entity("Kavita.Models.Entities.ReadingListItem", b => { - b.HasOne("API.Entities.Chapter", "Chapter") + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") .WithMany() .HasForeignKey("ChapterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.ReadingList", "ReadingList") + b.HasOne("Kavita.Models.Entities.ReadingList", "ReadingList") .WithMany("Items") .HasForeignKey("ReadingListId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.Series", "Series") .WithMany() .HasForeignKey("SeriesId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Volume", "Volume") + b.HasOne("Kavita.Models.Entities.Volume", "Volume") .WithMany() .HasForeignKey("VolumeId") .OnDelete(DeleteBehavior.Cascade) @@ -4027,13 +3943,13 @@ namespace Kavita.Database.Migrations b.Navigation("Volume"); }); - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + modelBuilder.Entity("Kavita.Models.Entities.Scrobble.ScrobbleError", b => { - b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + b.HasOne("Kavita.Models.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") .WithMany() .HasForeignKey("ScrobbleEventId1"); - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.Series", "Series") .WithMany() .HasForeignKey("SeriesId") .OnDelete(DeleteBehavior.Cascade) @@ -4044,21 +3960,21 @@ namespace Kavita.Database.Migrations b.Navigation("Series"); }); - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + modelBuilder.Entity("Kavita.Models.Entities.Scrobble.ScrobbleEvent", b => { - b.HasOne("API.Entities.AppUser", "AppUser") + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") .WithMany() .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Library", "Library") + b.HasOne("Kavita.Models.Entities.Library", "Library") .WithMany() .HasForeignKey("LibraryId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.Series", "Series") .WithMany() .HasForeignKey("SeriesId") .OnDelete(DeleteBehavior.Cascade) @@ -4071,15 +3987,15 @@ namespace Kavita.Database.Migrations b.Navigation("Series"); }); - modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + modelBuilder.Entity("Kavita.Models.Entities.Scrobble.ScrobbleHold", b => { - b.HasOne("API.Entities.AppUser", "AppUser") + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") .WithMany("ScrobbleHolds") .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.Series", "Series") .WithMany() .HasForeignKey("SeriesId") .OnDelete(DeleteBehavior.Cascade) @@ -4090,9 +4006,9 @@ namespace Kavita.Database.Migrations b.Navigation("Series"); }); - modelBuilder.Entity("API.Entities.Series", b => + modelBuilder.Entity("Kavita.Models.Entities.Series", b => { - b.HasOne("API.Entities.Library", "Library") + b.HasOne("Kavita.Models.Entities.Library", "Library") .WithMany("Series") .HasForeignKey("LibraryId") .OnDelete(DeleteBehavior.Cascade) @@ -4101,9 +4017,44 @@ namespace Kavita.Database.Migrations b.Navigation("Library"); }); - modelBuilder.Entity("API.Entities.User.AppUserAuthKey", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserAnnotation", b => { - b.HasOne("API.Entities.AppUser", "AppUser") + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("Annotations") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserAuthKey", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") .WithMany("AuthKeys") .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) @@ -4112,21 +4063,56 @@ namespace Kavita.Database.Migrations b.Navigation("AppUser"); }); - modelBuilder.Entity("API.Entities.User.AppUserChapterRating", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserBookmark", b => { - b.HasOne("API.Entities.AppUser", "AppUser") + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserChapterRating", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") .WithMany("ChapterRatings") .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Chapter", "Chapter") + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") .WithMany("Ratings") .HasForeignKey("ChapterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.Series", "Series") .WithMany() .HasForeignKey("SeriesId") .OnDelete(DeleteBehavior.Cascade) @@ -4139,15 +4125,73 @@ namespace Kavita.Database.Migrations b.Navigation("Series"); }); - modelBuilder.Entity("API.Entities.User.AppUserPreferences", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserCollection", b => { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithOne("UserPreferences") - .HasForeignKey("API.Entities.User.AppUserPreferences", "AppUserId") + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("Collections") + .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("API.Entities.SiteTheme", "Theme") + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserDashboardStream", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.User.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserExternalSource", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserOnDeckRemoval", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserPreferences", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("Kavita.Models.Entities.User.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.SiteTheme", "Theme") .WithMany() .HasForeignKey("ThemeId"); @@ -4156,9 +4200,132 @@ namespace Kavita.Database.Migrations b.Navigation("Theme"); }); - modelBuilder.Entity("API.Entities.User.ClientDevice", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserRating", b => { - b.HasOne("API.Entities.AppUser", "AppUser") + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserReadingProfile", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("ReadingProfiles") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserRole", b => + { + b.HasOne("Kavita.Models.Entities.User.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.User.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserSideNavStream", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.User.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserSmartFilter", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserTableOfContent", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.AppUserWantToRead", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Kavita.Models.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.User.ClientDevice", b => + { + b.HasOne("Kavita.Models.Entities.User.AppUser", "AppUser") .WithMany("ClientDevices") .HasForeignKey("AppUserId") .OnDelete(DeleteBehavior.Cascade) @@ -4167,9 +4334,20 @@ namespace Kavita.Database.Migrations b.Navigation("AppUser"); }); - modelBuilder.Entity("API.Entities.Volume", b => + modelBuilder.Entity("Kavita.Models.Entities.User.ClientDeviceHistory", b => { - b.HasOne("API.Entities.Series", "Series") + b.HasOne("Kavita.Models.Entities.User.ClientDevice", "Device") + .WithMany("History") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Volume", b => + { + b.HasOne("Kavita.Models.Entities.Series", "Series") .WithMany("Volumes") .HasForeignKey("SeriesId") .OnDelete(DeleteBehavior.Cascade) @@ -4178,129 +4356,9 @@ namespace Kavita.Database.Migrations 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("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { - b.HasOne("API.Entities.AppRole", null) + b.HasOne("Kavita.Models.Entities.User.AppRole", null) .WithMany() .HasForeignKey("RoleId") .OnDelete(DeleteBehavior.Cascade) @@ -4309,7 +4367,7 @@ namespace Kavita.Database.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.HasOne("API.Entities.AppUser", null) + b.HasOne("Kavita.Models.Entities.User.AppUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -4318,7 +4376,7 @@ namespace Kavita.Database.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.HasOne("API.Entities.AppUser", null) + b.HasOne("Kavita.Models.Entities.User.AppUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -4327,19 +4385,91 @@ namespace Kavita.Database.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.HasOne("API.Entities.AppUser", null) + b.HasOne("Kavita.Models.Entities.User.AppUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("API.Entities.AppRole", b => + modelBuilder.Entity("Kavita.Models.Entities.Chapter", b => + { + b.Navigation("ExternalRatings"); + + b.Navigation("ExternalReviews"); + + b.Navigation("Files"); + + b.Navigation("People"); + + b.Navigation("Ratings"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("LibraryExcludePatterns"); + + b.Navigation("LibraryFileTypes"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Metadata.SeriesMetadata", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.MetadataMatching.MetadataSettings", b => + { + b.Navigation("FieldMappings"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Person.Person", b => + { + b.Navigation("Aliases"); + + b.Navigation("ChapterPeople"); + + b.Navigation("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.Progress.AppUserReadingSession", b => + { + b.Navigation("ActivityData"); + }); + + modelBuilder.Entity("Kavita.Models.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Kavita.Models.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("Kavita.Models.Entities.User.AppRole", b => { b.Navigation("UserRoles"); }); - modelBuilder.Entity("API.Entities.AppUser", b => + modelBuilder.Entity("Kavita.Models.Entities.User.AppUser", b => { b.Navigation("Annotations"); @@ -4387,84 +4517,12 @@ namespace Kavita.Database.Migrations 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.Progress.AppUserReadingSession", b => - { - b.Navigation("ActivityData"); - }); - - 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.User.ClientDevice", b => + modelBuilder.Entity("Kavita.Models.Entities.User.ClientDevice", b => { b.Navigation("History"); }); - modelBuilder.Entity("API.Entities.Volume", b => + modelBuilder.Entity("Kavita.Models.Entities.Volume", b => { b.Navigation("Chapters"); }); diff --git a/Kavita.Database/Repositories/SeriesRepository.cs b/Kavita.Database/Repositories/SeriesRepository.cs index e1aadf59e..0a197ea16 100644 --- a/Kavita.Database/Repositories/SeriesRepository.cs +++ b/Kavita.Database/Repositories/SeriesRepository.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using AutoMapper; using AutoMapper.QueryableExtensions; using Kavita.API.Repositories; +using Kavita.API.Services.Helpers; using Kavita.API.Services.Plus; using Kavita.API.Services.Reading; using Kavita.Common.Extensions; @@ -629,16 +630,15 @@ public class SeriesRepository(DataContext context, IMapper mapper) : ISeriesRepo AltSeriesName = series.LocalizedName, AniListId = series.ExternalSeriesMetadata.AniListId != 0 ? series.ExternalSeriesMetadata.AniListId - : ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingHelper.AniListWeblinkWebsite), + : WeblinkParser.GetAniListId(series.Metadata.WebLinks), MalId = series.ExternalSeriesMetadata.MalId != 0 ? series.ExternalSeriesMetadata.MalId - : ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingHelper.MalWeblinkWebsite), + : WeblinkParser.GetMalId(series.Metadata.WebLinks), CbrId = series.ExternalSeriesMetadata.CbrId, GoogleBooksId = !string.IsNullOrEmpty(series.ExternalSeriesMetadata.GoogleBooksId) ? series.ExternalSeriesMetadata.GoogleBooksId - : ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingHelper.GoogleBooksWeblinkWebsite), - MangaDexId = ScrobblingHelper.ExtractId(series.Metadata.WebLinks, - ScrobblingHelper.MangaDexWeblinkWebsite), + : WeblinkParser.GetGoogleBooksId(series.Metadata.WebLinks), + MangaDexId = WeblinkParser.GetMangaDexId(series.Metadata.WebLinks), VolumeCount = series.Volumes.Count, ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial), Year = series.Metadata.ReleaseYear diff --git a/Kavita.Models/DTOs/ChapterDto.cs b/Kavita.Models/DTOs/ChapterDto.cs index e935fcd60..3fac07994 100644 --- a/Kavita.Models/DTOs/ChapterDto.cs +++ b/Kavita.Models/DTOs/ChapterDto.cs @@ -15,7 +15,7 @@ namespace Kavita.Models.DTOs; /// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying /// file (abstracted from type). /// -public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage +public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage, IHasMetadataIds { /// public int Id { get; init; } @@ -183,4 +183,13 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage PrimaryColor = string.Empty; SecondaryColor = string.Empty; } + + #region Metadata + public int AniListId { get; set; } + public long MalId { get; set; } + public int HardcoverId { get; set; } + public long MetronId { get; set; } + public string? ComicVineId { get; set; } + public long MangaBakaId { get; set; } + #endregion } diff --git a/Kavita.Models/DTOs/Common/IUpdateExternalMetadataIds.cs b/Kavita.Models/DTOs/Common/IUpdateExternalMetadataIds.cs new file mode 100644 index 000000000..5bc193831 --- /dev/null +++ b/Kavita.Models/DTOs/Common/IUpdateExternalMetadataIds.cs @@ -0,0 +1,15 @@ +namespace Kavita.Models.DTOs.Common; +#nullable enable + +/// +/// Provides a set of optional (non-API breaking) fields for updating external metadata ids +/// +public interface IUpdateExternalMetadataIds +{ + public int? AniListId { get; set; } + public long? MalId { get; set; } + public int? HardcoverId { get; set; } + public long? MetronId { get; set; } + public string? ComicVineId { get; set; } + public long? MangaBakaId { get; set; } +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/CblBook.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblBook.cs deleted file mode 100644 index 177423371..000000000 --- a/Kavita.Models/DTOs/ReadingLists/CBL/CblBook.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Xml.Serialization; - -namespace Kavita.Models.DTOs.ReadingLists.CBL; - - -[XmlRoot(ElementName="Book")] -public sealed record CblBook -{ - [XmlAttribute("Series")] - public string Series { get; set; } - /// - /// Chapter Number - /// - [XmlAttribute("Number")] - public string Number { get; set; } - /// - /// Volume Number (usually for Comics they are the year) - /// - [XmlAttribute("Volume")] - public string Volume { get; set; } - [XmlAttribute("Year")] - public string Year { get; set; } - /// - /// Main Series, Annual, Limited Series - /// - /// This maps to Format tag - [XmlAttribute("Format")] - public string Format { get; set; } - /// - /// The underlying filetype - /// - /// This is not part of the standard and explicitly for Kavita to support non cbz/cbr files - [XmlAttribute("FileType")] - public string FileType { get; set; } -} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/CblExternalDbProvider.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblExternalDbProvider.cs new file mode 100644 index 000000000..d97166a4a --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblExternalDbProvider.cs @@ -0,0 +1,24 @@ +namespace Kavita.Models.DTOs.ReadingLists.CBL; + +/// +/// Known external comic database providers used for issue/series identification. +/// +public enum CblExternalDbProvider +{ + /// + /// Comic Vine (comicvine.gamespot.com). Provider short-name: "cv" + /// + ComicVine, + /// + /// Metron (metron.cloud). Provider short-name: "metron" + /// + Metron, + /// + /// Grand Comics Database (comics.org). Provider short-name: "gcd" + /// + GrandComicsDatabase, + /// + /// Unrecognised or missing provider + /// + Unknown +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/CblExternalId.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblExternalId.cs new file mode 100644 index 000000000..7d408acfc --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblExternalId.cs @@ -0,0 +1,21 @@ +namespace Kavita.Models.DTOs.ReadingLists.CBL; + +/// +/// A resolved external-database reference for a series/issue pair. +/// Populated from V1 Database elements or V2 issueList[].id[] entries. +/// +public sealed record CblExternalId +{ + /// + /// The external database provider (e.g. ComicVine, Metron). + /// + public CblExternalDbProvider Provider { get; set; } + /// + /// Provider-specific series identifier. + /// + public string SeriesId { get; set; } = string.Empty; + /// + /// Provider-specific issue identifier. + /// + public string IssueId { get; set; } = string.Empty; +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/CblImportDecisions.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblImportDecisions.cs new file mode 100644 index 000000000..2ea1a5023 --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblImportDecisions.cs @@ -0,0 +1,9 @@ +namespace Kavita.Models.DTOs.ReadingLists.CBL; + +/// +/// Represents a set of decisions against ambiguity in CBL Import +/// +public record CblImportDecisions +{ + +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/CblImportOptions.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblImportOptions.cs new file mode 100644 index 000000000..9eaec347b --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblImportOptions.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Kavita.Models.DTOs.ReadingLists.CBL; + +public record CblImportOptions +{ + /// + /// Weighs ComicVine Matching higher + /// + public bool PreferComicVineMatching { get; set; } + /// + /// Libraries to search against. If empty, will include all + /// + public IList ApplicableLibraries { get; set; } + +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/CblImportSummary.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblImportSummary.cs index 3315837c1..2d6cdd8d5 100644 --- a/Kavita.Models/DTOs/ReadingLists/CBL/CblImportSummary.cs +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblImportSummary.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.ComponentModel; +using Kavita.Models.DTOs.ReadingLists.CBL.V1; namespace Kavita.Models.DTOs.ReadingLists.CBL; diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/CblIssueType.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblIssueType.cs new file mode 100644 index 000000000..941f1bbcf --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblIssueType.cs @@ -0,0 +1,28 @@ +namespace Kavita.Models.DTOs.ReadingLists.CBL; + +/// +/// Categorisation of an issue's role within a reading list (V2 only). +/// +public enum CblIssueType +{ + /// + /// Unrecognised or unspecified issue type. + /// + Unknown, + /// + /// A core issue in an event storyline. + /// + EventCore, + /// + /// A tie-in issue that crosses over with an event. + /// + EventTieIn, + /// + /// A standalone one-shot related to an event. + /// + EventOneShot, + /// + /// A regular ongoing series issue. + /// + Ongoing +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/CblListType.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblListType.cs new file mode 100644 index 000000000..e0113dcfb --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblListType.cs @@ -0,0 +1,36 @@ +namespace Kavita.Models.DTOs.ReadingLists.CBL; + +/// +/// Classification of a CBL reading list, indicating its scope or purpose. +/// +public enum CblListType +{ + /// + /// Unrecognised or unspecified list type + /// + Unknown, + /// + /// A master reading order spanning an entire publisher's output + /// + Master, + /// + /// Crosses multiple fictional universes within a publisher + /// + Interuniversal, + /// + /// Scoped to a single fictional universe + /// + Universal, + /// + /// Focused on a specific super-hero team (e.g. Avengers, Justice League) + /// + Team, + /// + /// Focused on a single character (e.g. Spider-Man, Batman) + /// + Character, + /// + /// Follows a specific story arc or crossover event + /// + Story +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/CblRelationship.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblRelationship.cs new file mode 100644 index 000000000..d61ab3417 --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblRelationship.cs @@ -0,0 +1,21 @@ +namespace Kavita.Models.DTOs.ReadingLists.CBL; + +/// +/// A link to a related reading list (e.g. prequel, sequel, companion) +/// Populated from V2 listDetails.relationships[] +/// +public sealed record CblRelationship +{ + /// + /// Display name of the related reading list + /// + public string Name { get; set; } = string.Empty; + /// + /// UUID of the related reading list's CBL file + /// + public string Uuid { get; set; } = string.Empty; + /// + /// Nature of the relationship (e.g. "prequel", "sequel", "companion") + /// + public string Relationship { get; set; } = string.Empty; +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/CblSource.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblSource.cs new file mode 100644 index 000000000..4fe4fe24e --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblSource.cs @@ -0,0 +1,17 @@ +namespace Kavita.Models.DTOs.ReadingLists.CBL; + +/// +/// An external source from which a reading list was derived +/// Populated from V2 listDetails.source[] +/// +public sealed record CblSource +{ + /// + /// Name of the source (e.g. "Comic Book Herald") + /// + public string Name { get; set; } = string.Empty; + /// + /// URL pointing to the source material + /// + public string Url { get; set; } = string.Empty; +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/ParsedCblItem.cs b/Kavita.Models/DTOs/ReadingLists/CBL/ParsedCblItem.cs new file mode 100644 index 000000000..55ac19445 --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/ParsedCblItem.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; + +namespace Kavita.Models.DTOs.ReadingLists.CBL; + +/// +/// A single issue/book entry in a unified (V1+V2) parsed reading list +/// +public sealed record ParsedCblItem +{ + /// + /// Zero-based position of this item in the reading list + /// + public int Order { get; set; } + /// + /// Name of the comic series. Sourced from V1 Book/@Series or V2 seriesName + /// + public string SeriesName { get; set; } = string.Empty; + /// + /// Issue/chapter number. Sourced from V1 Book/@Number or V2 issueNumber + /// + public string Number { get; set; } = string.Empty; + /// + /// Volume identifier. V1: Book/@Volume (often the year). V2: derived from seriesStartYear + /// + public string Volume { get; set; } = string.Empty; + /// + /// Publication year. V1: Book/@Year. V2: extracted from issueCoverDate + /// + public string Year { get; set; } = string.Empty; + /// + /// V1-only format tag (e.g. "Main Series", "Annual"). Maps to ComicInfo Format + /// + public string Format { get; set; } = string.Empty; + /// + /// V1-only file type hint (Kavita extension, not part of the CBL standard) + /// + public string FileType { get; set; } = string.Empty; + /// + /// Full cover date string from V2 (ISO 8601 YYYY-MM-DD). Empty for V1 + /// + public string CoverDate { get; set; } = string.Empty; + /// + /// Issue classification from V2. Always for V1 + /// + public CblIssueType IssueType { get; set; } = CblIssueType.Unknown; + /// + /// External database references for this issue. + /// + public List ExternalIds { get; set; } = new(); +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/ParsedCblReadingList.cs b/Kavita.Models/DTOs/ReadingLists/CBL/ParsedCblReadingList.cs new file mode 100644 index 000000000..2a63bd724 --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/ParsedCblReadingList.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; + +namespace Kavita.Models.DTOs.ReadingLists.CBL; + +/// +/// Unified reading list model produced by parsing either a V1 XML or V2 JSON CBL file +/// +public sealed record ParsedCblReadingList +{ + /// + /// Unique file identifier + /// + /// V2 only - empty for V1 + public string Uuid { get; set; } = string.Empty; + /// + /// CBL schema version (1 for XML, 2+ for JSON) + /// + public int SchemaVersion { get; set; } = 1; + /// + /// Display name of the reading list + /// + public string Name { get; set; } = string.Empty; + /// + /// Human-readable summary or description + /// + public string Summary { get; set; } = string.Empty; + /// + /// Free-form notes + /// + /// V2 only - empty for V1 + public string Notes { get; set; } = string.Empty; + /// + /// Start year of the reading list. -1 if not specified + /// + public int StartYear { get; set; } = -1; + /// + /// Start month. V1 only - -1 if not specified + /// + public int StartMonth { get; set; } = -1; + /// + /// End year of the reading list. -1 if not specified + /// + public int EndYear { get; set; } = -1; + /// + /// End month + /// + /// V1 only - -1 if not specified. + public int EndMonth { get; set; } = -1; + /// + /// Primary publisher + /// + /// V2 only - empty for V1. + public string Publisher { get; set; } = string.Empty; + /// + /// Publisher imprint + /// + /// V2 only - empty for V1. + public string Imprint { get; set; } = string.Empty; + /// + /// Classification of the list (master, character, story, etc.) + /// + /// V2 only + public CblListType ListType { get; set; } = CblListType.Unknown; + /// + /// User-defined tags + /// + /// V2 only + public List Tags { get; set; } = new(); + /// + /// Cover image URLs + /// + /// V2 only + public List CoverImageUrls { get; set; } = new(); + /// + /// Related reading lists + /// + /// V2 only + public List Relationships { get; set; } = new(); + /// + /// External sources the list was derived from + /// + /// V2 only + public List Sources { get; set; } = new(); + /// + /// Ordered list of issues/books in the reading list. + /// + public List Items { get; set; } = new(); +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/V1/CblBook.cs b/Kavita.Models/DTOs/ReadingLists/CBL/V1/CblBook.cs new file mode 100644 index 000000000..34d5bda12 --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/V1/CblBook.cs @@ -0,0 +1,66 @@ +using System.Xml.Serialization; + +namespace Kavita.Models.DTOs.ReadingLists.CBL.V1; + +/// +/// External database reference embedded in a V1 CBL Book entry. +/// Maps a provider name to its series and issue identifiers +/// +[XmlRoot(ElementName="Database")] +public sealed record CblBookDatabase +{ + /// + /// Provider short-name (e.g. "cv" for ComicVine, "metron", "gcd") + /// + [XmlAttribute("Name")] + public string Name { get; set; } + /// + /// The provider's unique identifier for the series + /// + [XmlAttribute("Series")] + public string Series { get; set; } + /// + /// The provider's unique identifier for the issue + /// + [XmlAttribute("Issue")] + public string Issue { get; set; } +} + +/// +/// A single book (issue) entry in a V1 XML CBL reading list +/// +[XmlRoot(ElementName="Book")] +public sealed record CblBook +{ + [XmlAttribute("Series")] + public string Series { get; set; } + /// + /// Chapter Number + /// + [XmlAttribute("Number")] + public string Number { get; set; } + /// + /// Volume Number (usually for Comics they are the year) + /// + [XmlAttribute("Volume")] + public string Volume { get; set; } + [XmlAttribute("Year")] + public string Year { get; set; } + /// + /// Main Series, Annual, Limited Series + /// + /// This maps to ComicInfo.Format tag + [XmlAttribute("Format")] + public string Format { get; set; } + /// + /// The underlying filetype + /// + /// This is not part of the standard and explicitly for Kavita to support non cbz/cbr files + [XmlAttribute("FileType")] + public string FileType { get; set; } + /// + /// External database reference (e.g. ComicVine) + /// + [XmlElement("Database")] + public CblBookDatabase Database { get; set; } +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/CblReadingList.cs b/Kavita.Models/DTOs/ReadingLists/CBL/V1/CblReadingList.cs similarity index 91% rename from Kavita.Models/DTOs/ReadingLists/CBL/CblReadingList.cs rename to Kavita.Models/DTOs/ReadingLists/CBL/V1/CblReadingList.cs index aa368f09d..9ebcd2775 100644 --- a/Kavita.Models/DTOs/ReadingLists/CBL/CblReadingList.cs +++ b/Kavita.Models/DTOs/ReadingLists/CBL/V1/CblReadingList.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Xml.Serialization; -namespace Kavita.Models.DTOs.ReadingLists.CBL; +namespace Kavita.Models.DTOs.ReadingLists.CBL.V1; [XmlRoot(ElementName="Books")] @@ -12,6 +12,9 @@ public sealed record CblBooks } +/// +/// Top-level V1 XML CBL reading list. Deserialized from .cbl/.xml files +/// [XmlRoot(ElementName="ReadingList")] public sealed record CblReadingList { diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2ExternalId.cs b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2ExternalId.cs new file mode 100644 index 000000000..c7890faa1 --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2ExternalId.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace Kavita.Models.DTOs.ReadingLists.CBL.V2; + + +/// +/// An entry in issueList[].id[] — external database reference for an issue. +/// +public sealed class CblV2ExternalId +{ + /// + /// Provider short-name (e.g. "cv", "metron", "gcd") + /// + [JsonPropertyName("name")] + public string Name { get; set; } + /// + /// The provider's series identifier + /// + [JsonPropertyName("series")] + public string Series { get; set; } + /// + /// The provider's issue identifier + /// + [JsonPropertyName("issue")] + public string Issue { get; set; } +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2FileDetails.cs b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2FileDetails.cs new file mode 100644 index 000000000..b63ef023d --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2FileDetails.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace Kavita.Models.DTOs.ReadingLists.CBL.V2; + + +/// +/// The fileDetails block — identifies the file with a UUID and schema version. +/// +public sealed class CblV2FileDetails +{ + /// + /// Unique identifier for this CBL file + /// + public string UUID { get; set; } + /// + /// Schema version number (e.g. 1.0) + /// + [JsonPropertyName("version")] + public double? Version { get; set; } +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Issue.cs b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Issue.cs new file mode 100644 index 000000000..ea2fadeda --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Issue.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Kavita.Models.DTOs.ReadingLists.CBL.V2; + + +/// +/// An entry in issueList[] — a single issue in the reading list +/// +public sealed class CblV2Issue +{ + /// + /// Name of the comic series + /// + [JsonPropertyName("seriesName")] + public string SeriesName { get; set; } + /// + /// Year the series started (used to disambiguate reboots) + /// + [JsonPropertyName("seriesStartYear")] + public int? SeriesStartYear { get; set; } + /// + /// Display issue number (e.g. "1", "Annual 2") + /// + [JsonPropertyName("issueNumber")] + public string IssueNumber { get; set; } + /// + /// Cover date in ISO 8601 format (YYYY-MM-DD) + /// + [JsonPropertyName("issueCoverDate")] + public string IssueCoverDate { get; set; } + /// + /// Categorisation of the issue (e.g. "event-core", "ongoing") + /// + [JsonPropertyName("issueType")] + public string IssueType { get; set; } + /// + /// External database identifiers for this issue + /// + [JsonPropertyName("id")] + public List Id { get; set; } +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2ListDetails.cs b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2ListDetails.cs new file mode 100644 index 000000000..2341cebb2 --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2ListDetails.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Kavita.Models.DTOs.ReadingLists.CBL.V2; + + +/// +/// The listDetails block — descriptive metadata for the reading list. +/// +public sealed class CblV2ListDetails +{ + /// + /// Display name of the reading list + /// + [JsonPropertyName("name")] + public string Name { get; set; } + /// + /// Human-readable description / summary + /// + [JsonPropertyName("description")] + public string Description { get; set; } + /// + /// Earliest publication year covered by the list + /// + [JsonPropertyName("startYear")] + public int? StartYear { get; set; } + /// + /// Latest publication year covered by the list + /// + [JsonPropertyName("endYear")] + public int? EndYear { get; set; } + /// + /// Primary publisher (e.g. "Marvel", "DC") + /// + [JsonPropertyName("publisher")] + public string Publisher { get; set; } + /// + /// Publisher imprint (e.g. "Vertigo", "Icon") + /// + [JsonPropertyName("imprint")] + public string Imprint { get; set; } + /// + /// List type as a free-form string (mapped to ) + /// + [JsonPropertyName("type")] + public string Type { get; set; } + /// + /// User-defined tags for categorisation + /// + [JsonPropertyName("tags")] + public List Tags { get; set; } + /// + /// URLs for cover images associated with the list + /// + [JsonPropertyName("coverImageURLs")] + public List CoverImageURLs { get; set; } + /// + /// Links to related reading lists (prequels, sequels, etc.) + /// + [JsonPropertyName("relationships")] + public List Relationships { get; set; } + /// + /// External sources that this list was derived from + /// + [JsonPropertyName("source")] + public List Source { get; set; } +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Relationship.cs b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Relationship.cs new file mode 100644 index 000000000..8e8d203f3 --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Relationship.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; + +namespace Kavita.Models.DTOs.ReadingLists.CBL.V2; + + +/// +/// An entry in listDetails.relationships[] — links to a related reading list. +/// +public sealed class CblV2Relationship +{ + /// + /// Display name of the related reading list + /// + [JsonPropertyName("name")] + public string Name { get; set; } + /// + /// UUID of the related reading list file + /// + public string UUID { get; set; } + /// + /// Nature of the relationship (e.g. "prequel", "sequel", "companion") + /// + [JsonPropertyName("relationship")] + public string Relationship { get; set; } +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Root.cs b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Root.cs new file mode 100644 index 000000000..4bdfed035 --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Root.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Kavita.Models.DTOs.ReadingLists.CBL.V2; + +/// +/// Top-level V2 JSON CBL document. +/// +/// https://github.com/ComicReadingLists/json-cbl-standard/blob/main/schema/1.0/comic-reading-list.schema.json +public sealed class CblV2Root +{ + /// + /// File-level metadata (UUID, schema version) + /// + [JsonPropertyName("fileDetails")] + public CblV2FileDetails FileDetails { get; set; } + /// + /// Descriptive metadata for the reading list + /// + [JsonPropertyName("listDetails")] + public CblV2ListDetails ListDetails { get; set; } + /// + /// Ordered list of issues in the reading list + /// + [JsonPropertyName("issueList")] + public List IssueList { get; set; } + /// + /// Free-form notes about the reading list + /// + [JsonPropertyName("notes")] + public string Notes { get; set; } +} diff --git a/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Source.cs b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Source.cs new file mode 100644 index 000000000..82a01b44b --- /dev/null +++ b/Kavita.Models/DTOs/ReadingLists/CBL/V2/CblV2Source.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace Kavita.Models.DTOs.ReadingLists.CBL.V2; + +/// +/// An entry in listDetails.source[] — origin of the reading list data +/// +public sealed class CblV2Source +{ + /// + /// Name of the source (e.g. "Comic Book Herald") + /// + [JsonPropertyName("name")] + public string Name { get; set; } + /// + /// URL of the source + /// + [JsonPropertyName("url")] + public string Url { get; set; } +} diff --git a/Kavita.Models/DTOs/SeriesDto.cs b/Kavita.Models/DTOs/SeriesDto.cs index 4a3edd417..a64b2f306 100644 --- a/Kavita.Models/DTOs/SeriesDto.cs +++ b/Kavita.Models/DTOs/SeriesDto.cs @@ -5,7 +5,7 @@ using Kavita.Models.Entities.Interfaces; namespace Kavita.Models.DTOs; #nullable enable -public sealed record SeriesDto : IHasReadTimeEstimate, IHasCoverImage +public sealed record SeriesDto : IHasReadTimeEstimate, IHasCoverImage, IHasMetadataIds { /// public int Id { get; init; } @@ -100,6 +100,15 @@ public sealed record SeriesDto : IHasReadTimeEstimate, IHasCoverImage public string? SecondaryColor { get; set; } = string.Empty; #endregion + #region Metadata + public int AniListId { get; set; } + public long MalId { get; set; } + public int HardcoverId { get; set; } + public long MetronId { get; set; } + public string? ComicVineId { get; set; } + public long MangaBakaId { get; set; } + #endregion + public void ResetColorScape() { PrimaryColor = string.Empty; diff --git a/Kavita.Models/DTOs/UpdateChapterDto.cs b/Kavita.Models/DTOs/UpdateChapterDto.cs index 32fad623e..80068adfe 100644 --- a/Kavita.Models/DTOs/UpdateChapterDto.cs +++ b/Kavita.Models/DTOs/UpdateChapterDto.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; +using Kavita.Models.DTOs.Common; using Kavita.Models.DTOs.Metadata; using Kavita.Models.DTOs.Person; using Kavita.Models.Entities.Enums; namespace Kavita.Models.DTOs; -public sealed record UpdateChapterDto +#nullable enable + +public sealed record UpdateChapterDto : IUpdateExternalMetadataIds { public int Id { get; init; } public string Summary { get; set; } = string.Empty; @@ -92,4 +95,11 @@ public sealed record UpdateChapterDto /// /// This should not be confused with Title which is used for special filenames. public string TitleName { get; set; } = string.Empty; + + public int? AniListId { get; set; } + public long? MalId { get; set; } + public int? HardcoverId { get; set; } + public long? MetronId { get; set; } + public string? ComicVineId { get; set; } + public long? MangaBakaId { get; set; } } diff --git a/Kavita.Models/DTOs/UpdateSeriesDto.cs b/Kavita.Models/DTOs/UpdateSeriesDto.cs index 6c99d0bd3..fad4acfa1 100644 --- a/Kavita.Models/DTOs/UpdateSeriesDto.cs +++ b/Kavita.Models/DTOs/UpdateSeriesDto.cs @@ -1,7 +1,9 @@ -namespace Kavita.Models.DTOs; +using Kavita.Models.DTOs.Common; + +namespace Kavita.Models.DTOs; #nullable enable -public sealed record UpdateSeriesDto +public sealed record UpdateSeriesDto : IUpdateExternalMetadataIds { public int Id { get; init; } public string? LocalizedName { get; init; } @@ -10,4 +12,13 @@ public sealed record UpdateSeriesDto public bool SortNameLocked { get; set; } public bool LocalizedNameLocked { get; set; } + + #region External Metadata Ids + public int? AniListId { get; set; } + public long? MalId { get; set; } + public int? HardcoverId { get; set; } + public long? MetronId { get; set; } + public string? ComicVineId { get; set; } + public long? MangaBakaId { get; set; } + #endregion } diff --git a/Kavita.Models/DTOs/UpdateSeriesMetadataDto.cs b/Kavita.Models/DTOs/UpdateSeriesMetadataDto.cs index c9e9783ee..86eaf2077 100644 --- a/Kavita.Models/DTOs/UpdateSeriesMetadataDto.cs +++ b/Kavita.Models/DTOs/UpdateSeriesMetadataDto.cs @@ -1,4 +1,6 @@ -namespace Kavita.Models.DTOs; +using Kavita.Models.DTOs.Common; + +namespace Kavita.Models.DTOs; public sealed record UpdateSeriesMetadataDto { diff --git a/Kavita.Models/DTOs/UpdateVolumeDto.cs b/Kavita.Models/DTOs/UpdateVolumeDto.cs new file mode 100644 index 000000000..f229f8e20 --- /dev/null +++ b/Kavita.Models/DTOs/UpdateVolumeDto.cs @@ -0,0 +1,15 @@ +using Kavita.Models.DTOs.Common; + +namespace Kavita.Models.DTOs; + +public sealed record UpdateVolumeDto : IUpdateExternalMetadataIds +{ + public int Id { get; init; } + + public int? AniListId { get; set; } + public long? MalId { get; set; } + public int? HardcoverId { get; set; } + public long? MetronId { get; set; } + public string ComicVineId { get; set; } + public long? MangaBakaId { get; set; } +} diff --git a/Kavita.Models/DTOs/VolumeDto.cs b/Kavita.Models/DTOs/VolumeDto.cs index 80c422289..bb1641ec4 100644 --- a/Kavita.Models/DTOs/VolumeDto.cs +++ b/Kavita.Models/DTOs/VolumeDto.cs @@ -4,7 +4,7 @@ using Kavita.Models.Entities.Interfaces; namespace Kavita.Models.DTOs; -public sealed record VolumeDto : IHasReadTimeEstimate, IHasCoverImage +public sealed record VolumeDto : IHasReadTimeEstimate, IHasCoverImage, IHasMetadataIds { /// public int Id { get; set; } @@ -56,6 +56,15 @@ public sealed record VolumeDto : IHasReadTimeEstimate, IHasCoverImage /// public string? SecondaryColor { get; set; } = string.Empty; + #region Metadata + public int AniListId { get; set; } + public long MalId { get; set; } + public int HardcoverId { get; set; } + public long MetronId { get; set; } + public string? ComicVineId { get; set; } + public long MangaBakaId { get; set; } + #endregion + public void ResetColorScape() { PrimaryColor = string.Empty; diff --git a/Kavita.Models/Entities/Chapter.cs b/Kavita.Models/Entities/Chapter.cs index fdb6f478e..444073388 100644 --- a/Kavita.Models/Entities/Chapter.cs +++ b/Kavita.Models/Entities/Chapter.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using Kavita.Models.Entities.Enums; using Kavita.Models.Entities.Interfaces; using Kavita.Models.Entities.Metadata; @@ -11,7 +10,7 @@ using Kavita.Models.Entities.User; namespace Kavita.Models.Entities; -public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKPlusMetadata +public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKPlusMetadata, IHasMetadataIds { public int Id { get; set; } /// @@ -39,10 +38,6 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKP /// Can the sort order be updated on scan or is it locked from UI /// public bool SortOrderLocked { get; set; } - /// - /// The files that represent this Chapter - /// - public ICollection Files { get; set; } = null!; public DateTime Created { get; set; } public DateTime LastModified { get; set; } public DateTime CreatedUtc { get; set; } @@ -137,6 +132,15 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKP /// public float AverageExternalRating { get; set; } = 0f; + #region Metadata + public int AniListId { get; set; } + public long MalId { get; set; } + public int HardcoverId { get; set; } + public long MetronId { get; set; } + public string? ComicVineId { get; set; } + public long MangaBakaId { get; set; } + #endregion + #region Locks public bool AgeRatingLocked { get; set; } @@ -175,9 +179,12 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKP public ICollection Ratings { get; set; } = []; public ICollection UserProgress { get; set; } + /// + /// The files that represent this Chapter + /// + public ICollection Files { get; set; } = null!; - // Relationships public Volume Volume { get; set; } = null!; public int VolumeId { get; set; } diff --git a/Kavita.Models/Entities/Interfaces/IHasMetadataIds.cs b/Kavita.Models/Entities/Interfaces/IHasMetadataIds.cs new file mode 100644 index 000000000..31f69fb6f --- /dev/null +++ b/Kavita.Models/Entities/Interfaces/IHasMetadataIds.cs @@ -0,0 +1,18 @@ +namespace Kavita.Models.Entities.Interfaces; +#nullable enable + +/// +/// An entity has metadata markers +/// +public interface IHasMetadataIds +{ + public int AniListId { get; set; } + /// + /// https://myanimelist.net/store/manga/{MalId}/Blue_Lock + /// + public long MalId { get; set; } + public int HardcoverId { get; set; } + public long MetronId { get; set; } + public string? ComicVineId { get; set; } + public long MangaBakaId { get; set; } +} diff --git a/Kavita.Models/Entities/Person/Person.cs b/Kavita.Models/Entities/Person/Person.cs index 8a360d154..f07e8a8c0 100644 --- a/Kavita.Models/Entities/Person/Person.cs +++ b/Kavita.Models/Entities/Person/Person.cs @@ -38,7 +38,6 @@ public class Person : IHasCoverImage /// /// Kavita+ Only public string? HardcoverId { get; set; } - /// /// https://metron.cloud/creator/{slug}/ /// diff --git a/Kavita.Models/Entities/Series.cs b/Kavita.Models/Entities/Series.cs index f9d4f760d..299c0dbee 100644 --- a/Kavita.Models/Entities/Series.cs +++ b/Kavita.Models/Entities/Series.cs @@ -8,7 +8,7 @@ using Kavita.Models.Entities.User; namespace Kavita.Models.Entities; -public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage +public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasMetadataIds { public int Id { get; set; } /// @@ -16,11 +16,11 @@ public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage /// public required string Name { get; set; } /// - /// Used internally for name matching. + /// Used internally for name matching. /// public required string NormalizedName { get; set; } /// - /// Used internally for localized name matching. + /// Used internally for localized name matching. /// public required string NormalizedLocalizedName { get; set; } /// @@ -116,6 +116,15 @@ public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage public bool IsBlacklisted { get; set; } #endregion + #region Metadata + public int AniListId { get; set; } + public long MalId { get; set; } + public int HardcoverId { get; set; } + public long MetronId { get; set; } + public string ComicVineId { get; set; } + public long MangaBakaId { get; set; } + #endregion + public SeriesMetadata Metadata { get; set; } = null!; public ExternalSeriesMetadata ExternalSeriesMetadata { get; set; } = null!; diff --git a/Kavita.Models/Entities/Volume.cs b/Kavita.Models/Entities/Volume.cs index 22faa2173..fd5eb5dac 100644 --- a/Kavita.Models/Entities/Volume.cs +++ b/Kavita.Models/Entities/Volume.cs @@ -5,7 +5,7 @@ using Kavita.Models.Entities.Interfaces; namespace Kavita.Models.Entities; -public class Volume : IEntityDate, IHasReadTimeEstimate, IHasCoverImage +public class Volume : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasMetadataIds { public int Id { get; set; } /// @@ -54,6 +54,15 @@ public class Volume : IEntityDate, IHasReadTimeEstimate, IHasCoverImage public int MaxHoursToRead { get; set; } public float AvgHoursToRead { get; set; } + #region Metadata + public int AniListId { get; set; } + public long MalId { get; set; } + public int HardcoverId { get; set; } + public long MetronId { get; set; } + public string ComicVineId { get; set; } + public long MangaBakaId { get; set; } + #endregion + // Relationships public IList Chapters { get; set; } = null!; diff --git a/Kavita.Models/Parser/ParserInfo.cs b/Kavita.Models/Parser/ParserInfo.cs index 759c2795f..484580a02 100644 --- a/Kavita.Models/Parser/ParserInfo.cs +++ b/Kavita.Models/Parser/ParserInfo.cs @@ -1,8 +1,10 @@ using Kavita.Models.Entities; using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; using Kavita.Models.Metadata; namespace Kavita.Models.Parser; +#nullable enable /// /// This represents all parsed information from a single file @@ -83,5 +85,32 @@ public class ParserInfo /// public ComicInfo? ComicInfo { get; set; } - + /// + /// Extracted from Notes/Weblink fields, not explicitly part of spec + /// + public int? AniListId { get; set; } + /// + /// Extracted from Notes field, not explicitly part of spec + /// + public long? MalId { get; set; } + /// + /// Extracted from Notes field, not explicitly part of spec + /// + public int? HardcoverId { get; set; } + /// + /// Extracted from Notes field, not explicitly part of spec + /// + public long? MetronId { get; set; } + /// + /// Extracted from Notes field, not explicitly part of spec + /// + public string? ComicVineId { get; set; } + /// + /// If the ComicVine slug starts with 4050, it's a Volume/Series Id + /// + public string? ComicVineSeriesId { get; set; } + /// + /// Extracted from Notes field, not explicitly part of spec + /// + public long? MangaBakaId { get; set; } } diff --git a/Kavita.Server/Controllers/CBLController.cs b/Kavita.Server/Controllers/CBLController.cs index 1e0159674..1097498aa 100644 --- a/Kavita.Server/Controllers/CBLController.cs +++ b/Kavita.Server/Controllers/CBLController.cs @@ -5,8 +5,10 @@ using System.Threading.Tasks; using Kavita.API.Attributes; using Kavita.API.Services; using Kavita.API.Services.Reading; +using Kavita.API.Services.ReadingLists; using Kavita.Models.Constants; using Kavita.Models.DTOs.ReadingLists.CBL; +using Kavita.Models.DTOs.ReadingLists.CBL.V1; using Kavita.Server.Attributes; using Kavita.Services.Reading; using Microsoft.AspNetCore.Http; @@ -18,10 +20,7 @@ namespace Kavita.Server.Controllers; /// /// Responsible for the CBL import flow /// -public class CblController( - IReadingListService readingListService, - IDirectoryService directoryService) - : BaseApiController +public class CblController( IReadingListService readingListService, IDirectoryService directoryService) : BaseApiController { /// /// The first step in a cbl import. This validates the cbl file that if an import occured, would it be successful. diff --git a/Kavita.Server/Controllers/ChapterController.cs b/Kavita.Server/Controllers/ChapterController.cs index b18e9c8b0..45e6a9658 100644 --- a/Kavita.Server/Controllers/ChapterController.cs +++ b/Kavita.Server/Controllers/ChapterController.cs @@ -11,9 +11,12 @@ using Kavita.Common.Extensions; using Kavita.Models.Constants; using Kavita.Models.DTOs; using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; using Kavita.Models.Entities.MetadataMatching; using Kavita.Server.Attributes; +using Kavita.Server.Helpers; using Kavita.Services.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -247,6 +250,8 @@ public class ChapterController( ); } + ExternalMetadataIdHelper.SetExternalMetadataIds(chapter, dto); + #region Genres chapter.Genres ??= []; @@ -412,6 +417,7 @@ public class ChapterController( return Ok(); } + /// /// Returns Ratings and Reviews for an individual Chapter /// diff --git a/Kavita.Server/Controllers/ImageController.cs b/Kavita.Server/Controllers/ImageController.cs index d19fca007..1517c6dc0 100644 --- a/Kavita.Server/Controllers/ImageController.cs +++ b/Kavita.Server/Controllers/ImageController.cs @@ -7,6 +7,7 @@ using Kavita.API.Database; using Kavita.API.Services; using Kavita.API.Services.Metadata; using Kavita.API.Services.Reading; +using Kavita.API.Services.ReadingLists; using Kavita.Models.Constants; using Kavita.Models.Entities.Enums; using Kavita.Models.Extensions; diff --git a/Kavita.Server/Controllers/ReadingListController.cs b/Kavita.Server/Controllers/ReadingListController.cs index 4d4de1159..da0e6d6ef 100644 --- a/Kavita.Server/Controllers/ReadingListController.cs +++ b/Kavita.Server/Controllers/ReadingListController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using Kavita.API.Attributes; @@ -6,6 +7,7 @@ using Kavita.API.Database; using Kavita.API.Repositories; using Kavita.API.Services; using Kavita.API.Services.Reading; +using Kavita.API.Services.ReadingLists; using Kavita.Common; using Kavita.Common.Helpers; using Kavita.Models.Constants; @@ -15,6 +17,7 @@ using Kavita.Models.Entities.Enums; using Kavita.Server.Attributes; using Kavita.Server.Extensions; using Kavita.Services.Reading; +using Kavita.Services.ReadingLists; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -24,7 +27,8 @@ namespace Kavita.Server.Controllers; public class ReadingListController( IUnitOfWork unitOfWork, IReadingListService readingListService, - ILocalizationService localizationService) + ILocalizationService localizationService, + ICblExportService cblExportService) : BaseApiController { /// @@ -610,4 +614,21 @@ public class ReadingListController( return Ok(result); } + + /// + /// Export a Reading List to CBL format + /// + /// + /// + /// + [ReadingListAccess] + [HttpPost("export-as-cbl")] + public async Task ExportAsCbl([FromQuery] int readingListId, [FromQuery] bool asV2 = false) + { + var filepath = await cblExportService.ExportReadingList(readingListId, UserId, asV2); + if (string.IsNullOrEmpty(filepath)) return BadRequest(localizationService.Translate(UserId, "cbl-export-failed")); + + var contentType = asV2 ? "application/json" : "application/xml"; + return PhysicalFile(filepath, contentType, Path.GetFileName(filepath)); + } } diff --git a/Kavita.Server/Controllers/SeriesController.cs b/Kavita.Server/Controllers/SeriesController.cs index 02d4795c1..0c35c8bb9 100644 --- a/Kavita.Server/Controllers/SeriesController.cs +++ b/Kavita.Server/Controllers/SeriesController.cs @@ -21,6 +21,7 @@ using Kavita.Models.Entities.Enums; using Kavita.Models.Entities.MetadataMatching; using Kavita.Server.Attributes; using Kavita.Server.Extensions; +using Kavita.Server.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -178,6 +179,8 @@ public class SeriesController( series.SortNameLocked = updateSeries.SortNameLocked; series.LocalizedNameLocked = updateSeries.LocalizedNameLocked; + ExternalMetadataIdHelper.SetExternalMetadataIds(series, updateSeries); + var needsRefreshMetadata = false; // This is when you hit Reset diff --git a/Kavita.Server/Controllers/UploadController.cs b/Kavita.Server/Controllers/UploadController.cs index ea24e4502..edf8ec785 100644 --- a/Kavita.Server/Controllers/UploadController.cs +++ b/Kavita.Server/Controllers/UploadController.cs @@ -7,6 +7,7 @@ using Kavita.API.Repositories; using Kavita.API.Services; using Kavita.API.Services.Metadata; using Kavita.API.Services.Reading; +using Kavita.API.Services.ReadingLists; using Kavita.API.Services.SignalR; using Kavita.Common.Extensions; using Kavita.Models.Constants; diff --git a/Kavita.Server/Controllers/VolumeController.cs b/Kavita.Server/Controllers/VolumeController.cs index 7b2a76804..62617f871 100644 --- a/Kavita.Server/Controllers/VolumeController.cs +++ b/Kavita.Server/Controllers/VolumeController.cs @@ -7,6 +7,7 @@ using Kavita.Models.Constants; using Kavita.Models.DTOs; using Kavita.Models.DTOs.SignalR; using Kavita.Server.Attributes; +using Kavita.Server.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -27,6 +28,33 @@ public class VolumeController(IUnitOfWork unitOfWork, ILocalizationService local return Ok(await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, UserId)); } + /// + /// Updates the information on the Volume + /// + /// + /// + [HttpPost("update")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] + public async Task> UpdateVolume(UpdateVolumeDto dto) + { + var volume = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(dto.Id); + if (volume == null) return BadRequest(localizationService.Translate(UserId, "volume-doesnt-exist")); + + ExternalMetadataIdHelper.SetExternalMetadataIds(volume, dto); + + unitOfWork.VolumeRepository.Update(volume); + + if (unitOfWork.HasChanges() && !await unitOfWork.CommitAsync()) + return BadRequest(localizationService.Translate(UserId, "generic-error")); + + return Ok(await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volume.Id, UserId)); + } + + /// + /// Delete the Volume from the DB + /// + /// + /// [HttpDelete] [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> DeleteVolume(int volumeId) @@ -47,6 +75,11 @@ public class VolumeController(IUnitOfWork unitOfWork, ILocalizationService local return Ok(false); } + /// + /// Delete multiple Volumes from the DB + /// + /// + /// [HttpPost("multiple")] [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> DeleteMultipleVolumes(int[] volumesIds) diff --git a/Kavita.Server/Helpers/ExternalMetadataIdHelper.cs b/Kavita.Server/Helpers/ExternalMetadataIdHelper.cs new file mode 100644 index 000000000..e9805a984 --- /dev/null +++ b/Kavita.Server/Helpers/ExternalMetadataIdHelper.cs @@ -0,0 +1,41 @@ +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Common; +using Kavita.Models.Entities.Interfaces; + +namespace Kavita.Server.Helpers; + +public static class ExternalMetadataIdHelper +{ + public static void SetExternalMetadataIds(IHasMetadataIds entity, IUpdateExternalMetadataIds dto) + { + if (dto.AniListId is > 0) + { + entity.AniListId = dto.AniListId.Value; + } + + if (dto.MalId is > 0) + { + entity.MalId = dto.MalId.Value; + } + + if (dto.MangaBakaId is > 0) + { + entity.MangaBakaId = dto.MangaBakaId.Value; + } + + if (dto.HardcoverId is > 0) + { + entity.HardcoverId = dto.HardcoverId.Value; + } + + if (dto.MetronId is > 0) + { + entity.MetronId = dto.MetronId.Value; + } + + if (!string.IsNullOrWhiteSpace(dto.ComicVineId)) + { + entity.ComicVineId = dto.ComicVineId; + } + } +} diff --git a/Kavita.Server/I18N/en.json b/Kavita.Server/I18N/en.json index b74b1a7a9..bb9eca243 100644 --- a/Kavita.Server/I18N/en.json +++ b/Kavita.Server/I18N/en.json @@ -221,6 +221,7 @@ "genre-doesnt-exist": "Genre doesn't exist", "font-url-not-allowed": "Uploading a Font by url is only allowed from Google Fonts", "annotation-export-failed": "Unable to export Annotations, check logs", + "cbl-export-failed": "Unable to export CBL file, check logs", "download-not-allowed": "User does not have download permissions", "auth-key-unique": "The Auth Key name must be unique to your account", "role-restricted": "Access forbidden: Your role does not permit this action" diff --git a/Kavita.Server/Kavita.Server.csproj b/Kavita.Server/Kavita.Server.csproj index 210701930..fe1a0aff7 100644 --- a/Kavita.Server/Kavita.Server.csproj +++ b/Kavita.Server/Kavita.Server.csproj @@ -159,4 +159,11 @@ <_DeploymentManifestIconFile Remove="favicon.ico" /> + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Kavita.Services.Tests/ArchiveServiceTests.cs b/Kavita.Services.Tests/ArchiveServiceTests.cs index 538b5be8f..5bc3d9618 100644 --- a/Kavita.Services.Tests/ArchiveServiceTests.cs +++ b/Kavita.Services.Tests/ArchiveServiceTests.cs @@ -329,6 +329,18 @@ public class ArchiveServiceTests Assert.Equal("https://www.comixology.com/BTOOOM/digital-comic/450184", comicInfo.Web); } + // [Fact] + // public void CanParseMetadataIdFromComicInfo() + // { + // var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); + // var archive = Path.Join(testDirectory, "metadata_from_notes.cbz"); + // var comicInfo = _archiveService.GetComicInfo(archive); + // + // Assert.NotNull(comicInfo); + // Assert.Equal("Scraped metadata from ComicVine [CVDB734524]", comicInfo.Notes); + // Assert.Equal("734524", comicInfo.ComicVineId); + // } + #endregion #region CanParseComicInfo_DefaultNumberIsBlank diff --git a/Kavita.Services.Tests/Helpers/ParserInfoFactory.cs b/Kavita.Services.Tests/Helpers/ParserInfoFactory.cs index 5c1353974..8e6c5d3f8 100644 --- a/Kavita.Services.Tests/Helpers/ParserInfoFactory.cs +++ b/Kavita.Services.Tests/Helpers/ParserInfoFactory.cs @@ -5,6 +5,7 @@ using Kavita.Models.Parser; namespace Kavita.Services.Tests.Helpers; +// TODO: Investigate dead code public static class ParserInfoFactory { public static ParserInfo CreateParsedInfo(string series, string volumes, string chapters, string filename, bool isSpecial) diff --git a/Kavita.Services.Tests/Helpers/ScannerHelper.cs b/Kavita.Services.Tests/Helpers/ScannerHelper.cs index be8ebba65..7d159c786 100644 --- a/Kavita.Services.Tests/Helpers/ScannerHelper.cs +++ b/Kavita.Services.Tests/Helpers/ScannerHelper.cs @@ -9,10 +9,9 @@ using Kavita.API.Services; using Kavita.API.Services.Helpers; using Kavita.API.Services.Metadata; using Kavita.API.Services.Plus; -using Kavita.API.Services.Reading; +using Kavita.API.Services.ReadingLists; using Kavita.API.Services.Scanner; using Kavita.API.Services.SignalR; -using Kavita.Database; using Kavita.Models; using Kavita.Models.Builders; using Kavita.Models.Entities; @@ -64,10 +63,11 @@ public class ScannerHelper return library; } - public ScannerService CreateServices(DirectoryService ds = null, IFileSystem fs = null) + public ScannerService CreateServices(DirectoryService? ds = null, IFileSystem? fs = null) { fs ??= new FileSystem(); ds ??= new DirectoryService(Substitute.For>(), fs); + var archiveService = new ArchiveService(Substitute.For>(), ds, Substitute.For(), Substitute.For()); var readingItemService = new ReadingItemService(archiveService, Substitute.For(), @@ -158,9 +158,9 @@ public class ScannerHelper var fileDir = Path.GetDirectoryName(fullPath); // Create the directory if it doesn't exist - if (!Directory.Exists(fileDir)) + if (!string.IsNullOrEmpty(fileDir) && !Directory.Exists(fileDir)) { - Directory.CreateDirectory(fileDir); + Directory.CreateDirectory(fileDir!); Console.WriteLine($"Created directory: {fileDir}"); } diff --git a/Kavita.Services.Tests/OpdsServiceTests.cs b/Kavita.Services.Tests/OpdsServiceTests.cs index 88425af46..67e9f2a0b 100644 --- a/Kavita.Services.Tests/OpdsServiceTests.cs +++ b/Kavita.Services.Tests/OpdsServiceTests.cs @@ -8,6 +8,7 @@ using Kavita.API.Repositories; using Kavita.API.Services; using Kavita.API.Services.Plus; using Kavita.API.Services.Reading; +using Kavita.API.Services.ReadingLists; using Kavita.API.Services.SignalR; using Kavita.Common.Helpers; using Kavita.Database; diff --git a/Kavita.Services.Tests/ReadingListServiceTests.cs b/Kavita.Services.Tests/ReadingListServiceTests.cs index d8ddb21d6..020b418a9 100644 --- a/Kavita.Services.Tests/ReadingListServiceTests.cs +++ b/Kavita.Services.Tests/ReadingListServiceTests.cs @@ -5,12 +5,14 @@ using Kavita.API.Repositories; using Kavita.API.Services; using Kavita.API.Services.Plus; using Kavita.API.Services.Reading; +using Kavita.API.Services.ReadingLists; using Kavita.API.Services.SignalR; using Kavita.Database; using Kavita.Database.Tests; using Kavita.Models.Builders; using Kavita.Models.DTOs.ReadingLists; using Kavita.Models.DTOs.ReadingLists.CBL; +using Kavita.Models.DTOs.ReadingLists.CBL.V1; using Kavita.Models.Entities; using Kavita.Models.Entities.Enums; using Kavita.Models.Entities.User; diff --git a/Kavita.Services.Tests/ReadingLists/CblExportServiceTests.cs b/Kavita.Services.Tests/ReadingLists/CblExportServiceTests.cs new file mode 100644 index 000000000..3807905e0 --- /dev/null +++ b/Kavita.Services.Tests/ReadingLists/CblExportServiceTests.cs @@ -0,0 +1,440 @@ +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Person; +using Kavita.Services.Helpers; +using Kavita.Services.ReadingLists; + +namespace Kavita.Services.Tests.ReadingLists; + +public class CblExportServiceTests +{ + #region Helpers + + private static ReadingList CreateReadingList(string title = "Test List", string? summary = "A test reading list", + int startingYear = 2020, int startingMonth = 1, int endingYear = 2021, int endingMonth = 12) + { + return new ReadingList + { + Id = 1, + Title = title, + NormalizedTitle = title.ToLower(), + Summary = summary, + AgeRating = AgeRating.Unknown, + StartingYear = startingYear, + StartingMonth = startingMonth, + EndingYear = endingYear, + EndingMonth = endingMonth, + }; + } + + private static ReadingListItem CreateItem(int order, string seriesName, string chapterRange, + string volumeName, DateTime? releaseDate = null, bool isSpecial = false, + MangaFormat format = MangaFormat.Archive) + { + var series = new Series + { + Id = order + 1, + Name = seriesName, + NormalizedName = seriesName.ToLower(), + NormalizedLocalizedName = seriesName.ToLower(), + SortName = seriesName, + LocalizedName = seriesName, + OriginalName = seriesName, + Format = format, + Metadata = new SeriesMetadata + { + People = new List(), + }, + }; + + return new ReadingListItem + { + Order = order, + Series = series, + SeriesId = series.Id, + Volume = new Volume + { + Name = volumeName, + MinNumber = 0, + MaxNumber = 0, + LookupName = volumeName, + }, + Chapter = new Chapter + { + Range = chapterRange, + IsSpecial = isSpecial, + ReleaseDate = releaseDate ?? DateTime.MinValue, + }, + }; + } + + #endregion + + #region BuildCblReadingList + + [Fact] + public void ExportV1_BasicReadingList() + { + var readingList = CreateReadingList(); + var items = new List + { + CreateItem(0, "Batman", "1", "2016", new DateTime(2016, 6, 15)), + CreateItem(1, "Batman", "2", "2016", new DateTime(2016, 7, 6)), + CreateItem(2, "Superman", "10", "2011", new DateTime(2013, 3, 1)), + }; + + var result = CblExportService.BuildCblReadingList(readingList, items); + + Assert.Equal("Test List", result.Name); + Assert.Equal("A test reading list", result.Summary); + Assert.Equal(2020, result.StartYear); + Assert.Equal(1, result.StartMonth); + Assert.Equal(2021, result.EndYear); + Assert.Equal(12, result.EndMonth); + + Assert.Equal(3, result.Books.Book.Count); + + var first = result.Books.Book[0]; + Assert.Equal("Batman", first.Series); + Assert.Equal("1", first.Number); + Assert.Equal("2016", first.Volume); + Assert.Equal("2016", first.Year); + Assert.Equal(string.Empty, first.Format); + Assert.Equal("cbz", first.FileType); + Assert.Null(first.Database); + + var last = result.Books.Book[2]; + Assert.Equal("Superman", last.Series); + Assert.Equal("10", last.Number); + Assert.Equal("2011", last.Volume); + Assert.Equal("2013", last.Year); + } + + [Fact] + public void ExportV1_SpecialChapter() + { + var readingList = CreateReadingList(); + var items = new List + { + CreateItem(0, "Batman", "Annual 1", "2016", isSpecial: true), + }; + + var result = CblExportService.BuildCblReadingList(readingList, items); + + Assert.Single(result.Books.Book); + Assert.Equal("Annual", result.Books.Book[0].Format); + } + + [Theory] + [InlineData(MangaFormat.Archive, "cbz")] + [InlineData(MangaFormat.Epub, "epub")] + [InlineData(MangaFormat.Pdf, "pdf")] + [InlineData(MangaFormat.Image, "image")] + [InlineData(MangaFormat.Unknown, "")] + public void ExportV1_FileTypeMappings(MangaFormat format, string expected) + { + Assert.Equal(expected, CblExportService.MapMangaFormatToFileType(format)); + } + + [Fact] + public void ExportV1_EmptyItems() + { + var readingList = CreateReadingList(); + var items = new List(); + + var result = CblExportService.BuildCblReadingList(readingList, items); + + Assert.Equal("Test List", result.Name); + Assert.Empty(result.Books.Book); + } + + [Fact] + public void ExportV1_DefaultReleaseDate_EmptyYear() + { + var readingList = CreateReadingList(); + var items = new List + { + CreateItem(0, "Batman", "1", "2016"), + }; + + var result = CblExportService.BuildCblReadingList(readingList, items); + + Assert.Equal(string.Empty, result.Books.Book[0].Year); + } + + #endregion + + #region RoundTrip + + [Fact] + public void ExportV1_RoundTrip() + { + var readingList = CreateReadingList(title: "Round Trip Test", summary: "Testing round trip"); + var items = new List + { + CreateItem(0, "Batman", "1", "2016", new DateTime(2016, 6, 15)), + CreateItem(1, "Superman", "Annual 1", "2011", new DateTime(2013, 3, 1), isSpecial: true, format: MangaFormat.Epub), + }; + + var cbl = CblExportService.BuildCblReadingList(readingList, items); + + var tempFile = Path.Combine(Path.GetTempPath(), $"cbl-export-test-{Guid.NewGuid()}.cbl"); + try + { + CblExportService.SerializeV1(cbl, tempFile); + + var parsed = CblParser.ParseV1(tempFile); + + Assert.Equal("Round Trip Test", parsed.Name); + Assert.Equal("Testing round trip", parsed.Summary); + Assert.Equal(2020, parsed.StartYear); + Assert.Equal(1, parsed.StartMonth); + Assert.Equal(2021, parsed.EndYear); + Assert.Equal(12, parsed.EndMonth); + + Assert.Equal(2, parsed.Items.Count); + + var first = parsed.Items[0]; + Assert.Equal("Batman", first.SeriesName); + Assert.Equal("1", first.Number); + Assert.Equal("2016", first.Volume); + Assert.Equal("2016", first.Year); + Assert.Equal(string.Empty, first.Format); + Assert.Equal("cbz", first.FileType); + + var second = parsed.Items[1]; + Assert.Equal("Superman", second.SeriesName); + Assert.Equal("Annual 1", second.Number); + Assert.Equal("2011", second.Volume); + Assert.Equal("2013", second.Year); + Assert.Equal("Annual", second.Format); + Assert.Equal("epub", second.FileType); + } + finally + { + if (File.Exists(tempFile)) File.Delete(tempFile); + } + } + + #endregion + + #region BuildCblV2Root + + [Fact] + public void ExportV2_BasicReadingList() + { + var readingList = CreateReadingList(); + var items = new List + { + CreateItem(0, "Batman", "1", "2016", new DateTime(2016, 6, 15)), + CreateItem(1, "Superman", "10", "2011", new DateTime(2013, 3, 1)), + }; + + // Set ReleaseYear on series metadata + items[0].Series.Metadata.ReleaseYear = 2016; + items[1].Series.Metadata.ReleaseYear = 2011; + + var result = CblExportService.BuildCblV2Root(readingList, items); + + Assert.NotNull(result.FileDetails); + Assert.Equal(1.0, result.FileDetails.Version); + Assert.False(string.IsNullOrEmpty(result.FileDetails.UUID)); + + Assert.Equal("Test List", result.ListDetails.Name); + Assert.Equal("A test reading list", result.ListDetails.Description); + Assert.Equal(2020, result.ListDetails.StartYear); + Assert.Equal(2021, result.ListDetails.EndYear); + + Assert.Equal(2, result.IssueList.Count); + + var first = result.IssueList[0]; + Assert.Equal("Batman", first.SeriesName); + Assert.Equal("1", first.IssueNumber); + Assert.Equal(2016, first.SeriesStartYear); + Assert.Equal("2016-06-15", first.IssueCoverDate); + Assert.Null(first.Id); + + var second = result.IssueList[1]; + Assert.Equal("Superman", second.SeriesName); + Assert.Equal("10", second.IssueNumber); + Assert.Equal(2011, second.SeriesStartYear); + Assert.Equal("2013-03-01", second.IssueCoverDate); + } + + [Fact] + public void ExportV2_EmptyItems() + { + var readingList = CreateReadingList(); + var items = new List(); + + var result = CblExportService.BuildCblV2Root(readingList, items); + + Assert.Equal("Test List", result.ListDetails.Name); + Assert.Empty(result.IssueList); + Assert.Equal(string.Empty, result.ListDetails.Publisher); + Assert.Equal(string.Empty, result.ListDetails.Imprint); + } + + [Fact] + public void ExportV2_DefaultReleaseDate_EmptyCoverDate() + { + var readingList = CreateReadingList(); + var items = new List + { + CreateItem(0, "Batman", "1", "2016"), + }; + + var result = CblExportService.BuildCblV2Root(readingList, items); + + Assert.Equal(string.Empty, result.IssueList[0].IssueCoverDate); + Assert.Null(result.IssueList[0].SeriesStartYear); + } + + [Fact] + public void ExportV2_PublisherFromMostCommonPerson() + { + var readingList = CreateReadingList(); + var publisher = new Person + { + Id = 1, + Name = "Marvel", + NormalizedName = "marvel", + Description = string.Empty, + PrimaryColor = string.Empty, + SecondaryColor = string.Empty, + }; + + var items = new List + { + CreateItem(0, "Spider-Man", "1", "2018"), + CreateItem(1, "Avengers", "1", "2018"), + }; + + items[0].Series.Metadata.People = new List + { + new() { Role = PersonRole.Publisher, Person = publisher }, + }; + items[1].Series.Metadata.People = new List + { + new() { Role = PersonRole.Publisher, Person = publisher }, + }; + + var result = CblExportService.BuildCblV2Root(readingList, items); + + Assert.Equal("Marvel", result.ListDetails.Publisher); + } + + [Fact] + public void ExportV2_RoundTrip() + { + var readingList = CreateReadingList(title: "V2 Round Trip", summary: "Testing V2 round trip"); + var items = new List + { + CreateItem(0, "Batman", "1", "2016", new DateTime(2016, 6, 15)), + CreateItem(1, "Superman", "10", "2011", new DateTime(2013, 3, 1)), + }; + items[0].Series.Metadata.ReleaseYear = 2016; + items[1].Series.Metadata.ReleaseYear = 2011; + + var v2 = CblExportService.BuildCblV2Root(readingList, items); + + var tempFile = Path.Combine(Path.GetTempPath(), $"cbl-export-test-{Guid.NewGuid()}.json"); + try + { + CblExportService.SerializeV2(v2, tempFile); + + var parsed = CblParser.ParseV2(tempFile); + + Assert.Equal("V2 Round Trip", parsed.Name); + Assert.Equal("Testing V2 round trip", parsed.Summary); + Assert.Equal(2020, parsed.StartYear); + Assert.Equal(2021, parsed.EndYear); + + Assert.Equal(2, parsed.Items.Count); + + var first = parsed.Items[0]; + Assert.Equal("Batman", first.SeriesName); + Assert.Equal("1", first.Number); + Assert.Equal("2016", first.Volume); + Assert.Equal("2016", first.Year); + + var second = parsed.Items[1]; + Assert.Equal("Superman", second.SeriesName); + Assert.Equal("10", second.Number); + Assert.Equal("2011", second.Volume); + Assert.Equal("2013", second.Year); + } + finally + { + if (File.Exists(tempFile)) File.Delete(tempFile); + } + } + + #endregion + + #region GetMostCommonPerson + + [Fact] + public void GetMostCommonPerson_ReturnsMostFrequent() + { + var personA = new Person + { + Id = 1, + Name = "Publisher A", + NormalizedName = "publisher a", + Description = string.Empty, + PrimaryColor = string.Empty, + SecondaryColor = string.Empty, + }; + var personB = new Person + { + Id = 2, + Name = "Publisher B", + NormalizedName = "publisher b", + Description = string.Empty, + PrimaryColor = string.Empty, + SecondaryColor = string.Empty, + }; + + var items = new List + { + CreateItem(0, "Series1", "1", "2020"), + CreateItem(1, "Series2", "1", "2020"), + CreateItem(2, "Series3", "1", "2020"), + }; + + // Series1 and Series3 have Publisher A, Series2 has Publisher B + items[0].Series.Metadata.People = new List + { + new() { Role = PersonRole.Publisher, Person = personA }, + }; + items[1].Series.Metadata.People = new List + { + new() { Role = PersonRole.Publisher, Person = personB }, + }; + items[2].Series.Metadata.People = new List + { + new() { Role = PersonRole.Publisher, Person = personA }, + }; + + var result = CblExportService.GetMostCommonPerson(items, PersonRole.Publisher); + + Assert.Equal("Publisher A", result); + } + + [Fact] + public void GetMostCommonPerson_NoPeople_ReturnsNull() + { + var items = new List + { + CreateItem(0, "Series1", "1", "2020"), + }; + + var result = CblExportService.GetMostCommonPerson(items, PersonRole.Publisher); + + Assert.Null(result); + } + + #endregion +} diff --git a/Kavita.Services.Tests/ReadingLists/CblParserTests.cs b/Kavita.Services.Tests/ReadingLists/CblParserTests.cs new file mode 100644 index 000000000..a68fa5070 --- /dev/null +++ b/Kavita.Services.Tests/ReadingLists/CblParserTests.cs @@ -0,0 +1,202 @@ +using Kavita.Models.DTOs.ReadingLists.CBL; +using Kavita.Services.Helpers; +using Kavita.Services.ReadingLists; + +namespace Kavita.Services.Tests.ReadingLists; + +public class CblParserTests +{ + private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/CblParserTests/Test Cases"); + + #region V1 Spec + + [Fact] + public void ParseV1Test_NoSpecial() + { + const string filename = "[DC Comics] Aquaman- Death of a Prince (WEB-CBRO).cbl"; + var result = CblParser.ParseV1(Path.Join(_testDirectory, filename)); + + Assert.Equal(1, result.SchemaVersion); + Assert.Equal("[DC Comics] Aquaman- Death of a Prince (WEB-CBRO)", result.Name); + Assert.Empty(result.Uuid); + Assert.Equal(CblListType.Unknown, result.ListType); + Assert.Equal(-1, result.StartYear); + Assert.Equal(-1, result.EndYear); + + Assert.Equal(25, result.Items.Count); + + // First item + var first = result.Items[0]; + Assert.Equal(0, first.Order); + Assert.Equal("Adventure Comics", first.SeriesName); + Assert.Equal("435", first.Number); + Assert.Equal("1938", first.Volume); + Assert.Equal("1974", first.Year); + Assert.Equal(CblIssueType.Unknown, first.IssueType); + Assert.Single(first.ExternalIds); + Assert.Equal(CblExternalDbProvider.ComicVine, first.ExternalIds[0].Provider); + Assert.Equal("3105", first.ExternalIds[0].SeriesId); + Assert.Equal("124869", first.ExternalIds[0].IssueId); + + // Last item (Aquaman #63) + var last = result.Items[24]; + Assert.Equal(24, last.Order); + Assert.Equal("Aquaman", last.SeriesName); + Assert.Equal("63", last.Number); + Assert.Equal("1962", last.Volume); + Assert.Equal("1978", last.Year); + Assert.Equal(CblExternalDbProvider.ComicVine, last.ExternalIds[0].Provider); + Assert.Equal("2050", last.ExternalIds[0].SeriesId); + Assert.Equal("137565", last.ExternalIds[0].IssueId); + + // Item 15 (transition from Adventure Comics to Aquaman) + var item15 = result.Items[15]; + Assert.Equal("Aquaman", item15.SeriesName); + Assert.Equal("57", item15.Number); + } + + [Fact] + public void ParseV1Test_Special() + { + const string filename = "BOOM! Power Rangers Simplified 1a.cbl"; + var result = CblParser.ParseV1(Path.Join(_testDirectory, filename)); + + Assert.Equal("Simplified Power Rangers 1a", result.Name); + Assert.Equal(164, result.Items.Count); + + // First item + var first = result.Items[0]; + Assert.Equal("Mighty Morphin Power Rangers", first.SeriesName); + Assert.Equal("0", first.Number); + Assert.Equal("2016", first.Volume); + Assert.Equal("2016", first.Year); + Assert.Single(first.ExternalIds); + Assert.Equal(CblExternalDbProvider.ComicVine, first.ExternalIds[0].Provider); + Assert.Equal("87332", first.ExternalIds[0].SeriesId); + Assert.Equal("511002", first.ExternalIds[0].IssueId); + + // Last item + var last = result.Items[163]; + Assert.Equal("Power Rangers Unlimited: The Morphin Masters", last.SeriesName); + Assert.Equal("1", last.Number); + Assert.Equal("2024", last.Volume); + } + + [Fact] + public void ParseV1Test_DatabaseElementCaptured() + { + const string filename = "[DC Comics] Aquaman- Death of a Prince (WEB-CBRO).cbl"; + var result = CblParser.ParseV1(Path.Join(_testDirectory, filename)); + + // Every item in this file has a Database element + foreach (var item in result.Items) + { + Assert.Single(item.ExternalIds); + Assert.Equal(CblExternalDbProvider.ComicVine, item.ExternalIds[0].Provider); + Assert.False(string.IsNullOrEmpty(item.ExternalIds[0].SeriesId)); + Assert.False(string.IsNullOrEmpty(item.ExternalIds[0].IssueId)); + } + } + + #endregion + + + #region V2 Spec + + [Fact] + public void ParseV2Test() + { + const string filename = "2018-2021 Part 16.1 Reborn Again.json"; + var result = CblParser.ParseV2(Path.Join(_testDirectory, filename)); + + // File details + Assert.Equal("a59e4ad5-0d51-4afe-b0f5-6d3e01eb7cc7", result.Uuid); + Assert.Equal(1, result.SchemaVersion); + + // List details + Assert.Equal("Part 16.1 Reborn Again", result.Name); + Assert.Equal(2021, result.StartYear); + Assert.Equal(2022, result.EndYear); + Assert.Equal("Marvel", result.Publisher); + Assert.Equal(CblListType.Universal, result.ListType); + + // Tags + Assert.Equal(3, result.Tags.Count); + Assert.Contains("avengers", result.Tags); + Assert.Contains("marvel guides", result.Tags); + Assert.Contains("fresh start", result.Tags); + + // Relationships + Assert.Equal(2, result.Relationships.Count); + var prev = result.Relationships[0]; + Assert.Equal("Part 15.4 Trial by Fire", prev.Name); + Assert.Equal("e1eecedf-df97-4ab4-a476-ee85204e3b78", prev.Uuid); + Assert.Equal("previous", prev.Relationship); + var next = result.Relationships[1]; + Assert.Equal("Part 16.2 Sinister War", next.Name); + Assert.Equal("507e7444-8f5f-46fc-a2cc-17f08c000982", next.Uuid); + Assert.Equal("following", next.Relationship); + + // Sources + Assert.Single(result.Sources); + Assert.Equal("Marvel Guides", result.Sources[0].Name); + Assert.Equal("https://marvelguides.com/fresh-start-finale-reading-order", result.Sources[0].Url); + + // Issues + Assert.Equal(58, result.Items.Count); + + // First issue - Heroes Reborn #1 + var first = result.Items[0]; + Assert.Equal(0, first.Order); + Assert.Equal("Heroes Reborn", first.SeriesName); + Assert.Equal("1", first.Number); + Assert.Equal("2021", first.Volume); + Assert.Equal("2021", first.Year); + Assert.Equal("2021-07-01", first.CoverDate); + Assert.Equal(CblIssueType.Unknown, first.IssueType); + Assert.Equal(2, first.ExternalIds.Count); + Assert.Equal(CblExternalDbProvider.ComicVine, first.ExternalIds[0].Provider); + Assert.Equal("135903", first.ExternalIds[0].SeriesId); + Assert.Equal("847511", first.ExternalIds[0].IssueId); + Assert.Equal(CblExternalDbProvider.Metron, first.ExternalIds[1].Provider); + Assert.Equal("2139", first.ExternalIds[1].SeriesId); + Assert.Equal("29926", first.ExternalIds[1].IssueId); + + // Black Cat #8 (item 22) - only has comicvine, no metron + var blackCat = result.Items[22]; + Assert.Equal("Black Cat", blackCat.SeriesName); + Assert.Equal("8", blackCat.Number); + Assert.Equal("2020", blackCat.Volume); + Assert.Single(blackCat.ExternalIds); + Assert.Equal(CblExternalDbProvider.ComicVine, blackCat.ExternalIds[0].Provider); + + // Giant-Sized Black Cat (item 25) - no coverDate + var giantSized = result.Items[25]; + Assert.Equal("Giant-Sized Black Cat: Infinity Score", giantSized.SeriesName); + Assert.Empty(giantSized.CoverDate); + Assert.Empty(giantSized.Year); + } + + [Fact] + public void ParseV2Test_AutoDetect() + { + const string filename = "2018-2021 Part 16.1 Reborn Again.json"; + var result = CblParser.Parse(Path.Join(_testDirectory, filename)); + + Assert.Equal("a59e4ad5-0d51-4afe-b0f5-6d3e01eb7cc7", result.Uuid); + Assert.Equal("Part 16.1 Reborn Again", result.Name); + Assert.Equal(58, result.Items.Count); + } + + [Fact] + public void ParseV1Test_AutoDetect() + { + const string filename = "[DC Comics] Aquaman- Death of a Prince (WEB-CBRO).cbl"; + var result = CblParser.Parse(Path.Join(_testDirectory, filename)); + + Assert.Equal("[DC Comics] Aquaman- Death of a Prince (WEB-CBRO)", result.Name); + Assert.Equal(25, result.Items.Count); + } + + #endregion +} diff --git a/Kavita.Services.Tests/ScrobblingServiceTests.cs b/Kavita.Services.Tests/ScrobblingServiceTests.cs index aa33c5ed0..fec91d3e3 100644 --- a/Kavita.Services.Tests/ScrobblingServiceTests.cs +++ b/Kavita.Services.Tests/ScrobblingServiceTests.cs @@ -1,6 +1,7 @@ using Kavita.API.Database; using Kavita.API.Repositories; using Kavita.API.Services; +using Kavita.API.Services.Helpers; using Kavita.API.Services.Plus; using Kavita.API.Services.Reading; using Kavita.API.Services.SignalR; @@ -624,19 +625,4 @@ public class ScrobblingServiceTests(ITestOutputHelper outputHelper): AbstractDbT #endregion - [Theory] - [InlineData("https://anilist.co/manga/35851/Byeontaega-Doeja/", 35851)] - [InlineData("https://anilist.co/manga/30105", 30105)] - [InlineData("https://anilist.co/manga/30105/Kekkaishi/", 30105)] - public void CanParseWeblink_AniList(string link, int? expectedId) - { - Assert.Equal(ScrobblingHelper.ExtractId(link, ScrobblingService.AniListWeblinkWebsite), expectedId); - } - - [Theory] - [InlineData("https://mangadex.org/title/316d3d09-bb83-49da-9d90-11dc7ce40967/honzuki-no-gekokujou-shisho-ni-naru-tame-ni-wa-shudan-wo-erandeiraremasen-dai-3-bu-ryouchi-ni-hon-o", "316d3d09-bb83-49da-9d90-11dc7ce40967")] - public void CanParseWeblink_MangaDex(string link, string expectedId) - { - Assert.Equal(ScrobblingHelper.ExtractId(link, ScrobblingService.MangaDexWeblinkWebsite), expectedId); - } } diff --git a/Kavita.Services.Tests/SeriesServiceTests.cs b/Kavita.Services.Tests/SeriesServiceTests.cs index 347208250..9d987ca0b 100644 --- a/Kavita.Services.Tests/SeriesServiceTests.cs +++ b/Kavita.Services.Tests/SeriesServiceTests.cs @@ -4,6 +4,7 @@ using Kavita.API.Database; using Kavita.API.Repositories; using Kavita.API.Services; using Kavita.API.Services.Reading; +using Kavita.API.Services.ReadingLists; using Kavita.API.Services.SignalR; using Kavita.Common.Extensions; using Kavita.Database.Tests; diff --git a/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/metadata_from_notes.cbz b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/metadata_from_notes.cbz new file mode 100644 index 0000000000000000000000000000000000000000..aba0729ad3cd6de6cb107ec8c084e7bd83c3e37c GIT binary patch literal 860 zcmWIWW@Zs#U|`^2cvF%W)45EHX%7nBe^g^Oew>juX+nLxKad@lGZ28C)SmiO!>yDR%6!)wuS*tv2Z_Bh-4+?fxRZmzcVy*sSjjR2$ z-pIk%@C|8xM2C}s!igsDAIC)#necv_RxwmZ6l$70PG$wDnmGPxB zC$2?B*t^uxi+^IxC4H``-!>lpy;kx5qW{-M^+T*7>*W%MGtGZ$WL5cWcwRFK6%d^ZxSs$*;>FXJ2P; zU(Z*Up4B(=kiK=~euc@W>b-7x`zxlrT3`6-r_Z_Iy1sSO+RpEsKAlbf!Z!H_udITN zttKb>s;_)r^VLu8^8ME>^Ee&Vmt8*V>Zcd%|NirA?Tbt6ySL9^o0|SAzWYz(mB;!X zdG!nd-i%E047d`y3NWdIfC89AOZHq)VMYcC2H|XL=U1EWFv94c(UYw}`~YuOHjrLM OAdCmn&w+V}fdK$d;eaRr literal 0 HcmV?d00001 diff --git a/Kavita.Services.Tests/Test Data/CblParserTests/Test Cases/2018-2021 Part 16.1 Reborn Again.json b/Kavita.Services.Tests/Test Data/CblParserTests/Test Cases/2018-2021 Part 16.1 Reborn Again.json new file mode 100644 index 000000000..bb1dadcbe --- /dev/null +++ b/Kavita.Services.Tests/Test Data/CblParserTests/Test Cases/2018-2021 Part 16.1 Reborn Again.json @@ -0,0 +1,1052 @@ +{ + "fileDetails": { + "UUID": "a59e4ad5-0d51-4afe-b0f5-6d3e01eb7cc7", + "version": 1.0 + }, + "listDetails": { + "name": "Part 16.1 Reborn Again", + "startYear": 2021, + "endYear": 2022, + "publisher": "Marvel", + "type": "universal", + "tags": ["avengers", "marvel guides", "fresh start"], + "relationships": [ + { + "name": "Part 15.4 Trial by Fire", + "UUID": "e1eecedf-df97-4ab4-a476-ee85204e3b78", + "relationship": "previous" + }, + { + "name": "Part 16.2 Sinister War", + "UUID": "507e7444-8f5f-46fc-a2cc-17f08c000982", + "relationship": "following" + } + ], + "source": [ + { + "name": "Marvel Guides", + "url": "https://marvelguides.com/fresh-start-finale-reading-order" + } + ] + }, + "issueList": [ + { + "seriesName": "Heroes Reborn", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-07-01", + "id": [ + { + "name": "comicvine", + "series": "135903", + "issue": "847511" + }, + { + "name": "metron", + "series": "2139", + "issue": "29926" + } + ] + }, + { + "seriesName": "Heroes Reborn: Hyperion & the Imperial Guard", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-07-01", + "id": [ + { + "name": "comicvine", + "series": "136023", + "issue": "848853" + }, + { + "name": "metron", + "series": "2176", + "issue": "30391" + } + ] + }, + { + "seriesName": "Heroes Reborn", + "seriesStartYear": 2021, + "issueNumber": "2", + "issueCoverDate": "2021-07-01", + "id": [ + { + "name": "comicvine", + "series": "135903", + "issue": "848852" + }, + { + "name": "metron", + "series": "2139", + "issue": "30381" + } + ] + }, + { + "seriesName": "Heroes Reborn: Peter Parker, The Amazing Shutterbug", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-07-01", + "id": [ + { + "name": "comicvine", + "series": "136024", + "issue": "848854" + }, + { + "name": "metron", + "series": "2175", + "issue": "30390" + } + ] + }, + { + "seriesName": "Heroes Reborn", + "seriesStartYear": 2021, + "issueNumber": "3", + "issueCoverDate": "2021-07-01", + "id": [ + { + "name": "comicvine", + "series": "135903", + "issue": "851308" + }, + { + "name": "metron", + "series": "2139", + "issue": "30658" + } + ] + }, + { + "seriesName": "Heroes Reborn: Magneto & the Mutant Force", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-07-01", + "id": [ + { + "name": "comicvine", + "series": "136388", + "issue": "855617" + }, + { + "name": "metron", + "series": "2224", + "issue": "31011" + } + ] + }, + { + "seriesName": "Heroes Reborn", + "seriesStartYear": 2021, + "issueNumber": "4", + "issueCoverDate": "2021-07-01", + "id": [ + { + "name": "comicvine", + "series": "135903", + "issue": "855615" + }, + { + "name": "metron", + "series": "2139", + "issue": "31074" + } + ] + }, + { + "seriesName": "Heroes Reborn: Marvel Double Action", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-08-01", + "id": [ + { + "name": "comicvine", + "series": "136619", + "issue": "857653" + }, + { + "name": "metron", + "series": "2231", + "issue": "31285" + } + ] + }, + { + "seriesName": "Heroes Reborn: Young Squadron", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-07-01", + "id": [ + { + "name": "comicvine", + "series": "136390", + "issue": "855621" + }, + { + "name": "metron", + "series": "2225", + "issue": "31009" + } + ] + }, + { + "seriesName": "Heroes Reborn: Siege Society", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-07-01", + "id": [ + { + "name": "comicvine", + "series": "136389", + "issue": "855618" + }, + { + "name": "metron", + "series": "2226", + "issue": "31008" + } + ] + }, + { + "seriesName": "Heroes Reborn: Night-Gwen", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-08-01", + "id": [ + { + "name": "comicvine", + "series": "136813", + "issue": "858876" + }, + { + "name": "metron", + "series": "2278", + "issue": "31666" + } + ] + }, + { + "seriesName": "Heroes Reborn: American Knights", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-08-01", + "id": [ + { + "name": "comicvine", + "series": "136618", + "issue": "857652" + }, + { + "name": "metron", + "series": "2230", + "issue": "31284" + } + ] + }, + { + "seriesName": "Heroes Reborn", + "seriesStartYear": 2021, + "issueNumber": "5", + "issueCoverDate": "2021-08-01", + "id": [ + { + "name": "comicvine", + "series": "135903", + "issue": "857651" + }, + { + "name": "metron", + "series": "2139", + "issue": "31273" + } + ] + }, + { + "seriesName": "Heroes Reborn: Squadron Savage", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-08-01", + "id": [ + { + "name": "comicvine", + "series": "136814", + "issue": "858877" + }, + { + "name": "metron", + "series": "2279", + "issue": "31667" + } + ] + }, + { + "seriesName": "Heroes Reborn: Weapon X & Final Flight", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-08-01", + "id": [ + { + "name": "comicvine", + "series": "136946", + "issue": "861677" + }, + { + "name": "metron", + "series": "2314", + "issue": "32144" + } + ] + }, + { + "seriesName": "Heroes Reborn", + "seriesStartYear": 2021, + "issueNumber": "6", + "issueCoverDate": "2021-08-01", + "id": [ + { + "name": "comicvine", + "series": "135903", + "issue": "858875" + }, + { + "name": "metron", + "series": "2139", + "issue": "31656" + } + ] + }, + { + "seriesName": "Heroes Reborn", + "seriesStartYear": 2021, + "issueNumber": "7", + "issueCoverDate": "2021-08-01", + "id": [ + { + "name": "comicvine", + "series": "135903", + "issue": "861676" + }, + { + "name": "metron", + "series": "2139", + "issue": "32136" + } + ] + }, + { + "seriesName": "Heroes Return", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-08-01", + "id": [ + { + "name": "comicvine", + "series": "137049", + "issue": "863929" + }, + { + "name": "metron", + "series": "2335", + "issue": "32504" + } + ] + }, + { + "seriesName": "Thor Annual", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-09-01", + "id": [ + { + "name": "comicvine", + "series": "137700", + "issue": "872888" + }, + { + "name": "metron", + "series": "2482", + "issue": "34189" + } + ] + }, + { + "seriesName": "Guardians of the Galaxy Annual", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-10-01", + "id": [ + { + "name": "comicvine", + "series": "138051", + "issue": "877064" + }, + { + "name": "metron", + "series": "2502", + "issue": "35353" + } + ] + }, + { + "seriesName": "Miles Morales: Spider-Man Annual", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-10-01", + "id": [ + { + "name": "comicvine", + "series": "138346", + "issue": "880931" + }, + { + "name": "metron", + "series": "2566", + "issue": "36477" + } + ] + }, + { + "seriesName": "Avengers Annual", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-10-01", + "id": [ + { + "name": "comicvine", + "series": "138528", + "issue": "882057" + }, + { + "name": "metron", + "series": "2597", + "issue": "36817" + } + ] + }, + { + "seriesName": "Black Cat", + "seriesStartYear": 2020, + "issueNumber": "8", + "issueCoverDate": "2021-09-01", + "id": [ + { + "name": "comicvine", + "series": "132631", + "issue": "874551" + } + ] + }, + { + "seriesName": "Black Cat", + "seriesStartYear": 2020, + "issueNumber": "9", + "issueCoverDate": "2021-10-01", + "id": [ + { + "name": "comicvine", + "series": "132631", + "issue": "880919" + } + ] + }, + { + "seriesName": "Black Cat", + "seriesStartYear": 2020, + "issueNumber": "10", + "issueCoverDate": "2021-11-01", + "id": [ + { + "name": "comicvine", + "series": "132631", + "issue": "886958" + } + ] + }, + { + "seriesName": "Giant-Sized Black Cat: Infinity Score", + "seriesStartYear": 2021, + "issueNumber": "1", + "id": [ + { + "name": "comicvine", + "series": "140355", + "issue": "897333" + } + ] + }, + { + "seriesName": "Thor", + "seriesStartYear": 2020, + "issueNumber": "15", + "issueCoverDate": "2021-09-01", + "id": [ + { + "name": "comicvine", + "series": "123862", + "issue": "871610" + }, + { + "name": "metron", + "series": "949", + "issue": "33813" + } + ] + }, + { + "seriesName": "Thor", + "seriesStartYear": 2020, + "issueNumber": "16", + "issueCoverDate": "2021-10-01", + "id": [ + { + "name": "comicvine", + "series": "123862", + "issue": "882080" + }, + { + "name": "metron", + "series": "949", + "issue": "36813" + } + ] + }, + { + "seriesName": "Thor", + "seriesStartYear": 2020, + "issueNumber": "17", + "issueCoverDate": "2021-11-01", + "id": [ + { + "name": "comicvine", + "series": "123862", + "issue": "886976" + }, + { + "name": "metron", + "series": "949", + "issue": "38591" + } + ] + }, + { + "seriesName": "Thor", + "seriesStartYear": 2020, + "issueNumber": "18", + "issueCoverDate": "2021-12-01", + "id": [ + { + "name": "comicvine", + "series": "123862", + "issue": "890614" + }, + { + "name": "metron", + "series": "949", + "issue": "39489" + } + ] + }, + { + "seriesName": "Thor", + "seriesStartYear": 2020, + "issueNumber": "19", + "issueCoverDate": "2022-01-01", + "id": [ + { + "name": "comicvine", + "series": "123862", + "issue": "895350" + }, + { + "name": "metron", + "series": "949", + "issue": "40300" + } + ] + }, + { + "seriesName": "Thor", + "seriesStartYear": 2020, + "issueNumber": "20", + "issueCoverDate": "2022-03-01", + "id": [ + { + "name": "comicvine", + "series": "123862", + "issue": "901347" + }, + { + "name": "metron", + "series": "949", + "issue": "42150" + } + ] + }, + { + "seriesName": "Thor", + "seriesStartYear": 2020, + "issueNumber": "21", + "issueCoverDate": "2022-03-01", + "id": [ + { + "name": "comicvine", + "series": "123862", + "issue": "904778" + }, + { + "name": "metron", + "series": "949", + "issue": "42931" + } + ] + }, + { + "seriesName": "Thor", + "seriesStartYear": 2020, + "issueNumber": "22", + "issueCoverDate": "2022-04-01", + "id": [ + { + "name": "comicvine", + "series": "123862", + "issue": "907435" + }, + { + "name": "metron", + "series": "949", + "issue": "43543" + } + ] + }, + { + "seriesName": "Thor", + "seriesStartYear": 2020, + "issueNumber": "23", + "issueCoverDate": "2022-05-01", + "id": [ + { + "name": "comicvine", + "series": "123862", + "issue": "910495" + }, + { + "name": "metron", + "series": "949", + "issue": "44168" + } + ] + }, + { + "seriesName": "Iron Man", + "seriesStartYear": 2020, + "issueNumber": "3", + "issueCoverDate": "2021-01-01", + "id": [ + { + "name": "comicvine", + "series": "130396", + "issue": "817711" + }, + { + "name": "metron", + "series": "1540", + "issue": "18856" + } + ] + }, + { + "seriesName": "Iron Man", + "seriesStartYear": 2020, + "issueNumber": "4", + "issueCoverDate": "2021-02-01", + "id": [ + { + "name": "comicvine", + "series": "130396", + "issue": "821496" + }, + { + "name": "metron", + "series": "1540", + "issue": "19963" + } + ] + }, + { + "seriesName": "Iron Man", + "seriesStartYear": 2020, + "issueNumber": "5", + "issueCoverDate": "2021-03-01", + "id": [ + { + "name": "comicvine", + "series": "130396", + "issue": "824027" + }, + { + "name": "metron", + "series": "1540", + "issue": "22253" + } + ] + }, + { + "seriesName": "Iron Man", + "seriesStartYear": 2020, + "issueNumber": "6", + "issueCoverDate": "2021-04-01", + "id": [ + { + "name": "comicvine", + "series": "130396", + "issue": "828856" + }, + { + "name": "metron", + "series": "1540", + "issue": "25227" + } + ] + }, + { + "seriesName": "Black Panther", + "seriesStartYear": 2022, + "issueNumber": "1", + "issueCoverDate": "2022-01-01", + "id": [ + { + "name": "comicvine", + "series": "140222", + "issue": "895386" + }, + { + "name": "metron", + "series": "2832", + "issue": "40295" + } + ] + }, + { + "seriesName": "Black Panther", + "seriesStartYear": 2022, + "issueNumber": "2", + "issueCoverDate": "2022-02-01", + "id": [ + { + "name": "comicvine", + "series": "140222", + "issue": "899164" + }, + { + "name": "metron", + "series": "2832", + "issue": "41341" + } + ] + }, + { + "seriesName": "Black Panther", + "seriesStartYear": 2022, + "issueNumber": "3", + "issueCoverDate": "2022-03-01", + "id": [ + { + "name": "comicvine", + "series": "140222", + "issue": "904772" + }, + { + "name": "metron", + "series": "2832", + "issue": "42923" + } + ] + }, + { + "seriesName": "Black Panther", + "seriesStartYear": 2022, + "issueNumber": "4", + "issueCoverDate": "2022-05-01", + "id": [ + { + "name": "comicvine", + "series": "140222", + "issue": "909678" + }, + { + "name": "metron", + "series": "2832", + "issue": "43648" + } + ] + }, + { + "seriesName": "Black Panther", + "seriesStartYear": 2022, + "issueNumber": "5", + "issueCoverDate": "2022-06-01", + "id": [ + { + "name": "comicvine", + "series": "140222", + "issue": "916298" + }, + { + "name": "metron", + "series": "2832", + "issue": "44882" + } + ] + }, + { + "seriesName": "Black Panther", + "seriesStartYear": 2022, + "issueNumber": "6", + "issueCoverDate": "2022-08-01", + "id": [ + { + "name": "comicvine", + "series": "140222", + "issue": "928655" + }, + { + "name": "metron", + "series": "2832", + "issue": "47834" + } + ] + }, + { + "seriesName": "Black Panther", + "seriesStartYear": 2022, + "issueNumber": "7", + "issueCoverDate": "2022-09-01", + "id": [ + { + "name": "comicvine", + "series": "140222", + "issue": "934989" + }, + { + "name": "metron", + "series": "2832", + "issue": "49702" + } + ] + }, + { + "seriesName": "Black Panther", + "seriesStartYear": 2022, + "issueNumber": "8", + "issueCoverDate": "2022-10-01", + "id": [ + { + "name": "comicvine", + "series": "140222", + "issue": "941558" + }, + { + "name": "metron", + "series": "2832", + "issue": "50688" + } + ] + }, + { + "seriesName": "Eternals: Celestia", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-12-01", + "id": [ + { + "name": "comicvine", + "series": "139433", + "issue": "888648" + }, + { + "name": "metron", + "series": "2728", + "issue": "38891" + } + ] + }, + { + "seriesName": "Free Comic Book Day 2021: Avengers/Hulk", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-10-01", + "id": [ + { + "name": "comicvine", + "series": "138340", + "issue": "880888" + } + ] + }, + { + "seriesName": "Avengers", + "seriesStartYear": 2018, + "issueNumber": "46", + "issueCoverDate": "2021-09-01", + "id": [ + { + "name": "comicvine", + "series": "110496", + "issue": "868606" + }, + { + "name": "metron", + "series": "61", + "issue": "33670" + } + ] + }, + { + "seriesName": "Avengers", + "seriesStartYear": 2018, + "issueNumber": "47", + "issueCoverDate": "2021-10-01", + "id": [ + { + "name": "comicvine", + "series": "110496", + "issue": "877061" + }, + { + "name": "metron", + "series": "61", + "issue": "35346" + } + ] + }, + { + "seriesName": "Winter Guard", + "seriesStartYear": 2021, + "issueNumber": "1", + "issueCoverDate": "2021-10-01", + "id": [ + { + "name": "comicvine", + "series": "138533", + "issue": "882082" + }, + { + "name": "metron", + "series": "2602", + "issue": "36816" + } + ] + }, + { + "seriesName": "Winter Guard", + "seriesStartYear": 2021, + "issueNumber": "2", + "issueCoverDate": "2021-11-01", + "id": [ + { + "name": "comicvine", + "series": "138533", + "issue": "886977" + }, + { + "name": "metron", + "series": "2602", + "issue": "38592" + } + ] + }, + { + "seriesName": "Winter Guard", + "seriesStartYear": 2021, + "issueNumber": "3", + "issueCoverDate": "2022-01-01", + "id": [ + { + "name": "comicvine", + "series": "138533", + "issue": "892247" + }, + { + "name": "metron", + "series": "2602", + "issue": "39790" + } + ] + }, + { + "seriesName": "Winter Guard", + "seriesStartYear": 2021, + "issueNumber": "4", + "issueCoverDate": "2022-02-01", + "id": [ + { + "name": "comicvine", + "series": "138533", + "issue": "896096" + }, + { + "name": "metron", + "series": "2602", + "issue": "40630" + } + ] + }, + { + "seriesName": "Avengers", + "seriesStartYear": 2018, + "issueNumber": "48", + "issueCoverDate": "2021-11-01", + "id": [ + { + "name": "comicvine", + "series": "110496", + "issue": "882898" + }, + { + "name": "metron", + "series": "61", + "issue": "37084" + } + ] + }, + { + "seriesName": "Avengers", + "seriesStartYear": 2018, + "issueNumber": "49", + "issueCoverDate": "2021-12-01", + "id": [ + { + "name": "comicvine", + "series": "110496", + "issue": "889468" + }, + { + "name": "metron", + "series": "61", + "issue": "39369" + } + ] + }, + { + "seriesName": "Avengers", + "seriesStartYear": 2018, + "issueNumber": "50", + "issueCoverDate": "2022-02-01", + "id": [ + { + "name": "comicvine", + "series": "110496", + "issue": "896105" + }, + { + "name": "metron", + "series": "61", + "issue": "40620" + } + ] + } + ] +} diff --git a/Kavita.Services.Tests/Test Data/CblParserTests/Test Cases/BOOM! Power Rangers Simplified 1a.cbl b/Kavita.Services.Tests/Test Data/CblParserTests/Test Cases/BOOM! Power Rangers Simplified 1a.cbl new file mode 100644 index 000000000..3a6ded107 --- /dev/null +++ b/Kavita.Services.Tests/Test Data/CblParserTests/Test Cases/BOOM! Power Rangers Simplified 1a.cbl @@ -0,0 +1,500 @@ + + +Simplified Power Rangers 1a +164 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Kavita.Services.Tests/Test Data/CblParserTests/Test Cases/[DC Comics] Aquaman- Death of a Prince (WEB-CBRO).cbl b/Kavita.Services.Tests/Test Data/CblParserTests/Test Cases/[DC Comics] Aquaman- Death of a Prince (WEB-CBRO).cbl new file mode 100644 index 000000000..1a90944e0 --- /dev/null +++ b/Kavita.Services.Tests/Test Data/CblParserTests/Test Cases/[DC Comics] Aquaman- Death of a Prince (WEB-CBRO).cbl @@ -0,0 +1,83 @@ + + +[DC Comics] Aquaman- Death of a Prince (WEB-CBRO) +25 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Kavita.Services/Extensions/ApplicationServiceExtensions.cs b/Kavita.Services/Extensions/ApplicationServiceExtensions.cs index 9d879e07c..899468a5e 100644 --- a/Kavita.Services/Extensions/ApplicationServiceExtensions.cs +++ b/Kavita.Services/Extensions/ApplicationServiceExtensions.cs @@ -4,6 +4,7 @@ using Kavita.API.Services.Helpers; using Kavita.API.Services.Metadata; using Kavita.API.Services.Plus; using Kavita.API.Services.Reading; +using Kavita.API.Services.ReadingLists; using Kavita.API.Services.Scanner; using Kavita.API.Services.SignalR; using Kavita.Services.Helpers; @@ -11,6 +12,7 @@ using Kavita.Services.HostedServices; using Kavita.Services.Metadata; using Kavita.Services.Plus; using Kavita.Services.Reading; +using Kavita.Services.ReadingLists; using Kavita.Services.Scanner; using Kavita.Services.SignalR; using Microsoft.Extensions.DependencyInjection; @@ -55,6 +57,7 @@ public static class ApplicationServiceExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Kavita.Services/Helpers/CblParser.cs b/Kavita.Services/Helpers/CblParser.cs new file mode 100644 index 000000000..f7a088625 --- /dev/null +++ b/Kavita.Services/Helpers/CblParser.cs @@ -0,0 +1,231 @@ +using System; +using System.Globalization; +using System.IO; +using System.Text.Json; +using System.Xml.Serialization; +using Kavita.Models.DTOs.ReadingLists.CBL; +using Kavita.Models.DTOs.ReadingLists.CBL.V1; +using Kavita.Models.DTOs.ReadingLists.CBL.V2; + +namespace Kavita.Services.Helpers; + +/// +/// Responsible for reading v1 and v2 specs into a common format +/// +public static class CblParser +{ + private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true, + }; + + /// + /// Auto-detect format by file extension and parse accordingly. + /// + public static ParsedCblReadingList Parse(string filePath) + { + var ext = Path.GetExtension(filePath).ToLowerInvariant(); + return ext switch + { + ".cbl" or ".xml" => ParseV1(filePath), + ".json" => ParseV2(filePath), + _ => throw new ArgumentException($"Unsupported CBL file extension: {ext}") + }; + } + + /// + /// Parse a v1 XML CBL file into the unified model. + /// + public static ParsedCblReadingList ParseV1(string filePath) + { + var serializer = new XmlSerializer(typeof(CblReadingList)); + using var stream = File.OpenRead(filePath); + var cbl = (CblReadingList)serializer.Deserialize(stream); + + var result = new ParsedCblReadingList + { + SchemaVersion = 1, + Name = cbl.Name ?? string.Empty, + Summary = cbl.Summary ?? string.Empty, + StartYear = cbl.StartYear, + StartMonth = cbl.StartMonth, + EndYear = cbl.EndYear, + EndMonth = cbl.EndMonth, + }; + + if (cbl.Books?.Book != null) + { + for (var i = 0; i < cbl.Books.Book.Count; i++) + { + var book = cbl.Books.Book[i]; + var item = new ParsedCblItem + { + Order = i, + SeriesName = book.Series ?? string.Empty, + Number = book.Number ?? string.Empty, + Volume = book.Volume ?? string.Empty, + Year = book.Year ?? string.Empty, + Format = book.Format ?? string.Empty, + FileType = book.FileType ?? string.Empty, + IssueType = CblIssueType.Unknown, + }; + + if (book.Database != null) + { + var provider = MapProviderName(book.Database.Name); + item.ExternalIds.Add(new CblExternalId + { + Provider = provider, + SeriesId = book.Database.Series ?? string.Empty, + IssueId = book.Database.Issue ?? string.Empty, + }); + } + + result.Items.Add(item); + } + } + + return result; + } + + /// + /// Parse a v2 JSON CBL file into the unified model. + /// + /// https://github.com/ComicReadingLists/json-cbl-standard/blob/main/schema/1.0/comic-reading-list.schema.json + public static ParsedCblReadingList ParseV2(string filePath) + { + var json = File.ReadAllText(filePath); + var v2 = JsonSerializer.Deserialize(json, JsonSerializerOptions); + + var result = new ParsedCblReadingList + { + Uuid = v2.FileDetails?.UUID ?? string.Empty, + SchemaVersion = (int)(v2.FileDetails?.Version ?? 1), + Name = v2.ListDetails?.Name ?? string.Empty, + Summary = v2.ListDetails?.Description ?? string.Empty, + Notes = v2.Notes ?? string.Empty, + StartYear = v2.ListDetails?.StartYear ?? -1, + StartMonth = -1, + EndYear = v2.ListDetails?.EndYear ?? -1, + EndMonth = -1, + Publisher = v2.ListDetails?.Publisher ?? string.Empty, + Imprint = v2.ListDetails?.Imprint ?? string.Empty, + ListType = MapListType(v2.ListDetails?.Type), + Tags = v2.ListDetails?.Tags ?? [], + CoverImageUrls = v2.ListDetails?.CoverImageURLs ?? [], + }; + + if (v2.ListDetails?.Relationships != null) + { + foreach (var rel in v2.ListDetails.Relationships) + { + result.Relationships.Add(new CblRelationship + { + Name = rel.Name ?? string.Empty, + Uuid = rel.UUID ?? string.Empty, + Relationship = rel.Relationship ?? string.Empty, + }); + } + } + + if (v2.ListDetails?.Source != null) + { + foreach (var src in v2.ListDetails.Source) + { + result.Sources.Add(new CblSource + { + Name = src.Name ?? string.Empty, + Url = src.Url ?? string.Empty, + }); + } + } + + if (v2.IssueList != null) + { + for (var i = 0; i < v2.IssueList.Count; i++) + { + var issue = v2.IssueList[i]; + var item = new ParsedCblItem + { + Order = i, + SeriesName = issue.SeriesName ?? string.Empty, + Number = issue.IssueNumber ?? string.Empty, + Volume = issue.SeriesStartYear?.ToString(CultureInfo.InvariantCulture) ?? string.Empty, + Year = ExtractYear(issue.IssueCoverDate), + CoverDate = issue.IssueCoverDate ?? string.Empty, + IssueType = MapIssueType(issue.IssueType), + }; + + if (issue.Id != null) + { + foreach (var id in issue.Id) + { + item.ExternalIds.Add(new CblExternalId + { + Provider = MapProviderName(id.Name), + SeriesId = id.Series ?? string.Empty, + IssueId = id.Issue ?? string.Empty, + }); + } + } + + result.Items.Add(item); + } + } + + return result; + } + + private static CblExternalDbProvider MapProviderName(string name) + { + if (string.IsNullOrEmpty(name)) return CblExternalDbProvider.Unknown; + + return name.ToLowerInvariant() switch + { + "cv" or "comicvine" => CblExternalDbProvider.ComicVine, + "metron" => CblExternalDbProvider.Metron, + "gcd" or "grandcomicsdatabase" => CblExternalDbProvider.GrandComicsDatabase, + _ => CblExternalDbProvider.Unknown, + }; + } + + private static CblListType MapListType(string? type) + { + if (string.IsNullOrEmpty(type)) return CblListType.Unknown; + + return type.ToLowerInvariant() switch + { + "master" => CblListType.Master, + "interuniversal" => CblListType.Interuniversal, + "universal" => CblListType.Universal, + "team" => CblListType.Team, + "character" => CblListType.Character, + "story" => CblListType.Story, + _ => CblListType.Unknown, + }; + } + + private static CblIssueType MapIssueType(string type) + { + if (string.IsNullOrEmpty(type)) return CblIssueType.Unknown; + + return type.ToLowerInvariant() switch + { + "event-core" => CblIssueType.EventCore, + "event-tie-in" => CblIssueType.EventTieIn, + "event-one-shot" => CblIssueType.EventOneShot, + "ongoing" => CblIssueType.Ongoing, + _ => CblIssueType.Unknown, + }; + } + + private static string ExtractYear(string coverDate) + { + if (string.IsNullOrEmpty(coverDate)) return string.Empty; + + // Expected format: "YYYY-MM-DD" + var dashIndex = coverDate.IndexOf('-'); + return dashIndex > 0 ? coverDate[..dashIndex] : coverDate; + } + +} diff --git a/Kavita.Services/Kavita.Services.csproj b/Kavita.Services/Kavita.Services.csproj index 50250fde1..cf4ab4d3a 100644 --- a/Kavita.Services/Kavita.Services.csproj +++ b/Kavita.Services/Kavita.Services.csproj @@ -16,10 +16,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Kavita.Services/LocalizationService.cs b/Kavita.Services/LocalizationService.cs index 4a50868cd..18b776b0a 100644 --- a/Kavita.Services/LocalizationService.cs +++ b/Kavita.Services/LocalizationService.cs @@ -312,7 +312,7 @@ public class LocalizationService : ILocalizationService { try { - var jsonObject = System.Text.Json.JsonDocument.Parse(fileContent); + var jsonObject = JsonDocument.Parse(fileContent); int totalKeys = 0; int nonEmptyValues = 0; @@ -355,7 +355,7 @@ public class LocalizationService : ILocalizationService { foreach (var property in element.EnumerateObject()) { - if (property.Value.ValueKind == System.Text.Json.JsonValueKind.String) + if (property.Value.ValueKind == JsonValueKind.String) { totalKeys++; var value = property.Value.GetString(); @@ -371,7 +371,7 @@ public class LocalizationService : ILocalizationService } } } - else if (element.ValueKind == System.Text.Json.JsonValueKind.Array) + else if (element.ValueKind == JsonValueKind.Array) { foreach (var item in element.EnumerateArray()) { @@ -380,23 +380,23 @@ public class LocalizationService : ILocalizationService } } - private void CountEntries(System.Text.Json.JsonElement element, ref int total, ref int translated) + private void CountEntries(JsonElement element, ref int total, ref int translated) { - if (element.ValueKind == System.Text.Json.JsonValueKind.Object) + if (element.ValueKind == JsonValueKind.Object) { foreach (var property in element.EnumerateObject()) { CountEntries(property.Value, ref total, ref translated); } } - else if (element.ValueKind == System.Text.Json.JsonValueKind.Array) + else if (element.ValueKind == JsonValueKind.Array) { foreach (var item in element.EnumerateArray()) { CountEntries(item, ref total, ref translated); } } - else if (element.ValueKind == System.Text.Json.JsonValueKind.String) + else if (element.ValueKind == JsonValueKind.String) { total++; string value = element.GetString(); diff --git a/Kavita.Services/OpdsService.cs b/Kavita.Services/OpdsService.cs index 97a95be25..522f259cf 100644 --- a/Kavita.Services/OpdsService.cs +++ b/Kavita.Services/OpdsService.cs @@ -10,6 +10,7 @@ using Kavita.API.Database; using Kavita.API.Errors; using Kavita.API.Services; using Kavita.API.Services.Reading; +using Kavita.API.Services.ReadingLists; using Kavita.Common.Helpers; using Kavita.Models.DTOs; using Kavita.Models.DTOs.Filtering; diff --git a/Kavita.Services/Plus/ExternalMetadataService.cs b/Kavita.Services/Plus/ExternalMetadataService.cs index 329db7e0e..5f1ae5d80 100644 --- a/Kavita.Services/Plus/ExternalMetadataService.cs +++ b/Kavita.Services/Plus/ExternalMetadataService.cs @@ -9,6 +9,7 @@ using Flurl.Http; using Hangfire; using Kavita.API.Database; using Kavita.API.Repositories; +using Kavita.API.Services.Helpers; using Kavita.API.Services.Metadata; using Kavita.API.Services.Plus; using Kavita.API.Services.SignalR; @@ -177,8 +178,8 @@ public class ExternalMetadataService : IExternalMetadataService SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata | SeriesIncludes.Library, ct); if (series == null) return []; - var potentialAnilistId = ScrobblingHelper.ExtractId(dto.Query, ScrobblingService.AniListWeblinkWebsite); - var potentialMalId = ScrobblingHelper.ExtractId(dto.Query, ScrobblingService.MalWeblinkWebsite); + var potentialAnilistId = WeblinkParser.GetAniListId(dto.Query); + var potentialMalId = WeblinkParser.GetMalId(dto.Query); var format = series.Library.Type.ConvertToPlusMediaFormat(series.Format); var otherNames = ExtractAlternativeNames(series); @@ -412,7 +413,6 @@ public class ExternalMetadataService : IExternalMetadataService return _defaultReturn; } - // Clear out existing results var externalSeriesMetadata = await GetOrCreateExternalSeriesMetadataForSeries(seriesId, series); _unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalReviews); @@ -547,6 +547,7 @@ public class ExternalMetadataService : IExternalMetadataService madeModification = UpdateReleaseYear(series, settings, externalMetadata) || madeModification; madeModification = UpdateLocalizedName(series, settings, externalMetadata) || madeModification; madeModification = await UpdatePublicationStatus(series, settings, externalMetadata) || madeModification; + madeModification = UpdateExternalIds(series, settings, externalMetadata) || madeModification; // Apply field mappings GenerateGenreAndTagLists(externalMetadata, settings, ref processedTags, ref processedGenres); @@ -566,6 +567,8 @@ public class ExternalMetadataService : IExternalMetadataService madeModification = await UpdateChapters(series, settings, externalMetadata) || madeModification; + + return madeModification; } @@ -776,7 +779,7 @@ public class ExternalMetadataService : IExternalMetadataService .Select(w => new PersonDto() { Name = w.Name.Trim(), - AniListId = ScrobblingHelper.ExtractId(w.Url, ScrobblingService.AniListCharacterWebsite), + AniListId = WeblinkParser.GetAniListCharacterId(w.Url), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) .Concat(series.Metadata.People @@ -818,7 +821,7 @@ public class ExternalMetadataService : IExternalMetadataService foreach (var character in externalCharacters) { - var aniListId = ScrobblingHelper.ExtractId(character.Url, ScrobblingService.AniListCharacterWebsite); + var aniListId = WeblinkParser.GetAniListCharacterId(character.Url); if (aniListId <= 0) continue; var person = await _unitOfWork.PersonRepository.GetPersonByAniListId(aniListId); if (person != null && !string.IsNullOrEmpty(character.ImageUrl) && string.IsNullOrEmpty(person.CoverImage)) @@ -857,7 +860,7 @@ public class ExternalMetadataService : IExternalMetadataService .Select(w => new PersonDto() { Name = w.Name.Trim(), - AniListId = ScrobblingHelper.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), + AniListId = WeblinkParser.GetAniListStaffId(w.Url), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) .Concat(series.Metadata.People @@ -914,7 +917,7 @@ public class ExternalMetadataService : IExternalMetadataService .Select(w => new PersonDto() { Name = w.Name.Trim(), - AniListId = ScrobblingHelper.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), + AniListId = WeblinkParser.GetAniListStaffId(w.Url), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) .Concat(series.Metadata.People @@ -1093,6 +1096,24 @@ public class ExternalMetadataService : IExternalMetadataService return false; } + private static bool UpdateExternalIds(Series series, MetadataSettingsDto _, ExternalSeriesDetailDto externalMetadata) + { + var madeModification = false; + if (externalMetadata.AniListId is > 0) + { + series.AniListId = externalMetadata.AniListId.Value; + madeModification = true; + } + + if (externalMetadata.MALId is > 0) + { + series.MalId = externalMetadata.MALId.Value; + madeModification = true; + } + + return madeModification; + } + private async Task UpdateChapters(Series series, MetadataSettingsDto settings, ExternalSeriesDetailDto externalMetadata) @@ -1110,7 +1131,7 @@ public class ExternalMetadataService : IExternalMetadataService externalMetadata.ChapterDtos, chapter => chapter.Range, dto => dto.IssueNumber, - (chapter, dto) => (chapter, dto) // Create a tuple of matched pairs + (chapter, dto) => (chapter, dto) ) .ToList(); @@ -1540,9 +1561,9 @@ public class ExternalMetadataService : IExternalMetadataService { foreach (var staff in people) { - var aniListId = ScrobblingHelper.ExtractId(staff.Url, ScrobblingService.AniListStaffWebsite); - if (aniListId is null or <= 0) continue; - var person = await _unitOfWork.PersonRepository.GetPersonByAniListId(aniListId.Value); + var aniListId = WeblinkParser.GetAniListStaffId(staff.Url); + if (aniListId <= 0) continue; + var person = await _unitOfWork.PersonRepository.GetPersonByAniListId(aniListId); if (person == null || string.IsNullOrEmpty(staff.ImageUrl) || !string.IsNullOrEmpty(person.CoverImage) || staff.ImageUrl.EndsWith("default.jpg")) continue; @@ -1864,11 +1885,11 @@ public class ExternalMetadataService : IExternalMetadataService { if (payload.AniListId <= 0) { - payload.AniListId = ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingService.AniListWeblinkWebsite); + payload.AniListId = WeblinkParser.GetAniListId(series.Metadata.WebLinks); } if (payload.MalId <= 0) { - payload.MalId = ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingService.MalWeblinkWebsite); + payload.MalId = WeblinkParser.GetMalId(series.Metadata.WebLinks); } payload.SeriesName = series.Name; payload.LocalizedSeriesName = series.LocalizedName; diff --git a/Kavita.Services/ReadingLists/CblExportService.cs b/Kavita.Services/ReadingLists/CblExportService.cs new file mode 100644 index 000000000..2ff920a9d --- /dev/null +++ b/Kavita.Services/ReadingLists/CblExportService.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Globalization; +using System.Text.Json; +using System.Xml; +using System.Xml.Serialization; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Models.DTOs.ReadingLists.CBL.V1; +using Kavita.Models.DTOs.ReadingLists.CBL.V2; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services.ReadingLists; + +public interface ICblExportService +{ + /// + /// Exports the reading list to a temp file on disk. + /// + /// Will overwrite existing files + /// + /// + /// Export as CBLv2 (JSON) + /// Full file path of the exported file, or null if reading list not found + Task ExportReadingList(int readingListId, int userId, bool asV2 = false); +} + +public class CblExportService(IUnitOfWork unitOfWork, IDirectoryService directoryService, ILogger logger) : ICblExportService +{ + /// + public async Task ExportReadingList(int readingListId, int userId, bool asV2 = false) + { + try + { + var readingList = await unitOfWork.DataContext.ReadingList + .AsNoTracking() + .FirstOrDefaultAsync(rl => rl.Id == readingListId); + + if (readingList == null) return null; + + var items = await unitOfWork.DataContext.ReadingListItem + .AsNoTracking() + .Where(rli => rli.ReadingListId == readingListId) + .OrderBy(rli => rli.Order) + .Include(rli => rli.Chapter) + .Include(rli => rli.Volume) + .Include(rli => rli.Series) + .ThenInclude(s => s.Metadata) + .ThenInclude(m => m.People) + .ThenInclude(smp => smp.Person) + .ToListAsync(); + + var outputDir = Path.Combine(directoryService.TempDirectory, userId.ToString(), "cbl-export", $"{readingListId}"); + Directory.CreateDirectory(outputDir); + + var sanitizedName = SanitizeFileName(readingList.Title); + + if (asV2) + { + var jsonFileName = $"{sanitizedName}.json"; + var jsonFilePath = Path.Combine(outputDir, jsonFileName); + + var v2 = BuildCblV2Root(readingList, items); + SerializeV2(v2, jsonFilePath); + + return jsonFilePath; + } + + var cblFileName = $"{sanitizedName}.cbl"; + var cblFilePath = Path.Combine(outputDir, cblFileName); + + var cbl = BuildCblReadingList(readingList, items); + SerializeV1(cbl, cblFilePath); + + return cblFilePath; + } catch (Exception e) + { + logger.LogError(e, "Error while exporting reading list: {ReadingListId}", readingListId); + return null; + } + } + + public static CblReadingList BuildCblReadingList(ReadingList readingList, IList items) + { + var books = new List(); + + foreach (var item in items) + { + var year = item.Chapter.ReleaseDate != DateTime.MinValue + ? item.Chapter.ReleaseDate.Year.ToString() + : string.Empty; + + books.Add(new CblBook + { + Series = item.Series.Name, + Number = item.Chapter.Range, // Range can leak internal encodings. Need to understand how to map this. + Volume = item.Volume.Name, // TODO: If the library is Comic type, we can try and parse from Kavita Series first. Need to test with real user files + Year = year, + Format = item.Chapter.IsSpecial ? "Annual" : string.Empty, // TODO: Confirm with CBL Group on how to handle Format + FileType = MapMangaFormatToFileType(item.Series.Format), + Database = null, // TODO: If we have ComicVine metadata id in Chapter, populate this + }); + } + + return new CblReadingList + { + Name = readingList.Title, + Summary = readingList.Summary ?? string.Empty, + StartYear = readingList.StartingYear, + StartMonth = readingList.StartingMonth, + EndYear = readingList.EndingYear, + EndMonth = readingList.EndingMonth, + Books = new CblBooks { Book = books }, + }; + } + + public static void SerializeV1(CblReadingList cbl, string filePath) + { + var serializer = new XmlSerializer(typeof(CblReadingList)); + var settings = new XmlWriterSettings + { + Indent = true, + Encoding = System.Text.Encoding.UTF8, + }; + + using var stream = File.Create(filePath); + using var writer = XmlWriter.Create(stream, settings); + serializer.Serialize(writer, cbl); + } + + public static CblV2Root BuildCblV2Root(ReadingList readingList, IList items) + { + var publisher = GetMostCommonPerson(items, PersonRole.Publisher); + var imprint = GetMostCommonPerson(items, PersonRole.Imprint); + + var issues = new List(); + foreach (var item in items) + { + var coverDate = item.Chapter.ReleaseDate != DateTime.MinValue + ? item.Chapter.ReleaseDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + : string.Empty; + + var seriesStartYear = item.Series.Metadata?.ReleaseYear is > 0 + ? item.Series.Metadata.ReleaseYear + : (int?)null; + + issues.Add(new CblV2Issue + { + SeriesName = item.Series.Name, + SeriesStartYear = seriesStartYear, + IssueNumber = item.Chapter.Range, + IssueCoverDate = coverDate, + IssueType = string.Empty, + Id = null, // TODO: When we expand Chapter-level external metadata, create this + }); + } + + return new CblV2Root + { + FileDetails = new CblV2FileDetails + { + UUID = Guid.NewGuid().ToString(), + Version = 1.0, + }, + ListDetails = new CblV2ListDetails + { + Name = readingList.Title, + Description = readingList.Summary ?? string.Empty, + StartYear = readingList.StartingYear > 0 ? readingList.StartingYear : null, + EndYear = readingList.EndingYear > 0 ? readingList.EndingYear : null, + Publisher = publisher ?? string.Empty, + Imprint = imprint ?? string.Empty, + Type = string.Empty, + Tags = [], + CoverImageURLs = [], + Relationships = [], + Source = [], + }, + IssueList = issues, + Notes = string.Empty, + }; + } + + public static void SerializeV2(CblV2Root root, string filePath) + { + var options = new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + }; + + var json = JsonSerializer.Serialize(root, options); + File.WriteAllText(filePath, json); + } + + public static string MapMangaFormatToFileType(MangaFormat format) + { + return format switch + { + MangaFormat.Archive => "cbz", + MangaFormat.Epub => "epub", + MangaFormat.Pdf => "pdf", + MangaFormat.Image => "image", + _ => string.Empty, + }; + } + + public static string? GetMostCommonPerson(IList items, PersonRole role) + { + return items + .Where(i => i.Series?.Metadata?.People != null) + .SelectMany(i => i.Series.Metadata.People) + .Where(p => p.Role == role && p.Person != null) + .GroupBy(p => p.Person.Name) + .OrderByDescending(g => g.Count()) + .Select(g => g.Key) + .FirstOrDefault(); + } + + private static string SanitizeFileName(string name) + { + var invalid = Path.GetInvalidFileNameChars(); + return string.Concat(name.Select(c => invalid.Contains(c) ? '_' : c)); + } +} diff --git a/Kavita.Services/ReadingLists/CblImporterService.cs b/Kavita.Services/ReadingLists/CblImporterService.cs new file mode 100644 index 000000000..0f8e5868c --- /dev/null +++ b/Kavita.Services/ReadingLists/CblImporterService.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Kavita.API.Services.ReadingLists; +using Kavita.Models.DTOs.ReadingLists.CBL; + +namespace Kavita.Services.ReadingLists; + +public class CblImporterService : ICblImportService +{ + public Task ValidateList(int userId, string filePath, CblImportOptions options) + { + + throw new System.NotImplementedException(); + } + + public Task UpsertReadingList(int userId, string filePath, CblImportOptions options, CblImportDecisions decisions) + { + throw new System.NotImplementedException(); + } + + public Task SyncReadingList(int userId, int readingListId) + { + throw new System.NotImplementedException(); + } +} diff --git a/Kavita.Services/Reading/ReadingListService.cs b/Kavita.Services/ReadingLists/ReadingListService.cs similarity index 99% rename from Kavita.Services/Reading/ReadingListService.cs rename to Kavita.Services/ReadingLists/ReadingListService.cs index b4fd5dbcb..a3f001d32 100644 --- a/Kavita.Services/Reading/ReadingListService.cs +++ b/Kavita.Services/ReadingLists/ReadingListService.cs @@ -10,6 +10,7 @@ using Kavita.API.Database; using Kavita.API.Repositories; using Kavita.API.Services; using Kavita.API.Services.Reading; +using Kavita.API.Services.ReadingLists; using Kavita.API.Services.SignalR; using Kavita.Common; using Kavita.Common.Extensions; @@ -17,6 +18,7 @@ using Kavita.Common.Helpers; using Kavita.Models.Builders; using Kavita.Models.DTOs.ReadingLists; using Kavita.Models.DTOs.ReadingLists.CBL; +using Kavita.Models.DTOs.ReadingLists.CBL.V1; using Kavita.Models.DTOs.SignalR; using Kavita.Models.Entities; using Kavita.Models.Entities.Enums; diff --git a/Kavita.Services/Scanner/BasicParser.cs b/Kavita.Services/Scanner/BasicParser.cs index c747c1b67..33074d8a3 100644 --- a/Kavita.Services/Scanner/BasicParser.cs +++ b/Kavita.Services/Scanner/BasicParser.cs @@ -1,6 +1,7 @@ using System; using System.IO; using Kavita.API.Services; +using Kavita.Common.Helpers; using Kavita.Models.Entities.Enums; using Kavita.Models.Metadata; using Kavita.Models.Parser; @@ -17,9 +18,9 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag { 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. - if (type != LibraryType.Image && Scanner.Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null; + if (type != LibraryType.Image && Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null; - if (Scanner.Parser.IsImage(filePath)) + if (Parser.IsImage(filePath)) { return imageParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Image, enableMetadata, comicInfo); } @@ -27,44 +28,45 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag var ret = new ParserInfo() { Filename = Path.GetFileName(filePath), - Format = Scanner.Parser.ParseFormat(filePath), - Title = Scanner.Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Scanner.Parser.NormalizePath(filePath), - Series = Scanner.Parser.ParseSeries(fileName, type), + Format = Parser.ParseFormat(filePath), + Title = Parser.RemoveExtensionIfSupported(fileName)!, + FullFilePath = Parser.NormalizePath(filePath), + Series = Parser.ParseSeries(fileName, type), ComicInfo = comicInfo, - Chapters = Scanner.Parser.ParseChapter(fileName, type), - Volumes = Scanner.Parser.ParseVolume(fileName, type), + Chapters = Parser.ParseChapter(fileName, type), }; - if (ret.Series == string.Empty || Scanner.Parser.IsImage(filePath)) + ParseExternalIdsFromNotesAndWeblinks(ret); + + if (ret.Series == string.Empty || Parser.IsImage(filePath)) { // Try to parse information out of each folder all the way to rootPath ParseFromFallbackFolders(filePath, rootPath, type, ref ret); } - var edition = Scanner.Parser.ParseEdition(fileName); + var edition = Parser.ParseEdition(fileName); if (!string.IsNullOrEmpty(edition)) { - ret.Series = Scanner.Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); + ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); ret.Edition = edition; } - var isSpecial = Scanner.Parser.IsSpecial(fileName, type); + var isSpecial = Parser.IsSpecial(fileName, type); // We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that // could cause a problem as Omake is a special term, but there is valid volume/chapter information. - if (Scanner.Parser.IsDefaultChapter(ret.Chapters) && Scanner.Parser.IsLooseLeafVolume(ret.Volumes) && isSpecial) + if (Parser.IsDefaultChapter(ret.Chapters) && Parser.IsLooseLeafVolume(ret.Volumes) && isSpecial) { ret.IsSpecial = true; ParseFromFallbackFolders(filePath, rootPath, type, ref ret); // NOTE: This can cause some complications, we should try to be a bit less aggressive to fallback to folder } // If we are a special with marker, we need to ensure we use the correct series name. we can do this by falling back to Folder name - if (Scanner.Parser.HasSpecialMarker(fileName)) + if (Parser.HasSpecialMarker(fileName)) { ret.IsSpecial = true; - ret.SpecialIndex = Scanner.Parser.ParseSpecialIndex(fileName); - ret.Chapters = Scanner.Parser.DefaultChapter; - ret.Volumes = Scanner.Parser.SpecialVolume; + ret.SpecialIndex = Parser.ParseSpecialIndex(fileName); + ret.Chapters = Parser.DefaultChapter; + ret.Volumes = Parser.SpecialVolume; // NOTE: This uses rootPath. LibraryRoot works better for manga, but it's not always that way. // It might be worth writing some logic if the file is a special, to take the folder above the Specials/ @@ -81,22 +83,22 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag (fileDirectory.EndsWith("Specials", StringComparison.OrdinalIgnoreCase) || fileDirectory.EndsWith("Specials/", StringComparison.OrdinalIgnoreCase))) { - ret.Series = Scanner.Parser.CleanTitle(Directory.GetParent(fileDirectory)?.Name ?? string.Empty); + ret.Series = Parser.CleanTitle(Directory.GetParent(fileDirectory)?.Name ?? string.Empty); } else { ParseFromFallbackFolders(filePath, tempRootPath, type, ref ret); } - ret.Title = Scanner.Parser.CleanSpecialTitle(fileName); + ret.Title = Parser.CleanSpecialTitle(fileName); } if (string.IsNullOrEmpty(ret.Series)) { - ret.Series = Scanner.Parser.CleanTitle(fileName, type is LibraryType.Comic); + ret.Series = Parser.CleanTitle(fileName, type is LibraryType.Comic); } // Pdfs may have .pdf in the series name, remove that - if (Scanner.Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) + if (Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) { ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length); } @@ -109,7 +111,7 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag - if (Scanner.Parser.IsLooseLeafVolume(ret.Volumes) && Scanner.Parser.IsDefaultChapter(ret.Chapters)) + if (Parser.IsLooseLeafVolume(ret.Volumes) && Parser.IsDefaultChapter(ret.Chapters)) { ret.IsSpecial = true; } @@ -117,7 +119,7 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag // v0.8.x: Introducing a change where Specials will go in a separate Volume with a reserved number if (ret.IsSpecial) { - ret.Volumes = Scanner.Parser.SpecialVolume; + ret.Volumes = Parser.SpecialVolume; } return ret.Series == string.Empty ? null : ret; diff --git a/Kavita.Services/Scanner/BookParser.cs b/Kavita.Services/Scanner/BookParser.cs index edc9344fb..d897c2865 100644 --- a/Kavita.Services/Scanner/BookParser.cs +++ b/Kavita.Services/Scanner/BookParser.cs @@ -24,11 +24,11 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer { Filename = Path.GetFileName(filePath), Format = MangaFormat.Epub, - Title = Scanner.Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Scanner.Parser.NormalizePath(filePath), - Series = Scanner.Parser.ParseSeries(fileName, type), - Chapters = Scanner.Parser.ParseChapter(fileName, type), - Volumes = Scanner.Parser.ParseVolume(fileName, type), + Title = Parser.RemoveExtensionIfSupported(fileName)!, + FullFilePath = Parser.NormalizePath(filePath), + Series = Parser.ParseSeries(fileName, type), + Chapters = Parser.ParseChapter(fileName, type), + Volumes = Parser.ParseVolume(fileName, type), }; } @@ -41,19 +41,19 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer } // This catches when original library type is Manga/Comic and when parsing with non - if (!Scanner.Parser.IsLooseLeafVolume(Scanner.Parser.ParseVolume(info.Series, type))) + if (!Parser.IsLooseLeafVolume(Parser.ParseVolume(info.Series, type))) { - var parsedVolumeFromTitle = Scanner.Parser.ParseVolume(info.Title, type); - var parsedVolumeFromSeries = Scanner.Parser.ParseVolume(info.Series, type); + var parsedVolumeFromTitle = Parser.ParseVolume(info.Title, type); + var parsedVolumeFromSeries = Parser.ParseVolume(info.Series, type); - var hasVolumeInTitle = !Scanner.Parser.IsLooseLeafVolume(parsedVolumeFromTitle); - var hasVolumeInSeries = !Scanner.Parser.IsLooseLeafVolume(parsedVolumeFromSeries); + var hasVolumeInTitle = !Parser.IsLooseLeafVolume(parsedVolumeFromTitle); + var hasVolumeInSeries = !Parser.IsLooseLeafVolume(parsedVolumeFromSeries); if (string.IsNullOrEmpty(info.ComicInfo?.Volume) && hasVolumeInTitle && (hasVolumeInSeries || string.IsNullOrEmpty(info.Series))) { // NOTE: I'm not sure the comment is true. I've never seen this triggered // This is likely a light novel for which we can set series from parsed title - info.Series = Scanner.Parser.ParseSeries(info.Title, type); + info.Series = Parser.ParseSeries(info.Title, type); info.Volumes = parsedVolumeFromTitle; } else @@ -61,7 +61,7 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, enableMetadata, comicInfo); info.Merge(info2); - if (hasVolumeInSeries && info2 != null && Scanner.Parser.IsLooseLeafVolume(Scanner.Parser.ParseVolume(info2.Series, type))) + if (hasVolumeInSeries && info2 != null && Parser.IsLooseLeafVolume(Parser.ParseVolume(info2.Series, type))) { // Override the Series name so it groups appropriately info.Series = info2.Series; @@ -80,6 +80,6 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer /// public override bool IsApplicable(string filePath, LibraryType type) { - return Scanner.Parser.IsEpub(filePath); + return Parser.IsEpub(filePath); } } diff --git a/Kavita.Services/Scanner/ComicVineParser.cs b/Kavita.Services/Scanner/ComicVineParser.cs index a10ad7363..e924a7105 100644 --- a/Kavita.Services/Scanner/ComicVineParser.cs +++ b/Kavita.Services/Scanner/ComicVineParser.cs @@ -27,22 +27,24 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); // Mylar often outputs cover.jpg, ignore it by default - if (string.IsNullOrEmpty(fileName) || Scanner.Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null; + if (string.IsNullOrEmpty(fileName) || Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null; var directoryName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name; var info = new ParserInfo() { Filename = Path.GetFileName(filePath), - Format = Scanner.Parser.ParseFormat(filePath), - Title = Scanner.Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Scanner.Parser.NormalizePath(filePath), + Format = Parser.ParseFormat(filePath), + Title = Parser.RemoveExtensionIfSupported(fileName)!, + FullFilePath = Parser.NormalizePath(filePath), Series = string.Empty, ComicInfo = comicInfo, - Chapters = Scanner.Parser.ParseChapter(fileName, type), - Volumes = Scanner.Parser.ParseVolume(fileName, type) + Chapters = Parser.ParseChapter(fileName, type), + Volumes = Parser.ParseVolume(fileName, type) }; + ParseExternalIdsFromNotesAndWeblinks(info); + // See if we can formulate the name from the ComicInfo if (!string.IsNullOrEmpty(info.ComicInfo?.Series) && !string.IsNullOrEmpty(info.ComicInfo?.Volume)) { @@ -57,30 +59,30 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser { foreach (var directory in directories) { - if (!Scanner.Parser.IsSeriesAndYear(directory)) continue; + if (!Parser.IsSeriesAndYear(directory)) continue; info.Series = directory; - info.Volumes = Scanner.Parser.ParseYear(directory); + info.Volumes = Parser.ParseYear(directory); break; } // When there was at least one directory and we failed to parse the series, this is the final fallback if (string.IsNullOrEmpty(info.Series)) { - info.Series = Scanner.Parser.CleanTitle(directories[0], true); + info.Series = Parser.CleanTitle(directories[0], true); } } else { - if (Scanner.Parser.IsSeriesAndYear(directoryName)) + if (Parser.IsSeriesAndYear(directoryName)) { info.Series = directoryName; - info.Volumes = Scanner.Parser.ParseYear(directoryName); + info.Volumes = Parser.ParseYear(directoryName); } } } // Check if this is a Special/Annual - info.IsSpecial = Scanner.Parser.IsSpecial(info.Filename, type) || Scanner.Parser.IsSpecial(info.ComicInfo?.Format, type); + info.IsSpecial = Parser.IsSpecial(info.Filename, type) || Parser.IsSpecial(info.ComicInfo?.Format, type); // Patch in other information from ComicInfo if (enableMetadata) @@ -90,7 +92,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser if (string.IsNullOrEmpty(info.Series)) { - info.Series = Scanner.Parser.CleanTitle(directoryName, true); + info.Series = Parser.CleanTitle(directoryName, true); } @@ -123,10 +125,10 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser if (!string.IsNullOrEmpty(info.ComicInfo.Number)) { info.Chapters = info.ComicInfo.Number; - if (info.IsSpecial && !Scanner.Parser.IsDefaultChapter(info.Chapters)) + if (info.IsSpecial && !Parser.IsDefaultChapter(info.Chapters)) { info.IsSpecial = false; - info.Volumes = $"{Scanner.Parser.SpecialVolumeNumber}"; + info.Volumes = $"{Parser.SpecialVolumeNumber}"; } } diff --git a/Kavita.Services/Scanner/DefaultParser.cs b/Kavita.Services/Scanner/DefaultParser.cs index 0d3a1c7bd..446f8a9ee 100644 --- a/Kavita.Services/Scanner/DefaultParser.cs +++ b/Kavita.Services/Scanner/DefaultParser.cs @@ -1,5 +1,6 @@ using System.Linq; using Kavita.API.Services; +using Kavita.Common.Helpers; using Kavita.Models.Entities.Enums; using Kavita.Models.Metadata; using Kavita.Models.Parser; @@ -40,17 +41,17 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau public void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret) { var fallbackFolders = directoryService.GetFoldersTillRoot(rootPath, filePath) - .Where(f => !Scanner.Parser.IsSpecial(f, type)) + .Where(f => !Parser.IsSpecial(f, type)) .ToList(); if (fallbackFolders.Count == 0) { var rootFolderName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name; - var series = Scanner.Parser.ParseSeries(rootFolderName, type); + var series = Parser.ParseSeries(rootFolderName, type); if (string.IsNullOrEmpty(series)) { - ret.Series = Scanner.Parser.CleanTitle(rootFolderName, type is LibraryType.Comic); + ret.Series = Parser.CleanTitle(rootFolderName, type is LibraryType.Comic); return; } @@ -65,18 +66,18 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau { var folder = fallbackFolders[i]; - var parsedVolume = Scanner.Parser.ParseVolume(folder, type); - var parsedChapter = Scanner.Parser.ParseChapter(folder, type); + var parsedVolume = Parser.ParseVolume(folder, type); + var parsedChapter = Parser.ParseChapter(folder, type); - var isLooseLeafVolume = Scanner.Parser.IsLooseLeafVolume(parsedVolume); - var isDefaultChapter = Scanner.Parser.IsDefaultChapter(parsedChapter); + var isLooseLeafVolume = Parser.IsLooseLeafVolume(parsedVolume); + var isDefaultChapter = Parser.IsDefaultChapter(parsedChapter); - if ((string.IsNullOrEmpty(ret.Volumes) || Scanner.Parser.IsLooseLeafVolume(ret.Volumes)) + if ((string.IsNullOrEmpty(ret.Volumes) || Parser.IsLooseLeafVolume(ret.Volumes)) && !string.IsNullOrEmpty(parsedVolume) && !isLooseLeafVolume) { ret.Volumes = parsedVolume; } - if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters.Equals(Scanner.Parser.DefaultChapter)) + if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters.Equals(Parser.DefaultChapter)) && !string.IsNullOrEmpty(parsedChapter) && !isDefaultChapter) { ret.Chapters = parsedChapter; @@ -85,11 +86,11 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau // Generally users group in series folders. Let's try to parse series from the top folder if (!folder.Equals(ret.Series) && i == fallbackFolders.Count - 1) { - var series = Scanner.Parser.ParseSeries(folder, type); + var series = Parser.ParseSeries(folder, type); if (string.IsNullOrEmpty(series)) { - ret.Series = Scanner.Parser.CleanTitle(folder, type is LibraryType.Comic); + ret.Series = Parser.CleanTitle(folder, type is LibraryType.Comic); break; } @@ -123,11 +124,11 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim(); } - if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Scanner.Parser.HasComicInfoSpecial(info.ComicInfo.Format)) + if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Parser.HasComicInfoSpecial(info.ComicInfo.Format)) { info.IsSpecial = true; - info.Chapters = Scanner.Parser.DefaultChapter; - info.Volumes = Scanner.Parser.SpecialVolume; + info.Chapters = Parser.DefaultChapter; + info.Volumes = Parser.SpecialVolume; } // Patch is SeriesSort from ComicInfo @@ -142,7 +143,37 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau protected static bool IsEmptyOrDefault(string volumes, string chapters) { - return (string.IsNullOrEmpty(chapters) || Scanner.Parser.IsDefaultChapter(chapters)) && - (string.IsNullOrEmpty(volumes) || Scanner.Parser.IsLooseLeafVolume(volumes)); + return (string.IsNullOrEmpty(chapters) || Parser.IsDefaultChapter(chapters)) && + (string.IsNullOrEmpty(volumes) || Parser.IsLooseLeafVolume(volumes)); + } + + /// + /// Attempts to fill in as much information as possible from Notes then Weblinks fields + /// for different metadata Ids + /// + /// + protected static void ParseExternalIdsFromNotesAndWeblinks(ParserInfo info) + { + var notes = info.ComicInfo?.Notes; + var weblinks = info.ComicInfo?.Web; + + info.AniListId = WeblinkParser.GetAniListId(weblinks); + info.MalId = WeblinkParser.GetMalId(weblinks); + + var comicvineId = Parser.ParseComicVineIdFromComicInfoNote(notes); + var parsedCvWeblink = WeblinkParser.GetComicVineId(weblinks); + info.ComicVineId = !string.IsNullOrEmpty(comicvineId) + ? comicvineId + : parsedCvWeblink.Item1; + if (parsedCvWeblink.Item2) + { + info.ComicVineSeriesId = parsedCvWeblink.Item1; + } + + var metronId = Parser.ParseMetronIdFromComicInfoNote(notes); + info.MetronId = !string.IsNullOrEmpty(metronId) + ? long.Parse(metronId) + : 0L; + } } diff --git a/Kavita.Services/Scanner/ImageParser.cs b/Kavita.Services/Scanner/ImageParser.cs index ff92749bf..db3977161 100644 --- a/Kavita.Services/Scanner/ImageParser.cs +++ b/Kavita.Services/Scanner/ImageParser.cs @@ -18,12 +18,12 @@ public class ImageParser(IDirectoryService directoryService) : DefaultParser(dir var ret = new ParserInfo { Series = directoryName, - Volumes = Scanner.Parser.LooseLeafVolume, - Chapters = Scanner.Parser.DefaultChapter, + Volumes = Parser.LooseLeafVolume, + Chapters = Parser.DefaultChapter, ComicInfo = comicInfo, Format = MangaFormat.Image, Filename = Path.GetFileName(filePath), - FullFilePath = Scanner.Parser.NormalizePath(filePath), + FullFilePath = Parser.NormalizePath(filePath), Title = fileName, }; ParseFromFallbackFolders(filePath, libraryRoot, LibraryType.Image, ref ret); @@ -31,13 +31,13 @@ public class ImageParser(IDirectoryService directoryService) : DefaultParser(dir if (IsEmptyOrDefault(ret.Volumes, ret.Chapters)) { ret.IsSpecial = true; - ret.Volumes = Scanner.Parser.SpecialVolume; + ret.Volumes = Parser.SpecialVolume; } // Override the series name, as fallback folders needs it to try and parse folder name if (string.IsNullOrEmpty(ret.Series) || ret.Series.Equals(directoryName)) { - ret.Series = Scanner.Parser.CleanTitle(directoryName); + ret.Series = Parser.CleanTitle(directoryName); } @@ -52,6 +52,6 @@ public class ImageParser(IDirectoryService directoryService) : DefaultParser(dir /// public override bool IsApplicable(string filePath, LibraryType type) { - return type == LibraryType.Image && Scanner.Parser.IsImage(filePath); + return type == LibraryType.Image && Parser.IsImage(filePath); } } diff --git a/Kavita.Services/Scanner/ParseScannedFiles.cs b/Kavita.Services/Scanner/ParseScannedFiles.cs index 7a70914fc..4c6d057ce 100644 --- a/Kavita.Services/Scanner/ParseScannedFiles.cs +++ b/Kavita.Services/Scanner/ParseScannedFiles.cs @@ -81,7 +81,7 @@ public class ParseScannedFiles Library library, bool forceCheck, GlobMatcher matcher, List result, string fileExtensions) { var allDirectories = _directoryService.GetAllDirectories(folderPath, matcher) - .Select(Scanner.Parser.NormalizePath) + .Select(Parser.NormalizePath) .OrderByDescending(d => d.Length) .ToList(); @@ -247,10 +247,10 @@ public class ParseScannedFiles private async Task> ScanSingleDirectory(string folderPath, IDictionary> seriesPaths, Library library, bool forceCheck, List result, string fileExtensions, GlobMatcher matcher) { - var normalizedPath = Scanner.Parser.NormalizePath(folderPath); + var normalizedPath = Parser.NormalizePath(folderPath); var libraryRoot = library.Folders.FirstOrDefault(f => - normalizedPath.Contains(Scanner.Parser.NormalizePath(f.Path)))?.Path ?? + normalizedPath.Contains(Parser.NormalizePath(f.Path)))?.Path ?? folderPath; await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, @@ -286,7 +286,7 @@ public class ParseScannedFiles return new ScanResult() { Files = files, - Folder = Scanner.Parser.NormalizePath(folderPath), + Folder = Parser.NormalizePath(folderPath), LibraryRoot = libraryRoot, HasChanged = hasChanged }; @@ -637,7 +637,7 @@ public class ParseScannedFiles case 1: return seriesForLocalized[0]; case <= 2: - return seriesForLocalized.FirstOrDefault(s => !s.Equals(Scanner.Parser.Normalize(localizedSeries))); + return seriesForLocalized.FirstOrDefault(s => !s.Equals(Parser.Normalize(localizedSeries))); default: _logger.LogError( "[ScannerService] Multiple series detected across scan results that contain localized series. " + @@ -692,7 +692,7 @@ public class ParseScannedFiles /// private async Task ParseFiles(ScanResult result, IDictionary> seriesPaths, Library library) { - var normalizedFolder = Scanner.Parser.NormalizePath(result.Folder); + var normalizedFolder = Parser.NormalizePath(result.Folder); // If folder hasn't changed, generate fake ParserInfos if (!result.HasChanged) @@ -778,7 +778,7 @@ public class ParseScannedFiles if (specialTreatment) { chapters = infos - .OrderByNatural(info => Scanner.Parser.RemoveExtensionIfSupported(info.Filename)!) + .OrderByNatural(info => Parser.RemoveExtensionIfSupported(info.Filename)!) .ToList(); foreach (var chapter in chapters) @@ -800,7 +800,7 @@ public class ParseScannedFiles { // Use MinNumber in case there is a range, as otherwise sort order will cause it to be processed last var chapterNum = - $"{Scanner.Parser.MinNumberFromRange(chapter.Chapters).ToString(CultureInfo.InvariantCulture)}"; + $"{Parser.MinNumberFromRange(chapter.Chapters).ToString(CultureInfo.InvariantCulture)}"; if (float.TryParse(chapterNum, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedChapter)) { // Parsed successfully, use the numeric value diff --git a/Kavita.Services/Scanner/Parser.cs b/Kavita.Services/Scanner/Parser.cs index 5444aac28..700f0ab64 100644 --- a/Kavita.Services/Scanner/Parser.cs +++ b/Kavita.Services/Scanner/Parser.cs @@ -703,6 +703,22 @@ public static partial class Parser MatchOptions, RegexTimeout ); + /// + /// ComicTagger pattern for ComicInfo.Notes field + /// + /// Scraped metadata from ComicVine [CVDB734524] + private static readonly Regex ComicVineScrapperRegex = new Regex( + @"ComicVine\s\[CVDB(?\d+)\]", + MatchOptions, RegexTimeout); + + /// + /// Metron pattern for ComicInfo.Notes field + /// + /// Tagged with MetronTagger-4.4.0 using info from Metron on 2025-12-24 12:32:18. [issue_id:156409] + private static readonly Regex MetronScrapperRegex = new Regex( + @"MetronTagger-.*\[issue_id:(?\d+)\]", + MatchOptions, RegexTimeout); + public static MangaFormat ParseFormat(string filePath) @@ -1175,7 +1191,7 @@ public static partial class Parser public static string? ExtractFilename(string fileUrl) { - var matches = Parser.CssImageUrlRegex.Matches(fileUrl); + var matches = CssImageUrlRegex.Matches(fileUrl); foreach (Match match in matches) { if (!match.Success) continue; @@ -1309,7 +1325,25 @@ public static partial class Parser public static bool IsLikelyValidAsin(string? asin) { if (string.IsNullOrEmpty(asin)) return false; - return AsinRegex.Match(asin).Success; + return AsinRegex.IsMatch(asin); + } + + public static string? ParseComicVineIdFromComicInfoNote(string? note) + { + if (string.IsNullOrEmpty(note)) return null; + var match = ComicVineScrapperRegex.Match(note); + if (!match.Success) return null; + + return match.Groups["Id"].Value; + } + + public static string? ParseMetronIdFromComicInfoNote(string? note) + { + if (string.IsNullOrEmpty(note)) return null; + var match = MetronScrapperRegex.Match(note); + if (!match.Success) return null; + + return match.Groups["Id"].Value; } diff --git a/Kavita.Services/Scanner/PdfParser.cs b/Kavita.Services/Scanner/PdfParser.cs index 4bde04bd5..5bb3604fd 100644 --- a/Kavita.Services/Scanner/PdfParser.cs +++ b/Kavita.Services/Scanner/PdfParser.cs @@ -14,21 +14,21 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc var ret = new ParserInfo { Filename = Path.GetFileName(filePath), - Format = Scanner.Parser.ParseFormat(filePath), - Title = Scanner.Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Scanner.Parser.NormalizePath(filePath), + Format = Parser.ParseFormat(filePath), + Title = Parser.RemoveExtensionIfSupported(fileName)!, + FullFilePath = Parser.NormalizePath(filePath), Series = string.Empty, ComicInfo = comicInfo, - Chapters = Scanner.Parser.ParseChapter(fileName, type) + Chapters = Parser.ParseChapter(fileName, type) }; if (type == LibraryType.Book) { - ret.Chapters = Scanner.Parser.DefaultChapter; + ret.Chapters = Parser.DefaultChapter; } - ret.Series = Scanner.Parser.ParseSeries(fileName, type); - ret.Volumes = Scanner.Parser.ParseVolume(fileName, type); + ret.Series = Parser.ParseSeries(fileName, type); + ret.Volumes = Parser.ParseVolume(fileName, type); if (ret.Series == string.Empty) { @@ -36,17 +36,17 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc ParseFromFallbackFolders(filePath, rootPath, type, ref ret); } - var edition = Scanner.Parser.ParseEdition(fileName); + var edition = Parser.ParseEdition(fileName); if (!string.IsNullOrEmpty(edition)) { - ret.Series = Scanner.Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); + ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); ret.Edition = edition; } - var isSpecial = Scanner.Parser.IsSpecial(fileName, type); + var isSpecial = Parser.IsSpecial(fileName, type); // We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that // could cause a problem as Omake is a special term, but there is valid volume/chapter information. - if (Scanner.Parser.IsDefaultChapter(ret.Chapters) && Scanner.Parser.IsLooseLeafVolume(ret.Volumes) && isSpecial) + if (Parser.IsDefaultChapter(ret.Chapters) && Parser.IsLooseLeafVolume(ret.Volumes) && isSpecial) { ret.IsSpecial = true; // NOTE: This can cause some complications, we should try to be a bit less aggressive to fallback to folder @@ -54,12 +54,12 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc } // If we are a special with marker, we need to ensure we use the correct series name. we can do this by falling back to Folder name - if (Scanner.Parser.HasSpecialMarker(fileName)) + if (Parser.HasSpecialMarker(fileName)) { ret.IsSpecial = true; - ret.SpecialIndex = Scanner.Parser.ParseSpecialIndex(fileName); - ret.Chapters = Scanner.Parser.DefaultChapter; - ret.Volumes = Scanner.Parser.SpecialVolume; + ret.SpecialIndex = Parser.ParseSpecialIndex(fileName); + ret.Chapters = Parser.DefaultChapter; + ret.Volumes = Parser.SpecialVolume; var tempRootPath = rootPath; if (rootPath.EndsWith("Specials") || rootPath.EndsWith("Specials/")) @@ -82,11 +82,11 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc } - if (Scanner.Parser.IsDefaultChapter(ret.Chapters) && Scanner.Parser.IsLooseLeafVolume(ret.Volumes) && type == LibraryType.Book) + if (Parser.IsDefaultChapter(ret.Chapters) && Parser.IsLooseLeafVolume(ret.Volumes) && type == LibraryType.Book) { ret.IsSpecial = true; - ret.Chapters = Scanner.Parser.DefaultChapter; - ret.Volumes = Scanner.Parser.SpecialVolume; + ret.Chapters = Parser.DefaultChapter; + ret.Volumes = Parser.SpecialVolume; ParseFromFallbackFolders(filePath, rootPath, type, ref ret); } @@ -105,11 +105,11 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc if (string.IsNullOrEmpty(ret.Series)) { - ret.Series = Scanner.Parser.CleanTitle(fileName, type is LibraryType.Comic); + ret.Series = Parser.CleanTitle(fileName, type is LibraryType.Comic); } // Pdfs may have .pdf in the series name, remove that - if (Scanner.Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) + if (Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) { ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length); } @@ -117,7 +117,7 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc // v0.8.x: Introducing a change where Specials will go in a separate Volume with a reserved number if (ret.IsSpecial) { - ret.Volumes = $"{Scanner.Parser.SpecialVolumeNumber}"; + ret.Volumes = $"{Parser.SpecialVolumeNumber}"; } return string.IsNullOrEmpty(ret.Series) ? null : ret; @@ -131,6 +131,6 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc /// public override bool IsApplicable(string filePath, LibraryType type) { - return Scanner.Parser.IsPdf(filePath); + return Parser.IsPdf(filePath); } } diff --git a/Kavita.Services/Scanner/ProcessSeries.cs b/Kavita.Services/Scanner/ProcessSeries.cs index 4bb6790da..b77214bd7 100644 --- a/Kavita.Services/Scanner/ProcessSeries.cs +++ b/Kavita.Services/Scanner/ProcessSeries.cs @@ -11,10 +11,12 @@ using Kavita.API.Services; using Kavita.API.Services.Helpers; using Kavita.API.Services.Plus; using Kavita.API.Services.Reading; +using Kavita.API.Services.ReadingLists; using Kavita.API.Services.Scanner; using Kavita.API.Services.SignalR; using Kavita.Common; using Kavita.Common.Extensions; +using Kavita.Common.Helpers; using Kavita.Models.Builders; using Kavita.Models.DTOs.KavitaPlus.Metadata; using Kavita.Models.DTOs.SignalR; @@ -151,6 +153,13 @@ public class ProcessSeries( series.NormalizedLocalizedName = series.LocalizedName.ToNormalized(); } + // Check if there is a comicvineSeriesId on file + var comicVineSeriesIds = parsedInfos.Select(p => p.ComicVineSeriesId).Where(s => !string.IsNullOrEmpty(s)).Distinct().ToList(); + if (comicVineSeriesIds.Count == 1) + { + series.ComicVineId = comicVineSeriesIds[0]; + } + await UpdateSeriesMetadata(databasePeople, settings, series, library); // Update series FolderPath here @@ -348,6 +357,9 @@ public class ProcessSeries( if (!string.IsNullOrEmpty(firstChapter?.WebLinks) && library.InheritWebLinksFromFirstChapter) { series.Metadata.WebLinks = firstChapter.WebLinks; + series.AniListId = WeblinkParser.GetAniListId(series.Metadata.WebLinks) ?? 0; + series.MalId = WeblinkParser.GetMalId(series.Metadata.WebLinks) ?? 0; + series.ComicVineId = WeblinkParser.GetComicVineId(series.Metadata.WebLinks).Item1; } if (!string.IsNullOrEmpty(firstChapter?.SeriesGroup) && library.ManageCollections) @@ -716,6 +728,13 @@ public class ProcessSeries( logger.LogError(ex, "There was some issue when updating chapter's metadata"); } + // Try to patch in any External Metadata Ids we've seen during parsing + chapter.AniListId = info.AniListId ?? 0; + chapter.MalId = info.MalId ?? 0; + chapter.MangaBakaId = info.MangaBakaId ?? 0; + chapter.MetronId = info.MetronId ?? 0; + chapter.ComicVineId = info.ComicVineId; + chapter.HardcoverId = info.HardcoverId ?? 0; } RemoveChapters(args.Volume, args.ParsedInfos); @@ -873,10 +892,6 @@ public class ProcessSeries( if (!string.IsNullOrEmpty(comicInfo.Web)) { chapter.WebLinks = string.Join(",", comicInfo.Web.SplitBy(',')); - - // TODO: For each weblink, try to parse out some MetadataIds and store in the Chapter directly for matching (CBL) - // var aniListId = ScrobblingHelper.GetAniListId(chapter.WebLinks); - // var malId = ScrobblingHelper.GetMalId(chapter.WebLinks); } if (!chapter.ISBNLocked && !string.IsNullOrEmpty(comicInfo.Isbn)) diff --git a/Kavita.Services/Scanner/ScannerService.cs b/Kavita.Services/Scanner/ScannerService.cs index 224ec1406..e1ad89f87 100644 --- a/Kavita.Services/Scanner/ScannerService.cs +++ b/Kavita.Services/Scanner/ScannerService.cs @@ -730,7 +730,8 @@ public class ScannerService( /// /// /// The total amount of processed files - private async Task DbMetadataTask(Channel channel, MetadataSettingsDto settings, IList> toProcess, int libraryId, string libraryName, bool forceUpdate) + private async Task DbMetadataTask(Channel channel, MetadataSettingsDto settings, + IList> toProcess, int libraryId, string libraryName, bool forceUpdate) { var totalFiles = 0; var seriesLeftToProcess = toProcess.Count; @@ -748,7 +749,8 @@ public class ScannerService( var processSeries = scope.ServiceProvider.GetRequiredService(); // Library needs to be returned from the used UnitOfWork - var library = (await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns))!; + var library = (await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, + LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns))!; var seriesId = await processSeries.ProcessSeriesAsync(settings, pSeries, new ProcessSeriesArgs { diff --git a/Kavita.Services/SeriesService.cs b/Kavita.Services/SeriesService.cs index 4c01d05e2..5a95c14ec 100644 --- a/Kavita.Services/SeriesService.cs +++ b/Kavita.Services/SeriesService.cs @@ -7,6 +7,7 @@ using Kavita.API.Database; using Kavita.API.Repositories; using Kavita.API.Services; using Kavita.API.Services.Reading; +using Kavita.API.Services.ReadingLists; using Kavita.API.Services.SignalR; using Kavita.Common; using Kavita.Common.Extensions; diff --git a/UI/Web/src/app/_models/actionables/action.ts b/UI/Web/src/app/_models/actionables/action.ts index fc04110d7..5a0875807 100644 --- a/UI/Web/src/app/_models/actionables/action.ts +++ b/UI/Web/src/app/_models/actionables/action.ts @@ -112,4 +112,8 @@ export enum Action { Export = 32, Like = 33, UnLike = 34, + /** Export as CBLv1 */ + ExportAsV1 = 35, + /** Export as CBLv2 */ + ExportAsV2 = 36, } diff --git a/UI/Web/src/app/_models/chapter.ts b/UI/Web/src/app/_models/chapter.ts index 7c37bd5f4..a39e0c330 100644 --- a/UI/Web/src/app/_models/chapter.ts +++ b/UI/Web/src/app/_models/chapter.ts @@ -8,11 +8,12 @@ import {IHasCast} from "./common/i-has-cast"; import {IHasReadingTime} from "./common/i-has-reading-time"; import {IHasCover} from "./common/i-has-cover"; import {IHasProgress} from "./common/i-has-progress"; +import {IHasMetadataIds} from "./common/i-has-metadata-ids"; export const LooseLeafOrDefaultNumber = -100000; export const SpecialVolumeNumber = 100000; -export interface Chapter extends IHasCast, IHasReadingTime, IHasCover, IHasProgress { +export interface Chapter extends IHasCast, IHasReadingTime, IHasCover, IHasProgress, IHasMetadataIds { id: number; range: string; /** @@ -70,6 +71,13 @@ export interface Chapter extends IHasCast, IHasReadingTime, IHasCover, IHasProgr totalCount: number; totalReads: number; + aniListId: number; + malId: number; + hardcoverId: number; + metronId: number; + comicVineId: string | null; + mangaBakaId: number; + genres: Array; tags: Array; writers: Array; diff --git a/UI/Web/src/app/_models/common/i-has-metadata-ids.ts b/UI/Web/src/app/_models/common/i-has-metadata-ids.ts new file mode 100644 index 000000000..83026c76c --- /dev/null +++ b/UI/Web/src/app/_models/common/i-has-metadata-ids.ts @@ -0,0 +1,8 @@ +export interface IHasMetadataIds { + aniListId: number; + malId: number; + hardcoverId: number; + metronId: number; + comicVineId: string | null; + mangaBakaId: number; +} diff --git a/UI/Web/src/app/_models/metadata/series-metadata.ts b/UI/Web/src/app/_models/metadata/series-metadata.ts index fc691ee93..8a4b9f9b5 100644 --- a/UI/Web/src/app/_models/metadata/series-metadata.ts +++ b/UI/Web/src/app/_models/metadata/series-metadata.ts @@ -1,56 +1,56 @@ -import { Genre } from "./genre"; -import { AgeRating } from "./age-rating"; -import { PublicationStatus } from "./publication-status"; -import { Person } from "./person"; -import { Tag } from "../tag"; +import {Genre} from "./genre"; +import {AgeRating} from "./age-rating"; +import {PublicationStatus} from "./publication-status"; +import {Person} from "./person"; +import {Tag} from "../tag"; import {IHasCast} from "../common/i-has-cast"; export interface SeriesMetadata extends IHasCast { - seriesId: number; - summary: string; + seriesId: number; + summary: string; - totalCount: number; - maxCount: number; + totalCount: number; + maxCount: number; - genres: Array; - tags: Array; - writers: Array; - coverArtists: Array; - publishers: Array; - characters: Array; - pencillers: Array; - inkers: Array; - imprints: Array; - colorists: Array; - letterers: Array; - editors: Array; - translators: Array; - teams: Array; - locations: Array; - ageRating: AgeRating; - releaseYear: number; - language: string; - publicationStatus: PublicationStatus; - webLinks: string; + genres: Array; + tags: Array; + writers: Array; + coverArtists: Array; + publishers: Array; + characters: Array; + pencillers: Array; + inkers: Array; + imprints: Array; + colorists: Array; + letterers: Array; + editors: Array; + translators: Array; + teams: Array; + locations: Array; + ageRating: AgeRating; + releaseYear: number; + language: string; + publicationStatus: PublicationStatus; + webLinks: string; - summaryLocked: boolean; - genresLocked: boolean; - tagsLocked: boolean; - writerLocked: boolean; - coverArtistLocked: boolean; - publisherLocked: boolean; - characterLocked: boolean; - pencillerLocked: boolean; - inkerLocked: boolean; - imprintLocked: boolean; - coloristLocked: boolean; - lettererLocked: boolean; - editorLocked: boolean; - translatorLocked: boolean; - teamLocked: boolean; - locationLocked: boolean; - ageRatingLocked: boolean; - releaseYearLocked: boolean; - languageLocked: boolean; - publicationStatusLocked: boolean; + summaryLocked: boolean; + genresLocked: boolean; + tagsLocked: boolean; + writerLocked: boolean; + coverArtistLocked: boolean; + publisherLocked: boolean; + characterLocked: boolean; + pencillerLocked: boolean; + inkerLocked: boolean; + imprintLocked: boolean; + coloristLocked: boolean; + lettererLocked: boolean; + editorLocked: boolean; + translatorLocked: boolean; + teamLocked: boolean; + locationLocked: boolean; + ageRatingLocked: boolean; + releaseYearLocked: boolean; + languageLocked: boolean; + publicationStatusLocked: boolean; } diff --git a/UI/Web/src/app/_models/series.ts b/UI/Web/src/app/_models/series.ts index f6cab1228..392d72f11 100644 --- a/UI/Web/src/app/_models/series.ts +++ b/UI/Web/src/app/_models/series.ts @@ -3,8 +3,9 @@ import {Volume} from './volume'; import {IHasCover} from "./common/i-has-cover"; import {IHasReadingTime} from "./common/i-has-reading-time"; import {IHasProgress} from "./common/i-has-progress"; +import {IHasMetadataIds} from "./common/i-has-metadata-ids"; -export interface Series extends IHasCover, IHasReadingTime, IHasProgress { +export interface Series extends IHasCover, IHasReadingTime, IHasProgress, IHasMetadataIds { id: number; name: string; /** diff --git a/UI/Web/src/app/_models/tabs.ts b/UI/Web/src/app/_models/tabs.ts new file mode 100644 index 000000000..bc0a5da96 --- /dev/null +++ b/UI/Web/src/app/_models/tabs.ts @@ -0,0 +1,48 @@ +/** Represents a Tab in the system, use TabTitlePipe to transform to string **/ +export enum Tabs { + Details = 'details-tab', + Reviews = 'reviews-tab', + Storyline = 'storyline-tab', + Books = 'books-tab', + Volumes = 'volumes-tab', + Specials = 'specials-tab', + Related = 'related-tab', + General = 'general-tab', + Folder = 'folder-tab', + CoverImage = 'cover-tab', + Advanced = 'advanced-tab', + Tasks = 'tasks-tab', + Recommendations = 'recommendations-tab', + Info = 'info-tab', + Tags = 'tags-tab', + WebLinks = 'weblink-tab', + People = 'people-tab', + Metadata = 'metadata-tab', + Series = 'series-tab', + Account = 'account-tab', + Preferences = 'preferences-tab', + Theme = 'theme-tab', + Devices = 'devices-tab', + Stats = 'stats-tab', + Scrobbling = 'scrobbling-tab', + SmartFilters = 'smart-filters-tab', + Annotations = 'annotations-tab', + Overview = 'overview-tab', + Management = 'management-tab', + Activity = 'activity-tab', + ExternalMetadataIds = 'external-ids-tab', + Aliases = 'aliases-tab', + Chapters = 'chapters-tab', + + Dashboard = 'dashboard-tab', + SideNav = 'sidenav-tab', + ExternalSources = 'external-sources-tab', + + ImageReader = "image-reader-tab", + BookReader = "book-reader-tab", + PdfReader = "pdf-reader-tab", + + BookmarkImageTab = "bookmark-image-tab", + BookmarkTextTab = "bookmark-text-tab", + +} diff --git a/UI/Web/src/app/_models/update-volume.ts b/UI/Web/src/app/_models/update-volume.ts new file mode 100644 index 000000000..4b299e7a1 --- /dev/null +++ b/UI/Web/src/app/_models/update-volume.ts @@ -0,0 +1,5 @@ +import {IHasMetadataIds} from "./common/i-has-metadata-ids"; + +export interface UpdateVolume extends IHasMetadataIds { + +} diff --git a/UI/Web/src/app/_models/volume.ts b/UI/Web/src/app/_models/volume.ts index b7d1e4664..d3cc65b1b 100644 --- a/UI/Web/src/app/_models/volume.ts +++ b/UI/Web/src/app/_models/volume.ts @@ -3,8 +3,9 @@ import {HourEstimateRange} from './series-detail/hour-estimate-range'; import {IHasCover} from "./common/i-has-cover"; import {IHasReadingTime} from "./common/i-has-reading-time"; import {IHasProgress} from "./common/i-has-progress"; +import {IHasMetadataIds} from "./common/i-has-metadata-ids"; -export interface Volume extends IHasCover, IHasReadingTime, IHasProgress { +export interface Volume extends IHasCover, IHasReadingTime, IHasProgress, IHasMetadataIds { id: number; minNumber: number; maxNumber: number; @@ -27,4 +28,11 @@ export interface Volume extends IHasCover, IHasReadingTime, IHasProgress { coverImageLocked: boolean; primaryColor: string; secondaryColor: string; + + aniListId: number; + malId: number; + hardcoverId: number; + metronId: number; + comicVineId: string | null; + mangaBakaId: number; } diff --git a/UI/Web/src/app/_pipes/tab-title.pipe.ts b/UI/Web/src/app/_pipes/tab-title.pipe.ts new file mode 100644 index 000000000..c76588036 --- /dev/null +++ b/UI/Web/src/app/_pipes/tab-title.pipe.ts @@ -0,0 +1,17 @@ +import {inject, Pipe, PipeTransform} from '@angular/core'; +import {Tabs} from "../_models/tabs"; +import {TranslocoService} from "@jsverse/transloco"; + +@Pipe({ + name: 'tabTitle', + pure: true, + standalone: true +}) +export class TabTitlePipe implements PipeTransform { + private readonly translocoService = inject(TranslocoService); + + transform(value: Tabs): string { + return this.translocoService.translate('tabs.' + value); + } + +} diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 73be29286..d1eb6fc05 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -478,7 +478,7 @@ export class ActionFactoryService { { action: Action.RemoveFromWantToReadList, title: 'remove-from-want-to-read', - description: 'remove-to-want-to-read-tooltip', + description: 'remove-from-want-to-read-tooltip', callback: this.dummyCallback, shouldRender: this.dummyShouldRender, @@ -980,6 +980,38 @@ export class ActionFactoryService { requiredRoles: [], children: [], }, + { + action: Action.Submenu, + title: 'export', + description: 'export-tooltip', + + callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, + + requiredRoles: [], + children: [ + { + action: Action.ExportAsV1, + title: 'export-v1', + description: 'export-v1-tooltip', + + callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, + requiredRoles: [], + children: [], + }, + { + action: Action.ExportAsV2, + title: 'export-v2', + description: 'export-v2-tooltip', + + callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, + requiredRoles: [], + children: [], + } + ], + } ]; this.personActions = [ diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 30ee36594..fc2ea1298 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -672,6 +672,16 @@ export class ActionService { tap(() => this.toastr.success(translate('toasts.reading-list-unpromoted'))), map(() => this.fromAction(action, {...readingList, promoted: false}, 'update')) ); + + case Action.ExportAsV1: + return this.downloadService.exportReadingList(readingList.id, readingList.title).pipe( + map(() => this.fromAction(action, readingList, 'none')) + ); + case Action.ExportAsV2: + return this.downloadService.exportReadingList(readingList.id, readingList.title, true).pipe( + map(() => this.fromAction(action, readingList, 'none')) + ); + default: return of(this.fromAction(action, readingList, 'none')); } diff --git a/UI/Web/src/app/_services/card-config-factory.service.ts b/UI/Web/src/app/_services/card-config-factory.service.ts index fa748e260..3f5f000c6 100644 --- a/UI/Web/src/app/_services/card-config-factory.service.ts +++ b/UI/Web/src/app/_services/card-config-factory.service.ts @@ -262,8 +262,8 @@ export class CardConfigFactory { if ([LibraryType.LightNovel || LibraryType.Book].includes(params.libraryType)) { return v.name; } - if (v.hasOwnProperty('chapters') && v.chapters.length > 0 && v.chapters[0].titleName) { - v.chapters[0].titleName + if (v.hasOwnProperty('chapters') && v.chapters.length === 1 && v.chapters[0].titleName) { + return v.chapters[0].titleName; } return v.name; diff --git a/UI/Web/src/app/_services/reading-list.service.ts b/UI/Web/src/app/_services/reading-list.service.ts index f565b03e5..728d4ae9c 100644 --- a/UI/Web/src/app/_services/reading-list.service.ts +++ b/UI/Web/src/app/_services/reading-list.service.ts @@ -135,5 +135,4 @@ export class ReadingListService { return this.httpClient.post(this.baseUrl + 'readinglist/delete-multiple', {readingListIds: listIds}, TextResonse); } - } diff --git a/UI/Web/src/app/_services/volume.service.ts b/UI/Web/src/app/_services/volume.service.ts index a74e3bf33..1a6ae44e5 100644 --- a/UI/Web/src/app/_services/volume.service.ts +++ b/UI/Web/src/app/_services/volume.service.ts @@ -1,8 +1,9 @@ -import { Injectable, inject } from '@angular/core'; +import {inject, Injectable} from '@angular/core'; import {environment} from "../../environments/environment"; -import { HttpClient } from "@angular/common/http"; +import {HttpClient} from "@angular/common/http"; import {Volume} from "../_models/volume"; import {TextResonse} from "../_types/text-response"; +import {UpdateVolume} from "../_models/update-volume"; @Injectable({ providedIn: 'root' @@ -25,7 +26,7 @@ export class VolumeService { return this.httpClient.post(this.baseUrl + "volume/multiple", volumeIds) } - updateVolume(volume: any) { + updateVolume(volume: UpdateVolume) { return this.httpClient.post(this.baseUrl + 'volume/update', volume, TextResonse); } diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html index 4ec1dd735..7e336c2be 100644 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.html +++ b/UI/Web/src/app/_single-module/details-tab/details-tab.component.html @@ -24,6 +24,16 @@ } + @let metadataEntity = entity(); + @if(metadataEntity) { +
+

{{t('external-metadata-title')}}

+
+ +
+
+ } + @if (showGenres()) {

{{t('genres-title')}}

diff --git a/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts b/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts index 3fe82f5e4..2300cc0c2 100644 --- a/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts +++ b/UI/Web/src/app/_single-module/details-tab/details-tab.component.ts @@ -16,6 +16,12 @@ import {MangaFormat} from "../../_models/manga-format"; import {SafeUrlPipe} from "../../_pipes/safe-url.pipe"; import {AccountService} from "../../_services/account.service"; import {MangaFile} from "../../_models/manga-file"; +import {Series} from "../../_models/series"; +import {Volume} from "../../_models/volume"; +import {Chapter} from "../../_models/chapter"; +import { + ExternalMetadataDetailComponent +} from "../../shared/_components/external-metadata-detail/external-metadata-detail.component"; @Component({ selector: 'app-details-tab', @@ -26,6 +32,8 @@ import {MangaFile} from "../../_models/manga-file"; ImageComponent, BadgeExpanderComponent, SafeUrlPipe, + ExternalMetadataDetailComponent, + ], templateUrl: './details-tab.component.html', styleUrl: './details-tab.component.scss', @@ -42,6 +50,7 @@ export class DetailsTabComponent { protected readonly MangaFormat = MangaFormat; metadata = input.required(); + entity = input(); genres = input([]); tags = input([]); webLinks = input([]); diff --git a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html index c6f437239..99848caa9 100644 --- a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html +++ b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.html @@ -14,8 +14,8 @@ @if (accountService.hasAdminRole()) { -
  • - {{t(TabID.General)}} +
  • + {{Tabs.General | tabTitle}}
    @@ -163,8 +163,8 @@ @if (accountService.hasAdminRole()) { -
  • - {{t(TabID.Tags)}} +
  • + {{Tabs.Tags | tabTitle}}
    @@ -334,8 +334,8 @@ @if (accountService.hasAdminRole()) { -
  • - {{t(TabID.People)}} +
  • + {{Tabs.People | tabTitle}}
    @@ -521,12 +521,21 @@
  • } + + @if (accountService.hasAdminRole()) { +
  • + {{Tabs.ExternalMetadataIds | tabTitle}} + + + +
  • + } @if (accountService.hasAdminRole()) { -
  • - {{t(TabID.CoverImage)}} +
  • + {{Tabs.CoverImage | tabTitle}}
  • - {{t(TabID.Info)}} +
  • + {{Tabs.Info | tabTitle}}
    @@ -643,8 +652,8 @@ -
  • - {{t(TabID.Tasks)}} +
  • + {{Tabs.Tasks | tabTitle}} @for(task of tasks; track task.action) { @if (accountService.canCurrentUserInvokeAction(task.action)) { diff --git a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts index c25617c60..af33e4c34 100644 --- a/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts +++ b/UI/Web/src/app/_single-module/edit-chapter-modal/edit-chapter-modal.component.ts @@ -51,16 +51,12 @@ import {ActionItem} from "../../_models/actionables/action-item"; import {Action} from "../../_models/actionables/action"; import {ActionFactoryService} from "../../_services/action-factory.service"; import {modalDeleted, modalSaved} from "../../_models/modal/modal-result"; +import {Tabs} from "../../_models/tabs"; +import {TabTitlePipe} from "../../_pipes/tab-title.pipe"; +import { + EditExternalMetadataFormComponent +} from "../../shared/_components/edit-external-metadata-form/edit-external-metadata-form.component"; -enum TabID { - General = 'general-tab', - CoverImage = 'cover-image-tab', - Info = 'info-tab', - People = 'people-tab', - Tasks = 'tasks-tab', - Tags = 'tags-tab', - Weblinks = 'weblinks-tab', // TODO: Weblinks are not implemented -} const blackList = [Action.Edit, Action.IncognitoRead, Action.AddToReadingList]; @@ -90,6 +86,8 @@ const blackList = [Action.Edit, Action.IncognitoRead, Action.AddToReadingList]; ImageComponent, SafeHtmlPipe, ReadTimePipe, + TabTitlePipe, + EditExternalMetadataFormComponent, ], templateUrl: './edit-chapter-modal.component.html', styleUrl: './edit-chapter-modal.component.scss', @@ -116,7 +114,7 @@ export class EditChapterModalComponent implements OnInit { @Input({required: true}) libraryId!: number; @Input({required: true}) seriesId!: number; - activeId = TabID.General; + activeId = Tabs.General; editForm: FormGroup = new FormGroup({}); selectedCover: string = ''; coverImageReset = false; @@ -147,7 +145,7 @@ export class EditChapterModalComponent implements OnInit { constructor() { effect(() => { if (!this.accountService.hasAdminRole()) { - this.activeId = TabID.Info; + this.activeId = Tabs.Info; this.cdRef.markForCheck(); } }); @@ -233,7 +231,7 @@ export class EditChapterModalComponent implements OnInit { } save() { - const model = this.editForm.value; + const model = this.editForm.getRawValue(); const selectedIndex = this.editForm.get('coverImageIndex')?.value || 0; // Patch in data from the model that is not typeahead (as those are updated during setting) @@ -508,7 +506,7 @@ export class EditChapterModalComponent implements OnInit { return this.peopleSettings[role]; } - protected readonly TabID = TabID; + protected readonly Tabs = Tabs; protected readonly Action = Action; protected readonly PersonRole = PersonRole; protected readonly MangaFormat = MangaFormat; diff --git a/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.html b/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.html index 1188040b2..9fc8e9436 100644 --- a/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.html +++ b/UI/Web/src/app/_single-module/edit-volume-modal/edit-volume-modal.component.html @@ -8,9 +8,20 @@