From c62b20f54bec708986d856ea829a44995d61ee88 Mon Sep 17 00:00:00 2001 From: Fesaa <77553571+Fesaa@users.noreply.github.com> Date: Sat, 7 Mar 2026 19:04:08 +0100 Subject: [PATCH] BE Tech Debt (#4497) Co-authored-by: Joseph Milazzo Co-authored-by: Joe Milazzo --- .github/workflows/build-and-test.yml | 2 +- .github/workflows/build-ui.yml | 2 +- .gitignore | 98 +- API.Benchmark/API.Benchmark.csproj | 35 - API.Tests/API.Tests.csproj | 45 - .../Comparers/ChapterSortComparerTest.cs | 19 - API.Tests/Comparers/NumericComparerTests.cs | 30 - .../Comparers/SortComparerZeroLastTests.cs | 17 - .../Comparers/StringLogicalComparerTest.cs | 33 - .../Extensions/FileInfoExtensionsTests.cs | 32 - .../Extensions/Test Data/not modified.txt | 1 - API.Tests/Helpers/ParserInfoHelperTests.cs | 55 - API.Tests/Helpers/ReviewHelperTests.cs | 258 -- API/API.csproj | 216 - API/API.csproj.DotSettings | 4 - API/Comparators/NumericComparer.cs | 18 - API/Comparators/StringLogicalComparer.cs | 130 - API/Controllers/DeviceController.cs | 250 -- API/Controllers/EmailController.cs | 27 - API/Controllers/FallbackController.cs | 37 - API/Controllers/ImageController.cs | 288 -- API/Controllers/VolumeController.cs | 82 - .../AppUserSmartFilterRepository.cs | 72 - .../Repositories/CollectionTagRepository.cs | 285 -- API/Data/Repositories/DeviceRepository.cs | 52 - .../Repositories/EmailHistoryRepository.cs | 36 - API/Data/Repositories/EpubFontRepository.cs | 102 - API/Data/Repositories/MangaFileRepository.cs | 46 - API/Data/Repositories/MediaErrorRepository.cs | 87 - .../Repositories/ScrobbleEventRepository.cs | 225 - .../Repositories/SeriesMetadataRepository.cs | 23 - API/Data/Repositories/SettingsRepository.cs | 101 - API/Data/Repositories/SiteThemeRepository.cs | 114 - .../UserTableOfContentRepository.cs | 74 - API/Extensions/AppUserExtensions.cs | 57 - API/Extensions/EnumExtensions.cs | 43 - API/Extensions/EnumerableExtensions.cs | 111 - API/Extensions/FileInfoExtensions.cs | 19 - API/Extensions/PathExtensions.cs | 15 - .../Filtering/AnnotationFilter.cs | 187 - .../QueryExtensions/Filtering/SeriesFilter.cs | 945 ---- .../QueryExtensions/ProjectToExtensions.cs | 26 - API/Extensions/StringExtensions.cs | 149 - API/Helpers/Builders/MediaErrorBuilder.cs | 32 - API/Helpers/Builders/PlusSeriesDtoBuilder.cs | 39 - API/Helpers/PagedList.cs | 46 - API/Helpers/ParserInfoHelpers.cs | 50 - API/Properties/launchSettings.json | 31 - API/Services/AccountService.cs | 335 -- API/Services/AuthKeyService.cs | 24 - .../Caching/AuthKeyCacheInvalidator.cs | 26 - API/Services/CollectionTagService.cs | 126 - API/Services/DeviceService.cs | 151 - API/Services/KoreaderService.cs | 117 - API/Services/MediaConversionService.cs | 326 -- API/Services/MediaErrorService.cs | 70 - API/Services/RatingService.cs | 126 - API/Services/Store/UserContext.cs | 104 - API/Services/StreamService.cs | 435 -- API/Services/Tasks/BackupService.cs | 311 -- API/Services/Tasks/CleanupService.cs | 449 -- API/SignalR/EventHub.cs | 58 - API/redo-migration.sh | 28 - CONTRIBUTING.md | 9 +- Dockerfile | 2 +- .../Attributes/SkipDeviceTrackingAttribute.cs | 10 + Kavita.API/Database/IDataContext.cs | 98 + Kavita.API/Database/IUnitOfWork.cs | 42 + {API => Kavita.API}/Errors/ApiException.cs | 3 +- .../Errors}/OpdsException.cs | 6 +- Kavita.API/Kavita.API.csproj | 22 + .../Repositories/IAnnotationRepository.cs | 27 + .../IAppUserExternalSourceRepository.cs | 16 + .../IAppUserProgressRepository.cs | 33 + .../IAppUserReadingProfileRepository.cs | 72 + .../IAppUserSmartFilterRepository.cs | 18 + Kavita.API/Repositories/IChapterRepository.cs | 63 + .../Repositories/IClientDeviceRepository.cs | 16 + .../Repositories/ICollectionTagRepository.cs | 57 + Kavita.API/Repositories/IDeviceRepository.cs | 14 + .../Repositories/IEmailHistoryRepository.cs | 12 + .../Repositories/IEpubFontRepository.cs | 20 + .../IExternalSeriesMetadataRepository.cs | 28 + Kavita.API/Repositories/IGenreRepository.cs | 25 + Kavita.API/Repositories/ILibraryRepository.cs | 54 + .../Repositories/IMangaFileRepository.cs | 13 + .../Repositories/IMediaErrorRepository.cs | 19 + Kavita.API/Repositories/IPersonRepository.cs | 75 + .../Repositories/IReadingListRepository.cs | 55 + .../Repositories/IReadingSessionRepository.cs | 11 + .../Repositories/IScrobbleRepository.cs | 50 + .../Repositories/ISeriesMetadataRepository.cs | 8 + Kavita.API/Repositories/ISeriesRepository.cs | 146 + .../Repositories/ISettingsRepository.cs | 25 + .../Repositories/ISiteThemeRepository.cs | 20 + Kavita.API/Repositories/ITagRepository.cs | 22 + Kavita.API/Repositories/IUserRepository.cs | 161 + .../IUserTableOfContentRepository.cs | 16 + Kavita.API/Repositories/IVolumeRepository.cs | 43 + Kavita.API/Services/Helpers/ICacheHelper.cs | 18 + Kavita.API/Services/IAccountService.cs | 54 + Kavita.API/Services/IAnnotationService.cs | 21 + Kavita.API/Services/IArchiveService.cs | 39 + Kavita.API/Services/IAuthKeyService.cs | 19 + Kavita.API/Services/IBackupService.cs | 18 + Kavita.API/Services/IBookService.cs | 58 + Kavita.API/Services/IBookmarkService.cs | 15 + Kavita.API/Services/ICacheService.cs | 37 + Kavita.API/Services/ICleanupService.cs | 29 + Kavita.API/Services/IClientDeviceService.cs | 15 + Kavita.API/Services/IClientInfoAccessor.cs | 22 + Kavita.API/Services/ICollectionTagService.cs | 23 + Kavita.API/Services/IDeviceService.cs | 16 + Kavita.API/Services/IDeviceTrackingService.cs | 12 + Kavita.API/Services/IDirectoryService.cs | 82 + Kavita.API/Services/IDownloadService.cs | 11 + Kavita.API/Services/IEmailService.cs | 27 + .../Services/IEntityNamingService.cs | 54 +- Kavita.API/Services/IFileService.cs | 12 + Kavita.API/Services/IFontService.cs | 13 + Kavita.API/Services/IImageService.cs | 61 + Kavita.API/Services/IKoreaderService.cs | 11 + Kavita.API/Services/ILocalizationService.cs | 12 + Kavita.API/Services/ILoggingService.cs | 6 + .../Services/IMediaConversionService.cs | 19 + Kavita.API/Services/IMediaErrorService.cs | 14 + Kavita.API/Services/IMetadataService.cs | 35 + Kavita.API/Services/IOidcService.cs | 38 + Kavita.API/Services/IOpdsService.cs | 32 + Kavita.API/Services/IPersonService.cs | 29 + Kavita.API/Services/IRatingService.cs | 27 + Kavita.API/Services/IReadingItemService.cs | 12 + Kavita.API/Services/ISeriesService.cs | 21 + Kavita.API/Services/ISettingsService.cs | 31 + Kavita.API/Services/IStatisticService.cs | 58 + Kavita.API/Services/IStatsService.cs | 12 + Kavita.API/Services/IStreamService.cs | 32 + Kavita.API/Services/ITachiyomiService.cs | 30 + Kavita.API/Services/ITaskScheduler.cs | 27 + Kavita.API/Services/IThemeService.cs | 25 + Kavita.API/Services/ITokenService.cs | 14 + Kavita.API/Services/IVersionUpdaterService.cs | 15 + .../Services/Metadata/ICoverDbService.cs | 21 + .../Metadata/IWordCountAnalyzerService.cs | 13 + .../Services/Plus/IExternalMetadataService.cs | 93 + .../Services/Plus/IKavitaPlusApiService.cs | 26 + Kavita.API/Services/Plus/ILicenseService.cs | 17 + .../Services/Plus/IScrobblingService.cs | 222 + .../Plus/ISmartCollectionSyncService.cs | 25 + .../Services/Plus/IWantToReadSyncService.cs | 9 + Kavita.API/Services/Reading/IReaderService.cs | 36 + .../Reading/IReadingHistoryService.cs | 9 + .../Services/Reading/IReadingListService.cs | 49 + .../Reading/IReadingProfileService.cs | 159 + .../Reading/IReadingSessionService.cs | 10 + .../Services/Scanner/ILibraryWatcher.cs | 21 + Kavita.API/Services/Scanner/IProcessSeries.cs | 20 + .../Services/Scanner/IScannerService.cs | 34 + Kavita.API/Services/SignalR/IEventHub.cs | 14 + .../Services/SignalR/IPresenceTracker.cs | 17 + Kavita.API/Store/IUserContext.cs | 43 + .../ArchiveServiceBenchmark.cs | 11 +- .../CleanTitleBenchmark.cs | 9 +- .../Data/AesopsFables.epub | Bin .../Data/Comics.txt | 0 .../Data/SeriesNamesForNormalization.txt | 0 Kavita.Benchmark/Kavita.Benchmark.csproj | 44 + .../KoreaderHashBenchmark.cs | 7 +- .../ParserBenchmarks.cs | 7 +- .../Program.cs | 2 +- .../TestBenchmark.cs | 12 +- .../Extensions/EnumerableExtensionsTests.cs | 38 +- .../Extensions/PathExtensionsTests.cs | 6 +- .../Extensions/VersionExtensionTests.cs | 6 +- .../Helpers}/CronConverterTests.cs | 7 +- .../Helpers/HtmlHelperTests.cs | 134 + .../Helpers/RandfHelper.cs | 9 +- .../Helpers/RateLimiterTests.cs | 7 +- .../Helpers/StringHelperTests.cs | 5 +- .../Kavita.Common.Tests.csproj | 31 + {API => Kavita.Common}/Constants/Headers.cs | 2 +- .../Extensions/ClaimsPrincipalExtensions.cs | 8 +- .../Extensions/DateTimeExtensions.cs | 5 +- .../Extensions/DoubleExtensions.cs | 4 +- Kavita.Common/Extensions/EnumExtensions.cs | 38 +- .../Extensions/EnumerableExtensions.cs | 68 + .../Extensions/FloatExtensions.cs | 4 +- .../Extensions/FlurlExtensions.cs | 11 +- .../Extensions/ImageExtensions.cs | 4 +- Kavita.Common/Extensions/PathExtensions.cs | 7 +- Kavita.Common/Extensions/StringExtensions.cs | 193 + .../Extensions/VersionExtensions.cs | 4 +- .../Helpers/AuthKeyHelper.cs | 2 +- .../Helpers}/CronConverter.cs | 13 +- .../Helpers/DayOfWeekHelper.cs | 4 +- .../Helpers/HtmlHelper.cs | 47 +- {API => Kavita.Common}/Helpers/JwtHelper.cs | 2 +- .../Helpers/NumberHelper.cs | 2 +- Kavita.Common/Helpers/PagedList.cs | 26 + .../Helpers/PaginationHeader.cs | 3 +- {API => Kavita.Common}/Helpers/RateLimiter.cs | 2 +- .../Helpers/StringHelper.cs | 2 +- {API => Kavita.Common}/Helpers/UserParams.cs | 2 +- Kavita.Common/Kavita.Common.csproj | 14 +- .../AbstractDbTest.cs | 41 +- .../AbstractFsTest.cs | 7 +- .../Extensions/QueryableExtensionsTests.cs | 19 +- .../Kavita.Database.Tests.csproj | 32 + .../ExternalSeriesMetadataRepositoryTests.cs | 284 ++ .../Repositories}/GenreRepositoryTests.cs | 15 +- .../Repositories}/PersonRepositoryTests.cs | 42 +- .../Repositories}/SeriesRepositoryTests.cs | 11 +- .../Repositories}/TagRepositoryTests.cs | 17 +- .../AnnotationFilterFieldValueConverter.cs | 6 +- .../Converters/FilterFieldValueConverter.cs | 11 +- .../PersonFilterFieldValueConverter.cs | 10 +- {API/Data => Kavita.Database}/DataContext.cs | 36 +- .../ApplicationServiceExtensions.cs | 42 + .../Extensions}/AuthKeyQueryExtensions.cs | 7 +- .../Extensions/BookmarkSortExtensions.cs | 18 +- .../Extensions}/ChapterQueryExtensions.cs | 15 +- .../Extensions/DataContextExtensions.cs | 4 +- .../Extensions/Filters}/ActivityFilter.cs | 13 +- .../Extensions/Filters/AnnotationFilter.cs | 188 + .../Extensions/Filters}/PersonFilter.cs | 10 +- .../Extensions/Filters/SeriesFilter.cs | 944 ++++ .../Extensions}/IncludesExtensions.cs | 12 +- .../Extensions/PagedListExtensions.cs | 29 + .../Extensions/ProjectToExtensions.cs | 25 + .../Extensions}/QueryableExtensions.cs | 21 +- .../Extensions}/RestrictByAgeExtensions.cs | 138 +- .../RestrictByLibraryExtensions.cs | 9 +- .../Extensions}/SearchQueryableExtensions.cs | 14 +- .../Extensions/SeriesSortExtensions.cs | 10 +- .../Extensions}/StatisticsQueryExtensions.cs | 8 +- Kavita.Database/Kavita.Database.csproj | 24 + .../20201213205325_AddUser.Designer.cs | 4 +- .../Migrations/20201213205325_AddUser.cs | 2 +- .../20201215195007_AddedLibrary.Designer.cs | 4 +- .../Migrations/20201215195007_AddedLibrary.cs | 2 +- ...1218173135_ManyToManyLibraries.Designer.cs | 4 +- .../20201218173135_ManyToManyLibraries.cs | 2 +- .../20201221141047_IdentityAdded.Designer.cs | 4 +- .../20201221141047_IdentityAdded.cs | 2 +- .../20201224155621_MiscCleanup.Designer.cs | 4 +- .../Migrations/20201224155621_MiscCleanup.cs | 2 +- ...190216_SeriesAndVolumeEntities.Designer.cs | 4 +- .../20201229190216_SeriesAndVolumeEntities.cs | 2 +- ...180935_AddedCoverImageToSeries.Designer.cs | 4 +- .../20210101180935_AddedCoverImageToSeries.cs | 2 +- ...0210102165536_EntityTimestamps.Designer.cs | 4 +- .../20210102165536_EntityTimestamps.cs | 2 +- ...102173326_VolumeNumberRefactor.Designer.cs | 4 +- .../20210102173326_VolumeNumberRefactor.cs | 2 +- ...210103201043_RemoveUserIsAdmin.Designer.cs | 4 +- .../20210103201043_RemoveUserIsAdmin.cs | 2 +- ...0210103230812_SeriesCoverImage.Designer.cs | 4 +- .../20210103230812_SeriesCoverImage.cs | 2 +- ...0210104011624_VolumeCoverImage.Designer.cs | 4 +- .../20210104011624_VolumeCoverImage.cs | 2 +- .../20210109205034_CacheMetadata.Designer.cs | 4 +- .../20210109205034_CacheMetadata.cs | 2 +- .../20210111231840_VolumePages.Designer.cs | 4 +- .../Migrations/20210111231840_VolumePages.cs | 2 +- .../20210114214506_UserProgress.Designer.cs | 4 +- .../Migrations/20210114214506_UserProgress.cs | 2 +- ...180406_ReadStatusModifications.Designer.cs | 4 +- .../20210117180406_ReadStatusModifications.cs | 2 +- .../20210117181421_SeriesPages.Designer.cs | 4 +- .../Migrations/20210117181421_SeriesPages.cs | 2 +- ...213837_AppUserRatingAndReviews.Designer.cs | 4 +- .../20210119213837_AppUserRatingAndReviews.cs | 2 +- ...0121180051_AddedServerSettings.Designer.cs | 4 +- .../20210121180051_AddedServerSettings.cs | 2 +- ...15532_ServerSettingsAdjustment.Designer.cs | 4 +- ...20210121215532_ServerSettingsAdjustment.cs | 2 +- ...122165809_ServerSettingsChange.Designer.cs | 4 +- .../20210122165809_ServerSettingsChange.cs | 2 +- ...72455_ServerSettingsPrimaryKey.Designer.cs | 4 +- ...20210122172455_ServerSettingsPrimaryKey.cs | 2 +- ...3348_SeriesVolumeChapterChange.Designer.cs | 4 +- ...0210128143348_SeriesVolumeChapterChange.cs | 2 +- ...2_MangaFileChapterRelationship.Designer.cs | 4 +- ...0128201832_MangaFileChapterRelationship.cs | 2 +- ...210203164258_ServerSettingsKey.Designer.cs | 4 +- .../20210203164258_ServerSettingsKey.cs | 2 +- ...20210205220227_UserPreferences.Designer.cs | 4 +- .../20210205220227_UserPreferences.cs | 2 +- ...207231256_SeriesNormalizedName.Designer.cs | 4 +- .../20210207231256_SeriesNormalizedName.cs | 2 +- ...0210225150830_AddLocalizedName.Designer.cs | 4 +- .../20210225150830_AddLocalizedName.cs | 2 +- ...28_SearchIndexAndProgressDates.Designer.cs | 4 +- ...10315134028_SearchIndexAndProgressDates.cs | 2 +- ...0210322212724_MangaFileToPages.Designer.cs | 4 +- .../20210322212724_MangaFileToPages.cs | 2 +- ...13507_LastModifiedOnMangaFiles.Designer.cs | 4 +- ...20210323213507_LastModifiedOnMangaFiles.cs | 2 +- ...0330134414_IsSpecialOnChapters.Designer.cs | 4 +- .../20210330134414_IsSpecialOnChapters.cs | 2 +- ...19222000_BookReaderPreferences.Designer.cs | 4 +- .../20210419222000_BookReaderPreferences.cs | 2 +- ..._BookReaderPreferencesFontSize.Designer.cs | 4 +- ...419234652_BookReaderPreferencesFontSize.cs | 2 +- ...10423132900_CustomChapterTitle.Designer.cs | 4 +- .../20210423132900_CustomChapterTitle.cs | 2 +- ...210504184715_TapToPaginatePref.Designer.cs | 4 +- .../20210504184715_TapToPaginatePref.cs | 2 +- ...9014029_SiteDarkModePreference.Designer.cs | 4 +- .../20210509014029_SiteDarkModePreference.cs | 2 +- .../20210519215934_CollectionTag.Designer.cs | 4 +- .../20210519215934_CollectionTag.cs | 5 +- ...528150353_CollectionCoverImage.Designer.cs | 4 +- .../20210528150353_CollectionCoverImage.cs | 2 +- ...210530201541_CollectionSummary.Designer.cs | 4 +- .../20210530201541_CollectionSummary.cs | 2 +- ...33957_BookReadingDirectionPref.Designer.cs | 4 +- ...20210603133957_BookReadingDirectionPref.cs | 2 +- ...603212429_BookScrollIdProgress.Designer.cs | 4 +- .../20210603212429_BookScrollIdProgress.cs | 2 +- ...10622164318_NewUserPreferences.Designer.cs | 4 +- .../20210622164318_NewUserPreferences.cs | 2 +- ...210722223304_AddedSeriesFormat.Designer.cs | 4 +- .../20210722223304_AddedSeriesFormat.cs | 2 +- .../20210809210326_BookmarkPages.Designer.cs | 4 +- .../20210809210326_BookmarkPages.cs | 2 +- ...0210_CoverImageLockFieldsPart1.Designer.cs | 4 +- ...0210813010210_CoverImageLockFieldsPart1.cs | 2 +- ...31_CoverImageLockedFieldsPart2.Designer.cs | 4 +- ...10814215831_CoverImageLockedFieldsPart2.cs | 2 +- ...152226_ProgressConcurencyCheck.Designer.cs | 4 +- .../20210817152226_ProgressConcurencyCheck.cs | 2 +- .../20210826203258_userApiKey.Designer.cs | 4 +- .../Migrations/20210826203258_userApiKey.cs | 2 +- .../20210901150310_ReadingLists.Designer.cs | 4 +- .../Migrations/20210901150310_ReadingLists.cs | 2 +- ...01200442_ReadingListsAdditions.Designer.cs | 4 +- .../20210901200442_ReadingListsAdditions.cs | 2 +- ...eadingListsExtraRealationships.Designer.cs | 4 +- ...2110705_ReadingListsExtraRealationships.cs | 2 +- ...0906140845_ReadingListsChanges.Designer.cs | 4 +- .../20210906140845_ReadingListsChanges.cs | 2 +- ...0916142418_EntityImageRefactor.Designer.cs | 4 +- .../20210916142418_EntityImageRefactor.cs | 2 +- ...11001113608_LastScannedLibrary.Designer.cs | 4 +- .../20211001113608_LastScannedLibrary.cs | 2 +- ...11127200244_MetadataFoundation.Designer.cs | 4 +- .../20211127200244_MetadataFoundation.cs | 2 +- ...29231007_RemoveChapterMetadata.Designer.cs | 4 +- .../20211129231007_RemoveChapterMetadata.cs | 2 +- .../20211130134642_GenreProvider.Designer.cs | 4 +- .../20211130134642_GenreProvider.cs | 2 +- .../20211201230003_GenreTitle.Designer.cs | 4 +- .../Migrations/20211201230003_GenreTitle.cs | 2 +- ...211205185207_MetadataAgeRating.Designer.cs | 4 +- .../20211205185207_MetadataAgeRating.cs | 2 +- ...193225_AgeRatingAndReleaseDate.Designer.cs | 4 +- .../20211206193225_AgeRatingAndReleaseDate.cs | 2 +- ...0211217013734_BookmarkRefactor.Designer.cs | 4 +- .../20211217013734_BookmarkRefactor.cs | 2 +- ...0211217180457_filteringChanges.Designer.cs | 4 +- .../20211217180457_filteringChanges.cs | 2 +- .../20211227180752_FullscreenPref.Designer.cs | 4 +- .../20211227180752_FullscreenPref.cs | 2 +- ...22_ChapterMetadataOptimization.Designer.cs | 4 +- ...20107232822_ChapterMetadataOptimization.cs | 2 +- .../20220108200822_CountMetadata.Designer.cs | 4 +- .../20220108200822_CountMetadata.cs | 2 +- ...220108202027_PublicationStatus.Designer.cs | 4 +- .../20220108202027_PublicationStatus.cs | 2 +- .../20220215163317_SiteTheme.Designer.cs | 4 +- .../Migrations/20220215163317_SiteTheme.cs | 2 +- ...20303205301_SeriesLockedFields.Designer.cs | 4 +- .../20220303205301_SeriesLockedFields.cs | 2 +- ...aReaderBackgroundAndLayoutMode.Designer.cs | 4 +- ...5456_MangaReaderBackgroundAndLayoutMode.cs | 4 +- .../20220307153053_ScreenHints.Designer.cs | 4 +- .../Migrations/20220307153053_ScreenHints.cs | 2 +- ...dedAndReadingListNormalization.Designer.cs | 4 +- ...ChapterAddedAndReadingListNormalization.cs | 2 +- ...220416211340_RemoveCustomIndex.Designer.cs | 4 +- .../20220416211340_RemoveCustomIndex.cs | 2 +- ...20220421214448_SeriesRelations.Designer.cs | 4 +- .../20220421214448_SeriesRelations.cs | 2 +- ...125505_ChangeCountToTotalCount.Designer.cs | 4 +- .../20220425125505_ChangeCountToTotalCount.cs | 2 +- ...22_AddMaxCountToSeriesMetadata.Designer.cs | 4 +- ...20425131122_AddMaxCountToSeriesMetadata.cs | 2 +- ...0220508162841_BookReaderUpdate.Designer.cs | 4 +- .../20220508162841_BookReaderUpdate.cs | 2 +- ...234708_BookReaderImmersiveMode.Designer.cs | 4 +- .../20220513234708_BookReaderImmersiveMode.cs | 2 +- .../20220524172543_WordCount.Designer.cs | 4 +- .../Migrations/20220524172543_WordCount.cs | 2 +- ...0220610153822_TimeEstimateInDB.Designer.cs | 4 +- .../20220610153822_TimeEstimateInDB.cs | 2 +- ...25_RenamedBookReaderLayoutMode.Designer.cs | 4 +- ...20613131125_RenamedBookReaderLayoutMode.cs | 2 +- ...lobalPageLayoutModeUserSetting.Designer.cs | 4 +- ...3131302_GlobalPageLayoutModeUserSetting.cs | 2 +- ...0220615190640_LastFileAnalysis.Designer.cs | 4 +- .../20220615190640_LastFileAnalysis.cs | 2 +- ...0625215526_BlurUnreadSummaries.Designer.cs | 4 +- .../20220625215526_BlurUnreadSummaries.cs | 2 +- ...romptForDownloadSizeUserOption.Designer.cs | 4 +- ...2161611_PromptForDownloadSizeUserOption.cs | 2 +- ...717145254_UserConfirmationLink.Designer.cs | 4 +- .../20220717145254_UserConfirmationLink.cs | 2 +- .../20220728193758_WantToReadList.Designer.cs | 4 +- .../20220728193758_WantToReadList.cs | 2 +- ...20220802222910_BookmarkHasDate.Designer.cs | 4 +- .../20220802222910_BookmarkHasDate.cs | 2 +- ...814134725_MangaFileCreatedDate.Designer.cs | 4 +- .../20220814134725_MangaFileCreatedDate.cs | 2 +- .../20220817173731_SeriesFolder.Designer.cs | 4 +- .../Migrations/20220817173731_SeriesFolder.cs | 2 +- ...223212_NormalizedLocalizedName.Designer.cs | 4 +- .../20220819223212_NormalizedLocalizedName.cs | 2 +- .../20220921023455_DeviceSupport.Designer.cs | 4 +- .../20220921023455_DeviceSupport.cs | 2 +- ...0220926145902_AddNoTransitions.Designer.cs | 4 +- .../20220926145902_AddNoTransitions.cs | 2 +- ...013956_ReleaseYearOnSeriesEdit.Designer.cs | 4 +- .../20221006013956_ReleaseYearOnSeriesEdit.cs | 2 +- ...009172653_ReadingListAgeRating.Designer.cs | 4 +- .../20221009172653_ReadingListAgeRating.cs | 2 +- .../20221009211237_UserAgeRating.Designer.cs | 4 +- .../20221009211237_UserAgeRating.cs | 4 +- ...20221017131711_IncludeUnknowns.Designer.cs | 4 +- .../20221017131711_IncludeUnknowns.cs | 2 +- ...115021908_SeriesRelationChange.Designer.cs | 4 +- .../20221115021908_SeriesRelationChange.cs | 2 +- ...131123_ExtendedLibrarySettings.Designer.cs | 4 +- .../20221118131123_ExtendedLibrarySettings.cs | 2 +- ...6133824_FileLengthAndExtension.Designer.cs | 4 +- .../20221126133824_FileLengthAndExtension.cs | 2 +- ...28230726_UserProgressLibraryId.Designer.cs | 4 +- .../20221128230726_UserProgressLibraryId.cs | 2 +- ...20221212215914_EmulateBookPref.Designer.cs | 4 +- .../20221212215914_EmulateBookPref.cs | 2 +- .../20230111014852_YearlyStats.Designer.cs | 4 +- .../Migrations/20230111014852_YearlyStats.cs | 2 +- ...0129210741_SwipeToPaginatePref.Designer.cs | 4 +- .../20230129210741_SwipeToPaginatePref.cs | 2 +- ...20230130210252_AutoCollections.Designer.cs | 4 +- .../20230130210252_AutoCollections.cs | 2 +- ...230202182602_ReadingListFields.Designer.cs | 4 +- .../20230202182602_ReadingListFields.cs | 2 +- ..._RemoveExternalFromTagAndGenre.Designer.cs | 4 +- ...203112022_RemoveExternalFromTagAndGenre.cs | 2 +- .../20230210153842_UtcTimes.Designer.cs | 4 +- .../Migrations/20230210153842_UtcTimes.cs | 2 +- ...28_CollapseSeriesRelationships.Designer.cs | 4 +- ...30220203128_CollapseSeriesRelationships.cs | 2 +- ...304202540_BookWritingStylePref.Designer.cs | 4 +- .../20230304202540_BookWritingStylePref.cs | 2 +- ...0_MoveCollapseSeriesToUserPref.Designer.cs | 4 +- ...0310142630_MoveCollapseSeriesToUserPref.cs | 2 +- ...313125914_ReadingListDateRange.Designer.cs | 4 +- .../20230313125914_ReadingListDateRange.cs | 2 +- .../20230316123908_SecurityEvent.Designer.cs | 4 +- .../20230316123908_SecurityEvent.cs | 2 +- ...0316233133_RemoveSecurityEvent.Designer.cs | 4 +- .../20230316233133_RemoveSecurityEvent.cs | 2 +- ...449_ManageReadingListOnLibrary.Designer.cs | 4 +- ...230415123449_ManageReadingListOnLibrary.cs | 2 +- .../20230505124430_MediaError.Designer.cs | 4 +- .../Migrations/20230505124430_MediaError.cs | 2 +- ...30511165427_WebLinksForChapter.Designer.cs | 4 +- .../20230511165427_WebLinksForChapter.cs | 2 +- ...230511183339_WebLinksForSeries.Designer.cs | 4 +- .../20230511183339_WebLinksForSeries.cs | 2 +- .../20230512004545_ChapterISBN.Designer.cs | 4 +- .../Migrations/20230512004545_ChapterISBN.cs | 2 +- ...30527215722_LicenseAndScrobble.Designer.cs | 4 +- .../20230527215722_LicenseAndScrobble.cs | 2 +- .../20230601172306_ScrobbleErrors.Designer.cs | 4 +- .../20230601172306_ScrobbleErrors.cs | 2 +- ...2154313_ScrobbleEventProcessed.Designer.cs | 4 +- .../20230612154313_ScrobbleEventProcessed.cs | 2 +- ...19_ReviewTaglineAndOptInShares.Designer.cs | 4 +- ...30615133219_ReviewTaglineAndOptInShares.cs | 2 +- .../20230618150728_ScrobbleHolds.Designer.cs | 4 +- .../20230618150728_ScrobbleHolds.cs | 2 +- ...230621211421_RemoveUserLicense.Designer.cs | 4 +- .../20230621211421_RemoveUserLicense.cs | 2 +- .../20230623192231_ScrobbleReview.Designer.cs | 4 +- .../20230623192231_ScrobbleReview.cs | 2 +- .../20230715125951_OnDeckRemoval.Designer.cs | 4 +- .../20230715125951_OnDeckRemoval.cs | 2 +- .../20230719173458_PersonalToC.Designer.cs | 4 +- .../Migrations/20230719173458_PersonalToC.cs | 2 +- ...230725133536_ChangeRatingScale.Designer.cs | 4 +- .../20230725133536_ChangeRatingScale.cs | 2 +- ...0230727175518_AddLocaleOnPrefs.Designer.cs | 4 +- .../20230727175518_AddLocaleOnPrefs.cs | 2 +- .../20230904184205_SmartFilters.Designer.cs | 4 +- .../Migrations/20230904184205_SmartFilters.cs | 2 +- ...20230908190713_DashboardStream.Designer.cs | 4 +- .../20230908190713_DashboardStream.cs | 2 +- ...SideNavStreamAndExternalSource.Designer.cs | 4 +- ...13194957_SideNavStreamAndExternalSource.cs | 2 +- ...0231113215006_LibraryFileTypes.Designer.cs | 4 +- .../20231113215006_LibraryFileTypes.cs | 2 +- ...7234829_LibraryExcludePatterns.Designer.cs | 4 +- .../20231117234829_LibraryExcludePatterns.cs | 2 +- ...1223643_ExternalSeriesMetadata.Designer.cs | 4 +- .../20240121223643_ExternalSeriesMetadata.cs | 2 +- ...0128153433_VolumeMinMaxNumbers.Designer.cs | 4 +- .../20240128153433_VolumeMinMaxNumbers.cs | 2 +- .../20240130190617_WantToReadFix.Designer.cs | 4 +- .../20240130190617_WantToReadFix.cs | 2 +- ...20240204141206_BlackListSeries.Designer.cs | 4 +- .../20240204141206_BlackListSeries.cs | 2 +- ...40205184724_ScrobbleEventError.Designer.cs | 4 +- .../20240205184724_ScrobbleEventError.cs | 2 +- .../20240209224347_DBTweaks.Designer.cs | 4 +- .../Migrations/20240209224347_DBTweaks.cs | 2 +- .../20240214232436_ChapterNumber.Designer.cs | 4 +- .../20240214232436_ChapterNumber.cs | 2 +- ...240216000223_MangaFileNameTemp.Designer.cs | 4 +- .../20240216000223_MangaFileNameTemp.cs | 2 +- ...0240222125420_ChapterIssueSort.Designer.cs | 4 +- .../20240222125420_ChapterIssueSort.cs | 2 +- ...0240225235816_VolumeLookupName.Designer.cs | 4 +- .../20240225235816_VolumeLookupName.cs | 2 +- .../20240309140117_SeriesImprints.Designer.cs | 4 +- .../20240309140117_SeriesImprints.cs | 2 +- ...3112552_SeriesLowestFolderPath.Designer.cs | 4 +- .../20240313112552_SeriesLowestFolderPath.cs | 2 +- ...240314194402_TeamsAndLocations.Designer.cs | 4 +- .../20240314194402_TeamsAndLocations.cs | 2 +- .../20240321173812_UserMalToken.Designer.cs | 4 +- .../Migrations/20240321173812_UserMalToken.cs | 2 +- .../20240328130057_PdfSettings.Designer.cs | 4 +- .../Migrations/20240328130057_PdfSettings.cs | 2 +- ...331172900_UserBasedCollections.Designer.cs | 4 +- .../20240331172900_UserBasedCollections.cs | 2 +- ...418163829_ChapterSortOrderLock.Designer.cs | 4 +- .../20240418163829_ChapterSortOrderLock.cs | 2 +- ...03120147_SmartCollectionFields.Designer.cs | 4 +- .../20240503120147_SmartCollectionFields.cs | 2 +- ...20240510134030_SiteThemeFields.Designer.cs | 4 +- .../20240510134030_SiteThemeFields.cs | 2 +- .../20240704144224_PersonFields.Designer.cs | 4 +- .../Migrations/20240704144224_PersonFields.cs | 2 +- ...40808100353_CoverPrimaryColors.Designer.cs | 4 +- .../20240808100353_CoverPrimaryColors.cs | 2 +- ...811154857_ChapterMetadataLocks.Designer.cs | 4 +- .../20240811154857_ChapterMetadataLocks.cs | 2 +- ...240813194728_VolumeCoverLocked.Designer.cs | 4 +- .../20240813194728_VolumeCoverLocked.cs | 2 +- ...0917180034_AvgReadingTimeFloat.Designer.cs | 4 +- .../20240917180034_AvgReadingTimeFloat.cs | 2 +- ...1011143144_PeopleOverhaulPart1.Designer.cs | 4 +- .../20241011143144_PeopleOverhaulPart1.cs | 2 +- ...1011152321_PeopleOverhaulPart2.Designer.cs | 4 +- .../20241011152321_PeopleOverhaulPart2.cs | 2 +- ...1011172428_PeopleOverhaulPart3.Designer.cs | 4 +- .../20241011172428_PeopleOverhaulPart3.cs | 2 +- ...31_SeriesDontMatchAndBlacklist.Designer.cs | 4 +- ...50105180131_SeriesDontMatchAndBlacklist.cs | 2 +- .../20250109173537_EmailHistory.Designer.cs | 4 +- .../Migrations/20250109173537_EmailHistory.cs | 2 +- ...itaPlusUserAndMetadataSettings.Designer.cs | 4 +- ...63454_KavitaPlusUserAndMetadataSettings.cs | 2 +- ...0208200843_MoreMetadtaSettings.Designer.cs | 4 +- .../20250208200843_MoreMetadtaSettings.cs | 2 +- ...012_AutomaticWebtoonReaderMode.Designer.cs | 4 +- ...250328125012_AutomaticWebtoonReaderMode.cs | 2 +- ...30_ScrobbleGenerationDbCapture.Designer.cs | 4 +- ...50408222330_ScrobbleGenerationDbCapture.cs | 2 +- .../20250415194829_KavitaPlusCBR.Designer.cs | 4 +- .../20250415194829_KavitaPlusCBR.cs | 2 +- ...150140_ChapterRatingAndReviews.Designer.cs | 4 +- .../20250429150140_ChapterRatingAndReviews.cs | 2 +- .../20250507221026_PersonAliases.Designer.cs | 4 +- .../20250507221026_PersonAliases.cs | 2 +- .../20250519151126_KoreaderHash.Designer.cs | 4 +- .../Migrations/20250519151126_KoreaderHash.cs | 2 +- ...20250601200056_ReadingProfiles.Designer.cs | 4 +- .../20250601200056_ReadingProfiles.cs | 2 +- ...DisableWidthOverrideBreakPoint.Designer.cs | 4 +- ...ngProfileDisableWidthOverrideBreakPoint.cs | 2 +- ...20215058_EnableMetadataLibrary.Designer.cs | 4 +- .../20250620215058_EnableMetadataLibrary.cs | 2 +- ...162548_TrackKavitaPlusMetadata.Designer.cs | 5 +- .../20250626162548_TrackKavitaPlusMetadata.cs | 2 +- ...153840_LibraryRemoveSortPrefix.Designer.cs | 5 +- .../20250629153840_LibraryRemoveSortPrefix.cs | 2 +- ...ableExtendedMetadataProcessing.Designer.cs | 5 +- ...204_AddEnableExtendedMetadataProcessing.cs | 2 +- .../20250802103258_OpenIDConnect.Designer.cs | 5 +- .../20250802103258_OpenIDConnect.cs | 2 +- ...20250820150458_BookAnnotations.Designer.cs | 5 +- .../20250820150458_BookAnnotations.cs | 2 +- ...250919114119_ColorScapeSetting.Designer.cs | 5 +- .../20250919114119_ColorScapeSetting.cs | 2 +- ...20250920212509_CustomEpubFonts.Designer.cs | 5 +- .../20250920212509_CustomEpubFonts.cs | 2 +- ...50921211542_EpubPageCalcMethod.Designer.cs | 5 +- .../20250921211542_EpubPageCalcMethod.cs | 2 +- ...2016_AddAnnotationsHtmlContent.Designer.cs | 5 +- ...0250924142016_AddAnnotationsHtmlContent.cs | 2 +- ...50928181727_RemoveEpubPageCalc.Designer.cs | 5 +- .../20250928181727_RemoveEpubPageCalc.cs | 2 +- ...251003110154_SocialAnnotations.Designer.cs | 5 +- .../20251003110154_SocialAnnotations.cs | 2 +- ...009150922_DataSaverUserSetting.Designer.cs | 5 +- .../20251009150922_DataSaverUserSetting.cs | 2 +- ...nheritWebLinksFromFirstChapter.Designer.cs | 5 +- ...6_SeriesInheritWebLinksFromFirstChapter.cs | 2 +- ...yDefaultLanguageCustomKeyBinds.Designer.cs | 5 +- ...45_LibraryDefaultLanguageCustomKeyBinds.cs | 2 +- .../20251101152738_OpdsSettings.Designer.cs | 5 +- .../Migrations/20251101152738_OpdsSettings.cs | 2 +- ...51207204514_StatsRevampPartOne.Designer.cs | 6 +- .../20251207204514_StatsRevampPartOne.cs | 2 +- ...markRelationshipAndSearchIndex.Designer.cs | 6 +- ...5923_BookmarkRelationshipAndSearchIndex.cs | 2 +- ...eadingSessionFormatAndIndecies.Designer.cs | 6 +- ...8200802_ReadingSessionFormatAndIndecies.cs | 2 +- ...24133055_AddDataProtectionKeys.Designer.cs | 6 +- .../20251224133055_AddDataProtectionKeys.cs | 2 +- ..._AddDeviceIdsToReadingProfiles.Designer.cs | 6 +- ...229144718_AddDeviceIdsToReadingProfiles.cs | 2 +- ...0109144351_ReadingSessionIndex.Designer.cs | 6 +- .../20260109144351_ReadingSessionIndex.cs | 2 +- ...64419_AppUserAuthKeyUtcMissing.Designer.cs | 6 +- ...20260110164419_AppUserAuthKeyUtcMissing.cs | 2 +- ...12165908_ReadingHistoryChanges.Designer.cs | 6 +- .../20260112165908_ReadingHistoryChanges.cs | 2 +- .../Migrations/DataContextModelSnapshot.cs | 6 +- .../Repositories/AnnotationRepository.cs | 85 +- .../AppUserExternalSourceRepository.cs | 56 +- .../Repositories/AppUserProgressRepository.cs | 205 +- .../AppUserReadingProfileRepository.cs | 103 +- .../AppUserSmartFilterRepository.cs | 56 + .../Repositories/ChapterRepository.cs | 342 +- .../Repositories/ClientDeviceRepository.cs | 60 + .../Repositories/CollectionTagRepository.cs | 239 + .../Repositories/CoverDbRepository.cs | 6 +- .../Repositories/DeviceRepository.cs | 36 + .../Repositories/EmailHistoryRepository.cs | 23 + .../Repositories/EpubFontRepository.cs | 81 + .../ExternalSeriesMetadataRepository.cs | 137 +- .../Repositories/GenreRepository.cs | 141 +- .../Repositories/LibraryRepository.cs | 252 +- .../Repositories/MangaFileRepository.cs | 33 + .../Repositories/MediaErrorRepository.cs | 68 + .../Repositories/PersonRepository.cs | 306 +- .../Repositories/ReadingListRepository.cs | 300 +- .../Repositories/ReadingSessionRepository.cs | 24 +- .../Repositories/ScrobbleEventRepository.cs | 186 + .../Repositories/SeriesMetadataRepository.cs | 14 + .../Repositories/SeriesRepository.cs | 1107 +++-- .../Repositories/SettingsRepository.cs | 81 + .../Repositories/SiteThemeRepository.cs | 91 + .../Repositories/TagRepository.cs | 113 +- .../Repositories/UserRepository.cs | 753 ++-- .../UserTableOfContentRepository.cs | 55 + .../Repositories/VolumeRepository.cs | 204 +- {API/Data => Kavita.Database}/Seed.cs | 300 +- {API/Data => Kavita.Database}/UnitOfWork.cs | 59 +- .../Extensions/EncodeFormatExtensionsTests.cs | 12 +- .../Extensions/EnumExtensionTests.cs | 9 +- .../Kavita.Models.Tests.csproj | 31 + .../AutoMapper/AutoMapperChapterProfile.cs | 28 +- .../AutoMapper/AutoMapperProfiles.cs | 84 +- .../AutoMapperReadingListProfile.cs | 6 +- .../AutoMapper/AutoMapperSeriesProfile.cs | 6 +- .../AutoMapper/AutoMapperVolumeProfile.cs | 6 +- .../Converters/ServerSettingConverter.cs | 11 +- .../Builders/AppUserBuilder.cs | 17 +- .../Builders/AppUserChapterRatingBuilder.cs | 7 +- .../Builders/AppUserCollectionBuilder.cs | 10 +- .../Builders/AppUserReadingProfileBuilder.cs | 9 +- .../Builders/DeviceBuilder.cs | 6 +- .../Builders/EntityBuilder.cs | 0 .../Builders/ExternalSeriesMetadataBuilder.cs | 4 +- .../Builders/FolderPathBuilder.cs | 4 +- .../Builders/GenreBuilder.cs | 8 +- Kavita.Models/Builders/IEntityBuilder.cs | 6 + .../Builders/KoreaderBookDtoBuilder.cs | 4 +- .../Builders/LibraryBuilder.cs | 7 +- Kavita.Models/Builders/MediaErrorBuilder.cs | 28 + .../Builders/PersonAliasBuilder.cs | 6 +- .../Builders/PersonBuilder.cs | 6 +- .../Builders/RatingBuilder.cs | 6 +- .../Builders/ReadingListBuilder.cs | 8 +- .../Builders/ReadingListItemBuilder.cs | 4 +- .../Builders/ScrobbleHoldBuilder.cs | 5 +- .../Builders/SeriesBuilder.cs | 12 +- .../Builders/SeriesMetadataBuilder.cs | 10 +- .../Builders/TagBuilder.cs | 8 +- .../Constants/CacheProfiles.cs | 2 +- .../Constants/ControllerConstants.cs | 2 +- Kavita.Models/Constants/ParserConstants.cs | 14 + .../Constants/PolicyConstants.cs | 2 +- .../Constants/PolicyGroups.cs | 6 +- .../Constants/ResponseCacheProfiles.cs | 2 +- .../Constants/TaskSchedulerConstants.cs | 26 + .../DTOs/Account/AgeRestrictionDto.cs | 6 +- .../DTOs/Account/AuthKeyDto.cs | 7 +- .../DTOs/Account/AuthKeyExpiresAtDto.cs | 8 + .../DTOs/Account/ConfirmEmailDto.cs | 2 +- .../DTOs/Account/ConfirmEmailUpdateDto.cs | 2 +- .../DTOs/Account/ConfirmMigrationEmailDto.cs | 2 +- .../DTOs/Account/ConfirmPasswordResetDto.cs | 2 +- .../DTOs/Account/InviteUserDto.cs | 2 +- .../DTOs/Account/InviteUserResponse.cs | 2 +- .../DTOs/Account/LoginDto.cs | 2 +- .../DTOs/Account/MemberDto.cs | 5 +- .../DTOs/Account/MemberInfoDto.cs | 6 +- .../DTOs/Account/MigrateUserEmailDto.cs | 2 +- .../DTOs/Account/ResetPasswordDto.cs | 2 +- .../DTOs/Account/RotateAuthKeyRequestDto.cs | 2 +- .../DTOs/Account/TokenRequestDto.cs | 2 +- .../DTOs/Account/UpdateAgeRestrictionDto.cs | 4 +- .../DTOs/Account/UpdateEmailDto.cs | 2 +- .../DTOs/Account/UpdateUserDto.cs | 13 +- .../DTOs/Annotations/FullAnnotationDto.cs | 2 +- .../DTOs/Archive/ArchiveLibrary.cs | 2 +- {API => Kavita.Models}/DTOs/BulkActionDto.cs | 2 +- .../DTOs/ChapterDetailPlusDto.cs | 4 +- {API => Kavita.Models}/DTOs/ChapterDto.cs | 108 +- .../DTOs/CheckForFilesInFolderRootsDto.cs | 2 +- .../DTOs/Collection/AppUserCollectionDto.cs | 7 +- .../DTOs/Collection/DeleteCollectionsDto.cs | 2 +- .../DTOs/Collection/MalStackDto.cs | 2 +- .../DTOs/Collection/PromoteCollectionsDto.cs | 2 +- .../CollectionTags/CollectionTagBulkAddDto.cs | 2 +- .../DTOs/CollectionTags/CollectionTagDto.cs | 2 +- .../CollectionTags/UpdateSeriesForTagDto.cs | 4 +- {API => Kavita.Models}/DTOs/ColorScape.cs | 2 +- .../DTOs/CopySettingsFromLibraryDto.cs | 2 +- .../DTOs/CoverDb/CoverDbAuthor.cs | 2 +- .../DTOs/CoverDb/CoverDbPeople.cs | 2 +- .../DTOs/CoverDb/CoverDbPersonIds.cs | 2 +- .../DTOs/Dashboard/DashboardStreamDto.cs | 5 +- .../DTOs/Dashboard/GroupedSeriesDto.cs | 4 +- .../DTOs/Dashboard/SmartFilterDto.cs | 4 +- .../UpdateDashboardStreamPositionDto.cs | 2 +- .../DTOs/Dashboard/UpdateStreamPositionDto.cs | 2 +- .../DTOs/DeleteChaptersDto.cs | 2 +- .../DTOs/DeleteSeriesDto.cs | 2 +- .../ClientDevice/UpdateClientDeviceNameDto.cs | 2 +- .../EmailDevice/CreateEmailDeviceDto.cs | 4 +- .../DTOs/Device/EmailDevice/DeviceDto.cs | 6 +- .../EmailDevice/SendSeriesToEmailDeviceDto.cs | 2 +- .../EmailDevice/SendToEmailDeviceDto.cs | 2 +- .../Device/EmailDevice/UpdateDeviceDto.cs | 4 +- .../DTOs/Downloads/DownloadBookmarkDto.cs | 4 +- .../DTOs/Email/ConfirmationEmailDto.cs | 2 +- .../DTOs/Email/EmailHistoryDto.cs | 2 +- .../DTOs/Email/EmailMigrationDto.cs | 2 +- .../DTOs/Email/EmailTestResultDto.cs | 2 +- .../DTOs/Email/PasswordResetEmailDto.cs | 2 +- .../DTOs/Email/SendToDto.cs | 2 +- .../DTOs/Email/TestEmailDto.cs | 2 +- .../DTOs/Filtering/FilterDto.cs | 8 +- .../DTOs/Filtering/LanguageDto.cs | 2 +- .../DTOs/Filtering/PersonSortField.cs | 2 +- .../DTOs/Filtering/Range.cs | 2 +- .../DTOs/Filtering/ReadStatus.cs | 2 +- .../DTOs/Filtering/SortField.cs | 2 +- .../DTOs/Filtering/SortOptions.cs | 2 +- .../DTOs/Filtering/v2/DecodeFilterDto.cs | 2 +- .../DTOs/Filtering/v2/FilterCombination.cs | 2 +- .../DTOs/Filtering/v2/FilterComparision.cs | 2 +- .../DTOs/Filtering/v2/FilterField.cs | 2 +- .../DTOs/Filtering/v2/FilterStatementDto.cs | 2 +- .../DTOs/Filtering/v2/FilterV2Dto.cs | 2 +- .../DTOs/Font/EpubFontDto.cs | 4 +- .../DTOs/ImportFieldMappings.cs | 4 +- .../DTOs/Internal/AppSettingsDto.cs | 2 +- {API => Kavita.Models}/DTOs/Jobs/JobDto.cs | 2 +- .../DTOs/JumpBar/JumpKeyDto.cs | 2 +- {API => Kavita.Models}/DTOs/KavitaLocale.cs | 2 +- .../KavitaPlus/Account/AniListUpdateDto.cs | 2 +- .../DTOs/KavitaPlus/Account/UserTokenInfo.cs | 2 +- .../ExternalMetadataIdsDto.cs | 4 +- .../ExternalMetadata/MatchSeriesRequestDto.cs | 4 +- .../SeriesDetailPlusApiDto.cs | 8 +- .../KavitaPlus/License/EncryptLicenseDto.cs | 2 +- .../DTOs/KavitaPlus/License/LicenseInfoDto.cs | 2 +- .../KavitaPlus/License/LicenseValidDto.cs | 2 +- .../KavitaPlus/License/ResetLicenseDto.cs | 2 +- .../KavitaPlus/License/UpdateLicenseDto.cs | 2 +- .../KavitaPlus/Manage/ManageMatchFilterDto.cs | 2 +- .../KavitaPlus/Manage/ManageMatchSeriesDto.cs | 2 +- .../KavitaPlus/Metadata/ExternalChapterDto.cs | 4 +- .../Metadata/ExternalSeriesDetailDto.cs | 12 +- .../Metadata/MetadataFieldMappingDto.cs | 4 +- .../Metadata/MetadataSettingsDto.cs | 8 +- .../KavitaPlus/Metadata/SeriesCharacter.cs | 2 +- .../KavitaPlus/Metadata/SeriesRelationship.cs | 8 +- .../DTOs/Koreader/KoreaderBookDto.cs | 4 +- .../Koreader/KoreaderProgressUpdateDto.cs | 2 +- {API => Kavita.Models}/DTOs/LibraryDto.cs | 6 +- {API => Kavita.Models}/DTOs/MangaFileDto.cs | 4 +- .../DTOs/MediaErrors/MediaErrorDto.cs | 2 +- .../DTOs/Metadata/AgeRatingDto.cs | 4 +- .../DTOs/Metadata/Browse/BrowseGenreDto.cs | 2 +- .../DTOs/Metadata/Browse/BrowsePersonDto.cs | 4 +- .../DTOs/Metadata/Browse/BrowseTagDto.cs | 2 +- .../Requests/BrowseAnnotationFilterDto.cs | 6 +- .../Browse/Requests/BrowsePersonFilterDto.cs | 6 +- .../DTOs/Metadata/ChapterMetadataDto.cs | 6 +- .../DTOs/Metadata/GenreTagDto.cs | 2 +- .../Matching/ExternalSeriesMatchDto.cs | 4 +- .../DTOs/Metadata/Matching/MatchSeriesDto.cs | 2 +- .../DTOs/Metadata/PublicationStatusDto.cs | 4 +- .../DTOs/Metadata/TagDto.cs | 2 +- .../DTOs/Misc/ParseBulkRequestDto.cs | 4 +- .../DTOs/Misc/ParseBulkResponseDto.cs | 2 +- .../DTOs/Misc/ParseResultDto.cs | 2 +- .../DTOs/OPDS/Internal/Feed.cs | 3 +- .../DTOs/OPDS/Internal/FeedAuthor.cs | 2 +- .../DTOs/OPDS/Internal/FeedCategory.cs | 2 +- .../DTOs/OPDS/Internal/FeedEntry.cs | 2 +- .../DTOs/OPDS/Internal/FeedEntryContent.cs | 2 +- .../DTOs/OPDS/Internal/FeedLink.cs | 2 +- .../DTOs/OPDS/Internal/FeedLinkRelation.cs | 2 +- .../DTOs/OPDS/Internal/FeedLinkType.cs | 2 +- .../OPDS/Internal/OpenSearchDescription.cs | 2 +- .../DTOs/OPDS/Internal/SearchLink.cs | 2 +- .../DTOs/OPDS/Requests/IOpdsPagination.cs | 2 +- .../DTOs/OPDS/Requests/IOpdsRequest.cs | 4 +- .../DTOs/OPDS/Requests/OpdsCatalogeRequest.cs | 4 +- .../OpdsItemsFromCompoundEntityIdsRequest.cs | 4 +- .../DTOs/OPDS/Requests/OpdsSearchRequest.cs | 4 +- .../OpdsSmartFilterCatalogueRequest.cs | 4 +- .../OPDS/Requests/OpdsSmartFilterRequest.cs | 4 +- .../DTOs/Person/PersonAliasCheckDto.cs | 2 +- .../DTOs/Person/PersonDto.cs | 4 +- .../DTOs/Person/PersonMergeDto.cs | 2 +- .../DTOs/Person/UpdatePersonDto.cs | 2 +- .../DTOs/Progress/ClientDeviceDto.cs | 2 +- .../DTOs/Progress/ClientInfoDto.cs | 7 +- .../DTOs/Progress/DailyReadingDataDto.cs | 7 +- .../DTOs/Progress/FullProgressDto.cs | 2 +- .../DTOs/Progress/ProgressDto.cs | 2 +- .../DTOs/Progress/ReadingActivityDataDto.cs | 2 +- .../DTOs/Progress/ReadingSessionDto.cs | 2 +- {API => Kavita.Models}/DTOs/RatingDto.cs | 7 +- .../DTOs/Reader/AnnotationDto.cs | 7 +- .../DTOs/Reader/BookChapterItem.cs | 2 +- .../DTOs/Reader/BookInfoDto.cs | 4 +- .../DTOs/Reader/BookResourceResultDto.cs | 2 +- .../DTOs/Reader/BookmarkDto.cs | 2 +- .../DTOs/Reader/BookmarkInfoDto.cs | 4 +- .../Reader/BulkRemoveBookmarkForSeriesDto.cs | 2 +- .../DTOs/Reader/ChapterInfoDto.cs | 4 +- .../DTOs/Reader/CreatePersonalToCDto.cs | 2 +- .../DTOs/Reader/FileDimensionDto.cs | 2 +- .../DTOs/Reader/HourEstimateRangeDto.cs | 2 +- .../DTOs/Reader/IChapterInfoDto.cs | 4 +- .../Reader/MarkMultipleSeriesAsReadDto.cs | 2 +- .../DTOs/Reader/MarkReadDto.cs | 2 +- .../DTOs/Reader/MarkVolumeReadDto.cs | 2 +- .../DTOs/Reader/MarkVolumesReadDto.cs | 2 +- .../DTOs/Reader/PersonalToCDto.cs | 2 +- .../DTOs/Reader/ReReadDto.cs | 4 +- .../DTOs/Reader/RemoveBookmarkForSeriesDto.cs | 2 +- .../DTOs/ReadingLists/CBL/CblBook.cs | 3 +- .../DTOs/ReadingLists/CBL/CblConflictsDto.cs | 2 +- .../DTOs/ReadingLists/CBL/CblImportSummary.cs | 2 +- .../DTOs/ReadingLists/CBL/CblReadingList.cs | 2 +- .../DTOs/ReadingLists/CreateReadingListDto.cs | 2 +- .../ReadingLists/DeleteReadingListsDto.cs | 2 +- .../ReadingLists/PromoteReadingListsDto.cs | 2 +- .../DTOs/ReadingLists/ReadingListCast.cs | 4 +- .../DTOs/ReadingLists/ReadingListDto.cs | 6 +- .../DTOs/ReadingLists/ReadingListInfoDto.cs | 4 +- .../DTOs/ReadingLists/ReadingListItemDto.cs | 4 +- .../UpdateReadingListByChapterDto.cs | 2 +- .../UpdateReadingListByMultipleDto.cs | 2 +- .../UpdateReadingListByMultipleSeriesDto.cs | 2 +- .../UpdateReadingListBySeriesDto.cs | 2 +- .../UpdateReadingListByVolumeDto.cs | 2 +- .../DTOs/ReadingLists/UpdateReadingListDto.cs | 2 +- .../ReadingLists/UpdateReadingListPosition.cs | 2 +- .../DTOs/Recommendation/ExternalSeriesDto.cs | 6 +- .../DTOs/Recommendation/MetadataTagDto.cs | 2 +- .../DTOs/Recommendation/RecommendationDto.cs | 2 +- .../DTOs/Recommendation/SeriesStaffDto.cs | 2 +- .../DTOs/RefreshSeriesDto.cs | 2 +- {API => Kavita.Models}/DTOs/RegisterDto.cs | 2 +- {API => Kavita.Models}/DTOs/ScanFolderDto.cs | 2 +- .../DTOs/Scrobbling/MalUserInfoDto.cs | 2 +- .../DTOs/Scrobbling/MediaRecommendationDto.cs | 4 +- .../DTOs/Scrobbling/PlusSeriesDto.cs | 2 +- .../DTOs/Scrobbling/ScrobbleDto.cs | 2 +- .../DTOs/Scrobbling/ScrobbleErrorDto.cs | 2 +- .../DTOs/Scrobbling/ScrobbleEventDto.cs | 2 +- .../DTOs/Scrobbling/ScrobbleHoldDto.cs | 2 +- .../DTOs/Scrobbling/ScrobbleResponseDto.cs | 2 +- .../DTOs/Search/BookmarkSearchResultDto.cs | 2 +- .../DTOs/Search/SearchResultDto.cs | 4 +- .../DTOs/Search/SearchResultGroupDto.cs | 12 +- {API => Kavita.Models}/DTOs/SeriesByIdsDto.cs | 2 +- .../SeriesDetail/NextExpectedChapterDto.cs | 2 +- .../SeriesDetail/RecentlyAddedSeriesDto.cs | 10 +- .../DTOs/SeriesDetail/RelatedSeriesDto.cs | 2 +- .../DTOs/SeriesDetail/SeriesDetailDto.cs | 2 +- .../DTOs/SeriesDetail/SeriesDetailPlusDto.cs | 6 +- .../SeriesDetail/UpdateRelatedSeriesDto.cs | 2 +- .../DTOs/SeriesDetail/UpdateUserReviewDto.cs | 2 +- .../DTOs/SeriesDetail/UserReviewDto.cs | 5 +- .../SeriesDetail/UserReviewExtendedDto.cs | 4 +- {API => Kavita.Models}/DTOs/SeriesDto.cs | 6 +- .../DTOs/SeriesMetadataDto.cs | 8 +- .../DTOs/Settings/AuthorityValidationDto.cs | 2 +- .../DTOs/Settings/ImportFieldMappingsDto.cs | 4 +- .../DTOs/Settings/OidcConfigDto.cs | 4 +- .../DTOs/Settings/OidcPublicConfigDto.cs | 2 +- .../DTOs/Settings/SMTPConfigDto.cs | 2 +- .../DTOs/Settings/ServerSettingDTO.cs | 5 +- .../BulkUpdateSideNavStreamVisibilityDto.cs | 2 +- .../DTOs/SideNav/ExternalSourceDto.cs | 2 +- .../DTOs/SideNav/SideNavStreamDto.cs | 4 +- .../DTOs}/SignalR/MessageFactory.cs | 32 +- .../DTOs}/SignalR/ProgressEventType.cs | 2 +- .../DTOs}/SignalR/ProgressType.cs | 2 +- .../DTOs}/SignalR/SignalRMessage.cs | 2 +- .../DTOs/StandaloneChapterDto.cs | 4 +- .../DTOs/Statistics/BreakDownDto.cs | 2 +- .../DTOs/Statistics/Count.cs | 2 +- .../Statistics/FileExtensionBreakdownDto.cs | 4 +- .../DTOs/Statistics/ICount.cs | 2 +- .../DTOs/Statistics/MostActiveUserDto.cs | 2 +- .../DTOs/Statistics/MostReadAuthorsDto.cs | 2 +- .../DTOs/Statistics/PagesReadOnADayCount.cs | 4 +- .../DTOs/Statistics/ProfileStatBarDto.cs | 2 +- .../DTOs/Statistics/ReadHistoryEvent.cs | 2 +- .../DTOs/Statistics/ReadTimeByHourDto.cs | 2 +- .../Statistics/ReadingActivityGraphDto.cs | 2 +- .../DTOs/Statistics/ReadingHistoryItemDto.cs | 4 +- .../DTOs/Statistics/ReadingPaceDto.cs | 2 +- .../DTOs/Statistics/ServerStatisticsDto.cs | 2 +- .../DTOs/Statistics/SpreadStatsDto.cs | 2 +- .../DTOs/Statistics/StatBucketDto.cs | 2 +- .../DTOs/Statistics/StatsFilterDto.cs | 2 +- .../DTOs/Statistics/TopReadsDto.cs | 2 +- .../DTOs/Statistics/UserReadStatistics.cs | 2 +- .../DTOs/Statistics/YearMonthGroupingDto.cs | 2 +- .../DTOs/Stats/FileExtensionExportDto.cs | 2 +- .../DTOs/Stats/ServerInfoSlimDto.cs | 2 +- .../ClientDevice/DeviceClientBreakdownDto.cs | 6 +- .../DTOs/Stats/V3/LibraryStatV3.cs | 4 +- .../DTOs/Stats/V3/RelationshipStatV3.cs | 4 +- .../DTOs/Stats/V3/ServerInfoV3Dto.cs | 4 +- .../DTOs/Stats/V3/UserStatV3.cs | 8 +- .../DTOs/System/DirectoryDto.cs | 2 +- .../DTOs/TachiyomiChapterDto.cs | 2 +- .../DTOs/Theme/ColorScapeDto.cs | 2 +- .../DTOs/Theme/DownloadableSiteThemeDto.cs | 2 +- .../DTOs/Theme/SiteThemeDto.cs | 5 +- .../DTOs/Theme/UpdateDefaultThemeDto.cs | 2 +- .../DTOs/Update/UpdateNotificationDto.cs | 2 +- .../DTOs/UpdateChapterDto.cs | 8 +- .../DTOs/UpdateLibraryDto.cs | 6 +- .../DTOs/UpdateLibraryForUserDto.cs | 2 +- {API => Kavita.Models}/DTOs/UpdateRBSDto.cs | 2 +- .../DTOs/UpdateRatingDto.cs | 2 +- .../DTOs/UpdateSeriesDto.cs | 2 +- .../DTOs/UpdateSeriesMetadataDto.cs | 2 +- .../DTOs/Uploads/UploadFileDto.cs | 2 +- .../DTOs/Uploads/UploadUrlDto.cs | 2 +- {API => Kavita.Models}/DTOs/UserDto.cs | 13 +- .../DTOs/UserPreferencesDto.cs | 10 +- .../DTOs/UserReadingProfileDto.cs | 9 +- {API => Kavita.Models}/DTOs/VolumeDto.cs | 24 +- .../DTOs/WantToRead/UpdateWantToReadDto.cs | 2 +- Kavita.Models/Defaults.cs | 253 ++ .../Entities}/AgeRestriction.cs | 4 +- {API => Kavita.Models}/Entities/Chapter.cs | 78 +- .../Entities/CollectionTag.cs | 4 +- {API => Kavita.Models}/Entities/Device.cs | 7 +- .../Entities/EmailHistory.cs | 5 +- .../Entities/Enums/AgeRating.cs | 2 +- .../Entities/Enums/BookPageLayoutMode.cs | 2 +- .../Entities/Enums/ClientDevicePlatform.cs | 2 +- .../Entities/Enums/ClientDeviceType.cs | 2 +- .../Entities/Enums/CoverImageSize.cs | 2 +- .../Entities/Enums/DashboardStreamType.cs | 2 +- .../Entities/Enums/Device/DevicePlatform.cs | 2 +- .../Entities/Enums/EncodeFormat.cs | 2 +- .../Enums/EpubPageCalculationMethod.cs | 2 +- .../Entities/Enums/FileTypeGroup.cs | 2 +- .../Entities/Enums/Font/FontProvider.cs | 2 +- .../Entities/Enums/IdentityProvider.cs | 2 +- .../Entities/Enums/LayoutMode.cs | 2 +- .../Entities/Enums/LibraryType.cs | 2 +- .../Entities/Enums/MangaFormat.cs | 2 +- .../Entities/Enums/MediaErrorProducer.cs | 7 + .../Entities/Enums/MetadataFieldType.cs | 2 +- .../Entities/Enums/PageSplitOption.cs | 2 +- .../Entities/Enums/PdfRenderResolution.cs | 2 +- .../Enums/PdfRenderResolutionExtensions.cs | 2 +- .../Entities/Enums/PersonRole.cs | 2 +- .../Entities/Enums/PublicationStatus.cs | 2 +- .../Entities/Enums/RatingAuthority.cs | 2 +- .../Entities/Enums/ReaderMode.cs | 2 +- .../Entities/Enums/ReadingDirection.cs | 2 +- .../Entities/Enums/ReadingProfileKind.cs | 2 +- .../Entities/Enums/RelationKind.cs | 2 +- .../Entities/Enums/ScalingOption.cs | 2 +- .../Entities/Enums/ScrobbleProvider.cs | 20 + .../Entities/Enums/ServerSettingKey.cs | 2 +- .../Entities/Enums/SyncKey.cs | 2 +- .../Entities/Enums/Theme/ThemeProvider.cs | 2 +- .../Entities/Enums/User/AuthKeyProvider.cs | 2 +- .../UserPreferences/AppUserOpdsPreferences.cs | 2 +- .../AppUserSocialPreferences.cs | 2 +- .../Entities/Enums/UserPreferences/KeyBind.cs | 2 +- .../Enums/UserPreferences/KeyBindTarget.cs | 2 +- .../Enums/UserPreferences/PageLayoutMode.cs | 2 +- .../Enums/UserPreferences/PdfBookMode.cs | 2 +- .../Enums/UserPreferences/PdfScrollMode.cs | 2 +- .../Enums/UserPreferences/PdfSpreadMode.cs | 2 +- .../Enums/UserPreferences/PdfTheme.cs | 2 +- .../Entities/Enums/WritingStyle.cs | 2 +- {API => Kavita.Models}/Entities/EpubFont.cs | 9 +- {API => Kavita.Models}/Entities/FolderPath.cs | 2 +- {API => Kavita.Models}/Entities/Genre.cs | 4 +- .../Entities/HighlightSlot.cs | 2 +- .../Entities/History/KavitaPlusHistory.cs | 2 +- .../History/ManualMigrationHistory.cs | 2 +- .../Entities/Interfaces/IEntityDate.cs | 2 +- .../Interfaces/IHasConcurrencyToken.cs | 2 +- .../Entities/Interfaces/IHasCoverImage.cs | 2 +- .../Entities/Interfaces/IHasKPlusMetadata.cs | 4 +- .../Interfaces/IHasReadTimeEstimate.cs | 5 +- .../Entities/Interfaces/ITheme.cs | 4 +- {API => Kavita.Models}/Entities/Library.cs | 7 +- .../Entities/LibraryExcludedGlob.cs | 2 +- .../Entities/LibraryFileTypeGroup.cs | 4 +- {API => Kavita.Models}/Entities/MangaFile.cs | 6 +- {API => Kavita.Models}/Entities/MediaError.cs | 4 +- .../Entities/Metadata/ExternalRating.cs | 5 +- .../Metadata/ExternalRecommendation.cs | 4 +- .../Entities/Metadata/ExternalReview.cs | 5 +- .../Metadata/ExternalSeriesMetadata.cs | 2 +- .../Entities/Metadata/SeriesBlacklist.cs | 2 +- .../Entities/Metadata/SeriesMetadata.cs | 10 +- .../Entities/Metadata/SeriesRelation.cs | 4 +- .../MetadataMatching/MetadataFieldMapping.cs | 6 +- .../MetadataMatching/MetadataSettingField.cs | 2 +- .../MetadataMatching/MetadataSettings.cs | 4 +- .../Entities/Person/ChapterPeople.cs | 4 +- .../Entities/Person/Person.cs | 4 +- .../Entities/Person/PersonAlias.cs | 2 +- .../Entities/Person/SeriesMetadataPeople.cs | 6 +- .../Entities/Progress/AppUserProgress.cs | 5 +- .../Progress/AppUserReadingHistory.cs | 5 +- .../Progress/AppUserReadingSession.cs | 6 +- .../AppUserReadingSessionActivityData.cs | 7 +- .../Entities/Progress/ClientInfoData.cs | 5 +- .../Entities/ReadingList.cs | 7 +- .../Entities/ReadingListItem.cs | 2 +- .../Entities/Scrobble/ScrobbleError.cs | 4 +- .../Entities/Scrobble/ScrobbleEvent.cs | 7 +- .../Entities/Scrobble/ScrobbleEventFilter.cs | 2 +- .../Scrobble/ScrobbleEventSortField.cs | 2 +- .../Entities/Scrobble/ScrobbleHold.cs | 5 +- {API => Kavita.Models}/Entities/Series.cs | 11 +- .../Entities/ServerSetting.cs | 6 +- .../Entities/ServerStatistics.cs | 2 +- .../Entities/SideNavStreamType.cs | 2 +- {API => Kavita.Models}/Entities/SiteTheme.cs | 18 +- {API => Kavita.Models}/Entities/Tag.cs | 4 +- .../Entities/User/AppRole.cs | 2 +- .../Entities/User/AppUser.cs | 14 +- .../Entities/User/AppUserAnnotation.cs | 4 +- .../Entities/User/AppUserAuthKey.cs | 4 +- .../Entities/User/AppUserBookmark.cs | 9 +- .../Entities/User/AppUserChapterRating.cs | 4 +- .../Entities/User/AppUserCollection.cs | 8 +- .../Entities/User/AppUserDashboardStream.cs | 5 +- .../Entities/User/AppUserExternalSource.cs | 2 +- .../Entities/User/AppUserOnDeckRemoval.cs | 2 +- .../Entities/User/AppUserPreferences.cs | 12 +- .../Entities/User/AppUserRating.cs | 7 +- .../Entities/User/AppUserReadingProfile.cs | 9 +- .../Entities/User/AppUserRole.cs | 2 +- .../Entities/User/AppUserSideNavStream.cs | 2 +- .../Entities/User/AppUserSmartFilter.cs | 4 +- .../Entities/User/AppUserTableOfContent.cs | 4 +- .../Entities/User/AppUserWantToRead.cs | 2 +- .../Entities/User/ClientDevice.cs | 5 +- .../Entities/User/ClientDeviceHistory.cs | 5 +- {API => Kavita.Models}/Entities/Volume.cs | 4 +- Kavita.Models/Extensions/AppUserExtensions.cs | 53 + .../ApplicationServiceExtensions.cs | 11 + .../Extensions/EncodeFormatExtensions.cs | 7 +- .../Extensions/EnumerableExtensions.cs | 47 + .../Extensions/FilterDtoExtensions.cs | 9 +- .../Extensions/PlusMediaFormatExtensions.cs | 6 +- .../Helpers/OrderableHelper.cs | 7 +- Kavita.Models/Kavita.Models.csproj | 21 + .../Metadata/ComicInfo.cs | 102 +- .../Scanner => Kavita.Models/Misc}/Chunk.cs | 2 +- Kavita.Models/Parser/ParseScannedFiles.cs | 78 + .../Parser/ParserInfo.cs | 39 +- .../Helpers/BrowserHelperTests.cs | 8 +- .../Kavita.Server.Tests.csproj | 34 + .../MigrateSmartFilterEncodingTests.cs | 36 + .../Middleware/ClientInfoMiddlewareTests.cs | 17 +- .../Assets/anilist-no-image-placeholder.jpg | Bin .../Attributes}/DisallowRoleAttribute.cs | 6 +- .../Attributes/EntityAccessAttribute.cs | 150 + Kavita.Server/Attributes/KPlusAttribute.cs | 35 + .../Attributes}/ProfilePrivacyAttribute.cs | 8 +- .../Controllers/AccountController.cs | 641 ++- .../Controllers/ActivityController.cs | 8 +- .../Controllers/AdminController.cs | 8 +- .../Controllers/AnnotationController.cs | 28 +- .../Controllers/BaseApiController.cs | 6 +- .../Controllers/BookController.cs | 97 +- .../Controllers/CBLController.cs | 77 +- .../Controllers/ChapterController.cs | 168 +- .../Controllers/CollectionController.cs | 153 +- .../Controllers/ColorScapeController.cs | 29 +- .../Controllers/DeprecatedController.cs | 117 +- Kavita.Server/Controllers/DeviceController.cs | 246 ++ .../Controllers/DownloadController.cs | 165 +- Kavita.Server/Controllers/EmailController.cs | 20 + .../Controllers/FallbackController.cs | 24 + .../Controllers/FilterController.cs | 95 +- .../Controllers/FontController.cs | 74 +- .../Controllers/HealthController.cs | 8 +- Kavita.Server/Controllers/ImageController.cs | 263 ++ .../Controllers/KoreaderController.cs | 31 +- .../Controllers/LibraryController.cs | 323 +- .../Controllers/LicenseController.cs | 29 +- .../Controllers/LocaleController.cs | 28 +- .../Controllers/ManageController.cs | 35 +- .../Controllers/MetadataController.cs | 44 +- .../Controllers/OPDSController.cs | 241 +- .../Controllers/OidcController.cs | 18 +- .../Controllers/PanelsController.cs | 31 +- .../Controllers/PersonController.cs | 144 +- .../Controllers/PluginController.cs | 24 +- .../Controllers/RatingController.cs | 56 +- .../Controllers/ReaderController.cs | 441 +- .../Controllers/ReadingListController.cs | 272 +- .../Controllers/ReadingProfileController.cs | 37 +- .../Controllers/ReviewController.cs | 83 +- .../Controllers/ScrobblingController.cs | 125 +- .../Controllers/SearchController.cs | 52 +- .../Controllers/SeriesController.cs | 230 +- .../Controllers/ServerController.cs | 127 +- .../Controllers/SettingsController.cs | 153 +- .../Controllers/StatsController.cs | 62 +- .../Controllers/StreamController.cs | 68 +- .../Controllers/TachiyomiController.cs | 38 +- .../Controllers/ThemeController.cs | 63 +- .../Controllers/UploadController.cs | 38 +- .../Controllers/UsersController.cs | 107 +- Kavita.Server/Controllers/VolumeController.cs | 74 + .../Controllers/WantToReadController.cs | 80 +- .../EmailTemplates/AuthKeyExpired.html | 0 .../AuthKeyExpiredFragment.html | 0 .../AuthKeyExpiringFragment.html | 0 .../EmailTemplates/AuthKeyExpiringSoon.html | 0 .../EmailTemplates/EmailChange.html | 0 .../EmailTemplates/EmailConfirm.html | 0 .../EmailTemplates/EmailPasswordReset.html | 0 .../EmailTemplates/EmailTest.html | 0 .../EmailTemplates/KavitaPlusDebug.html | 0 .../EmailTemplates/SendToDevice.html | 0 .../EmailTemplates/TokenExpiration.html | 0 .../EmailTemplates/TokenExpiringSoon.html | 0 .../EmailTemplates/base.html | 0 .../ApplicationServiceExtensions.cs | 62 + .../Extensions/HttpExtensions.cs | 7 +- .../Extensions/IdentityServiceExtensions.cs | 22 +- .../Helpers/BrowserHelper.cs | 5 +- .../Helpers/OpenIdConnectEventsHelper.cs | 8 +- {API => Kavita.Server}/I18N/ar.json | 0 {API => Kavita.Server}/I18N/as.json | 0 {API => Kavita.Server}/I18N/ca.json | 0 {API => Kavita.Server}/I18N/cs.json | 0 {API => Kavita.Server}/I18N/da.json | 0 {API => Kavita.Server}/I18N/de.json | 0 {API => Kavita.Server}/I18N/el.json | 0 {API => Kavita.Server}/I18N/en.json | 0 {API => Kavita.Server}/I18N/es.json | 0 {API => Kavita.Server}/I18N/et.json | 0 {API => Kavita.Server}/I18N/fa.json | 0 {API => Kavita.Server}/I18N/fi.json | 0 {API => Kavita.Server}/I18N/fr.json | 0 {API => Kavita.Server}/I18N/ga.json | 0 {API => Kavita.Server}/I18N/he.json | 0 {API => Kavita.Server}/I18N/hi.json | 0 {API => Kavita.Server}/I18N/hr.json | 0 {API => Kavita.Server}/I18N/hu.json | 0 {API => Kavita.Server}/I18N/id.json | 0 {API => Kavita.Server}/I18N/it.json | 0 {API => Kavita.Server}/I18N/ja.json | 0 {API => Kavita.Server}/I18N/ko.json | 0 {API => Kavita.Server}/I18N/lt.json | 0 {API => Kavita.Server}/I18N/ms.json | 0 {API => Kavita.Server}/I18N/nb_NO.json | 0 {API => Kavita.Server}/I18N/nl.json | 0 {API => Kavita.Server}/I18N/pl.json | 0 {API => Kavita.Server}/I18N/pt.json | 0 {API => Kavita.Server}/I18N/pt_BR.json | 0 {API => Kavita.Server}/I18N/ru.json | 0 {API => Kavita.Server}/I18N/sk.json | 0 {API => Kavita.Server}/I18N/sl.json | 0 {API => Kavita.Server}/I18N/sv.json | 0 {API => Kavita.Server}/I18N/ta.json | 0 {API => Kavita.Server}/I18N/te.json | 0 {API => Kavita.Server}/I18N/th.json | 0 {API => Kavita.Server}/I18N/tr.json | 0 {API => Kavita.Server}/I18N/uk.json | 0 {API => Kavita.Server}/I18N/vi.json | 0 {API => Kavita.Server}/I18N/zh_Hans.json | 0 {API => Kavita.Server}/I18N/zh_Hant.json | 0 Kavita.Server/Kavita.Server.csproj | 162 + {API => Kavita.Server}/Logging/LogEnricher.cs | 2 +- .../Logging/LogLevelOptions.cs | 4 +- Kavita.Server/Logging/LoggingService.cs | 11 + .../ManualMigrations}/ManualMigration.cs | 5 +- .../MigrateLibrariesToHaveAllFileTypes.cs | 13 +- .../v0.7.11/MigrateSmartFilterEncoding.cs | 11 +- ...igrateClearNightlyExternalSeriesRecords.cs | 9 +- .../v0.7.14/MigrateEmailTemplates.cs | 4 +- .../v0.7.14/MigrateManualHistory.cs | 5 +- .../v0.7.14/MigrateVolumeLookupName.cs | 6 +- .../v0.7.14/MigrateVolumeNumber.cs | 7 +- .../v0.7.14/MigrateWantToReadExport.cs | 6 +- .../v0.7.14/MigrateWantToReadImport.cs | 11 +- .../v0.7.9/MigrateUserLibrarySideNavStream.cs | 15 +- .../v0.8.0/ManualMigrateLooseLeafChapters.cs | 20 +- .../v0.8.0/ManualMigrateMixedSpecials.cs | 20 +- .../v0.8.0/MigrateChapterFields.cs | 28 +- .../v0.8.0/MigrateChapterNumber.cs | 13 +- .../v0.8.0/MigrateChapterRange.cs | 11 +- .../MigrateCollectionTagToUserCollections.cs | 14 +- .../v0.8.0/MigrateDuplicateDarkTheme.cs | 5 +- .../v0.8.0/MigrateMangaFilePath.cs | 7 +- .../v0.8.0/MigrateProgressExport.cs | 7 +- .../v0.8.1/MigrateLowestSeriesFolderPath.cs | 6 +- .../v0.8.2/ManualMigrateSwitchToWal.cs | 5 +- .../v0.8.2/ManualMigrateThemeDescription.cs | 8 +- .../v0.8.2/MigrateInitialInstallData.cs | 9 +- .../v0.8.2/MigrateSeriesLowestFolderPath.cs | 9 +- .../v0.8.4/ManualMigrateEncodeSettings.cs | 7 +- .../v0.8.4/ManualMigrateRemovePeople.cs | 5 +- .../ManualMigrateUnscrobbleBookLibraries.cs | 7 +- .../v0.8.4/MigrateLowestSeriesFolderPath2.cs | 6 +- .../ManualMigrateBlacklistTableToSeries.cs | 8 +- .../ManualMigrateInvalidBlacklistSeries.cs | 5 +- .../v0.8.5/ManualMigrateNeedsManualMatch.cs | 9 +- .../v0.8.5/ManualMigrateScrobbleErrors.cs | 5 +- .../v0.8.5/MigrateProgressExport.cs | 7 +- .../v0.8.6/ManualMigrateScrobbleEventGen.cs | 5 +- .../v0.8.6/ManualMigrateScrobbleSpecials.cs | 7 +- .../v0.8.7/ManualMigrateReadingProfiles.cs | 11 +- .../ManualMigrateBookReadingProgress.cs | 6 +- ...ualMigrateEnableMetadataMatchingDefault.cs | 6 +- .../v0.8.9/MigrateBadKoreaderProgress.cs | 4 +- .../v0.8.9/MigrateFormatToActivityData.cs | 6 +- .../MigrateIncorrectUtcMidnightRollovers.cs | 4 +- .../MigrateMissingAppUserRatingDateColumns.cs | 4 +- .../v0.8.9/MigrateMissingCreatedUtcDate.cs | 6 +- .../MigrateProgressToReadingSessions.cs | 14 +- .../v0.8.9/MigrateToAuthKeys.cs | 15 +- .../v0.8.9/MigrateTotalReads.cs | 9 +- .../AuthKeyAuthenticationHandler.cs | 11 +- .../AuthenticationRateLimiterPolicy.cs | 3 +- .../Middleware}/ClientInfoAccessor.cs | 27 +- .../Middleware/ClientInfoMiddleware.cs | 16 +- .../Middleware/DeviceTrackingMiddleware.cs | 17 +- .../Middleware/ExceptionMiddleware.cs | 6 +- .../Middleware/SecurityMiddleware.cs | 6 +- .../UpdateUserAsActiveMiddleware.cs | 8 +- .../Middleware/UserContextMiddleware.cs | 12 +- {API => Kavita.Server}/Program.cs | 23 +- Kavita.Server/Properties/launchSettings.json | 32 + {API => Kavita.Server}/Startup.cs | 55 +- .../Store/CustomTicketStore.cs | 2 +- Kavita.Server/Store/UserContext.cs | 64 + .../config/appsettings.Development.json | 0 .../config/appsettings.json | 0 .../config/templates/EmailChange.html | 0 .../config/templates/EmailConfirm.html | 0 .../config/templates/EmailMigration.html | 0 .../config/templates/EmailPasswordReset.html | 0 .../config/templates/EmailTest.html | 0 .../config/templates/SendToDevice.html | 0 .../config/templates/TokenExpiration.html | 0 .../config/templates/TokenExpiringSoon.html | 0 .../AccountServiceTests.cs | 28 +- .../AnnotationServiceTests.cs | 29 +- .../ArchiveServiceTests.cs | 46 +- .../BackupServiceTests.cs | 33 +- .../BookServiceTests.cs | 37 +- .../BookmarkServiceTests.cs | 33 +- .../Cache}/FakeHybridCache.cs | 9 +- .../Cache}/FakeHybridCacheWithTracking.cs | 8 +- .../CacheServiceTests.cs | 21 +- .../CleanupServiceTests.cs | 60 +- .../ClientDeviceServiceTests.cs | 138 +- .../CollectionTagServiceTests.cs | 40 +- .../Comparers/ChapterSortComparerTest.cs | 18 + .../ChapterSortComparerZeroFirstTests.cs | 6 +- .../Comparers/SortComparerZeroLastTests.cs | 16 + .../CoverDbServiceTests.cs | 27 +- .../Data/AesopsFables.epub | Bin .../DeviceServiceTests.cs | 19 +- .../DeviceTrackingServiceTests.cs | 22 +- .../DirectoryServiceTests.cs | 51 +- .../Entities/ComicInfoTests.cs | 12 +- .../EntityNamingServiceTests.cs | 14 +- .../Extensions/ChapterListExtensionsTests.cs | 48 +- .../Extensions/FilterDtoExtensionsTests.cs | 11 +- .../ParserInfoListExtensionsTests.cs | 22 +- .../Extensions/SeriesExtensionsTests.cs | 15 +- .../Extensions/SeriesFilterTests.cs | 35 +- .../Extensions/VolumeListExtensionsTests.cs | 61 +- .../ExternalMetadataServiceTests.cs | 48 +- .../FileSystemTests.cs | 8 +- .../Helpers/BookSortTitlePrefixHelperTests.cs | 5 +- .../Helpers/CacheHelperTests.cs | 17 +- .../Helpers/KoreaderHelperTests.cs | 8 +- .../Helpers/OrderableHelperTests.cs | 11 +- .../Helpers/ParserInfoFactory.cs | 12 +- .../Helpers/PersonHelperTests.cs | 14 +- .../Helpers/ReviewHelperTests.cs | 127 + .../Helpers/ScannerHelper.cs | 46 +- .../Helpers/SeriesHelperTests.cs | 17 +- .../Helpers/SmartFilterHelperTests.cs | 44 +- .../Helpers/TestCaseGenerator.cs | 4 +- .../ImageServiceTests.cs | 14 +- .../Kavita.Services.Tests.csproj | 42 + .../MetadataServiceTests.cs | 15 +- .../OidcServiceTests.cs | 29 +- .../OpdsServiceTests.cs | 51 +- .../ParseScannedFilesTests.cs | 29 +- .../Parsers/BasicParserTests.cs | 12 +- .../Parsers/BookParserTests.cs | 9 +- .../Parsers/ComicVineParserTests.cs | 10 +- .../Parsers/DefaultParserTests.cs | 12 +- .../Parsers/ImageParserTests.cs | 8 +- .../Parsers/PdfParserTests.cs | 8 +- .../Parsing/BookParsingTests.cs | 10 +- .../Parsing/ComicParsingTests.cs | 7 +- .../Parsing/ImageParsingTests.cs | 11 +- .../Parsing/MangaParsingTests.cs | 7 +- .../Parsing/ParserInfoTests.cs | 9 +- .../Parsing/ParsingTests.cs | 7 +- .../PersonServiceTests.cs | 18 +- .../ProcessSeriesTests.cs | 4 +- .../RatingServiceTests.cs | 18 +- .../ReaderServiceRereadTests.cs | 28 +- .../ReaderServiceTests.cs | 284 +- .../ReadingHistoryServiceTests.cs | 26 +- .../ReadingListServiceTests.cs | 66 +- .../ReadingProfileServiceTest.cs | 25 +- .../ScannerServiceTests.cs | 37 +- .../ScrobblingServiceTests.cs | 39 +- .../SeriesServiceTests.cs | 45 +- .../SettingsServiceTests.cs | 28 +- .../SiteThemeServiceTests.cs | 18 +- .../TachiyomiServiceTests.cs | 58 +- .../ArchiveService/Archives/LICENSE.md | 0 .../ArchiveService/Archives/empty.zip | Bin .../Archives/file in folder in folder.zip | Bin .../Archives/file in folder.zip | Bin .../Archives/file in folder_alt.zip | Bin .../ArchiveService/Archives/flat file.zip | Bin .../ArchiveService/Archives/macos_native.zip | Bin .../ArchiveService/Archives/macos_none.zip | Bin .../ArchiveService/Archives/macos_one.zip | Bin .../Archives/macos_withdotunder_one.zip | Bin .../ArchiveService/Archives/winrar.rar | Bin .../ArchiveService/ComicInfos/ComicInfo.xml | 0 .../ArchiveService/ComicInfos/ComicInfo.zip | Bin .../ArchiveService/ComicInfos/ComicInfo2.zip | Bin .../ComicInfos/ComicInfo_authors.zip | Bin .../ComicInfos/ComicInfo_duplicateInfos.rar | Bin .../ComicInfos/ComicInfo_duplicateInfos.zip | Bin .../ComicInfo_duplicateInfos_reversed.zip | Bin .../ComicInfos/ComicInfo_outside_root.zip | Bin .../ComicInfo_outside_root_SharpCompress.cb7 | Bin .../ArchiveService/ComicInfos/Umlaut.zip | Bin .../ComicInfos/file in folder.zip | Bin .../CoverImages/macos_native.png | Bin .../CoverImages/macos_native.zip | Bin .../CoverImages/sorting.expected.png | Bin .../ArchiveService/CoverImages/sorting.zip | Bin .../CoverImages/test.expected.jpg | Bin .../ArchiveService/CoverImages/test.zip | Bin .../CoverImages/thumbnail.expected.jpg | Bin .../ArchiveService/CoverImages/thumbnail.jpg | Bin .../CoverImages/v10 - duplicate covers.cbz | Bin .../v10 - duplicate covers.expected.png | Bin .../CoverImages/v10 - nested folder.cbz | Bin .../v10 - nested folder.expected.old.png | Bin .../v10 - nested folder.expected.png | Bin .../CoverImages/v10 - with folder.cbz | Bin .../v10 - with folder.expected.jpg | Bin .../v10 - with folder.expected.png | Bin .../ArchiveService/CoverImages/v10.cbz | Bin .../CoverImages/v10.expected.png | Bin .../Formats/One File with DB_Supported.zip | Bin .../ArchiveService/Thumbnails/001.jpg | Bin ...tions_Chromatiques_de_concert_Theme_A4.pdf | Bin .../BookService/Relative Key Test File.epub | Bin .../BookService/Rollo at Work SP01.pdf | Bin ... Floes A Story of the Whaling Grounds.epub | Bin .../BookService/TitleWithVolume.epub | Bin ...TitleWithVolume_NoSeriesOrSeriesIndex.epub | Bin .../Test Data/BookService/content.opf | 0 .../Test Data/BookService/encrypted.pdf | Bin .../Test Data/BookService/indirect.pdf | Bin .../Test Data/BookService/test.pdf | Bin .../Test Data/BookService/test_ſ.pdf | Bin .../Archives/file in folder in folder.zip | Bin .../Test Data/CoverDbService/Existing/01.webp | Bin .../CoverDbService/Favicons/anilist.co.webp | Bin .../TestCases/Manga-testcase.txt | 0 .../DirectoryService/extension/file.cbz | 0 .../DirectoryService/extension/file.rar | 0 .../DirectoryService/extension/file2.cbz | 0 .../Test Data/DirectoryService/regex/file.txt | 0 .../DirectoryService/regex/file2.txt | 0 .../ImageService/ColorScapes/blue-2.png | Bin .../ImageService/ColorScapes/blue.jpg | Bin .../ImageService/ColorScapes/green-red.png | Bin .../ImageService/ColorScapes/green.png | Bin .../ImageService/ColorScapes/lightblue-2.png | Bin .../ImageService/ColorScapes/lightblue.png | Bin .../ImageService/ColorScapes/pink.png | Bin .../ImageService/ColorScapes/yellow-blue.png | Bin .../ImageService/Covers/comic-normal-2.jpg | Bin .../Covers/comic-normal-2_baseline.png | Bin 0 -> 248370 bytes .../ImageService/Covers/comic-normal-3.jpg | Bin .../Covers/comic-normal-3_baseline.png | Bin 0 -> 288418 bytes .../ImageService/Covers/comic-normal.jpg | Bin .../Covers/comic-normal_baseline.png | Bin 0 -> 235849 bytes .../ImageService/Covers/comic-square.jpg | Bin .../Covers/comic-square_baseline.png | Bin 0 -> 385306 bytes .../ImageService/Covers/comic-wide.jpg | Bin .../Covers/comic-wide_baseline.png | Bin 0 -> 269575 bytes .../ImageService/Covers/manga-cover.png | Bin .../Covers/manga-cover_baseline.png | Bin 0 -> 314481 bytes .../ImageService/Covers/spread-cover.jpg | Bin .../Covers/spread-cover_baseline.png | Bin 0 -> 327077 bytes .../ImageService/Covers/webtoon-strip-2.png | Bin .../Covers/webtoon-strip-2_baseline.png | Bin 0 -> 237273 bytes .../ImageService/Covers/webtoon-strip.jpg | Bin .../Covers/webtoon-strip_baseline.png | Bin 0 -> 176585 bytes .../Test Data/ImageService/Covers/wide-ad.png | Bin .../ImageService/Covers/wide-ad_baseline.png | Bin 0 -> 286549 bytes .../Test Data/ImageService/cover.expected.jpg | Bin .../Test Data/OpdsService/test.zip | Bin .../Test Data/ReadingListService/Annual.cbl | 0 .../Test Data/ReadingListService/Fables.cbl | 0 .../Test Data/ScannerService/1x1.png | Bin .../Alternating Removal - Manga.json | 0 .../ScannerService/TestCases/Base.zip | Bin .../Delete Series In UI - Manga.json | 0 .../TestCases/Exclude Pattern 1 - Manga.json | 0 .../TestCases/Flat Series - Manga.json | 0 ...t Series with Specials Folder - Manga.json | 0 ...th Specials Folder Alt Naming - Manga.json | 0 .../TestCases/Flat Special - Manga.json | 0 ... with SP Folder (Non English) - Image.json | 0 .../Image Series with SP Folder - Manga.json | 0 ...calized Name matches Filename - Manga.json | 0 .../TestCases/Manga-testcase.txt | 0 .../TestCases/Multiple Roots - Manga.json | 0 .../TestCases/Nested Chapters - Manga.json | 0 .../TestCases/PDF Comic Chapters - Comic.json | 0 .../PDF Comic Chapters - LightNovel.json | 0 .../TestCases/Publisher - ComicVine.json | 0 .../Scan Library Parses as ( - Manga.json | 0 ...es and Series-Series Combined - Manga.json | 0 ...hen no other changes are made - Manga.json | 0 .../TestCases/Series with Extra - Manga.json | 0 .../Series with Localized - Manga.json | 0 .../Series with Localized 2 - Manga.json | 0 ...es with Localized No Metadata - Manga.json | 0 .../TestCases/Series with Prefix - Book.json | 0 ...light differences No Metadata - Manga.json | 0 .../TestCases/Sort Order - Manga.json | 0 ...scanning fix publisher layout - Comic.json | 0 ...s scanning all series changes - Manga.json | 0 ...folders and files at root (2) - Manga.json | 0 .../Subfolders and files at root - Manga.json | 0 .../TokenServiceTests.cs | 5 +- .../VersionUpdaterServiceTests.cs | 18 +- .../WordCountAnalysisTests.cs | 40 +- Kavita.Services/AccountService.cs | 268 ++ .../AnnotationService.cs | 65 +- .../ArchiveService.cs | 211 +- Kavita.Services/AuthKeyService.cs | 34 + Kavita.Services/BackupService.cs | 292 ++ .../BookService.cs | 246 +- .../BookmarkService.cs | 121 +- .../Builders/ChapterBuilder.cs | 14 +- .../Builders/MangaFileBuilder.cs | 12 +- .../Builders/VolumeBuilder.cs | 10 +- .../CacheService.cs | 159 +- Kavita.Services/CleanupService.cs | 417 ++ .../ClientDeviceService.cs | 96 +- Kavita.Services/CollectionTagService.cs | 125 + .../Comparators/ChapterSortComparer.cs | 10 +- Kavita.Services/DeviceService.cs | 137 + .../DeviceTrackingService.cs | 17 +- .../DirectoryService.cs | 105 +- .../DownloadService.cs | 10 +- .../EmailService.cs | 116 +- .../EntityNamingService.cs | 55 +- .../ApplicationServiceExtensions.cs | 100 +- .../Extensions/ChapterExtensions.cs | 65 + .../Extensions/ChapterListExtensions.cs | 19 +- .../Extensions/ComicInfoExtensions.cs | 77 + .../Extensions/FileTypeGroupExtensions.cs | 15 +- .../Extensions/IHasKPlusMetadataExtensions.cs | 6 +- .../Extensions/ParserExtensions.cs | 36 + .../Extensions/ParserInfoListExtensions.cs | 13 +- .../Extensions/SeriesExtensions.cs | 21 +- .../Extensions/VolumeExtensions.cs | 30 + .../Extensions/VolumeListExtensions.cs | 25 +- .../Extensions/ZipArchiveExtensions.cs | 7 +- .../FileService.cs | 13 +- .../FontService.cs | 110 +- .../Helpers/AnnotationHelper.cs | 7 +- .../Helpers/BookChapterItemHelper.cs | 7 +- .../Helpers/BookSortTitlePrefixHelper.cs | 4 +- .../Helpers/CacheHelper.cs | 25 +- .../Helpers/GenreHelper.cs | 13 +- .../Helpers/KoreaderHelper.cs | 4 +- .../Helpers/PdfComicInfoExtractor.cs | 43 +- .../Helpers/PdfMetadataExtractor.cs | 11 +- .../Helpers/PersonHelper.cs | 24 +- Kavita.Services/Helpers/ReviewHelper.cs | 48 + .../Helpers/SeriesHelper.cs | 15 +- .../Helpers/SmartFilterHelper.cs | 8 +- {API => Kavita.Services}/Helpers/TagHelper.cs | 32 +- .../ReadingSessionInitializer.cs | 4 +- .../StartupTasksHostedService.cs | 14 +- .../ImageService.cs | 121 +- Kavita.Services/Kavita.Services.csproj | 70 + Kavita.Services/KoreaderService.cs | 105 + .../LocalizationService.cs | 15 +- Kavita.Services/MediaConversionService.cs | 327 ++ Kavita.Services/MediaErrorService.cs | 52 + .../Metadata/CoverDbService.cs | 111 +- .../Metadata/WordCountAnalyzerService.cs | 136 +- .../MetadataService.cs | 213 +- .../OidcService.cs | 83 +- .../OpdsService.cs | 356 +- .../PersonService.cs | 41 +- .../Plus/ExternalMetadataService.cs | 266 +- .../Plus/KavitaPlusApiService.cs | 73 +- .../Plus/LicenseService.cs | 97 +- .../Plus/ScrobblingService.cs | 374 +- .../Plus/SmartCollectionSyncService.cs | 140 +- .../Plus/WantToReadSyncService.cs | 60 +- Kavita.Services/RatingService.cs | 99 + .../Reading/ReaderService.cs | 68 +- .../Reading/ReadingHistoryService.cs | 59 +- .../Reading}/ReadingItemService.cs | 19 +- .../Reading}/ReadingListService.cs | 232 +- .../Reading}/ReadingProfileService.cs | 170 +- .../Reading/ReadingSessionService.cs | 34 +- .../Repositories/CoverDbRepository.cs | 84 + .../Scanner}/BasicParser.cs | 55 +- .../Scanner}/BookParser.cs | 35 +- .../Scanner}/ComicVineParser.cs | 38 +- .../Scanner}/DefaultParser.cs | 41 +- .../Scanner}/ImageParser.cs | 20 +- .../Scanner/LibraryWatcher.cs | 39 +- .../Scanner/ParseScannedFiles.cs | 107 +- .../Scanner}/Parser.cs | 35 +- .../Scanner}/PdfParser.cs | 52 +- .../Scanner/ProcessSeries.cs | 86 +- .../Scanner}/ScannerService.cs | 340 +- .../SeriesService.cs | 271 +- .../SettingsService.cs | 203 +- Kavita.Services/SignalR/EventHub.cs | 92 + {API => Kavita.Services}/SignalR/LogHub.cs | 15 +- .../SignalR/MessageHub.cs | 10 +- .../SignalR}/PresenceTracker.cs | 28 +- .../SiteThemeService.cs | 250 +- .../StatisticService.cs | 409 +- .../Tasks => Kavita.Services}/StatsService.cs | 58 +- Kavita.Services/StreamService.cs | 415 ++ .../TachiyomiService.cs | 103 +- .../TaskScheduler.cs | 222 +- .../TokenService.cs | 81 +- .../VersionUpdaterService.cs | 40 +- Kavita.sln | 194 +- UI/Web/package-lock.json | 3919 +++++++---------- UI/Web/package.json | 66 +- .../src/app/_directives/echarts.directive.ts | 13 +- .../next-expected-card.component.html | 6 +- .../next-expected-card.component.ts | 26 +- .../person-card/person-card.component.ts | 4 - .../carousel-reel/carousel-reel.component.ts | 6 +- UI/Web/src/app/ng-swipe/ng-swipe.directive.ts | 14 +- UI/Web/src/main.ts | 8 +- build.sh | 19 +- docker-build.sh | 12 +- 1612 files changed, 23078 insertions(+), 22652 deletions(-) delete mode 100644 API.Benchmark/API.Benchmark.csproj delete mode 100644 API.Tests/API.Tests.csproj delete mode 100644 API.Tests/Comparers/ChapterSortComparerTest.cs delete mode 100644 API.Tests/Comparers/NumericComparerTests.cs delete mode 100644 API.Tests/Comparers/SortComparerZeroLastTests.cs delete mode 100644 API.Tests/Comparers/StringLogicalComparerTest.cs delete mode 100644 API.Tests/Extensions/FileInfoExtensionsTests.cs delete mode 100644 API.Tests/Extensions/Test Data/not modified.txt delete mode 100644 API.Tests/Helpers/ParserInfoHelperTests.cs delete mode 100644 API.Tests/Helpers/ReviewHelperTests.cs delete mode 100644 API/API.csproj delete mode 100644 API/API.csproj.DotSettings delete mode 100644 API/Comparators/NumericComparer.cs delete mode 100644 API/Comparators/StringLogicalComparer.cs delete mode 100644 API/Controllers/DeviceController.cs delete mode 100644 API/Controllers/EmailController.cs delete mode 100644 API/Controllers/FallbackController.cs delete mode 100644 API/Controllers/ImageController.cs delete mode 100644 API/Controllers/VolumeController.cs delete mode 100644 API/Data/Repositories/AppUserSmartFilterRepository.cs delete mode 100644 API/Data/Repositories/CollectionTagRepository.cs delete mode 100644 API/Data/Repositories/DeviceRepository.cs delete mode 100644 API/Data/Repositories/EmailHistoryRepository.cs delete mode 100644 API/Data/Repositories/EpubFontRepository.cs delete mode 100644 API/Data/Repositories/MangaFileRepository.cs delete mode 100644 API/Data/Repositories/MediaErrorRepository.cs delete mode 100644 API/Data/Repositories/ScrobbleEventRepository.cs delete mode 100644 API/Data/Repositories/SeriesMetadataRepository.cs delete mode 100644 API/Data/Repositories/SettingsRepository.cs delete mode 100644 API/Data/Repositories/SiteThemeRepository.cs delete mode 100644 API/Data/Repositories/UserTableOfContentRepository.cs delete mode 100644 API/Extensions/AppUserExtensions.cs delete mode 100644 API/Extensions/EnumExtensions.cs delete mode 100644 API/Extensions/EnumerableExtensions.cs delete mode 100644 API/Extensions/FileInfoExtensions.cs delete mode 100644 API/Extensions/PathExtensions.cs delete mode 100644 API/Extensions/QueryExtensions/Filtering/AnnotationFilter.cs delete mode 100644 API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs delete mode 100644 API/Extensions/QueryExtensions/ProjectToExtensions.cs delete mode 100644 API/Extensions/StringExtensions.cs delete mode 100644 API/Helpers/Builders/MediaErrorBuilder.cs delete mode 100644 API/Helpers/Builders/PlusSeriesDtoBuilder.cs delete mode 100644 API/Helpers/PagedList.cs delete mode 100644 API/Helpers/ParserInfoHelpers.cs delete mode 100644 API/Properties/launchSettings.json delete mode 100644 API/Services/AccountService.cs delete mode 100644 API/Services/AuthKeyService.cs delete mode 100644 API/Services/Caching/AuthKeyCacheInvalidator.cs delete mode 100644 API/Services/CollectionTagService.cs delete mode 100644 API/Services/DeviceService.cs delete mode 100644 API/Services/KoreaderService.cs delete mode 100644 API/Services/MediaConversionService.cs delete mode 100644 API/Services/MediaErrorService.cs delete mode 100644 API/Services/RatingService.cs delete mode 100644 API/Services/Store/UserContext.cs delete mode 100644 API/Services/StreamService.cs delete mode 100644 API/Services/Tasks/BackupService.cs delete mode 100644 API/Services/Tasks/CleanupService.cs delete mode 100644 API/SignalR/EventHub.cs delete mode 100755 API/redo-migration.sh create mode 100644 Kavita.API/Attributes/SkipDeviceTrackingAttribute.cs create mode 100644 Kavita.API/Database/IDataContext.cs create mode 100644 Kavita.API/Database/IUnitOfWork.cs rename {API => Kavita.API}/Errors/ApiException.cs (67%) rename {API/Exceptions => Kavita.API/Errors}/OpdsException.cs (80%) create mode 100644 Kavita.API/Kavita.API.csproj create mode 100644 Kavita.API/Repositories/IAnnotationRepository.cs create mode 100644 Kavita.API/Repositories/IAppUserExternalSourceRepository.cs create mode 100644 Kavita.API/Repositories/IAppUserProgressRepository.cs create mode 100644 Kavita.API/Repositories/IAppUserReadingProfileRepository.cs create mode 100644 Kavita.API/Repositories/IAppUserSmartFilterRepository.cs create mode 100644 Kavita.API/Repositories/IChapterRepository.cs create mode 100644 Kavita.API/Repositories/IClientDeviceRepository.cs create mode 100644 Kavita.API/Repositories/ICollectionTagRepository.cs create mode 100644 Kavita.API/Repositories/IDeviceRepository.cs create mode 100644 Kavita.API/Repositories/IEmailHistoryRepository.cs create mode 100644 Kavita.API/Repositories/IEpubFontRepository.cs create mode 100644 Kavita.API/Repositories/IExternalSeriesMetadataRepository.cs create mode 100644 Kavita.API/Repositories/IGenreRepository.cs create mode 100644 Kavita.API/Repositories/ILibraryRepository.cs create mode 100644 Kavita.API/Repositories/IMangaFileRepository.cs create mode 100644 Kavita.API/Repositories/IMediaErrorRepository.cs create mode 100644 Kavita.API/Repositories/IPersonRepository.cs create mode 100644 Kavita.API/Repositories/IReadingListRepository.cs create mode 100644 Kavita.API/Repositories/IReadingSessionRepository.cs create mode 100644 Kavita.API/Repositories/IScrobbleRepository.cs create mode 100644 Kavita.API/Repositories/ISeriesMetadataRepository.cs create mode 100644 Kavita.API/Repositories/ISeriesRepository.cs create mode 100644 Kavita.API/Repositories/ISettingsRepository.cs create mode 100644 Kavita.API/Repositories/ISiteThemeRepository.cs create mode 100644 Kavita.API/Repositories/ITagRepository.cs create mode 100644 Kavita.API/Repositories/IUserRepository.cs create mode 100644 Kavita.API/Repositories/IUserTableOfContentRepository.cs create mode 100644 Kavita.API/Repositories/IVolumeRepository.cs create mode 100644 Kavita.API/Services/Helpers/ICacheHelper.cs create mode 100644 Kavita.API/Services/IAccountService.cs create mode 100644 Kavita.API/Services/IAnnotationService.cs create mode 100644 Kavita.API/Services/IArchiveService.cs create mode 100644 Kavita.API/Services/IAuthKeyService.cs create mode 100644 Kavita.API/Services/IBackupService.cs create mode 100644 Kavita.API/Services/IBookService.cs create mode 100644 Kavita.API/Services/IBookmarkService.cs create mode 100644 Kavita.API/Services/ICacheService.cs create mode 100644 Kavita.API/Services/ICleanupService.cs create mode 100644 Kavita.API/Services/IClientDeviceService.cs create mode 100644 Kavita.API/Services/IClientInfoAccessor.cs create mode 100644 Kavita.API/Services/ICollectionTagService.cs create mode 100644 Kavita.API/Services/IDeviceService.cs create mode 100644 Kavita.API/Services/IDeviceTrackingService.cs create mode 100644 Kavita.API/Services/IDirectoryService.cs create mode 100644 Kavita.API/Services/IDownloadService.cs create mode 100644 Kavita.API/Services/IEmailService.cs rename API/Helpers/Formatting/LocalizedNamingContext.cs => Kavita.API/Services/IEntityNamingService.cs (52%) create mode 100644 Kavita.API/Services/IFileService.cs create mode 100644 Kavita.API/Services/IFontService.cs create mode 100644 Kavita.API/Services/IImageService.cs create mode 100644 Kavita.API/Services/IKoreaderService.cs create mode 100644 Kavita.API/Services/ILocalizationService.cs create mode 100644 Kavita.API/Services/ILoggingService.cs create mode 100644 Kavita.API/Services/IMediaConversionService.cs create mode 100644 Kavita.API/Services/IMediaErrorService.cs create mode 100644 Kavita.API/Services/IMetadataService.cs create mode 100644 Kavita.API/Services/IOidcService.cs create mode 100644 Kavita.API/Services/IOpdsService.cs create mode 100644 Kavita.API/Services/IPersonService.cs create mode 100644 Kavita.API/Services/IRatingService.cs create mode 100644 Kavita.API/Services/IReadingItemService.cs create mode 100644 Kavita.API/Services/ISeriesService.cs create mode 100644 Kavita.API/Services/ISettingsService.cs create mode 100644 Kavita.API/Services/IStatisticService.cs create mode 100644 Kavita.API/Services/IStatsService.cs create mode 100644 Kavita.API/Services/IStreamService.cs create mode 100644 Kavita.API/Services/ITachiyomiService.cs create mode 100644 Kavita.API/Services/ITaskScheduler.cs create mode 100644 Kavita.API/Services/IThemeService.cs create mode 100644 Kavita.API/Services/ITokenService.cs create mode 100644 Kavita.API/Services/IVersionUpdaterService.cs create mode 100644 Kavita.API/Services/Metadata/ICoverDbService.cs create mode 100644 Kavita.API/Services/Metadata/IWordCountAnalyzerService.cs create mode 100644 Kavita.API/Services/Plus/IExternalMetadataService.cs create mode 100644 Kavita.API/Services/Plus/IKavitaPlusApiService.cs create mode 100644 Kavita.API/Services/Plus/ILicenseService.cs create mode 100644 Kavita.API/Services/Plus/IScrobblingService.cs create mode 100644 Kavita.API/Services/Plus/ISmartCollectionSyncService.cs create mode 100644 Kavita.API/Services/Plus/IWantToReadSyncService.cs create mode 100644 Kavita.API/Services/Reading/IReaderService.cs create mode 100644 Kavita.API/Services/Reading/IReadingHistoryService.cs create mode 100644 Kavita.API/Services/Reading/IReadingListService.cs create mode 100644 Kavita.API/Services/Reading/IReadingProfileService.cs create mode 100644 Kavita.API/Services/Reading/IReadingSessionService.cs create mode 100644 Kavita.API/Services/Scanner/ILibraryWatcher.cs create mode 100644 Kavita.API/Services/Scanner/IProcessSeries.cs create mode 100644 Kavita.API/Services/Scanner/IScannerService.cs create mode 100644 Kavita.API/Services/SignalR/IEventHub.cs create mode 100644 Kavita.API/Services/SignalR/IPresenceTracker.cs create mode 100644 Kavita.API/Store/IUserContext.cs rename {API.Benchmark => Kavita.Benchmark}/ArchiveServiceBenchmark.cs (97%) rename {API.Benchmark => Kavita.Benchmark}/CleanTitleBenchmark.cs (63%) rename {API.Benchmark => Kavita.Benchmark}/Data/AesopsFables.epub (100%) rename {API.Benchmark => Kavita.Benchmark}/Data/Comics.txt (100%) rename {API.Benchmark => Kavita.Benchmark}/Data/SeriesNamesForNormalization.txt (100%) create mode 100644 Kavita.Benchmark/Kavita.Benchmark.csproj rename {API.Benchmark => Kavita.Benchmark}/KoreaderHashBenchmark.cs (91%) rename {API.Benchmark => Kavita.Benchmark}/ParserBenchmarks.cs (94%) rename {API.Benchmark => Kavita.Benchmark}/Program.cs (93%) rename {API.Benchmark => Kavita.Benchmark}/TestBenchmark.cs (89%) rename {API.Tests => Kavita.Common.Tests}/Extensions/EnumerableExtensionsTests.cs (84%) rename {API.Tests => Kavita.Common.Tests}/Extensions/PathExtensionsTests.cs (81%) rename {API.Tests => Kavita.Common.Tests}/Extensions/VersionExtensionTests.cs (95%) rename {API.Tests/Converters => Kavita.Common.Tests/Helpers}/CronConverterTests.cs (83%) create mode 100644 Kavita.Common.Tests/Helpers/HtmlHelperTests.cs rename {API.Tests => Kavita.Common.Tests}/Helpers/RandfHelper.cs (96%) rename {API.Tests => Kavita.Common.Tests}/Helpers/RateLimiterTests.cs (93%) rename {API.Tests => Kavita.Common.Tests}/Helpers/StringHelperTests.cs (96%) create mode 100644 Kavita.Common.Tests/Kavita.Common.Tests.csproj rename {API => Kavita.Common}/Constants/Headers.cs (93%) rename {API => Kavita.Common}/Extensions/ClaimsPrincipalExtensions.cs (89%) rename {API => Kavita.Common}/Extensions/DateTimeExtensions.cs (94%) rename {API => Kavita.Common}/Extensions/DoubleExtensions.cs (92%) create mode 100644 Kavita.Common/Extensions/EnumerableExtensions.cs rename {API => Kavita.Common}/Extensions/FloatExtensions.cs (91%) rename {API => Kavita.Common}/Extensions/FlurlExtensions.cs (93%) rename {API => Kavita.Common}/Extensions/ImageExtensions.cs (99%) create mode 100644 Kavita.Common/Extensions/StringExtensions.cs rename {API => Kavita.Common}/Extensions/VersionExtensions.cs (88%) rename {API => Kavita.Common}/Helpers/AuthKeyHelper.cs (95%) rename {API/Helpers/Converters => Kavita.Common/Helpers}/CronConverter.cs (77%) rename {API => Kavita.Common}/Helpers/DayOfWeekHelper.cs (89%) rename API/Helpers/ReviewHelper.cs => Kavita.Common/Helpers/HtmlHelper.cs (62%) rename {API => Kavita.Common}/Helpers/JwtHelper.cs (96%) rename {API => Kavita.Common}/Helpers/NumberHelper.cs (84%) create mode 100644 Kavita.Common/Helpers/PagedList.cs rename {API => Kavita.Common}/Helpers/PaginationHeader.cs (91%) rename {API => Kavita.Common}/Helpers/RateLimiter.cs (98%) rename {API => Kavita.Common}/Helpers/StringHelper.cs (98%) rename {API => Kavita.Common}/Helpers/UserParams.cs (96%) rename {API.Tests => Kavita.Database.Tests}/AbstractDbTest.cs (81%) rename {API.Tests => Kavita.Database.Tests}/AbstractFsTest.cs (90%) rename {API.Tests => Kavita.Database.Tests}/Extensions/QueryableExtensionsTests.cs (98%) create mode 100644 Kavita.Database.Tests/Kavita.Database.Tests.csproj create mode 100644 Kavita.Database.Tests/Repositories/ExternalSeriesMetadataRepositoryTests.cs rename {API.Tests/Repository => Kavita.Database.Tests/Repositories}/GenreRepositoryTests.cs (97%) rename {API.Tests/Repository => Kavita.Database.Tests/Repositories}/PersonRepositoryTests.cs (95%) rename {API.Tests/Repository => Kavita.Database.Tests/Repositories}/SeriesRepositoryTests.cs (95%) rename {API.Tests/Repository => Kavita.Database.Tests/Repositories}/TagRepositoryTests.cs (97%) rename {API/Helpers => Kavita.Database}/Converters/AnnotationFilterFieldValueConverter.cs (88%) rename {API/Helpers => Kavita.Database}/Converters/FilterFieldValueConverter.cs (96%) rename {API/Helpers => Kavita.Database}/Converters/PersonFilterFieldValueConverter.cs (80%) rename {API/Data => Kavita.Database}/DataContext.cs (97%) create mode 100644 Kavita.Database/Extensions/ApplicationServiceExtensions.cs rename {API/Extensions/QueryExtensions => Kavita.Database/Extensions}/AuthKeyQueryExtensions.cs (76%) rename API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs => Kavita.Database/Extensions/BookmarkSortExtensions.cs (84%) rename {API/Extensions/QueryExtensions => Kavita.Database/Extensions}/ChapterQueryExtensions.cs (61%) rename {API => Kavita.Database}/Extensions/DataContextExtensions.cs (92%) rename {API/Extensions/QueryExtensions/Filtering => Kavita.Database/Extensions/Filters}/ActivityFilter.cs (92%) create mode 100644 Kavita.Database/Extensions/Filters/AnnotationFilter.cs rename {API/Extensions/QueryExtensions/Filtering => Kavita.Database/Extensions/Filters}/PersonFilter.cs (97%) create mode 100644 Kavita.Database/Extensions/Filters/SeriesFilter.cs rename {API/Extensions/QueryExtensions => Kavita.Database/Extensions}/IncludesExtensions.cs (98%) create mode 100644 Kavita.Database/Extensions/PagedListExtensions.cs create mode 100644 Kavita.Database/Extensions/ProjectToExtensions.cs rename {API/Extensions/QueryExtensions => Kavita.Database/Extensions}/QueryableExtensions.cs (97%) rename {API/Extensions/QueryExtensions => Kavita.Database/Extensions}/RestrictByAgeExtensions.cs (75%) rename {API/Extensions/QueryExtensions => Kavita.Database/Extensions}/RestrictByLibraryExtensions.cs (89%) rename {API/Extensions/QueryExtensions/Filtering => Kavita.Database/Extensions}/SearchQueryableExtensions.cs (93%) rename API/Extensions/QueryExtensions/Filtering/SeriesSort.cs => Kavita.Database/Extensions/SeriesSortExtensions.cs (91%) rename {API/Extensions/QueryExtensions => Kavita.Database/Extensions}/StatisticsQueryExtensions.cs (74%) create mode 100644 Kavita.Database/Kavita.Database.csproj rename {API/Data => Kavita.Database}/Migrations/20201213205325_AddUser.Designer.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20201213205325_AddUser.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20201215195007_AddedLibrary.Designer.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20201215195007_AddedLibrary.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20201218173135_ManyToManyLibraries.Designer.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20201218173135_ManyToManyLibraries.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20201221141047_IdentityAdded.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20201221141047_IdentityAdded.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20201224155621_MiscCleanup.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20201224155621_MiscCleanup.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20201229190216_SeriesAndVolumeEntities.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210101180935_AddedCoverImageToSeries.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210102165536_EntityTimestamps.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210102165536_EntityTimestamps.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210102173326_VolumeNumberRefactor.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210103201043_RemoveUserIsAdmin.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210103230812_SeriesCoverImage.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210103230812_SeriesCoverImage.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20210104011624_VolumeCoverImage.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210104011624_VolumeCoverImage.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210109205034_CacheMetadata.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210109205034_CacheMetadata.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20210111231840_VolumePages.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210111231840_VolumePages.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210114214506_UserProgress.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210114214506_UserProgress.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20210117180406_ReadStatusModifications.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210117180406_ReadStatusModifications.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210117181421_SeriesPages.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210117181421_SeriesPages.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210119213837_AppUserRatingAndReviews.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210119213837_AppUserRatingAndReviews.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20210121180051_AddedServerSettings.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210121180051_AddedServerSettings.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210121215532_ServerSettingsAdjustment.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20210122165809_ServerSettingsChange.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210122165809_ServerSettingsChange.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210122172455_ServerSettingsPrimaryKey.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210128143348_SeriesVolumeChapterChange.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210128201832_MangaFileChapterRelationship.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20210203164258_ServerSettingsKey.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210203164258_ServerSettingsKey.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20210205220227_UserPreferences.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210205220227_UserPreferences.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20210207231256_SeriesNormalizedName.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210207231256_SeriesNormalizedName.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210225150830_AddLocalizedName.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210225150830_AddLocalizedName.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210315134028_SearchIndexAndProgressDates.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20210322212724_MangaFileToPages.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210322212724_MangaFileToPages.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210323213507_LastModifiedOnMangaFiles.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210323213507_LastModifiedOnMangaFiles.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20210330134414_IsSpecialOnChapters.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210330134414_IsSpecialOnChapters.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210419222000_BookReaderPreferences.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210419222000_BookReaderPreferences.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210419234652_BookReaderPreferencesFontSize.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210423132900_CustomChapterTitle.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210423132900_CustomChapterTitle.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20210504184715_TapToPaginatePref.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210504184715_TapToPaginatePref.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210509014029_SiteDarkModePreference.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210509014029_SiteDarkModePreference.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210519215934_CollectionTag.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210519215934_CollectionTag.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20210528150353_CollectionCoverImage.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210528150353_CollectionCoverImage.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210530201541_CollectionSummary.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210530201541_CollectionSummary.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210603133957_BookReadingDirectionPref.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210603133957_BookReadingDirectionPref.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210603212429_BookScrollIdProgress.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210603212429_BookScrollIdProgress.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210622164318_NewUserPreferences.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210622164318_NewUserPreferences.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20210722223304_AddedSeriesFormat.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210722223304_AddedSeriesFormat.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20210809210326_BookmarkPages.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210809210326_BookmarkPages.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210813010210_CoverImageLockFieldsPart1.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210814215831_CoverImageLockedFieldsPart2.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210817152226_ProgressConcurencyCheck.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210826203258_userApiKey.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210826203258_userApiKey.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20210901150310_ReadingLists.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210901150310_ReadingLists.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210901200442_ReadingListsAdditions.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210901200442_ReadingListsAdditions.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210902110705_ReadingListsExtraRealationships.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20210906140845_ReadingListsChanges.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210906140845_ReadingListsChanges.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20210916142418_EntityImageRefactor.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20210916142418_EntityImageRefactor.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20211001113608_LastScannedLibrary.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20211001113608_LastScannedLibrary.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20211127200244_MetadataFoundation.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20211127200244_MetadataFoundation.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20211129231007_RemoveChapterMetadata.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20211129231007_RemoveChapterMetadata.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20211130134642_GenreProvider.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20211130134642_GenreProvider.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20211201230003_GenreTitle.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20211201230003_GenreTitle.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20211205185207_MetadataAgeRating.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20211205185207_MetadataAgeRating.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20211206193225_AgeRatingAndReleaseDate.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20211217013734_BookmarkRefactor.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20211217013734_BookmarkRefactor.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20211217180457_filteringChanges.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20211217180457_filteringChanges.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20211227180752_FullscreenPref.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20211227180752_FullscreenPref.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20220107232822_ChapterMetadataOptimization.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220107232822_ChapterMetadataOptimization.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20220108200822_CountMetadata.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220108200822_CountMetadata.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20220108202027_PublicationStatus.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220108202027_PublicationStatus.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20220215163317_SiteTheme.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220215163317_SiteTheme.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20220303205301_SeriesLockedFields.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220303205301_SeriesLockedFields.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.cs (93%) rename {API/Data => Kavita.Database}/Migrations/20220307153053_ScreenHints.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220307153053_ScreenHints.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20220416211340_RemoveCustomIndex.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220416211340_RemoveCustomIndex.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20220421214448_SeriesRelations.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220421214448_SeriesRelations.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220425125505_ChangeCountToTotalCount.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220425131122_AddMaxCountToSeriesMetadata.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20220508162841_BookReaderUpdate.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220508162841_BookReaderUpdate.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220513234708_BookReaderImmersiveMode.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20220524172543_WordCount.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220524172543_WordCount.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20220610153822_TimeEstimateInDB.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220610153822_TimeEstimateInDB.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220613131125_RenamedBookReaderLayoutMode.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20220615190640_LastFileAnalysis.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220615190640_LastFileAnalysis.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220625215526_BlurUnreadSummaries.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220712161611_PromptForDownloadSizeUserOption.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20220717145254_UserConfirmationLink.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220717145254_UserConfirmationLink.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20220728193758_WantToReadList.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220728193758_WantToReadList.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20220802222910_BookmarkHasDate.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220802222910_BookmarkHasDate.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20220814134725_MangaFileCreatedDate.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220814134725_MangaFileCreatedDate.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20220817173731_SeriesFolder.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220817173731_SeriesFolder.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20220819223212_NormalizedLocalizedName.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220819223212_NormalizedLocalizedName.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20220921023455_DeviceSupport.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220921023455_DeviceSupport.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20220926145902_AddNoTransitions.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20220926145902_AddNoTransitions.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20221006013956_ReleaseYearOnSeriesEdit.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20221006013956_ReleaseYearOnSeriesEdit.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20221009172653_ReadingListAgeRating.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20221009172653_ReadingListAgeRating.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20221009211237_UserAgeRating.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20221009211237_UserAgeRating.cs (89%) rename {API/Data => Kavita.Database}/Migrations/20221017131711_IncludeUnknowns.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20221017131711_IncludeUnknowns.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20221115021908_SeriesRelationChange.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20221115021908_SeriesRelationChange.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20221118131123_ExtendedLibrarySettings.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20221126133824_FileLengthAndExtension.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20221126133824_FileLengthAndExtension.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20221128230726_UserProgressLibraryId.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20221128230726_UserProgressLibraryId.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20221212215914_EmulateBookPref.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20221212215914_EmulateBookPref.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20230111014852_YearlyStats.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230111014852_YearlyStats.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230129210741_SwipeToPaginatePref.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20230130210252_AutoCollections.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230130210252_AutoCollections.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20230202182602_ReadingListFields.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230202182602_ReadingListFields.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20230203112022_RemoveExternalFromTagAndGenre.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230203112022_RemoveExternalFromTagAndGenre.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20230210153842_UtcTimes.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230210153842_UtcTimes.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230220203128_CollapseSeriesRelationships.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230220203128_CollapseSeriesRelationships.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20230304202540_BookWritingStylePref.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230304202540_BookWritingStylePref.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20230313125914_ReadingListDateRange.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230313125914_ReadingListDateRange.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20230316123908_SecurityEvent.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230316123908_SecurityEvent.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230316233133_RemoveSecurityEvent.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230415123449_ManageReadingListOnLibrary.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20230505124430_MediaError.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230505124430_MediaError.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20230511165427_WebLinksForChapter.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230511165427_WebLinksForChapter.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20230511183339_WebLinksForSeries.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230511183339_WebLinksForSeries.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20230512004545_ChapterISBN.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230512004545_ChapterISBN.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20230527215722_LicenseAndScrobble.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230527215722_LicenseAndScrobble.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230601172306_ScrobbleErrors.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230601172306_ScrobbleErrors.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20230612154313_ScrobbleEventProcessed.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230612154313_ScrobbleEventProcessed.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230615133219_ReviewTaglineAndOptInShares.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230615133219_ReviewTaglineAndOptInShares.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20230618150728_ScrobbleHolds.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230618150728_ScrobbleHolds.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20230621211421_RemoveUserLicense.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230621211421_RemoveUserLicense.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20230623192231_ScrobbleReview.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230623192231_ScrobbleReview.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20230715125951_OnDeckRemoval.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230715125951_OnDeckRemoval.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20230719173458_PersonalToC.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230719173458_PersonalToC.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20230725133536_ChangeRatingScale.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230725133536_ChangeRatingScale.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230727175518_AddLocaleOnPrefs.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20230904184205_SmartFilters.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230904184205_SmartFilters.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20230908190713_DashboardStream.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20230908190713_DashboardStream.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20231013194957_SideNavStreamAndExternalSource.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20231113215006_LibraryFileTypes.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20231113215006_LibraryFileTypes.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20231117234829_LibraryExcludePatterns.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240121223643_ExternalSeriesMetadata.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240128153433_VolumeMinMaxNumbers.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20240130190617_WantToReadFix.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240130190617_WantToReadFix.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240204141206_BlackListSeries.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240204141206_BlackListSeries.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20240205184724_ScrobbleEventError.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240205184724_ScrobbleEventError.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20240209224347_DBTweaks.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240209224347_DBTweaks.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20240214232436_ChapterNumber.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240214232436_ChapterNumber.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20240216000223_MangaFileNameTemp.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240216000223_MangaFileNameTemp.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20240222125420_ChapterIssueSort.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240222125420_ChapterIssueSort.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20240225235816_VolumeLookupName.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240225235816_VolumeLookupName.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20240309140117_SeriesImprints.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240309140117_SeriesImprints.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240313112552_SeriesLowestFolderPath.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20240314194402_TeamsAndLocations.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240314194402_TeamsAndLocations.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20240321173812_UserMalToken.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240321173812_UserMalToken.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20240328130057_PdfSettings.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240328130057_PdfSettings.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20240331172900_UserBasedCollections.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240331172900_UserBasedCollections.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240418163829_ChapterSortOrderLock.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20240503120147_SmartCollectionFields.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240503120147_SmartCollectionFields.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20240510134030_SiteThemeFields.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240510134030_SiteThemeFields.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20240704144224_PersonFields.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240704144224_PersonFields.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20240808100353_CoverPrimaryColors.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240808100353_CoverPrimaryColors.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240811154857_ChapterMetadataLocks.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240813194728_VolumeCoverLocked.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240813194728_VolumeCoverLocked.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20240917180034_AvgReadingTimeFloat.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20241011143144_PeopleOverhaulPart1.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20241011152321_PeopleOverhaulPart2.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20241011172428_PeopleOverhaulPart3.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20250109173537_EmailHistory.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250109173537_EmailHistory.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250208200843_MoreMetadtaSettings.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20250415194829_KavitaPlusCBR.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250415194829_KavitaPlusCBR.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250429150140_ChapterRatingAndReviews.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250507221026_PersonAliases.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250507221026_PersonAliases.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20250519151126_KoreaderHash.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250519151126_KoreaderHash.cs (94%) rename {API/Data => Kavita.Database}/Migrations/20250601200056_ReadingProfiles.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250601200056_ReadingProfiles.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250620215058_EnableMetadataLibrary.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250626162548_TrackKavitaPlusMetadata.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250629153840_LibraryRemoveSortPrefix.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20250802103258_OpenIDConnect.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250802103258_OpenIDConnect.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20250820150458_BookAnnotations.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250820150458_BookAnnotations.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250919114119_ColorScapeSetting.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250919114119_ColorScapeSetting.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20250920212509_CustomEpubFonts.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250920212509_CustomEpubFonts.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20250921211542_EpubPageCalcMethod.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250921211542_EpubPageCalcMethod.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20250924142016_AddAnnotationsHtmlContent.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250924142016_AddAnnotationsHtmlContent.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20250928181727_RemoveEpubPageCalc.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20250928181727_RemoveEpubPageCalc.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20251003110154_SocialAnnotations.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20251003110154_SocialAnnotations.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20251009150922_DataSaverUserSetting.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20251009150922_DataSaverUserSetting.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20251101152738_OpdsSettings.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20251101152738_OpdsSettings.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20251207204514_StatsRevampPartOne.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20251207204514_StatsRevampPartOne.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20251218200802_ReadingSessionFormatAndIndecies.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20251218200802_ReadingSessionFormatAndIndecies.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20251224133055_AddDataProtectionKeys.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20251224133055_AddDataProtectionKeys.cs (96%) rename {API/Data => Kavita.Database}/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.cs (98%) rename {API/Data => Kavita.Database}/Migrations/20260109144351_ReadingSessionIndex.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20260109144351_ReadingSessionIndex.cs (97%) rename {API/Data => Kavita.Database}/Migrations/20260110164419_AppUserAuthKeyUtcMissing.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20260110164419_AppUserAuthKeyUtcMissing.cs (95%) rename {API/Data => Kavita.Database}/Migrations/20260112165908_ReadingHistoryChanges.Designer.cs (99%) rename {API/Data => Kavita.Database}/Migrations/20260112165908_ReadingHistoryChanges.cs (98%) rename {API/Data => Kavita.Database}/Migrations/DataContextModelSnapshot.cs (99%) rename {API/Data => Kavita.Database}/Repositories/AnnotationRepository.cs (78%) rename {API/Data => Kavita.Database}/Repositories/AppUserExternalSourceRepository.cs (50%) rename {API/Data => Kavita.Database}/Repositories/AppUserProgressRepository.cs (55%) rename {API/Data => Kavita.Database}/Repositories/AppUserReadingProfileRepository.cs (56%) create mode 100644 Kavita.Database/Repositories/AppUserSmartFilterRepository.cs rename {API/Data => Kavita.Database}/Repositories/ChapterRepository.cs (55%) create mode 100644 Kavita.Database/Repositories/ClientDeviceRepository.cs create mode 100644 Kavita.Database/Repositories/CollectionTagRepository.cs rename {API/Data => Kavita.Database}/Repositories/CoverDbRepository.cs (95%) create mode 100644 Kavita.Database/Repositories/DeviceRepository.cs create mode 100644 Kavita.Database/Repositories/EmailHistoryRepository.cs create mode 100644 Kavita.Database/Repositories/EpubFontRepository.cs rename {API/Data => Kavita.Database}/Repositories/ExternalSeriesMetadataRepository.cs (59%) rename {API/Data => Kavita.Database}/Repositories/GenreRepository.cs (55%) rename {API/Data => Kavita.Database}/Repositories/LibraryRepository.cs (53%) create mode 100644 Kavita.Database/Repositories/MangaFileRepository.cs create mode 100644 Kavita.Database/Repositories/MediaErrorRepository.cs rename {API/Data => Kavita.Database}/Repositories/PersonRepository.cs (52%) rename {API/Data => Kavita.Database}/Repositories/ReadingListRepository.cs (59%) rename {API/Data => Kavita.Database}/Repositories/ReadingSessionRepository.cs (83%) create mode 100644 Kavita.Database/Repositories/ScrobbleEventRepository.cs create mode 100644 Kavita.Database/Repositories/SeriesMetadataRepository.cs rename {API/Data => Kavita.Database}/Repositories/SeriesRepository.cs (70%) create mode 100644 Kavita.Database/Repositories/SettingsRepository.cs create mode 100644 Kavita.Database/Repositories/SiteThemeRepository.cs rename {API/Data => Kavita.Database}/Repositories/TagRepository.cs (55%) rename {API/Data => Kavita.Database}/Repositories/UserRepository.cs (57%) create mode 100644 Kavita.Database/Repositories/UserTableOfContentRepository.cs rename {API/Data => Kavita.Database}/Repositories/VolumeRepository.cs (59%) rename {API/Data => Kavita.Database}/Seed.cs (57%) rename {API/Data => Kavita.Database}/UnitOfWork.cs (72%) rename {API.Tests => Kavita.Models.Tests}/Extensions/EncodeFormatExtensionsTests.cs (67%) rename {API.Tests => Kavita.Models.Tests}/Extensions/EnumExtensionTests.cs (78%) create mode 100644 Kavita.Models.Tests/Kavita.Models.Tests.csproj rename {API/Data => Kavita.Models}/AutoMapper/AutoMapperChapterProfile.cs (83%) rename {API/Data => Kavita.Models}/AutoMapper/AutoMapperProfiles.cs (90%) rename {API/Data => Kavita.Models}/AutoMapper/AutoMapperReadingListProfile.cs (96%) rename {API/Data => Kavita.Models}/AutoMapper/AutoMapperSeriesProfile.cs (94%) rename {API/Data => Kavita.Models}/AutoMapper/AutoMapperVolumeProfile.cs (91%) rename {API/Helpers => Kavita.Models/AutoMapper}/Converters/ServerSettingConverter.cs (97%) rename {API/Helpers => Kavita.Models}/Builders/AppUserBuilder.cs (85%) rename {API/Helpers => Kavita.Models}/Builders/AppUserChapterRatingBuilder.cs (83%) rename {API/Helpers => Kavita.Models}/Builders/AppUserCollectionBuilder.cs (91%) rename {API/Helpers => Kavita.Models}/Builders/AppUserReadingProfileBuilder.cs (89%) rename {API/Helpers => Kavita.Models}/Builders/DeviceBuilder.cs (83%) rename {API/Helpers => Kavita.Models}/Builders/EntityBuilder.cs (100%) rename {API/Helpers => Kavita.Models}/Builders/ExternalSeriesMetadataBuilder.cs (89%) rename {API/Helpers => Kavita.Models}/Builders/FolderPathBuilder.cs (82%) rename {API/Helpers => Kavita.Models}/Builders/GenreBuilder.cs (80%) create mode 100644 Kavita.Models/Builders/IEntityBuilder.cs rename {API/Helpers => Kavita.Models}/Builders/KoreaderBookDtoBuilder.cs (95%) rename {API/Helpers => Kavita.Models}/Builders/LibraryBuilder.cs (95%) create mode 100644 Kavita.Models/Builders/MediaErrorBuilder.cs rename {API/Helpers => Kavita.Models}/Builders/PersonAliasBuilder.cs (76%) rename {API/Helpers => Kavita.Models}/Builders/PersonBuilder.cs (92%) rename {API/Helpers => Kavita.Models}/Builders/RatingBuilder.cs (90%) rename {API/Helpers => Kavita.Models}/Builders/ReadingListBuilder.cs (91%) rename {API/Helpers => Kavita.Models}/Builders/ReadingListItemBuilder.cs (87%) rename {API/Helpers => Kavita.Models}/Builders/ScrobbleHoldBuilder.cs (86%) rename {API/Helpers => Kavita.Models}/Builders/SeriesBuilder.cs (93%) rename {API/Helpers => Kavita.Models}/Builders/SeriesMetadataBuilder.cs (95%) rename {API/Helpers => Kavita.Models}/Builders/TagBuilder.cs (82%) rename {API => Kavita.Models}/Constants/CacheProfiles.cs (96%) rename {API => Kavita.Models}/Constants/ControllerConstants.cs (72%) create mode 100644 Kavita.Models/Constants/ParserConstants.cs rename {API => Kavita.Models}/Constants/PolicyConstants.cs (98%) rename {API => Kavita.Models}/Constants/PolicyGroups.cs (74%) rename {API => Kavita.Models}/Constants/ResponseCacheProfiles.cs (93%) create mode 100644 Kavita.Models/Constants/TaskSchedulerConstants.cs rename {API => Kavita.Models}/DTOs/Account/AgeRestrictionDto.cs (87%) rename {API => Kavita.Models}/DTOs/Account/AuthKeyDto.cs (88%) create mode 100644 Kavita.Models/DTOs/Account/AuthKeyExpiresAtDto.cs rename {API => Kavita.Models}/DTOs/Account/ConfirmEmailDto.cs (91%) rename {API => Kavita.Models}/DTOs/Account/ConfirmEmailUpdateDto.cs (85%) rename {API => Kavita.Models}/DTOs/Account/ConfirmMigrationEmailDto.cs (78%) rename {API => Kavita.Models}/DTOs/Account/ConfirmPasswordResetDto.cs (89%) rename {API => Kavita.Models}/DTOs/Account/InviteUserDto.cs (95%) rename {API => Kavita.Models}/DTOs/Account/InviteUserResponse.cs (92%) rename {API => Kavita.Models}/DTOs/Account/LoginDto.cs (88%) rename {API => Kavita.Models}/DTOs/Account/MemberDto.cs (91%) rename {API => Kavita.Models}/DTOs/Account/MemberInfoDto.cs (80%) rename {API => Kavita.Models}/DTOs/Account/MigrateUserEmailDto.cs (83%) rename {API => Kavita.Models}/DTOs/Account/ResetPasswordDto.cs (94%) rename {API => Kavita.Models}/DTOs/Account/RotateAuthKeyRequestDto.cs (89%) rename {API => Kavita.Models}/DTOs/Account/TokenRequestDto.cs (78%) rename {API => Kavita.Models}/DTOs/Account/UpdateAgeRestrictionDto.cs (74%) rename {API => Kavita.Models}/DTOs/Account/UpdateEmailDto.cs (77%) rename {API => Kavita.Models}/DTOs/Account/UpdateUserDto.cs (77%) rename {API => Kavita.Models}/DTOs/Annotations/FullAnnotationDto.cs (95%) rename {API => Kavita.Models}/DTOs/Archive/ArchiveLibrary.cs (91%) rename {API => Kavita.Models}/DTOs/BulkActionDto.cs (88%) rename {API => Kavita.Models}/DTOs/ChapterDetailPlusDto.cs (81%) rename {API => Kavita.Models}/DTOs/ChapterDto.cs (64%) rename {API => Kavita.Models}/DTOs/CheckForFilesInFolderRootsDto.cs (82%) rename {API => Kavita.Models}/DTOs/Collection/AppUserCollectionDto.cs (94%) rename {API => Kavita.Models}/DTOs/Collection/DeleteCollectionsDto.cs (82%) rename {API => Kavita.Models}/DTOs/Collection/MalStackDto.cs (93%) rename {API => Kavita.Models}/DTOs/Collection/PromoteCollectionsDto.cs (80%) rename {API => Kavita.Models}/DTOs/CollectionTags/CollectionTagBulkAddDto.cs (91%) rename {API => Kavita.Models}/DTOs/CollectionTags/CollectionTagDto.cs (95%) rename {API => Kavita.Models}/DTOs/CollectionTags/UpdateSeriesForTagDto.cs (73%) rename {API => Kavita.Models}/DTOs/ColorScape.cs (86%) rename {API => Kavita.Models}/DTOs/CopySettingsFromLibraryDto.cs (91%) rename {API => Kavita.Models}/DTOs/CoverDb/CoverDbAuthor.cs (93%) rename {API => Kavita.Models}/DTOs/CoverDb/CoverDbPeople.cs (87%) rename {API => Kavita.Models}/DTOs/CoverDb/CoverDbPersonIds.cs (95%) rename {API => Kavita.Models}/DTOs/Dashboard/DashboardStreamDto.cs (90%) rename {API => Kavita.Models}/DTOs/Dashboard/GroupedSeriesDto.cs (93%) rename {API => Kavita.Models}/DTOs/Dashboard/SmartFilterDto.cs (79%) rename {API => Kavita.Models}/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs (84%) rename {API => Kavita.Models}/DTOs/Dashboard/UpdateStreamPositionDto.cs (89%) rename {API => Kavita.Models}/DTOs/DeleteChaptersDto.cs (82%) rename {API => Kavita.Models}/DTOs/DeleteSeriesDto.cs (82%) rename {API => Kavita.Models}/DTOs/Device/ClientDevice/UpdateClientDeviceNameDto.cs (70%) rename {API => Kavita.Models}/DTOs/Device/EmailDevice/CreateEmailDeviceDto.cs (81%) rename {API => Kavita.Models}/DTOs/Device/EmailDevice/DeviceDto.cs (89%) rename {API => Kavita.Models}/DTOs/Device/EmailDevice/SendSeriesToEmailDeviceDto.cs (71%) rename {API => Kavita.Models}/DTOs/Device/EmailDevice/SendToEmailDeviceDto.cs (79%) rename {API => Kavita.Models}/DTOs/Device/EmailDevice/UpdateDeviceDto.cs (83%) rename {API => Kavita.Models}/DTOs/Downloads/DownloadBookmarkDto.cs (74%) rename {API => Kavita.Models}/DTOs/Email/ConfirmationEmailDto.cs (90%) rename {API => Kavita.Models}/DTOs/Email/EmailHistoryDto.cs (90%) rename {API => Kavita.Models}/DTOs/Email/EmailMigrationDto.cs (90%) rename {API => Kavita.Models}/DTOs/Email/EmailTestResultDto.cs (89%) rename {API => Kavita.Models}/DTOs/Email/PasswordResetEmailDto.cs (88%) rename {API => Kavita.Models}/DTOs/Email/SendToDto.cs (84%) rename {API => Kavita.Models}/DTOs/Email/TestEmailDto.cs (69%) rename {API => Kavita.Models}/DTOs/Filtering/FilterDto.cs (97%) rename {API => Kavita.Models}/DTOs/Filtering/LanguageDto.cs (75%) rename {API => Kavita.Models}/DTOs/Filtering/PersonSortField.cs (67%) rename {API => Kavita.Models}/DTOs/Filtering/Range.cs (86%) rename {API => Kavita.Models}/DTOs/Filtering/ReadStatus.cs (86%) rename {API => Kavita.Models}/DTOs/Filtering/SortField.cs (96%) rename {API => Kavita.Models}/DTOs/Filtering/SortOptions.cs (94%) rename {API => Kavita.Models}/DTOs/Filtering/v2/DecodeFilterDto.cs (78%) rename {API => Kavita.Models}/DTOs/Filtering/v2/FilterCombination.cs (56%) rename {API => Kavita.Models}/DTOs/Filtering/v2/FilterComparision.cs (97%) rename {API => Kavita.Models}/DTOs/Filtering/v2/FilterField.cs (97%) rename {API => Kavita.Models}/DTOs/Filtering/v2/FilterStatementDto.cs (93%) rename {API => Kavita.Models}/DTOs/Filtering/v2/FilterV2Dto.cs (94%) rename {API => Kavita.Models}/DTOs/Font/EpubFontDto.cs (72%) rename {API => Kavita.Models}/DTOs/ImportFieldMappings.cs (96%) rename {API => Kavita.Models}/DTOs/Internal/AppSettingsDto.cs (86%) rename {API => Kavita.Models}/DTOs/Jobs/JobDto.cs (94%) rename {API => Kavita.Models}/DTOs/JumpBar/JumpKeyDto.cs (91%) rename {API => Kavita.Models}/DTOs/KavitaLocale.cs (90%) rename {API => Kavita.Models}/DTOs/KavitaPlus/Account/AniListUpdateDto.cs (60%) rename {API => Kavita.Models}/DTOs/KavitaPlus/Account/UserTokenInfo.cs (89%) rename {API => Kavita.Models}/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs (81%) rename {API => Kavita.Models}/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs (86%) rename {API => Kavita.Models}/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs (71%) rename {API => Kavita.Models}/DTOs/KavitaPlus/License/EncryptLicenseDto.cs (82%) rename {API => Kavita.Models}/DTOs/KavitaPlus/License/LicenseInfoDto.cs (95%) rename {API => Kavita.Models}/DTOs/KavitaPlus/License/LicenseValidDto.cs (73%) rename {API => Kavita.Models}/DTOs/KavitaPlus/License/ResetLicenseDto.cs (78%) rename {API => Kavita.Models}/DTOs/KavitaPlus/License/UpdateLicenseDto.cs (88%) rename {API => Kavita.Models}/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs (91%) rename {API => Kavita.Models}/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs (80%) rename {API => Kavita.Models}/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs (90%) rename {API => Kavita.Models}/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs (88%) rename {API => Kavita.Models}/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs (87%) rename {API => Kavita.Models}/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs (96%) rename {API => Kavita.Models}/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs (87%) rename {API => Kavita.Models}/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs (79%) rename {API => Kavita.Models}/DTOs/Koreader/KoreaderBookDto.cs (94%) rename {API => Kavita.Models}/DTOs/Koreader/KoreaderProgressUpdateDto.cs (89%) rename {API => Kavita.Models}/DTOs/LibraryDto.cs (97%) rename {API => Kavita.Models}/DTOs/MangaFileDto.cs (92%) rename {API => Kavita.Models}/DTOs/MediaErrors/MediaErrorDto.cs (93%) rename {API => Kavita.Models}/DTOs/Metadata/AgeRatingDto.cs (62%) rename {API => Kavita.Models}/DTOs/Metadata/Browse/BrowseGenreDto.cs (85%) rename {API => Kavita.Models}/DTOs/Metadata/Browse/BrowsePersonDto.cs (83%) rename {API => Kavita.Models}/DTOs/Metadata/Browse/BrowseTagDto.cs (85%) rename {API => Kavita.Models}/DTOs/Metadata/Browse/Requests/BrowseAnnotationFilterDto.cs (84%) rename {API => Kavita.Models}/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs (84%) rename {API => Kavita.Models}/DTOs/Metadata/ChapterMetadataDto.cs (95%) rename {API => Kavita.Models}/DTOs/Metadata/GenreTagDto.cs (72%) rename {API => Kavita.Models}/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs (61%) rename {API => Kavita.Models}/DTOs/Metadata/Matching/MatchSeriesDto.cs (92%) rename {API => Kavita.Models}/DTOs/Metadata/PublicationStatusDto.cs (64%) rename {API => Kavita.Models}/DTOs/Metadata/TagDto.cs (71%) rename {API => Kavita.Models}/DTOs/Misc/ParseBulkRequestDto.cs (71%) rename {API => Kavita.Models}/DTOs/Misc/ParseBulkResponseDto.cs (94%) rename {API => Kavita.Models}/DTOs/Misc/ParseResultDto.cs (90%) rename {API => Kavita.Models}/DTOs/OPDS/Internal/Feed.cs (96%) rename {API => Kavita.Models}/DTOs/OPDS/Internal/FeedAuthor.cs (84%) rename {API => Kavita.Models}/DTOs/OPDS/Internal/FeedCategory.cs (92%) rename {API => Kavita.Models}/DTOs/OPDS/Internal/FeedEntry.cs (97%) rename {API => Kavita.Models}/DTOs/OPDS/Internal/FeedEntryContent.cs (83%) rename {API => Kavita.Models}/DTOs/OPDS/Internal/FeedLink.cs (97%) rename {API => Kavita.Models}/DTOs/OPDS/Internal/FeedLinkRelation.cs (95%) rename {API => Kavita.Models}/DTOs/OPDS/Internal/FeedLinkType.cs (91%) rename {API => Kavita.Models}/DTOs/OPDS/Internal/OpenSearchDescription.cs (98%) rename {API => Kavita.Models}/DTOs/OPDS/Internal/SearchLink.cs (89%) rename {API => Kavita.Models}/DTOs/OPDS/Requests/IOpdsPagination.cs (62%) rename {API => Kavita.Models}/DTOs/OPDS/Requests/IOpdsRequest.cs (72%) rename {API => Kavita.Models}/DTOs/OPDS/Requests/OpdsCatalogeRequest.cs (74%) rename {API => Kavita.Models}/DTOs/OPDS/Requests/OpdsItemsFromCompoundEntityIdsRequest.cs (87%) rename {API => Kavita.Models}/DTOs/OPDS/Requests/OpdsSearchRequest.cs (76%) rename {API => Kavita.Models}/DTOs/OPDS/Requests/OpdsSmartFilterCatalogueRequest.cs (81%) rename {API => Kavita.Models}/DTOs/OPDS/Requests/OpdsSmartFilterRequest.cs (79%) rename {API => Kavita.Models}/DTOs/Person/PersonAliasCheckDto.cs (94%) rename {API => Kavita.Models}/DTOs/Person/PersonDto.cs (95%) rename {API => Kavita.Models}/DTOs/Person/PersonMergeDto.cs (93%) rename {API => Kavita.Models}/DTOs/Person/UpdatePersonDto.cs (94%) rename {API => Kavita.Models}/DTOs/Progress/ClientDeviceDto.cs (94%) rename {API => Kavita.Models}/DTOs/Progress/ClientInfoDto.cs (94%) rename {API => Kavita.Models}/DTOs/Progress/DailyReadingDataDto.cs (94%) rename {API => Kavita.Models}/DTOs/Progress/FullProgressDto.cs (93%) rename {API => Kavita.Models}/DTOs/Progress/ProgressDto.cs (93%) rename {API => Kavita.Models}/DTOs/Progress/ReadingActivityDataDto.cs (95%) rename {API => Kavita.Models}/DTOs/Progress/ReadingSessionDto.cs (91%) rename {API => Kavita.Models}/DTOs/RatingDto.cs (77%) rename {API => Kavita.Models}/DTOs/Reader/AnnotationDto.cs (94%) rename {API => Kavita.Models}/DTOs/Reader/BookChapterItem.cs (93%) rename {API => Kavita.Models}/DTOs/Reader/BookInfoDto.cs (88%) rename {API => Kavita.Models}/DTOs/Reader/BookResourceResultDto.cs (93%) rename {API => Kavita.Models}/DTOs/Reader/BookmarkDto.cs (95%) rename {API => Kavita.Models}/DTOs/Reader/BookmarkInfoDto.cs (92%) rename {API => Kavita.Models}/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs (81%) rename {API => Kavita.Models}/DTOs/Reader/ChapterInfoDto.cs (97%) rename {API => Kavita.Models}/DTOs/Reader/CreatePersonalToCDto.cs (91%) rename {API => Kavita.Models}/DTOs/Reader/FileDimensionDto.cs (91%) rename {API => Kavita.Models}/DTOs/Reader/HourEstimateRangeDto.cs (92%) rename {API => Kavita.Models}/DTOs/Reader/IChapterInfoDto.cs (85%) rename {API => Kavita.Models}/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs (81%) rename {API => Kavita.Models}/DTOs/Reader/MarkReadDto.cs (65%) rename {API => Kavita.Models}/DTOs/Reader/MarkVolumeReadDto.cs (75%) rename {API => Kavita.Models}/DTOs/Reader/MarkVolumesReadDto.cs (93%) rename {API => Kavita.Models}/DTOs/Reader/PersonalToCDto.cs (96%) rename {API => Kavita.Models}/DTOs/Reader/ReReadDto.cs (94%) rename {API => Kavita.Models}/DTOs/Reader/RemoveBookmarkForSeriesDto.cs (69%) rename {API => Kavita.Models}/DTOs/ReadingLists/CBL/CblBook.cs (94%) rename {API => Kavita.Models}/DTOs/ReadingLists/CBL/CblConflictsDto.cs (79%) rename {API => Kavita.Models}/DTOs/ReadingLists/CBL/CblImportSummary.cs (98%) rename {API => Kavita.Models}/DTOs/ReadingLists/CBL/CblReadingList.cs (97%) rename {API => Kavita.Models}/DTOs/ReadingLists/CreateReadingListDto.cs (68%) rename {API => Kavita.Models}/DTOs/ReadingLists/DeleteReadingListsDto.cs (82%) rename {API => Kavita.Models}/DTOs/ReadingLists/PromoteReadingListsDto.cs (80%) rename {API => Kavita.Models}/DTOs/ReadingLists/ReadingListCast.cs (92%) rename {API => Kavita.Models}/DTOs/ReadingLists/ReadingListDto.cs (93%) rename {API => Kavita.Models}/DTOs/ReadingLists/ReadingListInfoDto.cs (89%) rename {API => Kavita.Models}/DTOs/ReadingLists/ReadingListItemDto.cs (95%) rename {API => Kavita.Models}/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs (79%) rename {API => Kavita.Models}/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs (87%) rename {API => Kavita.Models}/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs (83%) rename {API => Kavita.Models}/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs (75%) rename {API => Kavita.Models}/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs (79%) rename {API => Kavita.Models}/DTOs/ReadingLists/UpdateReadingListDto.cs (92%) rename {API => Kavita.Models}/DTOs/ReadingLists/UpdateReadingListPosition.cs (90%) rename {API => Kavita.Models}/DTOs/Recommendation/ExternalSeriesDto.cs (82%) rename {API => Kavita.Models}/DTOs/Recommendation/MetadataTagDto.cs (87%) rename {API => Kavita.Models}/DTOs/Recommendation/RecommendationDto.cs (85%) rename {API => Kavita.Models}/DTOs/Recommendation/SeriesStaffDto.cs (89%) rename {API => Kavita.Models}/DTOs/RefreshSeriesDto.cs (95%) rename {API => Kavita.Models}/DTOs/RegisterDto.cs (93%) rename {API => Kavita.Models}/DTOs/ScanFolderDto.cs (95%) rename {API => Kavita.Models}/DTOs/Scrobbling/MalUserInfoDto.cs (87%) rename {API => Kavita.Models}/DTOs/Scrobbling/MediaRecommendationDto.cs (86%) rename {API => Kavita.Models}/DTOs/Scrobbling/PlusSeriesDto.cs (95%) rename {API => Kavita.Models}/DTOs/Scrobbling/ScrobbleDto.cs (98%) rename {API => Kavita.Models}/DTOs/Scrobbling/ScrobbleErrorDto.cs (90%) rename {API => Kavita.Models}/DTOs/Scrobbling/ScrobbleEventDto.cs (94%) rename {API => Kavita.Models}/DTOs/Scrobbling/ScrobbleHoldDto.cs (86%) rename {API => Kavita.Models}/DTOs/Scrobbling/ScrobbleResponseDto.cs (87%) rename {API => Kavita.Models}/DTOs/Search/BookmarkSearchResultDto.cs (88%) rename {API => Kavita.Models}/DTOs/Search/SearchResultDto.cs (86%) rename {API => Kavita.Models}/DTOs/Search/SearchResultGroupDto.cs (81%) rename {API => Kavita.Models}/DTOs/SeriesByIdsDto.cs (74%) rename {API => Kavita.Models}/DTOs/SeriesDetail/NextExpectedChapterDto.cs (90%) rename API/Data/Misc/RecentlyAddedSeries.cs => Kavita.Models/DTOs/SeriesDetail/RecentlyAddedSeriesDto.cs (83%) rename {API => Kavita.Models}/DTOs/SeriesDetail/RelatedSeriesDto.cs (96%) rename {API => Kavita.Models}/DTOs/SeriesDetail/SeriesDetailDto.cs (96%) rename {API => Kavita.Models}/DTOs/SeriesDetail/SeriesDetailPlusDto.cs (78%) rename {API => Kavita.Models}/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs (95%) rename {API => Kavita.Models}/DTOs/SeriesDetail/UpdateUserReviewDto.cs (80%) rename {API => Kavita.Models}/DTOs/SeriesDetail/UserReviewDto.cs (95%) rename {API => Kavita.Models}/DTOs/SeriesDetail/UserReviewExtendedDto.cs (91%) rename {API => Kavita.Models}/DTOs/SeriesDto.cs (97%) rename {API => Kavita.Models}/DTOs/SeriesMetadataDto.cs (96%) rename {API => Kavita.Models}/DTOs/Settings/AuthorityValidationDto.cs (60%) rename {API => Kavita.Models}/DTOs/Settings/ImportFieldMappingsDto.cs (75%) rename {API => Kavita.Models}/DTOs/Settings/OidcConfigDto.cs (97%) rename {API => Kavita.Models}/DTOs/Settings/OidcPublicConfigDto.cs (94%) rename {API => Kavita.Models}/DTOs/Settings/SMTPConfigDto.cs (94%) rename {API => Kavita.Models}/DTOs/Settings/ServerSettingDTO.cs (98%) rename {API => Kavita.Models}/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs (84%) rename {API => Kavita.Models}/DTOs/SideNav/ExternalSourceDto.cs (84%) rename {API => Kavita.Models}/DTOs/SideNav/SideNavStreamDto.cs (93%) rename {API => Kavita.Models/DTOs}/SignalR/MessageFactory.cs (97%) rename {API => Kavita.Models/DTOs}/SignalR/ProgressEventType.cs (89%) rename {API => Kavita.Models/DTOs}/SignalR/ProgressType.cs (92%) rename {API => Kavita.Models/DTOs}/SignalR/SignalRMessage.cs (96%) rename {API => Kavita.Models}/DTOs/StandaloneChapterDto.cs (81%) rename {API => Kavita.Models}/DTOs/Statistics/BreakDownDto.cs (85%) rename {API => Kavita.Models}/DTOs/Statistics/Count.cs (75%) rename {API => Kavita.Models}/DTOs/Statistics/FileExtensionBreakdownDto.cs (86%) rename {API => Kavita.Models}/DTOs/Statistics/ICount.cs (69%) rename {API => Kavita.Models}/DTOs/Statistics/MostActiveUserDto.cs (92%) rename {API => Kavita.Models}/DTOs/Statistics/MostReadAuthorsDto.cs (91%) rename {API => Kavita.Models}/DTOs/Statistics/PagesReadOnADayCount.cs (82%) rename {API => Kavita.Models}/DTOs/Statistics/ProfileStatBarDto.cs (88%) rename {API => Kavita.Models}/DTOs/Statistics/ReadHistoryEvent.cs (93%) rename {API => Kavita.Models}/DTOs/Statistics/ReadTimeByHourDto.cs (82%) rename {API => Kavita.Models}/DTOs/Statistics/ReadingActivityGraphDto.cs (91%) rename {API => Kavita.Models}/DTOs/Statistics/ReadingHistoryItemDto.cs (94%) rename {API => Kavita.Models}/DTOs/Statistics/ReadingPaceDto.cs (86%) rename {API => Kavita.Models}/DTOs/Statistics/ServerStatisticsDto.cs (90%) rename {API => Kavita.Models}/DTOs/Statistics/SpreadStatsDto.cs (80%) rename {API => Kavita.Models}/DTOs/Statistics/StatBucketDto.cs (90%) rename {API => Kavita.Models}/DTOs/Statistics/StatsFilterDto.cs (93%) rename {API => Kavita.Models}/DTOs/Statistics/TopReadsDto.cs (90%) rename {API => Kavita.Models}/DTOs/Statistics/UserReadStatistics.cs (94%) rename {API => Kavita.Models}/DTOs/Statistics/YearMonthGroupingDto.cs (73%) rename {API => Kavita.Models}/DTOs/Stats/FileExtensionExportDto.cs (89%) rename {API => Kavita.Models}/DTOs/Stats/ServerInfoSlimDto.cs (95%) rename {API => Kavita.Models}/DTOs/Stats/V3/ClientDevice/DeviceClientBreakdownDto.cs (61%) rename {API => Kavita.Models}/DTOs/Stats/V3/LibraryStatV3.cs (94%) rename {API => Kavita.Models}/DTOs/Stats/V3/RelationshipStatV3.cs (73%) rename {API => Kavita.Models}/DTOs/Stats/V3/ServerInfoV3Dto.cs (98%) rename {API => Kavita.Models}/DTOs/Stats/V3/UserStatV3.cs (95%) rename {API => Kavita.Models}/DTOs/System/DirectoryDto.cs (87%) rename {API => Kavita.Models}/DTOs/TachiyomiChapterDto.cs (91%) rename {API => Kavita.Models}/DTOs/Theme/ColorScapeDto.cs (91%) rename {API => Kavita.Models}/DTOs/Theme/DownloadableSiteThemeDto.cs (97%) rename {API => Kavita.Models}/DTOs/Theme/SiteThemeDto.cs (95%) rename {API => Kavita.Models}/DTOs/Theme/UpdateDefaultThemeDto.cs (68%) rename {API => Kavita.Models}/DTOs/Update/UpdateNotificationDto.cs (98%) rename {API => Kavita.Models}/DTOs/UpdateChapterDto.cs (96%) rename {API => Kavita.Models}/DTOs/UpdateLibraryDto.cs (95%) rename {API => Kavita.Models}/DTOs/UpdateLibraryForUserDto.cs (88%) rename {API => Kavita.Models}/DTOs/UpdateRBSDto.cs (86%) rename {API => Kavita.Models}/DTOs/UpdateRatingDto.cs (83%) rename {API => Kavita.Models}/DTOs/UpdateSeriesDto.cs (90%) rename {API => Kavita.Models}/DTOs/UpdateSeriesMetadataDto.cs (78%) rename {API => Kavita.Models}/DTOs/Uploads/UploadFileDto.cs (90%) rename {API => Kavita.Models}/DTOs/Uploads/UploadUrlDto.cs (84%) rename {API => Kavita.Models}/DTOs/UserDto.cs (83%) rename {API => Kavita.Models}/DTOs/UserPreferencesDto.cs (93%) rename {API => Kavita.Models}/DTOs/UserReadingProfileDto.cs (96%) rename {API => Kavita.Models}/DTOs/VolumeDto.cs (83%) rename {API => Kavita.Models}/DTOs/WantToRead/UpdateWantToReadDto.cs (89%) create mode 100644 Kavita.Models/Defaults.cs rename {API/Data/Misc => Kavita.Models/Entities}/AgeRestriction.cs (63%) rename {API => Kavita.Models}/Entities/Chapter.cs (77%) rename {API => Kavita.Models}/Entities/CollectionTag.cs (96%) rename {API => Kavita.Models}/Entities/Device.cs (90%) rename {API => Kavita.Models}/Entities/EmailHistory.cs (88%) rename {API => Kavita.Models}/Entities/Enums/AgeRating.cs (96%) rename {API => Kavita.Models}/Entities/Enums/BookPageLayoutMode.cs (83%) rename {API => Kavita.Models}/Entities/Enums/ClientDevicePlatform.cs (89%) rename {API => Kavita.Models}/Entities/Enums/ClientDeviceType.cs (93%) rename {API => Kavita.Models}/Entities/Enums/CoverImageSize.cs (94%) rename {API => Kavita.Models}/Entities/Enums/DashboardStreamType.cs (82%) rename {API => Kavita.Models}/Entities/Enums/Device/DevicePlatform.cs (91%) rename {API => Kavita.Models}/Entities/Enums/EncodeFormat.cs (81%) rename {API => Kavita.Models}/Entities/Enums/EpubPageCalculationMethod.cs (88%) rename {API => Kavita.Models}/Entities/Enums/FileTypeGroup.cs (88%) rename {API => Kavita.Models}/Entities/Enums/Font/FontProvider.cs (82%) rename {API => Kavita.Models}/Entities/Enums/IdentityProvider.cs (85%) rename {API => Kavita.Models}/Entities/Enums/LayoutMode.cs (83%) rename {API => Kavita.Models}/Entities/Enums/LibraryType.cs (96%) rename {API => Kavita.Models}/Entities/Enums/MangaFormat.cs (95%) create mode 100644 Kavita.Models/Entities/Enums/MediaErrorProducer.cs rename {API => Kavita.Models}/Entities/Enums/MetadataFieldType.cs (59%) rename {API => Kavita.Models}/Entities/Enums/PageSplitOption.cs (73%) rename {API => Kavita.Models}/Entities/Enums/PdfRenderResolution.cs (86%) rename {API => Kavita.Models}/Entities/Enums/PdfRenderResolutionExtensions.cs (90%) rename {API => Kavita.Models}/Entities/Enums/PersonRole.cs (93%) rename {API => Kavita.Models}/Entities/Enums/PublicationStatus.cs (95%) rename {API => Kavita.Models}/Entities/Enums/RatingAuthority.cs (88%) rename {API => Kavita.Models}/Entities/Enums/ReaderMode.cs (84%) rename {API => Kavita.Models}/Entities/Enums/ReadingDirection.cs (63%) rename {API => Kavita.Models}/Entities/Enums/ReadingProfileKind.cs (91%) rename {API => Kavita.Models}/Entities/Enums/RelationKind.cs (98%) rename {API => Kavita.Models}/Entities/Enums/ScalingOption.cs (71%) create mode 100644 Kavita.Models/Entities/Enums/ScrobbleProvider.cs rename {API => Kavita.Models}/Entities/Enums/ServerSettingKey.cs (99%) rename {API => Kavita.Models}/Entities/Enums/SyncKey.cs (81%) rename {API => Kavita.Models}/Entities/Enums/Theme/ThemeProvider.cs (88%) rename {API => Kavita.Models}/Entities/Enums/User/AuthKeyProvider.cs (86%) rename {API => Kavita.Models}/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs (87%) rename {API => Kavita.Models}/Entities/Enums/UserPreferences/AppUserSocialPreferences.cs (96%) rename {API => Kavita.Models}/Entities/Enums/UserPreferences/KeyBind.cs (85%) rename {API => Kavita.Models}/Entities/Enums/UserPreferences/KeyBindTarget.cs (94%) rename {API => Kavita.Models}/Entities/Enums/UserPreferences/PageLayoutMode.cs (72%) rename {API => Kavita.Models}/Entities/Enums/UserPreferences/PdfBookMode.cs (89%) rename {API => Kavita.Models}/Entities/Enums/UserPreferences/PdfScrollMode.cs (87%) rename {API => Kavita.Models}/Entities/Enums/UserPreferences/PdfSpreadMode.cs (76%) rename {API => Kavita.Models}/Entities/Enums/UserPreferences/PdfTheme.cs (71%) rename {API => Kavita.Models}/Entities/Enums/WritingStyle.cs (91%) rename {API => Kavita.Models}/Entities/EpubFont.cs (85%) rename {API => Kavita.Models}/Entities/FolderPath.cs (95%) rename {API => Kavita.Models}/Entities/Genre.cs (85%) rename {API => Kavita.Models}/Entities/HighlightSlot.cs (89%) rename {API => Kavita.Models}/Entities/History/KavitaPlusHistory.cs (73%) rename {API => Kavita.Models}/Entities/History/ManualMigrationHistory.cs (91%) rename {API => Kavita.Models}/Entities/Interfaces/IEntityDate.cs (82%) rename {API => Kavita.Models}/Entities/Interfaces/IHasConcurrencyToken.cs (89%) rename {API => Kavita.Models}/Entities/Interfaces/IHasCoverImage.cs (93%) rename {API => Kavita.Models}/Entities/Interfaces/IHasKPlusMetadata.cs (71%) rename {API => Kavita.Models}/Entities/Interfaces/IHasReadTimeEstimate.cs (92%) rename {API => Kavita.Models}/Entities/Interfaces/ITheme.cs (76%) rename {API => Kavita.Models}/Entities/Library.cs (96%) rename {API => Kavita.Models}/Entities/LibraryExcludedGlob.cs (84%) rename {API => Kavita.Models}/Entities/LibraryFileTypeGroup.cs (74%) rename {API => Kavita.Models}/Entities/MangaFile.cs (95%) rename {API => Kavita.Models}/Entities/MediaError.cs (92%) rename {API => Kavita.Models}/Entities/Metadata/ExternalRating.cs (89%) rename {API => Kavita.Models}/Entities/Metadata/ExternalRecommendation.cs (91%) rename {API => Kavita.Models}/Entities/Metadata/ExternalReview.cs (93%) rename {API => Kavita.Models}/Entities/Metadata/ExternalSeriesMetadata.cs (96%) rename {API => Kavita.Models}/Entities/Metadata/SeriesBlacklist.cs (89%) rename {API => Kavita.Models}/Entities/Metadata/SeriesMetadata.cs (96%) rename {API => Kavita.Models}/Entities/Metadata/SeriesRelation.cs (87%) rename {API => Kavita.Models}/Entities/MetadataMatching/MetadataFieldMapping.cs (85%) rename {API => Kavita.Models}/Entities/MetadataMatching/MetadataSettingField.cs (90%) rename {API => Kavita.Models}/Entities/MetadataMatching/MetadataSettings.cs (97%) rename {API => Kavita.Models}/Entities/Person/ChapterPeople.cs (88%) rename {API => Kavita.Models}/Entities/Person/Person.cs (95%) rename {API => Kavita.Models}/Entities/Person/PersonAlias.cs (85%) rename {API => Kavita.Models}/Entities/Person/SeriesMetadataPeople.cs (84%) rename {API => Kavita.Models}/Entities/Progress/AppUserProgress.cs (94%) rename {API => Kavita.Models}/Entities/Progress/AppUserReadingHistory.cs (85%) rename {API => Kavita.Models}/Entities/Progress/AppUserReadingSession.cs (89%) rename {API => Kavita.Models}/Entities/Progress/AppUserReadingSessionActivityData.cs (95%) rename {API => Kavita.Models}/Entities/Progress/ClientInfoData.cs (96%) rename {API => Kavita.Models}/Entities/ReadingList.cs (93%) rename {API => Kavita.Models}/Entities/ReadingListItem.cs (94%) rename {API => Kavita.Models}/Entities/Scrobble/ScrobbleError.cs (90%) rename {API => Kavita.Models}/Entities/Scrobble/ScrobbleEvent.cs (93%) rename {API => Kavita.Models}/Entities/Scrobble/ScrobbleEventFilter.cs (93%) rename {API => Kavita.Models}/Entities/Scrobble/ScrobbleEventSortField.cs (78%) rename {API => Kavita.Models}/Entities/Scrobble/ScrobbleHold.cs (78%) rename {API => Kavita.Models}/Entities/Series.cs (96%) rename {API => Kavita.Models}/Entities/ServerSetting.cs (82%) rename {API => Kavita.Models}/Entities/ServerStatistics.cs (92%) rename {API => Kavita.Models}/Entities/SideNavStreamType.cs (85%) rename {API => Kavita.Models}/Entities/SiteTheme.cs (81%) rename {API => Kavita.Models}/Entities/Tag.cs (85%) rename {API => Kavita.Models}/Entities/User/AppRole.cs (82%) rename {API => Kavita.Models}/Entities/User/AppUser.cs (96%) rename {API => Kavita.Models}/Entities/User/AppUserAnnotation.cs (96%) rename {API => Kavita.Models}/Entities/User/AppUserAuthKey.cs (92%) rename {API => Kavita.Models}/Entities/User/AppUserBookmark.cs (86%) rename {API => Kavita.Models}/Entities/User/AppUserChapterRating.cs (93%) rename {API => Kavita.Models}/Entities/User/AppUserCollection.cs (95%) rename {API => Kavita.Models}/Entities/User/AppUserDashboardStream.cs (90%) rename {API => Kavita.Models}/Entities/User/AppUserExternalSource.cs (87%) rename {API => Kavita.Models}/Entities/User/AppUserOnDeckRemoval.cs (84%) rename {API => Kavita.Models}/Entities/User/AppUserPreferences.cs (96%) rename {API => Kavita.Models}/Entities/User/AppUserRating.cs (90%) rename {API => Kavita.Models}/Entities/User/AppUserReadingProfile.cs (96%) rename {API => Kavita.Models}/Entities/User/AppUserRole.cs (82%) rename {API => Kavita.Models}/Entities/User/AppUserSideNavStream.cs (95%) rename {API => Kavita.Models}/Entities/User/AppUserSmartFilter.cs (88%) rename {API => Kavita.Models}/Entities/User/AppUserTableOfContent.cs (95%) rename {API => Kavita.Models}/Entities/User/AppUserWantToRead.cs (91%) rename {API => Kavita.Models}/Entities/User/ClientDevice.cs (95%) rename {API => Kavita.Models}/Entities/User/ClientDeviceHistory.cs (85%) rename {API => Kavita.Models}/Entities/Volume.cs (97%) create mode 100644 Kavita.Models/Extensions/AppUserExtensions.cs create mode 100644 Kavita.Models/Extensions/ApplicationServiceExtensions.cs rename {API => Kavita.Models}/Extensions/EncodeFormatExtensions.cs (82%) create mode 100644 Kavita.Models/Extensions/EnumerableExtensions.cs rename {API => Kavita.Models}/Extensions/FilterDtoExtensions.cs (76%) rename {API => Kavita.Models}/Extensions/PlusMediaFormatExtensions.cs (95%) rename {API => Kavita.Models}/Helpers/OrderableHelper.cs (94%) create mode 100644 Kavita.Models/Kavita.Models.csproj rename {API/Data => Kavita.Models}/Metadata/ComicInfo.cs (65%) rename {API/Data/Scanner => Kavita.Models/Misc}/Chunk.cs (93%) create mode 100644 Kavita.Models/Parser/ParseScannedFiles.cs rename {API/Services/Tasks/Scanner => Kavita.Models}/Parser/ParserInfo.cs (71%) rename {API.Tests => Kavita.Server.Tests}/Helpers/BrowserHelperTests.cs (99%) create mode 100644 Kavita.Server.Tests/Kavita.Server.Tests.csproj create mode 100644 Kavita.Server.Tests/ManualMigrations/MigrateSmartFilterEncodingTests.cs rename {API.Tests => Kavita.Server.Tests}/Middleware/ClientInfoMiddlewareTests.cs (98%) rename {API => Kavita.Server}/Assets/anilist-no-image-placeholder.jpg (100%) rename {API/Middleware/Attribute => Kavita.Server/Attributes}/DisallowRoleAttribute.cs (94%) create mode 100644 Kavita.Server/Attributes/EntityAccessAttribute.cs create mode 100644 Kavita.Server/Attributes/KPlusAttribute.cs rename {API/Middleware => Kavita.Server/Attributes}/ProfilePrivacyAttribute.cs (93%) rename {API => Kavita.Server}/Controllers/AccountController.cs (51%) rename {API => Kavita.Server}/Controllers/ActivityController.cs (82%) rename {API => Kavita.Server}/Controllers/AdminController.cs (84%) rename {API => Kavita.Server}/Controllers/AnnotationController.cs (94%) rename {API => Kavita.Server}/Controllers/BaseApiController.cs (98%) rename {API => Kavita.Server}/Controllers/BookController.cs (56%) rename {API => Kavita.Server}/Controllers/CBLController.cs (67%) rename {API => Kavita.Server}/Controllers/ChapterController.cs (70%) rename {API => Kavita.Server}/Controllers/CollectionController.cs (56%) rename {API => Kavita.Server}/Controllers/ColorScapeController.cs (68%) rename {API => Kavita.Server}/Controllers/DeprecatedController.cs (74%) create mode 100644 Kavita.Server/Controllers/DeviceController.cs rename {API => Kavita.Server}/Controllers/DownloadController.cs (54%) create mode 100644 Kavita.Server/Controllers/EmailController.cs create mode 100644 Kavita.Server/Controllers/FallbackController.cs rename {API => Kavita.Server}/Controllers/FilterController.cs (55%) rename {API => Kavita.Server}/Controllers/FontController.cs (59%) rename {API => Kavita.Server}/Controllers/HealthController.cs (86%) create mode 100644 Kavita.Server/Controllers/ImageController.cs rename {API => Kavita.Server}/Controllers/KoreaderController.cs (73%) rename {API => Kavita.Server}/Controllers/LibraryController.cs (64%) rename {API => Kavita.Server}/Controllers/LicenseController.cs (92%) rename {API => Kavita.Server}/Controllers/LocaleController.cs (58%) rename {API => Kavita.Server}/Controllers/ManageController.cs (50%) rename {API => Kavita.Server}/Controllers/MetadataController.cs (93%) rename {API => Kavita.Server}/Controllers/OPDSController.cs (71%) rename {API => Kavita.Server}/Controllers/OidcController.cs (83%) rename {API => Kavita.Server}/Controllers/PanelsController.cs (54%) rename {API => Kavita.Server}/Controllers/PersonController.cs (59%) rename {API => Kavita.Server}/Controllers/PluginController.cs (93%) rename {API => Kavita.Server}/Controllers/RatingController.cs (57%) rename {API => Kavita.Server}/Controllers/ReaderController.cs (61%) rename {API => Kavita.Server}/Controllers/ReadingListController.cs (55%) rename {API => Kavita.Server}/Controllers/ReadingProfileController.cs (88%) rename {API => Kavita.Server}/Controllers/ReviewController.cs (56%) rename {API => Kavita.Server}/Controllers/ScrobblingController.cs (67%) rename {API => Kavita.Server}/Controllers/SearchController.cs (56%) rename {API => Kavita.Server}/Controllers/SeriesController.cs (69%) rename {API => Kavita.Server}/Controllers/ServerController.cs (61%) rename {API => Kavita.Server}/Controllers/SettingsController.cs (62%) rename {API => Kavita.Server}/Controllers/StatsController.cs (93%) rename {API => Kavita.Server}/Controllers/StreamController.cs (75%) rename {API => Kavita.Server}/Controllers/TachiyomiController.cs (53%) rename {API => Kavita.Server}/Controllers/ThemeController.cs (63%) rename {API => Kavita.Server}/Controllers/UploadController.cs (96%) rename {API => Kavita.Server}/Controllers/UsersController.cs (61%) create mode 100644 Kavita.Server/Controllers/VolumeController.cs rename {API => Kavita.Server}/Controllers/WantToReadController.cs (56%) rename {API => Kavita.Server}/EmailTemplates/AuthKeyExpired.html (100%) rename {API => Kavita.Server}/EmailTemplates/AuthKeyExpiredFragment.html (100%) rename {API => Kavita.Server}/EmailTemplates/AuthKeyExpiringFragment.html (100%) rename {API => Kavita.Server}/EmailTemplates/AuthKeyExpiringSoon.html (100%) rename {API => Kavita.Server}/EmailTemplates/EmailChange.html (100%) rename {API => Kavita.Server}/EmailTemplates/EmailConfirm.html (100%) rename {API => Kavita.Server}/EmailTemplates/EmailPasswordReset.html (100%) rename {API => Kavita.Server}/EmailTemplates/EmailTest.html (100%) rename {API => Kavita.Server}/EmailTemplates/KavitaPlusDebug.html (100%) rename {API => Kavita.Server}/EmailTemplates/SendToDevice.html (100%) rename {API => Kavita.Server}/EmailTemplates/TokenExpiration.html (100%) rename {API => Kavita.Server}/EmailTemplates/TokenExpiringSoon.html (100%) rename {API => Kavita.Server}/EmailTemplates/base.html (100%) create mode 100644 Kavita.Server/Extensions/ApplicationServiceExtensions.cs rename {API => Kavita.Server}/Extensions/HttpExtensions.cs (93%) rename {API => Kavita.Server}/Extensions/IdentityServiceExtensions.cs (96%) rename {API => Kavita.Server}/Helpers/BrowserHelper.cs (97%) rename {API => Kavita.Server}/Helpers/OpenIdConnectEventsHelper.cs (98%) rename {API => Kavita.Server}/I18N/ar.json (100%) rename {API => Kavita.Server}/I18N/as.json (100%) rename {API => Kavita.Server}/I18N/ca.json (100%) rename {API => Kavita.Server}/I18N/cs.json (100%) rename {API => Kavita.Server}/I18N/da.json (100%) rename {API => Kavita.Server}/I18N/de.json (100%) rename {API => Kavita.Server}/I18N/el.json (100%) rename {API => Kavita.Server}/I18N/en.json (100%) rename {API => Kavita.Server}/I18N/es.json (100%) rename {API => Kavita.Server}/I18N/et.json (100%) rename {API => Kavita.Server}/I18N/fa.json (100%) rename {API => Kavita.Server}/I18N/fi.json (100%) rename {API => Kavita.Server}/I18N/fr.json (100%) rename {API => Kavita.Server}/I18N/ga.json (100%) rename {API => Kavita.Server}/I18N/he.json (100%) rename {API => Kavita.Server}/I18N/hi.json (100%) rename {API => Kavita.Server}/I18N/hr.json (100%) rename {API => Kavita.Server}/I18N/hu.json (100%) rename {API => Kavita.Server}/I18N/id.json (100%) rename {API => Kavita.Server}/I18N/it.json (100%) rename {API => Kavita.Server}/I18N/ja.json (100%) rename {API => Kavita.Server}/I18N/ko.json (100%) rename {API => Kavita.Server}/I18N/lt.json (100%) rename {API => Kavita.Server}/I18N/ms.json (100%) rename {API => Kavita.Server}/I18N/nb_NO.json (100%) rename {API => Kavita.Server}/I18N/nl.json (100%) rename {API => Kavita.Server}/I18N/pl.json (100%) rename {API => Kavita.Server}/I18N/pt.json (100%) rename {API => Kavita.Server}/I18N/pt_BR.json (100%) rename {API => Kavita.Server}/I18N/ru.json (100%) rename {API => Kavita.Server}/I18N/sk.json (100%) rename {API => Kavita.Server}/I18N/sl.json (100%) rename {API => Kavita.Server}/I18N/sv.json (100%) rename {API => Kavita.Server}/I18N/ta.json (100%) rename {API => Kavita.Server}/I18N/te.json (100%) rename {API => Kavita.Server}/I18N/th.json (100%) rename {API => Kavita.Server}/I18N/tr.json (100%) rename {API => Kavita.Server}/I18N/uk.json (100%) rename {API => Kavita.Server}/I18N/vi.json (100%) rename {API => Kavita.Server}/I18N/zh_Hans.json (100%) rename {API => Kavita.Server}/I18N/zh_Hant.json (100%) create mode 100644 Kavita.Server/Kavita.Server.csproj rename {API => Kavita.Server}/Logging/LogEnricher.cs (95%) rename {API => Kavita.Server}/Logging/LogLevelOptions.cs (97%) create mode 100644 Kavita.Server/Logging/LoggingService.cs rename {API/Data/Misc => Kavita.Server/ManualMigrations}/ManualMigration.cs (92%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs (92%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs (95%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs (90%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs (97%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.7.14/MigrateManualHistory.cs (95%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs (90%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs (92%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs (95%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs (92%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs (90%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs (94%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs (95%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.0/MigrateChapterFields.cs (76%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.0/MigrateChapterNumber.cs (80%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.0/MigrateChapterRange.cs (87%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs (91%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs (95%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs (90%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.0/MigrateProgressExport.cs (97%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs (92%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs (93%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs (87%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs (91%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs (92%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs (93%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs (93%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs (91%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs (92%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs (90%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs (94%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs (90%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs (94%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.5/MigrateProgressExport.cs (96%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs (93%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs (92%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs (94%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.8/ManualMigrateBookReadingProgress.cs (96%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.8/ManualMigrateEnableMetadataMatchingDefault.cs (93%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.9/MigrateBadKoreaderProgress.cs (94%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.9/MigrateFormatToActivityData.cs (95%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.9/MigrateIncorrectUtcMidnightRollovers.cs (96%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.9/MigrateMissingAppUserRatingDateColumns.cs (98%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.9/MigrateMissingCreatedUtcDate.cs (90%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.9/MigrateProgressToReadingSessions.cs (96%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.9/MigrateToAuthKeys.cs (87%) rename {API/Data => Kavita.Server}/ManualMigrations/v0.8.9/MigrateTotalReads.cs (89%) rename {API => Kavita.Server}/Middleware/AuthKeyAuthenticationHandler.cs (96%) rename {API/Middleware/RateLimit => Kavita.Server/Middleware}/AuthenticationRateLimiterPolicy.cs (95%) rename {API/Services/Reading => Kavita.Server/Middleware}/ClientInfoAccessor.cs (66%) rename {API => Kavita.Server}/Middleware/ClientInfoMiddleware.cs (95%) rename {API => Kavita.Server}/Middleware/DeviceTrackingMiddleware.cs (81%) rename {API => Kavita.Server}/Middleware/ExceptionMiddleware.cs (89%) rename {API => Kavita.Server}/Middleware/SecurityMiddleware.cs (95%) rename {API => Kavita.Server}/Middleware/UpdateUserAsActiveMiddleware.cs (87%) rename {API => Kavita.Server}/Middleware/UserContextMiddleware.cs (91%) rename {API => Kavita.Server}/Program.cs (95%) create mode 100644 Kavita.Server/Properties/launchSettings.json rename {API => Kavita.Server}/Startup.cs (94%) rename {API/Services => Kavita.Server}/Store/CustomTicketStore.cs (98%) create mode 100644 Kavita.Server/Store/UserContext.cs rename {API => Kavita.Server}/config/appsettings.Development.json (100%) rename {API => Kavita.Server}/config/appsettings.json (100%) rename {API => Kavita.Server}/config/templates/EmailChange.html (100%) rename {API => Kavita.Server}/config/templates/EmailConfirm.html (100%) rename {API => Kavita.Server}/config/templates/EmailMigration.html (100%) rename {API => Kavita.Server}/config/templates/EmailPasswordReset.html (100%) rename {API => Kavita.Server}/config/templates/EmailTest.html (100%) rename {API => Kavita.Server}/config/templates/SendToDevice.html (100%) rename {API => Kavita.Server}/config/templates/TokenExpiration.html (100%) rename {API => Kavita.Server}/config/templates/TokenExpiringSoon.html (100%) rename {API.Tests/Services => Kavita.Services.Tests}/AccountServiceTests.cs (95%) rename {API.Tests/Services => Kavita.Services.Tests}/AnnotationServiceTests.cs (94%) rename {API.Tests/Services => Kavita.Services.Tests}/ArchiveServiceTests.cs (92%) rename {API.Tests/Services => Kavita.Services.Tests}/BackupServiceTests.cs (89%) rename {API.Tests/Services => Kavita.Services.Tests}/BookServiceTests.cs (87%) rename {API.Tests/Services => Kavita.Services.Tests}/BookmarkServiceTests.cs (91%) rename {API.Tests => Kavita.Services.Tests/Cache}/FakeHybridCache.cs (96%) rename {API.Tests => Kavita.Services.Tests/Cache}/FakeHybridCacheWithTracking.cs (87%) rename {API.Tests/Services => Kavita.Services.Tests}/CacheServiceTests.cs (97%) rename {API.Tests/Services => Kavita.Services.Tests}/CleanupServiceTests.cs (95%) rename {API.Tests/Services => Kavita.Services.Tests}/ClientDeviceServiceTests.cs (85%) rename {API.Tests/Services => Kavita.Services.Tests}/CollectionTagServiceTests.cs (96%) create mode 100644 Kavita.Services.Tests/Comparers/ChapterSortComparerTest.cs rename {API.Tests => Kavita.Services.Tests}/Comparers/ChapterSortComparerZeroFirstTests.cs (88%) create mode 100644 Kavita.Services.Tests/Comparers/SortComparerZeroLastTests.cs rename {API.Tests/Services => Kavita.Services.Tests}/CoverDbServiceTests.cs (90%) rename {API.Tests => Kavita.Services.Tests}/Data/AesopsFables.epub (100%) rename {API.Tests/Services => Kavita.Services.Tests}/DeviceServiceTests.cs (85%) rename {API.Tests/Services => Kavita.Services.Tests}/DeviceTrackingServiceTests.cs (98%) rename {API.Tests/Services => Kavita.Services.Tests}/DirectoryServiceTests.cs (95%) rename {API.Tests => Kavita.Services.Tests}/Entities/ComicInfoTests.cs (90%) rename {API.Tests/Services => Kavita.Services.Tests}/EntityNamingServiceTests.cs (99%) rename {API.Tests => Kavita.Services.Tests}/Extensions/ChapterListExtensionsTests.cs (67%) rename {API.Tests => Kavita.Services.Tests}/Extensions/FilterDtoExtensionsTests.cs (83%) rename {API.Tests => Kavita.Services.Tests}/Extensions/ParserInfoListExtensionsTests.cs (86%) rename {API.Tests => Kavita.Services.Tests}/Extensions/SeriesExtensionsTests.cs (98%) rename {API.Tests => Kavita.Services.Tests}/Extensions/SeriesFilterTests.cs (98%) rename {API.Tests => Kavita.Services.Tests}/Extensions/VolumeListExtensionsTests.cs (59%) rename {API.Tests/Services => Kavita.Services.Tests}/ExternalMetadataServiceTests.cs (99%) rename {API.Tests/Services => Kavita.Services.Tests}/FileSystemTests.cs (87%) rename {API.Tests => Kavita.Services.Tests}/Helpers/BookSortTitlePrefixHelperTests.cs (99%) rename {API.Tests => Kavita.Services.Tests}/Helpers/CacheHelperTests.cs (97%) rename {API.Tests => Kavita.Services.Tests}/Helpers/KoreaderHelperTests.cs (98%) rename {API.Tests => Kavita.Services.Tests}/Helpers/OrderableHelperTests.cs (97%) rename {API.Tests => Kavita.Services.Tests}/Helpers/ParserInfoFactory.cs (90%) rename {API.Tests => Kavita.Services.Tests}/Helpers/PersonHelperTests.cs (97%) create mode 100644 Kavita.Services.Tests/Helpers/ReviewHelperTests.cs rename {API.Tests => Kavita.Services.Tests}/Helpers/ScannerHelper.cs (95%) rename {API.Tests => Kavita.Services.Tests}/Helpers/SeriesHelperTests.cs (96%) rename {API.Tests => Kavita.Services.Tests}/Helpers/SmartFilterHelperTests.cs (62%) rename {API.Tests => Kavita.Services.Tests}/Helpers/TestCaseGenerator.cs (97%) rename {API.Tests/Services => Kavita.Services.Tests}/ImageServiceTests.cs (96%) create mode 100644 Kavita.Services.Tests/Kavita.Services.Tests.csproj rename {API.Tests/Services => Kavita.Services.Tests}/MetadataServiceTests.cs (81%) rename {API.Tests/Services => Kavita.Services.Tests}/OidcServiceTests.cs (98%) rename {API.Tests/Services => Kavita.Services.Tests}/OpdsServiceTests.cs (97%) rename {API.Tests/Services => Kavita.Services.Tests}/ParseScannedFilesTests.cs (98%) rename {API.Tests => Kavita.Services.Tests}/Parsers/BasicParserTests.cs (97%) rename {API.Tests => Kavita.Services.Tests}/Parsers/BookParserTests.cs (94%) rename {API.Tests => Kavita.Services.Tests}/Parsers/ComicVineParserTests.cs (96%) rename {API.Tests => Kavita.Services.Tests}/Parsers/DefaultParserTests.cs (99%) rename {API.Tests => Kavita.Services.Tests}/Parsers/ImageParserTests.cs (96%) rename {API.Tests => Kavita.Services.Tests}/Parsers/PdfParserTests.cs (95%) rename {API.Tests => Kavita.Services.Tests}/Parsing/BookParsingTests.cs (70%) rename {API.Tests => Kavita.Services.Tests}/Parsing/ComicParsingTests.cs (99%) rename {API.Tests => Kavita.Services.Tests}/Parsing/ImageParsingTests.cs (95%) rename {API.Tests => Kavita.Services.Tests}/Parsing/MangaParsingTests.cs (99%) rename {API.Tests => Kavita.Services.Tests}/Parsing/ParserInfoTests.cs (94%) rename {API.Tests => Kavita.Services.Tests}/Parsing/ParsingTests.cs (98%) rename {API.Tests/Services => Kavita.Services.Tests}/PersonServiceTests.cs (97%) rename {API.Tests/Services => Kavita.Services.Tests}/ProcessSeriesTests.cs (90%) rename {API.Tests/Services => Kavita.Services.Tests}/RatingServiceTests.cs (95%) rename {API.Tests/Services => Kavita.Services.Tests}/ReaderServiceRereadTests.cs (98%) rename {API.Tests/Services => Kavita.Services.Tests}/ReaderServiceTests.cs (88%) rename {API.Tests/Services => Kavita.Services.Tests}/ReadingHistoryServiceTests.cs (90%) rename {API.Tests/Services => Kavita.Services.Tests}/ReadingListServiceTests.cs (96%) rename {API.Tests/Services => Kavita.Services.Tests}/ReadingProfileServiceTest.cs (99%) rename {API.Tests/Services => Kavita.Services.Tests}/ScannerServiceTests.cs (98%) rename {API.Tests/Services => Kavita.Services.Tests}/ScrobblingServiceTests.cs (96%) rename {API.Tests/Services => Kavita.Services.Tests}/SeriesServiceTests.cs (99%) rename {API.Tests/Services => Kavita.Services.Tests}/SettingsServiceTests.cs (97%) rename {API.Tests/Services => Kavita.Services.Tests}/SiteThemeServiceTests.cs (94%) rename {API.Tests/Services => Kavita.Services.Tests}/TachiyomiServiceTests.cs (92%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/Archives/LICENSE.md (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/Archives/empty.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/Archives/file in folder in folder.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/Archives/file in folder.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/Archives/file in folder_alt.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/Archives/flat file.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/Archives/macos_native.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/Archives/macos_none.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/Archives/macos_one.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/Archives/macos_withdotunder_one.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/Archives/winrar.rar (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/ComicInfos/ComicInfo.xml (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/ComicInfos/ComicInfo.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/ComicInfos/ComicInfo2.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/ComicInfos/ComicInfo_authors.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.rar (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos_reversed.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root_SharpCompress.cb7 (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/ComicInfos/Umlaut.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/ComicInfos/file in folder.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/macos_native.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/macos_native.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/sorting.expected.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/sorting.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/test.expected.jpg (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/test.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/thumbnail.expected.jpg (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/thumbnail.jpg (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.cbz (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.expected.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/v10 - nested folder.cbz (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.old.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/v10 - with folder.cbz (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.jpg (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/v10.cbz (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/CoverImages/v10.expected.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/Formats/One File with DB_Supported.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ArchiveService/Thumbnails/001.jpg (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/BookService/Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/BookService/Relative Key Test File.epub (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/BookService/Rollo at Work SP01.pdf (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/BookService/The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/BookService/TitleWithVolume.epub (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/BookService/TitleWithVolume_NoSeriesOrSeriesIndex.epub (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/BookService/content.opf (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/BookService/encrypted.pdf (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/BookService/indirect.pdf (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/BookService/test.pdf (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/BookService/test_ſ.pdf (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/CacheService/Archives/file in folder in folder.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/CoverDbService/Existing/01.webp (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/CoverDbService/Favicons/anilist.co.webp (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/DirectoryService/TestCases/Manga-testcase.txt (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/DirectoryService/extension/file.cbz (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/DirectoryService/extension/file.rar (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/DirectoryService/extension/file2.cbz (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/DirectoryService/regex/file.txt (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/DirectoryService/regex/file2.txt (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/ColorScapes/blue-2.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/ColorScapes/blue.jpg (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/ColorScapes/green-red.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/ColorScapes/green.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/ColorScapes/lightblue-2.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/ColorScapes/lightblue.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/ColorScapes/pink.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/ColorScapes/yellow-blue.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/Covers/comic-normal-2.jpg (100%) create mode 100644 Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-2_baseline.png rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/Covers/comic-normal-3.jpg (100%) create mode 100644 Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-3_baseline.png rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/Covers/comic-normal.jpg (100%) create mode 100644 Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal_baseline.png rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/Covers/comic-square.jpg (100%) create mode 100644 Kavita.Services.Tests/Test Data/ImageService/Covers/comic-square_baseline.png rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/Covers/comic-wide.jpg (100%) create mode 100644 Kavita.Services.Tests/Test Data/ImageService/Covers/comic-wide_baseline.png rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/Covers/manga-cover.png (100%) create mode 100644 Kavita.Services.Tests/Test Data/ImageService/Covers/manga-cover_baseline.png rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/Covers/spread-cover.jpg (100%) create mode 100644 Kavita.Services.Tests/Test Data/ImageService/Covers/spread-cover_baseline.png rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/Covers/webtoon-strip-2.png (100%) create mode 100644 Kavita.Services.Tests/Test Data/ImageService/Covers/webtoon-strip-2_baseline.png rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/Covers/webtoon-strip.jpg (100%) create mode 100644 Kavita.Services.Tests/Test Data/ImageService/Covers/webtoon-strip_baseline.png rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/Covers/wide-ad.png (100%) create mode 100644 Kavita.Services.Tests/Test Data/ImageService/Covers/wide-ad_baseline.png rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ImageService/cover.expected.jpg (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/OpdsService/test.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ReadingListService/Annual.cbl (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ReadingListService/Fables.cbl (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/1x1.png (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Alternating Removal - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Base.zip (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Delete Series In UI - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Exclude Pattern 1 - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Flat Series - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Flat Series with Specials Folder - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Flat Series with Specials Folder Alt Naming - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Flat Special - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Image Series with SP Folder (Non English) - Image.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Image Series with SP Folder - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Localized Name matches Filename - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Manga-testcase.txt (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Multiple Roots - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Nested Chapters - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/PDF Comic Chapters - Comic.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/PDF Comic Chapters - LightNovel.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Publisher - ComicVine.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Scan Library Parses as ( - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Series and Series-Series Combined - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Series removed when no other changes are made - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Series with Extra - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Series with Localized - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Series with Localized 2 - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Series with Prefix - Book.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Series with slight differences No Metadata - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Sort Order - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Subfolder always scanning fix publisher layout - Comic.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Subfolders always scanning all series changes - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Subfolders and files at root (2) - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/Test Data/ScannerService/TestCases/Subfolders and files at root - Manga.json (100%) rename {API.Tests/Services => Kavita.Services.Tests}/TokenServiceTests.cs (93%) rename {API.Tests/Services => Kavita.Services.Tests}/VersionUpdaterServiceTests.cs (98%) rename {API.Tests/Services => Kavita.Services.Tests}/WordCountAnalysisTests.cs (89%) create mode 100644 Kavita.Services/AccountService.cs rename {API/Services => Kavita.Services}/AnnotationService.cs (89%) rename {API/Services => Kavita.Services}/ArchiveService.cs (63%) create mode 100644 Kavita.Services/AuthKeyService.cs create mode 100644 Kavita.Services/BackupService.cs rename {API/Services => Kavita.Services}/BookService.cs (88%) rename {API/Services => Kavita.Services}/BookmarkService.cs (53%) rename {API/Helpers => Kavita.Services}/Builders/ChapterBuilder.cs (94%) rename {API/Helpers => Kavita.Services}/Builders/MangaFileBuilder.cs (90%) rename {API/Helpers => Kavita.Services}/Builders/VolumeBuilder.cs (88%) rename {API/Services => Kavita.Services}/CacheService.cs (60%) create mode 100644 Kavita.Services/CleanupService.cs rename {API/Services => Kavita.Services}/ClientDeviceService.cs (78%) create mode 100644 Kavita.Services/CollectionTagService.cs rename {API => Kavita.Services}/Comparators/ChapterSortComparer.cs (94%) create mode 100644 Kavita.Services/DeviceService.cs rename {API/Services => Kavita.Services}/DeviceTrackingService.cs (84%) rename {API/Services => Kavita.Services}/DirectoryService.cs (88%) rename {API/Services => Kavita.Services}/DownloadService.cs (89%) rename {API/Services => Kavita.Services}/EmailService.cs (81%) rename {API/Services => Kavita.Services}/EntityNamingService.cs (85%) rename {API => Kavita.Services}/Extensions/ApplicationServiceExtensions.cs (56%) create mode 100644 Kavita.Services/Extensions/ChapterExtensions.cs rename {API => Kavita.Services}/Extensions/ChapterListExtensions.cs (80%) create mode 100644 Kavita.Services/Extensions/ComicInfoExtensions.cs rename {API => Kavita.Services}/Extensions/FileTypeGroupExtensions.cs (58%) rename {API => Kavita.Services}/Extensions/IHasKPlusMetadataExtensions.cs (79%) create mode 100644 Kavita.Services/Extensions/ParserExtensions.cs rename {API => Kavita.Services}/Extensions/ParserInfoListExtensions.cs (73%) rename {API => Kavita.Services}/Extensions/SeriesExtensions.cs (81%) create mode 100644 Kavita.Services/Extensions/VolumeExtensions.cs rename {API => Kavita.Services}/Extensions/VolumeListExtensions.cs (77%) rename {API => Kavita.Services}/Extensions/ZipArchiveExtensions.cs (70%) rename {API/Services => Kavita.Services}/FileService.cs (87%) rename {API/Services => Kavita.Services}/FontService.cs (61%) rename {API => Kavita.Services}/Helpers/AnnotationHelper.cs (99%) rename {API => Kavita.Services}/Helpers/BookChapterItemHelper.cs (96%) rename {API => Kavita.Services}/Helpers/BookSortTitlePrefixHelper.cs (98%) rename {API => Kavita.Services}/Helpers/CacheHelper.cs (84%) rename {API => Kavita.Services}/Helpers/GenreHelper.cs (96%) rename {API => Kavita.Services}/Helpers/KoreaderHelper.cs (98%) rename {API => Kavita.Services}/Helpers/PdfComicInfoExtractor.cs (79%) rename {API => Kavita.Services}/Helpers/PdfMetadataExtractor.cs (99%) rename {API => Kavita.Services}/Helpers/PersonHelper.cs (96%) create mode 100644 Kavita.Services/Helpers/ReviewHelper.cs rename {API => Kavita.Services}/Helpers/SeriesHelper.cs (89%) rename {API => Kavita.Services}/Helpers/SmartFilterHelper.cs (98%) rename {API => Kavita.Services}/Helpers/TagHelper.cs (85%) rename {API/Services => Kavita.Services}/HostedServices/ReadingSessionInitializer.cs (96%) rename {API/Services => Kavita.Services}/HostedServices/StartupTasksHostedService.cs (83%) rename {API/Services => Kavita.Services}/ImageService.cs (82%) create mode 100644 Kavita.Services/Kavita.Services.csproj create mode 100644 Kavita.Services/KoreaderService.cs rename {API/Services => Kavita.Services}/LocalizationService.cs (97%) create mode 100644 Kavita.Services/MediaConversionService.cs create mode 100644 Kavita.Services/MediaErrorService.cs rename {API/Services/Tasks => Kavita.Services}/Metadata/CoverDbService.cs (92%) rename {API/Services/Tasks => Kavita.Services}/Metadata/WordCountAnalyzerService.cs (61%) rename {API/Services => Kavita.Services}/MetadataService.cs (58%) rename {API/Services => Kavita.Services}/OidcService.cs (93%) rename {API/Services => Kavita.Services}/OpdsService.cs (74%) rename {API/Services => Kavita.Services}/PersonService.cs (75%) rename {API/Services => Kavita.Services}/Plus/ExternalMetadataService.cs (90%) rename {API/Services => Kavita.Services}/Plus/KavitaPlusApiService.cs (69%) rename {API/Services => Kavita.Services}/Plus/LicenseService.cs (84%) rename {API/Services => Kavita.Services}/Plus/ScrobblingService.cs (82%) rename {API/Services => Kavita.Services}/Plus/SmartCollectionSyncService.cs (61%) rename {API/Services => Kavita.Services}/Plus/WantToReadSyncService.cs (62%) create mode 100644 Kavita.Services/RatingService.cs rename {API/Services => Kavita.Services}/Reading/ReaderService.cs (95%) rename {API/Services => Kavita.Services}/Reading/ReadingHistoryService.cs (84%) rename {API/Services => Kavita.Services/Reading}/ReadingItemService.cs (92%) rename {API/Services => Kavita.Services/Reading}/ReadingListService.cs (75%) rename {API/Services => Kavita.Services/Reading}/ReadingProfileService.cs (74%) rename {API/Services => Kavita.Services}/Reading/ReadingSessionService.cs (96%) create mode 100644 Kavita.Services/Repositories/CoverDbRepository.cs rename {API/Services/Tasks/Scanner/Parser => Kavita.Services/Scanner}/BasicParser.cs (67%) rename {API/Services/Tasks/Scanner/Parser => Kavita.Services/Scanner}/BookParser.cs (65%) rename {API/Services/Tasks/Scanner/Parser => Kavita.Services/Scanner}/ComicVineParser.cs (73%) rename {API/Services/Tasks/Scanner/Parser => Kavita.Services/Scanner}/DefaultParser.cs (75%) rename {API/Services/Tasks/Scanner/Parser => Kavita.Services/Scanner}/ImageParser.cs (74%) rename {API/Services/Tasks => Kavita.Services}/Scanner/LibraryWatcher.cs (93%) rename {API/Services/Tasks => Kavita.Services}/Scanner/ParseScannedFiles.cs (92%) rename {API/Services/Tasks/Scanner/Parser => Kavita.Services/Scanner}/Parser.cs (98%) rename {API/Services/Tasks/Scanner/Parser => Kavita.Services/Scanner}/PdfParser.cs (66%) rename {API/Services/Tasks => Kavita.Services}/Scanner/ProcessSeries.cs (94%) rename {API/Services/Tasks => Kavita.Services/Scanner}/ScannerService.cs (65%) rename {API/Services => Kavita.Services}/SeriesService.cs (81%) rename {API/Services => Kavita.Services}/SettingsService.cs (78%) create mode 100644 Kavita.Services/SignalR/EventHub.cs rename {API => Kavita.Services}/SignalR/LogHub.cs (84%) rename {API => Kavita.Services}/SignalR/MessageHub.cs (87%) rename {API/SignalR/Presence => Kavita.Services/SignalR}/PresenceTracker.cs (86%) rename {API/Services => Kavita.Services}/SiteThemeService.cs (60%) rename {API/Services => Kavita.Services}/StatisticService.cs (88%) rename {API/Services/Tasks => Kavita.Services}/StatsService.cs (93%) create mode 100644 Kavita.Services/StreamService.cs rename {API/Services => Kavita.Services}/TachiyomiService.cs (52%) rename {API/Services => Kavita.Services}/TaskScheduler.cs (81%) rename {API/Services => Kavita.Services}/TokenService.cs (56%) rename {API/Services/Tasks => Kavita.Services}/VersionUpdaterService.cs (97%) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 50bfec06d..222f32903 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -12,7 +12,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 1 - name: Setup .NET Core uses: actions/setup-dotnet@v4 diff --git a/.github/workflows/build-ui.yml b/.github/workflows/build-ui.yml index 76f3dfb57..f227b3e69 100644 --- a/.github/workflows/build-ui.yml +++ b/.github/workflows/build-ui.yml @@ -12,7 +12,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 1 - name: NodeJS to Compile WebUI uses: actions/setup-node@v4 diff --git a/.gitignore b/.gitignore index 4de30b256..290dfcf79 100644 --- a/.gitignore +++ b/.gitignore @@ -485,64 +485,64 @@ Thumbs.db ssl/ # App specific -/API/kavita.db -/API/kavita.db-shm -/API/kavita.db-wal -/API/Hangfire.db -/API/Hangfire-log.db -API/cache/ -/API/wwwroot/ -/API/cache/ -/API/temp/ +/Kavita.Server/kavita.db +/Kavita.Server/kavita.db-shm +/Kavita.Server/kavita.db-wal +/Kavita.Server/Hangfire.db +/Kavita.Server/Hangfire-log.db +Kavita.Server/cache/ +/Kavita.Server/wwwroot/ +/Kavita.Server/cache/ +/Kavita.Server/temp/ _temp/ _output/ -API/stats/ +Kavita.Server/stats/ UI/Web/dist/ -/API.Tests/Extensions/Test Data/modified on run.txt +/Kavita.Services.Tests/Extensions/Test Data/modified on run.txt # All config files/folders in config except appsettings.json -/API/config-bak/ -/API/config-bak/*.* -/API/config-bak/**/ -/API/config/covers/ -/API/config/logs/ -/API/config/backups/ -/API/config/cache/ -/API/config/fonts/ -/API/config/temp/ -/API/config/themes/ -/API/config/stats/ -/API/config/bookmarks/ -/API/config/favicons/ -/API/config/cache-long/ -/API/config/*.db-shm -/API/config/*.db-wal -/API/config/*.db-journal -/API/config/*.db -/API/config/*.bak -/API/config/*.backup -/API/config/*.csv -/API/config/Hangfire.db -/API/config/Hangfire-log.db -API/config/covers/ -API/config/images/* -API/config/stats/* -API/config/stats/app_stats.json -API/config/pre-metadata/ -API/config/post-metadata/ -API/config/*.csv -API.Tests/TestResults/ +/Kavita.Server/config-bak/ +/Kavita.Server/config-bak/*.* +/Kavita.Server/config-bak/**/ +/Kavita.Server/config/covers/ +/Kavita.Server/config/logs/ +/Kavita.Server/config/backups/ +/Kavita.Server/config/cache/ +/Kavita.Server/config/fonts/ +/Kavita.Server/config/temp/ +/Kavita.Server/config/themes/ +/Kavita.Server/config/stats/ +/Kavita.Server/config/bookmarks/ +/Kavita.Server/config/favicons/ +/Kavita.Server/config/cache-long/ +/Kavita.Server/config/*.db-shm +/Kavita.Server/config/*.db-wal +/Kavita.Server/config/*.db-journal +/Kavita.Server/config/*.db +/Kavita.Server/config/*.bak +/Kavita.Server/config/*.backup +/Kavita.Server/config/*.csv +/Kavita.Server/config/Hangfire.db +/Kavita.Server/config/Hangfire-log.db +Kavita.Server/config/covers/ +Kavita.Server/config/images/* +Kavita.Server/config/stats/* +Kavita.Server/config/stats/app_stats.json +Kavita.Server/config/pre-metadata/ +Kavita.Server/config/post-metadata/ +Kavita.Server/config/*.csv +Kavita.Services.Tests/TestResults/ UI/Web/.vscode/settings.json -/API.Tests/Services/Test Data/ArchiveService/CoverImages/output/* +/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/output/* UI/Web/.angular/ BenchmarkDotNet.Artifacts .claude/ -API/config/*backup.zip +Kavita.Server/config/*backup.zip -API.Tests/Services/Test Data/ImageService/**/*_output* -API.Tests/Services/Test Data/ImageService/**/*_baseline* -API.Tests/Services/Test Data/ImageService/**/*.html +Kavita.Services.Tests/Test Data/ImageService/**/*_output*.* +Kavita.Services.Tests/Test Data/ImageService/**/*_baseline* +Kavita.Services.Tests/Test Data/ImageService/**/*.html +Kavita.Services.Tests/Test Data/ScannerService/ScanTests/**/* - -API.Tests/Services/Test Data/ScannerService/ScanTests/**/* +Kavita.Server/config/appsettings.*.json diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj deleted file mode 100644 index e1451620e..000000000 --- a/API.Benchmark/API.Benchmark.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - net10.0 - Exe - - - - - - - - - - - - - - - Always - - - - - Data - Always - - - - - PreserveNewest - - - - diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj deleted file mode 100644 index 098676370..000000000 --- a/API.Tests/API.Tests.csproj +++ /dev/null @@ -1,45 +0,0 @@ - - - - net10.0 - false - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - - diff --git a/API.Tests/Comparers/ChapterSortComparerTest.cs b/API.Tests/Comparers/ChapterSortComparerTest.cs deleted file mode 100644 index 39a68b3b0..000000000 --- a/API.Tests/Comparers/ChapterSortComparerTest.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Linq; -using API.Comparators; -using Xunit; - -namespace API.Tests.Comparers; - -public class ChapterSortComparerDefaultLastTest -{ - [Theory] - [InlineData(new[] {1, 2, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber}, new[] {1, 2, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] - [InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})] - [InlineData(new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber}, new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] - [InlineData(new[] {API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, 1}, new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] - public void ChapterSortTest(int[] input, int[] expected) - { - Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerDefaultLast()).ToArray()); - } - -} diff --git a/API.Tests/Comparers/NumericComparerTests.cs b/API.Tests/Comparers/NumericComparerTests.cs deleted file mode 100644 index 8a1f23773..000000000 --- a/API.Tests/Comparers/NumericComparerTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using API.Comparators; -using Xunit; - -namespace API.Tests.Comparers; - -public class NumericComparerTests -{ - [Theory] - [InlineData( - new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"}, - new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"} - )] - [InlineData( - new[] {"x1.0.jpg", "0.5.jpg", "0.3.jpg"}, - new[] {"0.3.jpg", "0.5.jpg", "x1.0.jpg",} - )] - public void NumericComparerTest(string[] input, string[] expected) - { - var nc = new NumericComparer(); - Array.Sort(input, nc); - - var i = 0; - foreach (var s in input) - { - Assert.Equal(s, expected[i]); - i++; - } - } -} diff --git a/API.Tests/Comparers/SortComparerZeroLastTests.cs b/API.Tests/Comparers/SortComparerZeroLastTests.cs deleted file mode 100644 index 9a0722984..000000000 --- a/API.Tests/Comparers/SortComparerZeroLastTests.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Linq; -using API.Comparators; -using Xunit; - -namespace API.Tests.Comparers; - -public class SortComparerZeroLastTests -{ - [Theory] - [InlineData(new[] {API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, 1, 2,}, new[] {1, 2, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] - [InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})] - [InlineData(new[] {API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, 1}, new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] - public void SortComparerZeroLastTest(int[] input, int[] expected) - { - Assert.Equal(expected, input.OrderBy(f => f, ChapterSortComparerDefaultLast.Default).ToArray()); - } -} diff --git a/API.Tests/Comparers/StringLogicalComparerTest.cs b/API.Tests/Comparers/StringLogicalComparerTest.cs deleted file mode 100644 index 13f88243d..000000000 --- a/API.Tests/Comparers/StringLogicalComparerTest.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using API.Comparators; -using Xunit; - -namespace API.Tests.Comparers; - -public class StringLogicalComparerTest -{ - [Theory] - [InlineData( - new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"}, - new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"} - )] - [InlineData( - new[] {"a.jpg", "aaa.jpg", "1.jpg", }, - new[] {"1.jpg", "a.jpg", "aaa.jpg"} - )] - [InlineData( - new[] {"a.jpg", "aaa.jpg", "1.jpg", "!cover.png"}, - new[] {"!cover.png", "1.jpg", "a.jpg", "aaa.jpg"} - )] - public void StringComparer(string[] input, string[] expected) - { - Array.Sort(input, StringLogicalComparer.Compare); - - var i = 0; - foreach (var s in input) - { - Assert.Equal(s, expected[i]); - i++; - } - } -} diff --git a/API.Tests/Extensions/FileInfoExtensionsTests.cs b/API.Tests/Extensions/FileInfoExtensionsTests.cs deleted file mode 100644 index e708356a9..000000000 --- a/API.Tests/Extensions/FileInfoExtensionsTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using API.Extensions; -using Xunit; - -namespace API.Tests.Extensions; - -public class FileInfoExtensionsTests -{ - private static readonly string TestDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Extensions/Test Data/"); - - [Fact] - public void HasFileBeenModifiedSince_ShouldBeFalse() - { - var filepath = Path.Join(TestDirectory, "not modified.txt"); - var date = new FileInfo(filepath).LastWriteTime; - Assert.False(new FileInfo(filepath).HasFileBeenModifiedSince(date)); - File.ReadAllText(filepath); - Assert.False(new FileInfo(filepath).HasFileBeenModifiedSince(date)); - } - - [Fact] - public void HasFileBeenModifiedSince_ShouldBeTrue() - { - var filepath = Path.Join(TestDirectory, "modified on run.txt"); - var date = new FileInfo(filepath).LastWriteTime; - Assert.False(new FileInfo(filepath).HasFileBeenModifiedSince(date)); - File.AppendAllLines(filepath, new[] { DateTime.Now.ToString(CultureInfo.InvariantCulture) }); - Assert.True(new FileInfo(filepath).HasFileBeenModifiedSince(date)); - } -} diff --git a/API.Tests/Extensions/Test Data/not modified.txt b/API.Tests/Extensions/Test Data/not modified.txt deleted file mode 100644 index d5c0ce0a5..000000000 --- a/API.Tests/Extensions/Test Data/not modified.txt +++ /dev/null @@ -1 +0,0 @@ -Hello, this file should not be modified \ No newline at end of file diff --git a/API.Tests/Helpers/ParserInfoHelperTests.cs b/API.Tests/Helpers/ParserInfoHelperTests.cs deleted file mode 100644 index 0bb7efb9b..000000000 --- a/API.Tests/Helpers/ParserInfoHelperTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner; -using API.Services.Tasks.Scanner.Parser; -using Xunit; - -namespace API.Tests.Helpers; - -public class ParserInfoHelperTests -{ - #region SeriesHasMatchingParserInfoFormat - - [Fact] - public void SeriesHasMatchingParserInfoFormat_ShouldBeFalse() - { - var infos = new Dictionary>(); - - ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Archive}); - //AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Epub}); - - var series = new SeriesBuilder("Darker Than Black") - .WithFormat(MangaFormat.Epub) - .WithVolume(new VolumeBuilder("1") - .WithName("1") - .Build()) - .WithLocalizedName("Darker Than Black") - .Build(); - - Assert.False(ParserInfoHelpers.SeriesHasMatchingParserInfoFormat(series, infos)); - } - - [Fact] - public void SeriesHasMatchingParserInfoFormat_ShouldBeTrue() - { - var infos = new Dictionary>(); - - ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Archive}); - ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Epub}); - - - var series = new SeriesBuilder("Darker Than Black") - .WithFormat(MangaFormat.Epub) - .WithVolume(new VolumeBuilder("1") - .WithName("1") - .Build()) - .WithLocalizedName("Darker Than Black") - .Build(); - - Assert.True(ParserInfoHelpers.SeriesHasMatchingParserInfoFormat(series, infos)); - } - - #endregion -} diff --git a/API.Tests/Helpers/ReviewHelperTests.cs b/API.Tests/Helpers/ReviewHelperTests.cs deleted file mode 100644 index 44e255390..000000000 --- a/API.Tests/Helpers/ReviewHelperTests.cs +++ /dev/null @@ -1,258 +0,0 @@ -using API.Helpers; -using System.Collections.Generic; -using System.Linq; -using Xunit; -using API.DTOs.SeriesDetail; - -namespace API.Tests.Helpers; - -public class ReviewHelperTests -{ - #region SelectSpectrumOfReviews Tests - - [Fact] - public void SelectSpectrumOfReviews_WhenLessThan10Reviews_ReturnsAllReviews() - { - - var reviews = CreateReviewList(8); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(8, result.Count); - Assert.Equal(reviews, result.OrderByDescending(r => r.Score)); - } - - [Fact] - public void SelectSpectrumOfReviews_WhenMoreThan10Reviews_Returns10Reviews() - { - - var reviews = CreateReviewList(20); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(10, result.Count); - Assert.Equal(reviews[0], result.First()); - Assert.Equal(reviews[19], result.Last()); - } - - [Fact] - public void SelectSpectrumOfReviews_WithExactly10Reviews_ReturnsAllReviews() - { - - var reviews = CreateReviewList(10); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(10, result.Count); - } - - [Fact] - public void SelectSpectrumOfReviews_WithLargeNumberOfReviews_ReturnsCorrectSpectrum() - { - - var reviews = CreateReviewList(100); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(10, result.Count); - Assert.Contains(reviews[0], result); - Assert.Contains(reviews[1], result); - Assert.Contains(reviews[98], result); - Assert.Contains(reviews[99], result); - } - - [Fact] - public void SelectSpectrumOfReviews_WithEmptyList_ReturnsEmptyList() - { - - var reviews = new List(); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Empty(result); - } - - [Fact] - public void SelectSpectrumOfReviews_ResultsOrderedByScoreDescending() - { - - var reviews = new List - { - new UserReviewDto { Tagline = "1", Score = 3 }, - new UserReviewDto { Tagline = "2", Score = 5 }, - new UserReviewDto { Tagline = "3", Score = 1 }, - new UserReviewDto { Tagline = "4", Score = 4 }, - new UserReviewDto { Tagline = "5", Score = 2 } - }; - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(5, result.Count); - Assert.Equal(5, result[0].Score); - Assert.Equal(4, result[1].Score); - Assert.Equal(3, result[2].Score); - Assert.Equal(2, result[3].Score); - Assert.Equal(1, result[4].Score); - } - - #endregion - - #region GetCharacters Tests - - [Fact] - public void GetCharacters_WithNullBody_ReturnsNull() - { - - string body = null; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Null(result); - } - - [Fact] - public void GetCharacters_WithEmptyBody_ReturnsEmptyString() - { - - var body = string.Empty; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal(string.Empty, result); - } - - [Fact] - public void GetCharacters_WithNoTextNodes_ReturnsEmptyString() - { - - const string body = "
"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal(string.Empty, result); - } - - [Fact] - public void GetCharacters_WithLessCharactersThanLimit_ReturnsFullText() - { - - var body = "

This is a short review.

"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal("This is a short review.…", result); - } - - [Fact] - public void GetCharacters_WithMoreCharactersThanLimit_TruncatesText() - { - - var body = "

" + new string('a', 200) + "

"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal(new string('a', 175) + "…", result); - Assert.Equal(176, result.Length); // 175 characters + ellipsis - } - - [Fact] - public void GetCharacters_IgnoresScriptTags() - { - - const string body = "

Visible text

"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal("Visible text…", result); - Assert.DoesNotContain("hidden", result); - } - - [Fact] - public void GetCharacters_RemovesMarkdownSymbols() - { - - const string body = "

This is **bold** and _italic_ text with [link](url).

"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal("This is bold and italic text with link.…", result); - } - - [Fact] - public void GetCharacters_HandlesComplexMarkdownAndHtml() - { - - const string body = """ - -
-

# Header

-

This is ~~strikethrough~~ and __underlined__ text

-

~~~code block~~~

-

+++highlighted+++

-

img123(image.jpg)

-
- """; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.DoesNotContain("~~", result); - Assert.DoesNotContain("__", result); - Assert.DoesNotContain("~~~", result); - Assert.DoesNotContain("+++", result); - Assert.DoesNotContain("img123(", result); - Assert.Contains("Header", result); - Assert.Contains("strikethrough", result); - Assert.Contains("underlined", result); - Assert.Contains("code block", result); - Assert.Contains("highlighted", result); - } - - #endregion - - #region Helper Methods - - private static List CreateReviewList(int count) - { - var reviews = new List(); - for (var i = 0; i < count; i++) - { - reviews.Add(new UserReviewDto - { - Tagline = $"{i + 1}", - Score = count - i // This makes them ordered by score descending initially - }); - } - return reviews; - } - - #endregion -} - diff --git a/API/API.csproj b/API/API.csproj deleted file mode 100644 index e5276ac0d..000000000 --- a/API/API.csproj +++ /dev/null @@ -1,216 +0,0 @@ - - - - Default - net10.0 - true - Linux - true - true - ../favicon.ico - warnings - latestmajor - false - - - - - false - ../favicon.ico - bin\$(Configuration)\$(AssemblyName).xml - - - - bin\$(Configuration)\$(AssemblyName).xml - 1701;1702;1591 - - - - - True - $(NoWarn);1591 - $(NoWarn);CA1873 - - - - en - - - - - Kavita - kareadita.github.io - Copyright 2020-$([System.DateTime]::Now.ToString('yyyy')) kavitareader.com (GNU General Public v3) - - $(Configuration)-dev - - false - false - false - - False - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Always - - - - - Always - - - - - - - - - Always - - - Always - - - - - - - <_DeploymentManifestIconFile Remove="favicon.ico" /> - - - diff --git a/API/API.csproj.DotSettings b/API/API.csproj.DotSettings deleted file mode 100644 index ced14c154..000000000 --- a/API/API.csproj.DotSettings +++ /dev/null @@ -1,4 +0,0 @@ - - True - True - True \ No newline at end of file diff --git a/API/Comparators/NumericComparer.cs b/API/Comparators/NumericComparer.cs deleted file mode 100644 index 17eeee059..000000000 --- a/API/Comparators/NumericComparer.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections; - -namespace API.Comparators; - -#nullable enable - -public class NumericComparer : IComparer -{ - - public int Compare(object? x, object? y) - { - if((x is string xs) && (y is string ys)) - { - return StringLogicalComparer.Compare(xs, ys); - } - return -1; - } -} diff --git a/API/Comparators/StringLogicalComparer.cs b/API/Comparators/StringLogicalComparer.cs deleted file mode 100644 index 6759454fb..000000000 --- a/API/Comparators/StringLogicalComparer.cs +++ /dev/null @@ -1,130 +0,0 @@ -//(c) Vasian Cepa 2005 -// Version 2 -// Taken from: https://www.codeproject.com/Articles/11016/Numeric-String-Sort-in-C - -using static System.Char; - -namespace API.Comparators; - - -public static class StringLogicalComparer -{ - public static int Compare(string s1, string s2) - { - //get rid of special cases - if((s1 == null) && (s2 == null)) return 0; - if(s1 == null) return -1; - if(s2 == null) return 1; - - if (string.IsNullOrEmpty(s1) && string.IsNullOrEmpty(s2)) return 0; - if (string.IsNullOrEmpty(s1)) return -1; - if (string.IsNullOrEmpty(s2)) return -1; - - //WE style, special case - var sp1 = IsLetterOrDigit(s1, 0); - var sp2 = IsLetterOrDigit(s2, 0); - if(sp1 && !sp2) return 1; - if(!sp1 && sp2) return -1; - - int i1 = 0, i2 = 0; //current index - while(true) - { - var c1 = IsDigit(s1, i1); - var c2 = IsDigit(s2, i2); - int r; // temp result - if(!c1 && !c2) - { - bool letter1 = IsLetter(s1, i1); - bool letter2 = IsLetter(s2, i2); - if((letter1 && letter2) || (!letter1 && !letter2)) - { - if(letter1 && letter2) - { - r = ToLower(s1[i1]).CompareTo(ToLower(s2[i2])); - } - else - { - r = s1[i1].CompareTo(s2[i2]); - } - if(r != 0) return r; - } - else if(!letter1 && letter2) return -1; - else if(letter1 && !letter2) return 1; - } - else if(c1 && c2) - { - r = CompareNum(s1, ref i1, s2, ref i2); - if(r != 0) return r; - } - else if(c1) - { - return -1; - } - else if(c2) - { - return 1; - } - i1++; - i2++; - if((i1 >= s1.Length) && (i2 >= s2.Length)) - { - return 0; - } - if(i1 >= s1.Length) - { - return -1; - } - if(i2 >= s2.Length) - { - return -1; - } - } - } - - private static int CompareNum(string s1, ref int i1, string s2, ref int i2) - { - int nzStart1 = i1, nzStart2 = i2; // nz = non zero - int end1 = i1, end2 = i2; - - ScanNumEnd(s1, i1, ref end1, ref nzStart1); - ScanNumEnd(s2, i2, ref end2, ref nzStart2); - var start1 = i1; i1 = end1 - 1; - var start2 = i2; i2 = end2 - 1; - - var nzLength1 = end1 - nzStart1; - var nzLength2 = end2 - nzStart2; - - if(nzLength1 < nzLength2) return -1; - if(nzLength1 > nzLength2) return 1; - - for(int j1 = nzStart1,j2 = nzStart2; j1 <= i1; j1++,j2++) - { - var r = s1[j1].CompareTo(s2[j2]); - if(r != 0) return r; - } - // the nz parts are equal - var length1 = end1 - start1; - var length2 = end2 - start2; - if(length1 == length2) return 0; - if(length1 > length2) return -1; - return 1; - } - - //lookahead - private static void ScanNumEnd(string s, int start, ref int end, ref int nzStart) - { - nzStart = start; - end = start; - var countZeros = true; - while(IsDigit(s, end)) - { - if(countZeros && s[end].Equals('0')) - { - nzStart++; - } - else countZeros = false; - end++; - if(end >= s.Length) break; - } - } -} diff --git a/API/Controllers/DeviceController.cs b/API/Controllers/DeviceController.cs deleted file mode 100644 index 869dd0b34..000000000 --- a/API/Controllers/DeviceController.cs +++ /dev/null @@ -1,250 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Device; -using API.DTOs.Device.ClientDevice; -using API.DTOs.Device.EmailDevice; -using API.DTOs.Progress; -using API.Services; -using API.SignalR; -using AutoMapper; -using Kavita.Common; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace API.Controllers; - -#nullable enable - -/// -/// Responsible interacting and creating Devices -/// -public class DeviceController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - private readonly IDeviceService _deviceService; - private readonly IEventHub _eventHub; - private readonly ILocalizationService _localizationService; - private readonly IMapper _mapper; - private readonly IClientDeviceService _clientDeviceService; - - public DeviceController(IUnitOfWork unitOfWork, IDeviceService deviceService,IEventHub eventHub, - ILocalizationService localizationService, IMapper mapper, IClientDeviceService clientDeviceService) - { - _unitOfWork = unitOfWork; - _deviceService = deviceService; - _eventHub = eventHub; - _localizationService = localizationService; - _mapper = mapper; - _clientDeviceService = clientDeviceService; - } - - - /// - /// Creates a new Device - /// - /// - /// - [HttpPost("create")] - public async Task> CreateOrUpdateDevice(CreateEmailDeviceDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); - if (user == null) return Unauthorized(); - try - { - var device = await _deviceService.Create(dto, user); - if (device == null) - return BadRequest(await _localizationService.Translate(UserId, "generic-device-create")); - - return Ok(_mapper.Map(device)); - } - catch (KavitaException ex) - { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); - } - } - - /// - /// Updates an existing Device - /// - /// - /// - [HttpPost("update")] - public async Task> UpdateDevice(UpdateEmailDeviceDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); - if (user == null) return Unauthorized(); - var device = await _deviceService.Update(dto, user); - - if (device == null) return BadRequest(await _localizationService.Translate(UserId, "generic-device-update")); - - return Ok(_mapper.Map(device)); - } - - /// - /// Deletes the device from the user - /// - /// - /// - [HttpDelete] - public async Task DeleteDevice(int deviceId) - { - if (deviceId <= 0) return BadRequest(await _localizationService.Translate(UserId, "device-doesnt-exist")); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); - if (user == null) return Unauthorized(); - if (await _deviceService.Delete(user, deviceId)) return Ok(); - - return BadRequest(await _localizationService.Translate(UserId, "generic-device-delete")); - } - - [HttpGet] - public async Task>> GetDevices() - { - return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(UserId)); - } - - /// - /// Sends a collection of chapters to the user's device - /// - /// - /// - [HttpPost("send-to")] - public async Task SendToDevice(SendToEmailDeviceDto dto) - { - var userId = UserId; - if (dto.ChapterIds.Any(i => i < 0)) return BadRequest(await _localizationService.Translate(userId, "greater-0", "ChapterIds")); - if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "DeviceId")); - - var isEmailSetup = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); - if (!isEmailSetup) - return BadRequest(await _localizationService.Translate(userId, "send-to-kavita-email")); - - // // Validate that the device belongs to the user - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.Devices); - if (user == null || user.Devices.All(d => d.Id != dto.DeviceId)) return BadRequest(await _localizationService.Translate(userId, "send-to-unallowed")); - - await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), - "started"), userId); - try - { - var success = await _deviceService.SendTo(dto.ChapterIds, dto.DeviceId); - if (success) return Ok(); - } - catch (KavitaException ex) - { - return BadRequest(await _localizationService.Translate(userId, ex.Message)); - } - finally - { - await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), - "ended"), userId); - } - - return BadRequest(await _localizationService.Translate(userId, "generic-send-to")); - } - - - /// - /// Attempts to send a whole series to a device. - /// - /// - /// - [HttpPost("send-series-to")] - public async Task SendSeriesToDevice(SendSeriesToEmailDeviceDto dto) - { - var userId = UserId; - if (dto.SeriesId <= 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "SeriesId")); - if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "DeviceId")); - - var isEmailSetup = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); - if (!isEmailSetup) - return BadRequest(await _localizationService.Translate(userId, "send-to-kavita-email")); - - await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), - "started"), userId); - - var series = - await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, - SeriesIncludes.Volumes | SeriesIncludes.Chapters); - if (series == null) return BadRequest(await _localizationService.Translate(userId, "series-doesnt-exist")); - var chapterIds = series.Volumes.SelectMany(v => v.Chapters.Select(c => c.Id)).ToList(); - try - { - var success = await _deviceService.SendTo(chapterIds, dto.DeviceId); - if (success) return Ok(); - } - catch (KavitaException ex) - { - return BadRequest(await _localizationService.Translate(userId, ex.Message)); - } - finally - { - await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), - "ended"), userId); - } - - return BadRequest(await _localizationService.Translate(userId, "generic-send-to")); - } - - #region Client Devices - /// - /// Get my client devices - /// - /// - /// - [HttpGet("client/devices")] - public async Task>> GetMyClientDevices(bool includeInactive = false) - { - return Ok(await _clientDeviceService.GetUserDeviceDtosAsync(UserId, includeInactive)); - } - - /// - /// Get All user client devices - /// - /// - /// - [HttpGet("client/all-devices")] - [Authorize(PolicyGroups.AdminPolicy)] - public async Task>> GetAllClientDevices(bool includeInactive = false) - { - return Ok(await _clientDeviceService.GetAllUserDeviceDtos(includeInactive)); - } - - - /// - /// Removes the client device from DB - /// - /// - /// - [HttpDelete("client/device")] - public async Task> DeleteClientDevice(int clientDeviceId) - { - return Ok(await _clientDeviceService.DeleteDeviceAsync(UserId, clientDeviceId)); - } - - /// - /// Update the friendly name of the Device - /// - /// - /// - [HttpPost("client/update-name")] - public async Task UpdateClientDeviceName(UpdateClientDeviceNameDto dto) - { - await _clientDeviceService.UpdateFriendlyNameAsync(UserId, dto); - return Ok(); - } - - - - #endregion Client Devices - -} - - diff --git a/API/Controllers/EmailController.cs b/API/Controllers/EmailController.cs deleted file mode 100644 index dd52805e9..000000000 --- a/API/Controllers/EmailController.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Email; -using API.Helpers; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace API.Controllers; - -[Authorize(Policy = PolicyGroups.AdminPolicy)] -public class EmailController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - - public EmailController(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - [HttpGet("all")] - public async Task>> GetEmails() - { - return Ok(await _unitOfWork.EmailHistoryRepository.GetEmailDtos(UserParams.Default)); - } -} diff --git a/API/Controllers/FallbackController.cs b/API/Controllers/FallbackController.cs deleted file mode 100644 index bb751d1df..000000000 --- a/API/Controllers/FallbackController.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.IO; -using API.Middleware; -using API.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace API.Controllers; - -#nullable enable - -[AllowAnonymous] -public class FallbackController : Controller -{ - // ReSharper disable once S4487 - // ReSharper disable once NotAccessedField.Local -#pragma warning disable S4487 - private readonly ITaskScheduler _taskScheduler; -#pragma warning restore S4487 - - public FallbackController(ITaskScheduler taskScheduler) - { - // This is used to load TaskScheduler on startup without having to navigate to a Controller that uses. - _taskScheduler = taskScheduler; // TODO: Validate if this is needed as a DI anymore since we have a HostedStartupService - } - - [SkipDeviceTracking] - public IActionResult Index() - { - if (HttpContext.Request.Path.StartsWithSegments("/api")) - { - return NotFound(); - } - - return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML"); - } -} - diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs deleted file mode 100644 index ec76421a6..000000000 --- a/API/Controllers/ImageController.cs +++ /dev/null @@ -1,288 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Entities.Enums; -using API.Extensions; -using API.Middleware; -using API.Services; -using API.Services.Tasks.Metadata; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using MimeTypes; - -namespace API.Controllers; - -#nullable enable - -/// -/// Responsible for servicing up images stored in Kavita for entities -/// -[AllowAnonymous] -[SkipDeviceTracking] -public class ImageController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly ILocalizationService _localizationService; - private readonly IReadingListService _readingListService; - private readonly ICoverDbService _coverDbService; - - /// - public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService, - ILocalizationService localizationService, IReadingListService readingListService, - ICoverDbService coverDbService) - { - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _localizationService = localizationService; - _readingListService = readingListService; - _coverDbService = coverDbService; - } - - /// - /// Returns cover image for Chapter - /// - /// - /// - /// - [HttpGet("chapter-cover")] - public async Task GetChapterCoverImage(int chapterId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId)); - return PhysicalFile(path); - } - - /// - /// Returns cover image for Library - /// - /// - /// - /// - [HttpGet("library-cover")] - public async Task GetLibraryCoverImage(int libraryId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId)); - return PhysicalFile(path); - } - - /// - /// Returns cover image for Volume - /// - /// - /// - /// - [HttpGet("volume-cover")] - public async Task GetVolumeCoverImage(int volumeId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId)); - return PhysicalFile(path); - } - - /// - /// Returns cover image for Series - /// - /// Id of Series - /// - /// - [HttpGet("series-cover")] - public async Task GetSeriesCoverImage(int seriesId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId)); - return PhysicalFile(path); - } - - /// - /// Returns cover image for Collection - /// - /// - /// - /// - [HttpGet("collection-cover")] - public async Task GetCollectionCoverImage(int collectionTagId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId)); - if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) - { - // TODO: Streamline this like ReadingList does - path = await GenerateCollectionCoverImage(collectionTagId); - } - - return PhysicalFile(path); - } - - /// - /// Returns cover image for a Reading List - /// - /// - /// - /// - [HttpGet("readinglist-cover")] - public async Task GetReadingListCoverImage(int readingListId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId)); - if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) - { - path = await _readingListService.GenerateReadingListCoverImage(readingListId); - } - - return PhysicalFile(path); - } - - private async Task GenerateCollectionCoverImage(int collectionId) - { - var covers = await _unitOfWork.CollectionTagRepository.GetRandomCoverImagesAsync(collectionId); - var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, - ImageService.GetCollectionTagFormat(collectionId)); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - destFile += settings.EncodeMediaAs.GetExtension(); - - if (_directoryService.FileSystem.File.Exists(destFile)) return destFile; - ImageService.CreateMergedImage( - covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(), - settings.CoverImageSize, - destFile); - // TODO: Refactor this so that collections have a dedicated cover image so we can calculate primary/secondary colors - return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; - } - - /// - /// Returns image for a given bookmark page - /// - /// This request is served unauthenticated, but user must be passed via api key to validate - /// - /// Starts at 0 - /// API Key for user. Needed to authenticate request - /// Only applicable for Epubs - handles multiple images on one page - /// - [HttpGet("bookmark")] - public async Task GetBookmarkImage(int chapterId, int pageNum, string apiKey, int imageOffset = 0) - { - var bookmark = await _unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, imageOffset, UserId); - if (bookmark == null) return BadRequest(await _localizationService.Translate(UserId, "bookmark-doesnt-exist")); - - var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; - var path = Path.Join(bookmarkDirectory, bookmark.FileName); - - return PhysicalFile(path); - } - - /// - /// Returns the image associated with a web-link - /// - /// - /// - /// - [HttpGet("web-link")] - public async Task GetWebLinkImage(string url, string apiKey) - { - if (string.IsNullOrEmpty(url)) return BadRequest(await _localizationService.Translate(UserId, "must-be-defined", "Url")); - - var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; - - // Check if the domain exists - var domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.FaviconDirectory, ImageService.GetWebLinkFormat(url, encodeFormat)); - if (!_directoryService.FileSystem.File.Exists(domainFilePath)) - { - // We need to request the favicon and save it - try - { - domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.FaviconDirectory, - await _coverDbService.DownloadFaviconAsync(url, encodeFormat)); - } - catch (Exception) - { - return BadRequest(await _localizationService.Translate(UserId, "generic-favicon")); - } - } - - return PhysicalFile(domainFilePath); - } - - - /// - /// Returns the image associated with a publisher - /// - /// - /// - /// - [HttpGet("publisher")] - public async Task GetPublisherImage(string publisherName, string apiKey) - { - if (string.IsNullOrEmpty(publisherName)) return BadRequest(await _localizationService.Translate(UserId, "must-be-defined", "publisherName")); - if (publisherName.Contains("..")) return BadRequest(); - - var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; - - // Check if the domain exists - var domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.PublisherDirectory, ImageService.GetPublisherFormat(publisherName, encodeFormat)); - if (!_directoryService.FileSystem.File.Exists(domainFilePath)) - { - // We need to request the favicon and save it - try - { - domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.PublisherDirectory, - await _coverDbService.DownloadPublisherImageAsync(publisherName, encodeFormat)); - } - catch (Exception) - { - return BadRequest(await _localizationService.Translate(UserId, "generic-favicon")); - } - } - - return CachedFile(domainFilePath); - } - - /// - /// Returns cover image for Person - /// - /// - /// - /// - [HttpGet("person-cover")] - public async Task GetPersonCoverImage(int personId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.UserRepository.GetPersonCoverImageAsync(personId)); - return PhysicalFile(path); - } - - /// - /// Returns cover image for User - /// - /// - /// - /// - [HttpGet("user-cover")] - public async Task GetUserCoverImage(int userId, string apiKey) - { - var filename = await _unitOfWork.UserRepository.GetCoverImageAsync(userId, UserId); - var path = Path.Join(_directoryService.CoverImageDirectory, filename); - return CachedFile(path); - } - - /// - /// Returns a temp coverupload image - /// - /// Requires Admin Role to perform upload - /// Filename of file. This is used with upload/upload-by-url - /// - /// - [HttpGet("cover-upload")] - public async Task GetCoverUploadImage(string filename, string apiKey) - { - if (!UserContext.IsAuthenticated) return Unauthorized(); - if (filename.Contains("..")) return BadRequest(await _localizationService.Translate(UserId, "invalid-filename")); - - var roles = await _unitOfWork.UserRepository.GetRolesByAuthKey(apiKey); - if (!roles.Contains(PolicyConstants.AdminRole)) - { - return Forbid(); - } - - var path = Path.Join(_directoryService.TempDirectory, filename); - return PhysicalFile(path); - } -} diff --git a/API/Controllers/VolumeController.cs b/API/Controllers/VolumeController.cs deleted file mode 100644 index 11d3d24ab..000000000 --- a/API/Controllers/VolumeController.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Services; -using API.SignalR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace API.Controllers; -#nullable enable - -public class VolumeController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IEventHub _eventHub; - - public VolumeController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _eventHub = eventHub; - } - - /// - /// Returns the appropriate Volume - /// - /// - /// - [HttpGet] - public async Task> GetVolume(int volumeId) - { - return Ok(await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, UserId)); - } - - [Authorize(Policy = PolicyGroups.AdminPolicy)] - [HttpDelete] - public async Task> DeleteVolume(int volumeId) - { - var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId, - VolumeIncludes.Chapters | VolumeIncludes.People | VolumeIncludes.Tags); - if (volume == null) - return BadRequest(_localizationService.Translate(UserId, "volume-doesnt-exist")); - - _unitOfWork.VolumeRepository.Remove(volume); - - if (await _unitOfWork.CommitAsync()) - { - await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false); - return Ok(true); - } - - return Ok(false); - } - - [Authorize(Policy = PolicyGroups.AdminPolicy)] - [HttpPost("multiple")] - public async Task> DeleteMultipleVolumes(int[] volumesIds) - { - var volumes = await _unitOfWork.VolumeRepository.GetVolumesById(volumesIds); - if (volumes.Count != volumesIds.Length) - { - return BadRequest(_localizationService.Translate(UserId, "volume-doesnt-exist")); - } - - _unitOfWork.VolumeRepository.Remove(volumes); - - if (!await _unitOfWork.CommitAsync()) - { - return Ok(false); - } - - foreach (var volume in volumes) - { - await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false); - } - - return Ok(true); - } -} diff --git a/API/Data/Repositories/AppUserSmartFilterRepository.cs b/API/Data/Repositories/AppUserSmartFilterRepository.cs deleted file mode 100644 index 4c1adf784..000000000 --- a/API/Data/Repositories/AppUserSmartFilterRepository.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Dashboard; -using API.Entities; -using API.Helpers; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface IAppUserSmartFilterRepository -{ - void Update(AppUserSmartFilter filter); - void Attach(AppUserSmartFilter filter); - void Delete(AppUserSmartFilter filter); - IEnumerable GetAllDtosByUserId(int userId); - Task> GetPagedDtosByUserIdAsync(int userId, UserParams userParams); - Task GetById(int smartFilterId); -} - -public class AppUserSmartFilterRepository : IAppUserSmartFilterRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public AppUserSmartFilterRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Update(AppUserSmartFilter filter) - { - _context.Entry(filter).State = EntityState.Modified; - } - - public void Attach(AppUserSmartFilter filter) - { - _context.AppUserSmartFilter.Attach(filter); - } - - public void Delete(AppUserSmartFilter filter) - { - _context.AppUserSmartFilter.Remove(filter); - } - - public IEnumerable GetAllDtosByUserId(int userId) - { - return _context.AppUserSmartFilter - .Where(f => f.AppUserId == userId) - .ProjectTo(_mapper.ConfigurationProvider) - .AsEnumerable(); - } - - public Task> GetPagedDtosByUserIdAsync(int userId, UserParams userParams) - { - var filters = _context.AppUserSmartFilter - .Where(f => f.AppUserId == userId) - .ProjectTo(_mapper.ConfigurationProvider); - - return PagedList.CreateAsync(filters, userParams); - } - - public async Task GetById(int smartFilterId) - { - return await _context.AppUserSmartFilter - .FirstOrDefaultAsync(d => d.Id == smartFilterId); - } -} diff --git a/API/Data/Repositories/CollectionTagRepository.cs b/API/Data/Repositories/CollectionTagRepository.cs deleted file mode 100644 index e1741c6c7..000000000 --- a/API/Data/Repositories/CollectionTagRepository.cs +++ /dev/null @@ -1,285 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data.Misc; -using API.DTOs.Collection; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; -using API.Services.Plus; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; - -#nullable enable - -[Flags] -public enum CollectionTagIncludes -{ - None = 1, - SeriesMetadata = 2, - SeriesMetadataWithSeries = 4 -} - -[Flags] -public enum CollectionIncludes -{ - None = 1, - Series = 2, -} - -public interface ICollectionTagRepository -{ - void Remove(AppUserCollection tag); - Task GetCoverImageAsync(int collectionTagId); - Task GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None); - void Update(AppUserCollection tag); - Task RemoveCollectionsWithoutSeries(); - - Task> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None); - /// - /// Returns all of the user's collections with the option of other user's promoted - /// - /// - /// - /// - Task> GetCollectionDtosAsync(int userId, bool includePromoted = false); - /// - /// Returns the collection if the user owns it or the collection is promoted - /// - /// - Task GetCollectionDtoAsync(int collectionId, int userId); - Task> GetCollectionDtosPagedAsync(int userId, UserParams userParams, bool includePromoted = false); - Task> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false); - - Task> GetAllCoverImagesAsync(); - Task CollectionExists(string title, int userId); - Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); - Task> GetRandomCoverImagesAsync(int collectionId); - Task> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None); - Task UpdateCollectionAgeRating(AppUserCollection tag); - Task> GetCollectionsByIds(IEnumerable tags, CollectionIncludes includes = CollectionIncludes.None); - Task> GetAllCollectionsForSyncing(DateTime expirationTime); -} - -public class CollectionTagRepository : ICollectionTagRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public CollectionTagRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Remove(AppUserCollection tag) - { - _context.AppUserCollection.Remove(tag); - } - - public void Update(AppUserCollection tag) - { - _context.Entry(tag).State = EntityState.Modified; - } - - /// - /// Removes any collection tags without any series - /// - public async Task RemoveCollectionsWithoutSeries() - { - var tagsToDelete = await _context.AppUserCollection - .Include(c => c.Items) - .Where(c => c.Items.Count == 0) - .AsSplitQuery() - .ToListAsync(); - - _context.RemoveRange(tagsToDelete); - - return await _context.SaveChangesAsync(); - } - - public async Task> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None) - { - return await _context.AppUserCollection - .OrderBy(c => c.NormalizedTitle) - .Includes(includes) - .ToListAsync(); - } - - public async Task GetCollectionDtoAsync(int collectionId, int userId) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.AppUserCollection - .Where(uc => (uc.AppUserId == userId || uc.Promoted) && uc.Id == collectionId) - .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) - .OrderBy(uc => uc.Title) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); - } - - public async Task> GetCollectionDtosAsync(int userId, bool includePromoted = false) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.AppUserCollection - .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) - .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) - .OrderBy(uc => uc.Title) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task> GetCollectionDtosPagedAsync(int userId, UserParams userParams, bool includePromoted = false) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var collections = _context.AppUserCollection - .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) - .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) - .OrderBy(uc => uc.Title) - .ProjectTo(_mapper.ConfigurationProvider); - - return await PagedList.CreateAsync(collections, userParams); - } - - public async Task> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.AppUserCollection - .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) - .Where(uc => uc.Items.Any(s => s.Id == seriesId)) - .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) - .OrderBy(uc => uc.Title) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task GetCoverImageAsync(int collectionTagId) - { - return await _context.AppUserCollection - .Where(c => c.Id == collectionTagId) - .Select(c => c.CoverImage) - .SingleOrDefaultAsync(); - } - - public async Task> GetAllCoverImagesAsync() - { - return await _context.AppUserCollection - .Select(t => t.CoverImage) - .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync(); - } - - /// - /// If any tag exists for that given user's collections - /// - /// - /// - /// - public async Task CollectionExists(string title, int userId) - { - var normalized = title.ToNormalized(); - return await _context.AppUserCollection - .Where(uc => uc.AppUserId == userId) - .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized)); - } - - public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) - { - var extension = encodeFormat.GetExtension(); - return await _context.AppUserCollection - .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) - .ToListAsync(); - } - - public async Task> GetRandomCoverImagesAsync(int collectionId) - { - var random = new Random(); - var data = await _context.AppUserCollection - .Where(t => t.Id == collectionId) - .SelectMany(uc => uc.Items.Select(series => series.CoverImage)) - .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync(); - - return data - .OrderBy(_ => random.Next()) - .Take(4) - .ToList(); - } - - public async Task> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None) - { - return await _context.AppUserCollection - .Where(c => c.AppUserId == userId) - .Includes(includes) - .ToListAsync(); - } - - public async Task UpdateCollectionAgeRating(AppUserCollection tag) - { - var maxAgeRating = await _context.AppUserCollection - .Where(t => t.Id == tag.Id) - .SelectMany(uc => uc.Items.Select(s => s.Metadata)) - .Select(sm => sm.AgeRating) - .ToListAsync(); - - - tag.AgeRating = maxAgeRating.Count != 0 ? maxAgeRating.Max() : AgeRating.Unknown; - await _context.SaveChangesAsync(); - } - - public async Task> GetCollectionsByIds(IEnumerable tags, CollectionIncludes includes = CollectionIncludes.None) - { - return await _context.AppUserCollection - .Where(c => tags.Contains(c.Id)) - .Includes(includes) - .AsSplitQuery() - .ToListAsync(); - } - - public async Task> GetAllCollectionsForSyncing(DateTime expirationTime) - { - return await _context.AppUserCollection - .Where(c => c.Source == ScrobbleProvider.Mal) - .Where(c => c.LastSyncUtc <= expirationTime) - .Include(c => c.Items) - .AsSplitQuery() - .ToListAsync(); - } - - public async Task GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None) - { - return await _context.AppUserCollection - .Where(c => c.Id == tagId) - .Includes(includes) - .AsSplitQuery() - .SingleOrDefaultAsync(); - } - - private async Task GetUserAgeRestriction(int userId) - { - return await _context.AppUser - .AsNoTracking() - .Where(u => u.Id == userId) - .Select(u => - new AgeRestriction(){ - AgeRating = u.AgeRestriction, - IncludeUnknowns = u.AgeRestrictionIncludeUnknowns - }) - .SingleAsync(); - } - - public async Task> SearchTagDtosAsync(string searchQuery, int userId) - { - var userRating = await GetUserAgeRestriction(userId); - return await _context.AppUserCollection - .Search(searchQuery, userId, userRating) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } -} diff --git a/API/Data/Repositories/DeviceRepository.cs b/API/Data/Repositories/DeviceRepository.cs deleted file mode 100644 index 8dff1c93d..000000000 --- a/API/Data/Repositories/DeviceRepository.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Device; -using API.DTOs.Device.EmailDevice; -using API.Entities; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface IDeviceRepository -{ - void Update(Device device); - Task> GetDevicesForUserAsync(int userId); - Task GetDeviceById(int deviceId); -} - -public class DeviceRepository : IDeviceRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public DeviceRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Update(Device device) - { - _context.Entry(device).State = EntityState.Modified; - } - - public async Task> GetDevicesForUserAsync(int userId) - { - return await _context.Device - .Where(d => d.AppUserId == userId) - .OrderBy(d => d.LastUsed) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task GetDeviceById(int deviceId) - { - return await _context.Device - .Where(d => d.Id == deviceId) - .SingleOrDefaultAsync(); - } -} diff --git a/API/Data/Repositories/EmailHistoryRepository.cs b/API/Data/Repositories/EmailHistoryRepository.cs deleted file mode 100644 index f6f49fa34..000000000 --- a/API/Data/Repositories/EmailHistoryRepository.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Email; -using API.Helpers; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; - -public interface IEmailHistoryRepository -{ - Task> GetEmailDtos(UserParams userParams); -} - -public class EmailHistoryRepository : IEmailHistoryRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public EmailHistoryRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - - public async Task> GetEmailDtos(UserParams userParams) - { - return await _context.EmailHistory - .OrderByDescending(h => h.SendDate) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } -} diff --git a/API/Data/Repositories/EpubFontRepository.cs b/API/Data/Repositories/EpubFontRepository.cs deleted file mode 100644 index cea0d068a..000000000 --- a/API/Data/Repositories/EpubFontRepository.cs +++ /dev/null @@ -1,102 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Font; -using API.Entities; -using API.Extensions; -using API.Services.Tasks; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; - -public interface IEpubFontRepository -{ - void Add(EpubFont font); - void Remove(EpubFont font); - void Update(EpubFont font); - Task> GetFontDtosAsync(); - Task GetFontDtoAsync(int fontId); - Task GetFontDtoByNameAsync(string name); - Task> GetFontsAsync(); - Task GetFontAsync(int fontId); - Task IsFontInUseAsync(int fontId); -} - -public class EpubFontRepository: IEpubFontRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public EpubFontRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Add(EpubFont font) - { - _context.Add(font); - } - - public void Remove(EpubFont font) - { - _context.Remove(font); - } - - public void Update(EpubFont font) - { - _context.Entry(font).State = EntityState.Modified; - } - - public async Task> GetFontDtosAsync() - { - return await _context.EpubFont - .OrderBy(s => s.Name == FontService.DefaultFont ? -1 : 0) - .ThenBy(s => s) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task GetFontDtoAsync(int fontId) - { - return await _context.EpubFont - .Where(f => f.Id == fontId) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); - } - - public async Task GetFontDtoByNameAsync(string name) - { - return await _context.EpubFont - .Where(f => f.NormalizedName.Equals(name.ToNormalized())) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); - } - - public async Task> GetFontsAsync() - { - return await _context.EpubFont - .ToListAsync(); - } - - public async Task GetFontAsync(int fontId) - { - return await _context.EpubFont - .Where(f => f.Id == fontId) - .FirstOrDefaultAsync(); - } - - public async Task IsFontInUseAsync(int fontId) - { - return await _context.AppUserReadingProfiles - .Join(_context.EpubFont, - preference => preference.BookReaderFontFamily, - font => font.Name, - (preference, font) => new { preference, font }) - .AnyAsync(joined => joined.font.Id == fontId); - } - -} diff --git a/API/Data/Repositories/MangaFileRepository.cs b/API/Data/Repositories/MangaFileRepository.cs deleted file mode 100644 index 89c6bb418..000000000 --- a/API/Data/Repositories/MangaFileRepository.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface IMangaFileRepository -{ - void Update(MangaFile file); - Task> GetAllWithMissingExtension(); - Task GetByKoreaderHash(string hash); -} - -public class MangaFileRepository : IMangaFileRepository -{ - private readonly DataContext _context; - - public MangaFileRepository(DataContext context) - { - _context = context; - } - - public void Update(MangaFile file) - { - _context.Entry(file).State = EntityState.Modified; - } - - public async Task> GetAllWithMissingExtension() - { - return await _context.MangaFile - .Where(f => string.IsNullOrEmpty(f.Extension)) - .ToListAsync(); - } - - public async Task GetByKoreaderHash(string hash) - { - if (string.IsNullOrEmpty(hash)) return null; - - return await _context.MangaFile - .FirstOrDefaultAsync(f => f.KoreaderHash != null && - f.KoreaderHash.Equals(hash.ToUpper())); - } -} diff --git a/API/Data/Repositories/MediaErrorRepository.cs b/API/Data/Repositories/MediaErrorRepository.cs deleted file mode 100644 index eac2ee295..000000000 --- a/API/Data/Repositories/MediaErrorRepository.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.MediaErrors; -using API.Entities; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface IMediaErrorRepository -{ - void Attach(MediaError error); - void Remove(MediaError error); - void Remove(IList errors); - Task Find(string filename); - IEnumerable GetAllErrorDtosAsync(); - Task ExistsAsync(MediaError error); - Task DeleteAll(); - Task> GetAllErrorsAsync(IList comments); -} - -public class MediaErrorRepository : IMediaErrorRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public MediaErrorRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Attach(MediaError? error) - { - if (error == null) return; - _context.MediaError.Attach(error); - } - - public void Remove(MediaError? error) - { - if (error == null) return; - _context.MediaError.Remove(error); - } - - public void Remove(IList errors) - { - _context.MediaError.RemoveRange(errors); - } - - public Task Find(string filename) - { - return _context.MediaError.Where(e => e.FilePath == filename).SingleOrDefaultAsync(); - } - - public IEnumerable GetAllErrorDtosAsync() - { - var query = _context.MediaError - .OrderByDescending(m => m.Created) - .ProjectTo(_mapper.ConfigurationProvider) - .AsNoTracking(); - return query.AsEnumerable(); - } - - public Task ExistsAsync(MediaError error) - { - return _context.MediaError.AnyAsync(m => m.FilePath.Equals(error.FilePath) - && m.Comment.Equals(error.Comment) - && m.Details.Equals(error.Details) - ); - } - - public async Task DeleteAll() - { - _context.MediaError.RemoveRange(await _context.MediaError.ToListAsync()); - await _context.SaveChangesAsync(); - } - - public Task> GetAllErrorsAsync(IList comments) - { - return _context.MediaError - .Where(m => comments.Contains(m.Comment)) - .ToListAsync(); - } -} diff --git a/API/Data/Repositories/ScrobbleEventRepository.cs b/API/Data/Repositories/ScrobbleEventRepository.cs deleted file mode 100644 index 8a484f697..000000000 --- a/API/Data/Repositories/ScrobbleEventRepository.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Scrobbling; -using API.Entities.Scrobble; -using API.Extensions.QueryExtensions; -using API.Helpers; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface IScrobbleRepository -{ - void Attach(ScrobbleEvent evt); - void Attach(ScrobbleError error); - void Remove(ScrobbleEvent evt); - void Remove(IEnumerable events); - void Remove(IEnumerable errors); - void Update(ScrobbleEvent evt); - Task> GetByEvent(ScrobbleEventType type, bool isProcessed = false); - Task> GetProcessedEvents(int daysAgo); - Task Exists(int userId, int seriesId, ScrobbleEventType eventType); - Task> GetScrobbleErrors(); - Task> GetAllScrobbleErrorsForSeries(int seriesId); - Task ClearScrobbleErrors(); - Task HasErrorForSeries(int seriesId); - /// - /// Get all events for a specific user and type - /// - /// - /// - /// - /// If true, only returned not processed events - /// - Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false); - Task> GetUserEventsForSeries(int userId, int seriesId); - /// - /// Return the events with given ids, when belonging to the passed user - /// - /// - /// - /// - Task> GetUserEvents(int userId, IList scrobbleEventIds); - Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination); - Task> GetAllEventsForSeries(int seriesId); - Task> GetAllEventsWithSeriesIds(IEnumerable seriesIds); - Task> GetEvents(); -} - -/// -/// This handles everything around Scrobbling -/// -public class ScrobbleRepository : IScrobbleRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public ScrobbleRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Attach(ScrobbleEvent evt) - { - _context.ScrobbleEvent.Attach(evt); - } - - public void Attach(ScrobbleError error) - { - _context.ScrobbleError.Attach(error); - } - - public void Remove(ScrobbleEvent evt) - { - _context.ScrobbleEvent.Remove(evt); - } - - public void Remove(IEnumerable events) - { - _context.ScrobbleEvent.RemoveRange(events); - } - - public void Remove(IEnumerable errors) - { - _context.ScrobbleError.RemoveRange(errors); - } - - public void Update(ScrobbleEvent evt) - { - _context.Entry(evt).State = EntityState.Modified; - } - - public async Task> GetByEvent(ScrobbleEventType type, bool isProcessed = false) - { - return await _context.ScrobbleEvent - .Include(s => s.Series) - .ThenInclude(s => s.Library) - .Include(s => s.Series) - .ThenInclude(s => s.Metadata) - .Include(s => s.AppUser) - .ThenInclude(u => u.UserPreferences) - .Where(s => s.ScrobbleEventType == type) - .Where(s => s.IsProcessed == isProcessed) - .AsSplitQuery() - .GroupBy(s => s.SeriesId) - .Select(g => g.OrderByDescending(e => e.ChapterNumber) - .ThenByDescending(e => e.VolumeNumber) - .FirstOrDefault()) - .ToListAsync(); - } - - /// - /// Returns all processed events that were processed 7 or more days ago - /// - /// - /// - public async Task> GetProcessedEvents(int daysAgo) - { - var date = DateTime.UtcNow.Subtract(TimeSpan.FromDays(daysAgo)); - return await _context.ScrobbleEvent - .Where(s => s.IsProcessed) - .Where(s => s.ProcessDateUtc != null && s.ProcessDateUtc < date) - .ToListAsync(); - } - - public async Task Exists(int userId, int seriesId, ScrobbleEventType eventType) - { - return await _context.ScrobbleEvent.AnyAsync(e => - e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType); - } - - public async Task> GetScrobbleErrors() - { - return await _context.ScrobbleError - .OrderBy(e => e.LastModifiedUtc) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task> GetAllScrobbleErrorsForSeries(int seriesId) - { - return await _context.ScrobbleError - .Where(e => e.SeriesId == seriesId) - .ToListAsync(); - } - - public async Task ClearScrobbleErrors() - { - _context.ScrobbleError.RemoveRange(_context.ScrobbleError); - await _context.SaveChangesAsync(); - } - - public async Task HasErrorForSeries(int seriesId) - { - return await _context.ScrobbleError.AnyAsync(n => n.SeriesId == seriesId); - } - - public async Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false) - { - return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType) - .WhereIf(isNotProcessed, e => !e.IsProcessed) - .OrderBy(e => e.LastModifiedUtc) - .FirstOrDefaultAsync(); - } - - public async Task> GetUserEventsForSeries(int userId, int seriesId) - { - return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId && !e.IsProcessed && e.SeriesId == seriesId) - .Include(e => e.Series) - .OrderBy(e => e.LastModifiedUtc) - .AsSplitQuery() - .ToListAsync(); - } - - public async Task> GetUserEvents(int userId, IList scrobbleEventIds) - { - return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId && scrobbleEventIds.Contains(e.Id)) - .ToListAsync(); - } - - public async Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination) - { - var query = _context.ScrobbleEvent - .Where(e => e.AppUserId == userId) - .Include(e => e.Series) - .WhereIf(!string.IsNullOrEmpty(filter.Query), s => - EF.Functions.Like(s.Series.Name, $"%{filter.Query}%") - ) - .WhereIf(!filter.IncludeReviews, e => e.ScrobbleEventType != ScrobbleEventType.Review) - .SortBy(filter.Field, filter.IsDescending) - .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider); - - return await PagedList.CreateAsync(query, pagination.PageNumber, pagination.PageSize); - } - - public async Task> GetAllEventsForSeries(int seriesId) - { - return await _context.ScrobbleEvent - .Where(e => e.SeriesId == seriesId) - .ToListAsync(); - } - - public async Task> GetAllEventsWithSeriesIds(IEnumerable seriesIds) - { - return await _context.ScrobbleEvent - .Where(e => seriesIds.Contains(e.SeriesId)) - .ToListAsync(); - } - - public async Task> GetEvents() - { - return await _context.ScrobbleEvent - .Include(e => e.AppUser) - .ToListAsync(); - } -} diff --git a/API/Data/Repositories/SeriesMetadataRepository.cs b/API/Data/Repositories/SeriesMetadataRepository.cs deleted file mode 100644 index 0a3efee26..000000000 --- a/API/Data/Repositories/SeriesMetadataRepository.cs +++ /dev/null @@ -1,23 +0,0 @@ -using API.Entities.Metadata; - -namespace API.Data.Repositories; - -public interface ISeriesMetadataRepository -{ - void Update(SeriesMetadata seriesMetadata); -} - -public class SeriesMetadataRepository : ISeriesMetadataRepository -{ - private readonly DataContext _context; - - public SeriesMetadataRepository(DataContext context) - { - _context = context; - } - - public void Update(SeriesMetadata seriesMetadata) - { - _context.SeriesMetadata.Update(seriesMetadata); - } -} diff --git a/API/Data/Repositories/SettingsRepository.cs b/API/Data/Repositories/SettingsRepository.cs deleted file mode 100644 index 2e9eb4262..000000000 --- a/API/Data/Repositories/SettingsRepository.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface ISettingsRepository -{ - void Update(ServerSetting settings); - void Update(MetadataSettings settings); - void RemoveRange(List fieldMappings); - Task GetSettingsDtoAsync(); - Task GetSettingAsync(ServerSettingKey key); - Task> GetSettingsAsync(); - void Remove(ServerSetting setting); - Task GetExternalSeriesMetadata(int seriesId); - Task GetMetadataSettings(); - Task GetMetadataSettingDto(); -} -public class SettingsRepository : ISettingsRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public SettingsRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Update(ServerSetting settings) - { - _context.Entry(settings).State = EntityState.Modified; - } - - public void Update(MetadataSettings settings) - { - _context.Entry(settings).State = EntityState.Modified; - } - - public void RemoveRange(List fieldMappings) - { - _context.MetadataFieldMapping.RemoveRange(fieldMappings); - } - - public void Remove(ServerSetting setting) - { - _context.Remove(setting); - } - - public async Task GetExternalSeriesMetadata(int seriesId) - { - return await _context.ExternalSeriesMetadata - .Where(s => s.SeriesId == seriesId) - .FirstOrDefaultAsync(); - } - - public async Task GetMetadataSettings() - { - return await _context.MetadataSettings - .Include(m => m.FieldMappings) - .FirstAsync(); - } - - public async Task GetMetadataSettingDto() - { - return await _context.MetadataSettings - .Include(m => m.FieldMappings) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstAsync(); - } - - public async Task GetSettingsDtoAsync() - { - var settings = await _context.ServerSetting - .Select(x => x) - .AsNoTracking() - .ToListAsync(); - return _mapper.Map(settings); - } - - public Task GetSettingAsync(ServerSettingKey key) - { - return _context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key)!; - } - - public async Task> GetSettingsAsync() - { - return await _context.ServerSetting.ToListAsync(); - } -} diff --git a/API/Data/Repositories/SiteThemeRepository.cs b/API/Data/Repositories/SiteThemeRepository.cs deleted file mode 100644 index 33517e846..000000000 --- a/API/Data/Repositories/SiteThemeRepository.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Theme; -using API.Entities; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface ISiteThemeRepository -{ - void Add(SiteTheme theme); - void Remove(SiteTheme theme); - void Update(SiteTheme siteTheme); - Task> GetThemeDtos(); - Task GetThemeDto(int themeId); - Task GetThemeDtoByName(string themeName); - Task GetDefaultTheme(); - Task> GetThemes(); - Task GetTheme(int themeId); - Task IsThemeInUse(int themeId); -} - -public class SiteThemeRepository : ISiteThemeRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public SiteThemeRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Add(SiteTheme theme) - { - _context.Add(theme); - } - - public void Remove(SiteTheme theme) - { - _context.Remove(theme); - } - - public void Update(SiteTheme siteTheme) - { - _context.Entry(siteTheme).State = EntityState.Modified; - } - - public async Task> GetThemeDtos() - { - return await _context.SiteTheme - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task GetThemeDtoByName(string themeName) - { - return await _context.SiteTheme - .Where(t => t.Name.Equals(themeName)) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); - } - - /// - /// Returns default theme, if the default theme is not available, returns the dark theme - /// - /// - public async Task GetDefaultTheme() - { - var result = await _context.SiteTheme - .Where(t => t.IsDefault) - .FirstOrDefaultAsync(); - - if (result == null) - { - return await _context.SiteTheme - .Where(t => t.NormalizedName == Seed.DefaultThemes[0].NormalizedName) - .SingleAsync(); - } - - return result; - } - - public async Task> GetThemes() - { - return await _context.SiteTheme - .ToListAsync(); - } - - public async Task GetTheme(int themeId) - { - return await _context.SiteTheme - .Where(t => t.Id == themeId) - .FirstOrDefaultAsync(); - } - - public async Task IsThemeInUse(int themeId) - { - return await _context.AppUserPreferences - .AnyAsync(p => p.Theme.Id == themeId); - } - - public async Task GetThemeDto(int themeId) - { - return await _context.SiteTheme - .Where(t => t.Id == themeId) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); - } -} diff --git a/API/Data/Repositories/UserTableOfContentRepository.cs b/API/Data/Repositories/UserTableOfContentRepository.cs deleted file mode 100644 index 34b3994de..000000000 --- a/API/Data/Repositories/UserTableOfContentRepository.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Reader; -using API.Entities; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface IUserTableOfContentRepository -{ - void Attach(AppUserTableOfContent toc); - void Remove(AppUserTableOfContent toc); - Task IsUnique(int userId, int chapterId, int page, string title); - IEnumerable GetPersonalToC(int userId, int chapterId); - Task> GetPersonalToCForPage(int userId, int chapterId, int page); - Task Get(int userId, int chapterId, int pageNum, string title); -} - -public class UserTableOfContentRepository : IUserTableOfContentRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public UserTableOfContentRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Attach(AppUserTableOfContent toc) - { - _context.AppUserTableOfContent.Attach(toc); - } - - public void Remove(AppUserTableOfContent toc) - { - _context.AppUserTableOfContent.Remove(toc); - } - - public async Task IsUnique(int userId, int chapterId, int page, string title) - { - return await _context.AppUserTableOfContent.AnyAsync(t => - t.AppUserId == userId && t.PageNumber == page && t.Title == title && t.ChapterId == chapterId); - } - - public IEnumerable GetPersonalToC(int userId, int chapterId) - { - return _context.AppUserTableOfContent - .Where(t => t.AppUserId == userId && t.ChapterId == chapterId) - .ProjectTo(_mapper.ConfigurationProvider) - .OrderBy(t => t.PageNumber) - .AsEnumerable(); - } - - public async Task> GetPersonalToCForPage(int userId, int chapterId, int page) - { - return await _context.AppUserTableOfContent - .Where(t => t.AppUserId == userId && t.ChapterId == chapterId && t.PageNumber == page) - .ProjectTo(_mapper.ConfigurationProvider) - .OrderBy(t => t.PageNumber) - .ToListAsync(); - } - - public async Task Get(int userId,int chapterId, int pageNum, string title) - { - return await _context.AppUserTableOfContent - .Where(t => t.AppUserId == userId && t.ChapterId == chapterId && t.PageNumber == pageNum && t.Title == title) - .FirstOrDefaultAsync(); - } -} diff --git a/API/Extensions/AppUserExtensions.cs b/API/Extensions/AppUserExtensions.cs deleted file mode 100644 index be3d2c064..000000000 --- a/API/Extensions/AppUserExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using API.Data.Misc; -using API.Entities; -using API.Helpers; - -namespace API.Extensions; -#nullable enable - -public static class AppUserExtensions -{ - /// - /// Adds a new SideNavStream to the user's SideNavStreams. This user should have these streams already loaded - /// - /// - /// - public static void CreateSideNavFromLibrary(this AppUser user, Library library) - { - user.SideNavStreams ??= new List(); - var maxCount = user.SideNavStreams.Select(s => s.Order).DefaultIfEmpty().Max(); - - if (user.SideNavStreams.FirstOrDefault(s => s.LibraryId == library.Id) != null) return; - - user.SideNavStreams.Add(new AppUserSideNavStream() - { - Name = library.Name, - Order = maxCount + 1, - IsProvided = false, - StreamType = SideNavStreamType.Library, - LibraryId = library.Id, - Visible = true, - }); - } - - - public static void RemoveSideNavFromLibrary(this AppUser user, Library library) - { - user.SideNavStreams ??= new List(); - - // Find the library and remove it - var item = user.SideNavStreams.FirstOrDefault(s => s.LibraryId == library.Id); - if (item == null) return; - user.SideNavStreams.Remove(item); - - OrderableHelper.ReorderItems(user.SideNavStreams); - - } - - public static AgeRestriction GetAgeRestriction(this AppUser user) - { - return new AgeRestriction() - { - AgeRating = user.AgeRestriction, - IncludeUnknowns = user.AgeRestrictionIncludeUnknowns, - }; - } -} diff --git a/API/Extensions/EnumExtensions.cs b/API/Extensions/EnumExtensions.cs deleted file mode 100644 index 63e28b8ab..000000000 --- a/API/Extensions/EnumExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -#nullable enable -using System; -using System.ComponentModel; -using System.Reflection; - -namespace API.Extensions; - -public static class EnumExtensions -{ - /// - /// Extension on Enum.TryParse which also tried matching on the description attribute - /// - /// if a match was found - /// First tries Enum.TryParse then fall back to the more expensive operation - public static bool TryParse(string? value, out TEnum result) where TEnum : struct, Enum - { - result = default; - - if (string.IsNullOrEmpty(value)) - { - return false; - } - - if (Enum.TryParse(value, out result)) - { - return true; - } - - foreach (var field in typeof(TEnum).GetFields(BindingFlags.Public | BindingFlags.Static)) - { - var description = field.GetCustomAttribute()?.Description; - - if (!string.IsNullOrEmpty(description) && - string.Equals(description, value, StringComparison.OrdinalIgnoreCase)) - { - result = (TEnum)field.GetValue(null)!; - return true; - } - } - - return false; - } -} diff --git a/API/Extensions/EnumerableExtensions.cs b/API/Extensions/EnumerableExtensions.cs deleted file mode 100644 index 903b6f869..000000000 --- a/API/Extensions/EnumerableExtensions.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using API.Data.Misc; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; - -namespace API.Extensions; -#nullable enable - -public static class EnumerableExtensions -{ - private static readonly Regex Regex = new Regex(@"\d+", RegexOptions.Compiled, TimeSpan.FromMilliseconds(500)); - - /// - /// A natural sort implementation - /// - /// IEnumerable to process - /// Function that produces a string. Does not support null values - /// Defaults to CurrentCulture - /// - /// Sorted Enumerable - public static IEnumerable OrderByNatural(this IEnumerable items, Func selector, StringComparer? stringComparer = null) - { - var list = items.ToList(); - var maxDigits = list - .SelectMany(i => Regex.Matches(selector(i)) - .Select(digitChunk => (int?)digitChunk.Value.Length)) - .Max() ?? 0; - - return list.OrderBy(i => Regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture); - } - - public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return items; - var q = items.Where(s => s.AgeRating <= restriction.AgeRating); - if (!restriction.IncludeUnknowns) - { - return q.Where(s => s.AgeRating != AgeRating.Unknown); - } - - return q; - } - - public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return items; - var q = items.Where(s => s.AgeRating <= restriction.AgeRating); - if (!restriction.IncludeUnknowns) - { - return q.Where(s => s.AgeRating != AgeRating.Unknown); - } - - return q; - } - - public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return items; - var q = items.Where(s => s.AgeRating <= restriction.AgeRating); - if (!restriction.IncludeUnknowns) - { - return q.Where(s => s.AgeRating != AgeRating.Unknown); - } - - return q; - } - - /// - /// Safety net around Max, returning the default value if source contains no elements - /// - /// - /// - /// - /// - /// - /// - public static TResult? MaxOrDefault( - this IList source, - Func selector, - TResult? defaultValue) - { - return source.Count == 0 ? defaultValue : source.Max(selector); - } - - /// - /// Safety wrapper around Min, returning the default value if source has no elements - /// - /// - /// - /// - /// - /// - /// - public static TResult? MinOrDefault( - this IList source, - Func selector, - TResult? defaultValue) - { - return source.Count == 0 ? defaultValue : source.Min(selector); - } - - public static IEnumerable WhereNotNull(this IEnumerable source) - where TSource : class - { - return source.Where(item => item != null)!; - } -} diff --git a/API/Extensions/FileInfoExtensions.cs b/API/Extensions/FileInfoExtensions.cs deleted file mode 100644 index 1403486dd..000000000 --- a/API/Extensions/FileInfoExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.IO; - -namespace API.Extensions; -#nullable enable - -public static class FileInfoExtensions -{ - /// - /// Checks if the last write time of the file is after the passed date - /// - /// - /// - /// - public static bool HasFileBeenModifiedSince(this FileInfo fileInfo, DateTime comparison) - { - return DateTime.Compare(fileInfo.LastWriteTime, comparison) > 0; - } -} diff --git a/API/Extensions/PathExtensions.cs b/API/Extensions/PathExtensions.cs deleted file mode 100644 index 64c0616ab..000000000 --- a/API/Extensions/PathExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.IO; - -namespace API.Extensions; -#nullable enable - -public static class PathExtensions -{ - public static string GetFullPathWithoutExtension(this string filepath) - { - if (string.IsNullOrEmpty(filepath)) return filepath; - var extension = Path.GetExtension(filepath); - if (string.IsNullOrEmpty(extension)) return filepath; - return Path.GetFullPath(filepath.Replace(extension, string.Empty)); - } -} diff --git a/API/Extensions/QueryExtensions/Filtering/AnnotationFilter.cs b/API/Extensions/QueryExtensions/Filtering/AnnotationFilter.cs deleted file mode 100644 index a98458ba8..000000000 --- a/API/Extensions/QueryExtensions/Filtering/AnnotationFilter.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities; -using Kavita.Common; -using Microsoft.EntityFrameworkCore; - -namespace API.Extensions.QueryExtensions.Filtering; - -public static class AnnotationFilter -{ - - public static IQueryable IsOwnedBy(this IQueryable queryable, bool condition, - FilterComparison comparison, IList ownerIds) - { - if (ownerIds.Count == 0 || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.AppUserId == ownerIds[0]), - FilterComparison.Contains => queryable.Where(a => ownerIds.Contains(a.AppUserId)), - FilterComparison.NotContains => queryable.Where(a => !ownerIds.Contains(a.AppUserId)), - FilterComparison.NotEqual => queryable.Where(a => a.AppUserId != ownerIds[0]), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable IsInLibrary(this IQueryable queryable, bool condition, - FilterComparison comparison, IList libraryIds) - { - if (libraryIds.Count == 0 || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.LibraryId == libraryIds[0]), - FilterComparison.Contains => queryable.Where(a => libraryIds.Contains(a.LibraryId)), - FilterComparison.NotContains => queryable.Where(a => !libraryIds.Contains(a.LibraryId)), - FilterComparison.NotEqual => queryable.Where(a => a.LibraryId != libraryIds[0]), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable HasSeries(this IQueryable queryable, bool condition, - FilterComparison comparison, IList seriesIds) - { - if (seriesIds.Count == 0 || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.SeriesId == seriesIds[0]), - FilterComparison.Contains => queryable.Where(a => seriesIds.Contains(a.SeriesId)), - FilterComparison.NotContains => queryable.Where(a => !seriesIds.Contains(a.SeriesId)), - FilterComparison.NotEqual => queryable.Where(a => a.SeriesId != seriesIds[0]), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable IsUsingHighlights(this IQueryable queryable, bool condition, - FilterComparison comparison, IList highlightSlotIdxs) - { - if (highlightSlotIdxs.Count == 0 || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.SelectedSlotIndex == highlightSlotIdxs[0]), - FilterComparison.Contains => queryable.Where(a => highlightSlotIdxs.Contains(a.SelectedSlotIndex)), - FilterComparison.NotContains => queryable.Where(a => !highlightSlotIdxs.Contains(a.SelectedSlotIndex)), - FilterComparison.NotEqual => queryable.Where(a => a.SelectedSlotIndex != highlightSlotIdxs[0]), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable HasSelected(this IQueryable queryable, bool condition, - FilterComparison comparison, string value) - { - if (string.IsNullOrEmpty(value) || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.SelectedText == value), - FilterComparison.NotEqual => queryable.Where(a => a.SelectedText != value), - FilterComparison.BeginsWith => queryable.Where(a => EF.Functions.Like(a.SelectedText, $"{value}%")), - FilterComparison.EndsWith => queryable.Where(a => EF.Functions.Like(a.SelectedText, $"%{value}")), - FilterComparison.Matches => queryable.Where(a => EF.Functions.Like(a.SelectedText, $"%{value}%")), - FilterComparison.GreaterThan or - FilterComparison.GreaterThanEqual or - FilterComparison.LessThan or - FilterComparison.LessThanEqual or - FilterComparison.Contains or - FilterComparison.MustContains or - FilterComparison.NotContains or - FilterComparison.IsBefore or - FilterComparison.IsAfter or - FilterComparison.IsInLast or - FilterComparison.IsNotInLast or - FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.SelectedText"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable HasCommented(this IQueryable queryable, bool condition, - FilterComparison comparison, string value) - { - if (string.IsNullOrEmpty(value) || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.CommentPlainText == value), - FilterComparison.NotEqual => queryable.Where(a => a.CommentPlainText != value), - FilterComparison.BeginsWith => queryable.Where(a => EF.Functions.Like(a.CommentPlainText, $"{value}%")), - FilterComparison.EndsWith => queryable.Where(a => EF.Functions.Like(a.CommentPlainText, $"%{value}")), - FilterComparison.Matches => queryable.Where(a => EF.Functions.Like(a.CommentPlainText, $"%{value}%")), - FilterComparison.GreaterThan or - FilterComparison.GreaterThanEqual or - FilterComparison.LessThan or - FilterComparison.LessThanEqual or - FilterComparison.Contains or - FilterComparison.MustContains or - FilterComparison.NotContains or - FilterComparison.IsBefore or - FilterComparison.IsAfter or - FilterComparison.IsInLast or - FilterComparison.IsNotInLast or - FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.CommentPlainText"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable HasLikes(this IQueryable queryable, bool condition, - FilterComparison comparison, int value) - { - if (!condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.Likes.Count == value), - FilterComparison.NotEqual => queryable.Where(a => a.Likes.Count != value), - FilterComparison.GreaterThan => queryable.Where(a => a.Likes.Count > value), - FilterComparison.GreaterThanEqual => queryable.Where(a => a.Likes.Count >= value), - FilterComparison.LessThan => queryable.Where(a => a.Likes.Count < value), - FilterComparison.LessThanEqual => queryable.Where(a => a.Likes.Count <= value), - FilterComparison.BeginsWith or - FilterComparison.EndsWith or - FilterComparison.Matches or - FilterComparison.Contains or - FilterComparison.MustContains or - FilterComparison.NotContains or - FilterComparison.IsBefore or - FilterComparison.IsAfter or - FilterComparison.IsInLast or - FilterComparison.IsNotInLast or - FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.Likes"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable IsLikedBy(this IQueryable queryable, bool condition, - FilterComparison comparison, IList value) - { - if (value.Count == 0 || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.Likes.Contains(value[0])), - FilterComparison.NotEqual => queryable.Where(a => a!.Likes.Contains(value[0])), - FilterComparison.Contains => queryable.Where(a => a.Likes.Any(value.Contains)), - FilterComparison.NotContains => queryable.Where(a => !a.Likes.Any(value.Contains)), - FilterComparison.GreaterThan or - FilterComparison.GreaterThanEqual or - FilterComparison.LessThan or - FilterComparison.LessThanEqual or - FilterComparison.BeginsWith or - FilterComparison.EndsWith or - FilterComparison.Matches or - FilterComparison.MustContains or - FilterComparison.IsBefore or - FilterComparison.IsAfter or - FilterComparison.IsInLast or - FilterComparison.IsNotInLast or - FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.Likes"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - -} diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs deleted file mode 100644 index 49b8c1de4..000000000 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ /dev/null @@ -1,945 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common; -using Microsoft.EntityFrameworkCore; - -namespace API.Extensions.QueryExtensions.Filtering; -#nullable enable - -public static class SeriesFilter -{ - private const float FloatingPointTolerance = 0.001f; - - public static IQueryable HasLanguage(this IQueryable queryable, bool condition, - FilterComparison comparison, IList languages) - { - if (languages.Count == 0 || !condition) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Metadata.Language.Equals(languages[0])); - case FilterComparison.Contains: - return queryable.Where(s => languages.Contains(s.Metadata.Language)); - case FilterComparison.MustContains: - return queryable.Where(s => languages.All(s2 => s2.Equals(s.Metadata.Language))); - case FilterComparison.NotContains: - return queryable.Where(s => !languages.Contains(s.Metadata.Language)); - case FilterComparison.NotEqual: - return queryable.Where(s => !s.Metadata.Language.Equals(languages[0])); - case FilterComparison.Matches: - return queryable.Where(s => EF.Functions.Like(s.Metadata.Language, $"{languages[0]}%")); - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.IsEmpty: - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasReleaseYear(this IQueryable queryable, bool condition, - FilterComparison comparison, int? releaseYear) - { - if (!condition || releaseYear == null) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Metadata.ReleaseYear == releaseYear); - case FilterComparison.GreaterThan: - case FilterComparison.IsAfter: - return queryable.Where(s => s.Metadata.ReleaseYear > releaseYear); - case FilterComparison.GreaterThanEqual: - return queryable.Where(s => s.Metadata.ReleaseYear >= releaseYear); - case FilterComparison.LessThan: - case FilterComparison.IsBefore: - return queryable.Where(s => s.Metadata.ReleaseYear < releaseYear); - case FilterComparison.LessThanEqual: - return queryable.Where(s => s.Metadata.ReleaseYear <= releaseYear); - case FilterComparison.IsInLast: - return queryable.Where(s => s.Metadata.ReleaseYear >= DateTime.Now.Year - (int) releaseYear); - case FilterComparison.IsNotInLast: - return queryable.Where(s => s.Metadata.ReleaseYear < DateTime.Now.Year - (int) releaseYear); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Metadata.ReleaseYear == 0); - case FilterComparison.Matches: - case FilterComparison.Contains: - case FilterComparison.NotContains: - case FilterComparison.NotEqual: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.MustContains: - throw new KavitaException($"{comparison} not applicable for Series.ReleaseYear"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - - public static IQueryable HasRating(this IQueryable queryable, bool condition, - FilterComparison comparison, float rating, int userId) - { - if (rating < 0 || !condition || userId <= 0) return queryable; - - // AppUserRating stores a 5-digit number. - rating = Math.Clamp(rating, 0f, 5f); - - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) <= FloatingPointTolerance && r.AppUserId == userId)); - case FilterComparison.GreaterThan: - return queryable.Where(s => s.Ratings.Any(r => r.Rating > rating && r.AppUserId == userId)); - case FilterComparison.GreaterThanEqual: - return queryable.Where(s => s.Ratings.Any(r => r.Rating >= rating && r.AppUserId == userId)); - case FilterComparison.LessThan: - return queryable.Where(s => s.Ratings.Any(r => r.Rating < rating && r.AppUserId == userId)); - case FilterComparison.LessThanEqual: - return queryable.Where(s => s.Ratings.Any(r => r.Rating <= rating && r.AppUserId == userId)); - case FilterComparison.NotEqual: - return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) >= FloatingPointTolerance && r.AppUserId == userId)); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Ratings.All(r => r.AppUserId != userId)); - case FilterComparison.Contains: - case FilterComparison.Matches: - case FilterComparison.NotContains: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - throw new KavitaException($"{comparison} not applicable for Series.Rating"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasAgeRating(this IQueryable queryable, bool condition, - FilterComparison comparison, IList ratings) - { - if (!condition || ratings.Count == 0) return queryable; - - var firstRating = ratings[0]; - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Metadata.AgeRating == firstRating); - case FilterComparison.GreaterThan: - return queryable.Where(s => s.Metadata.AgeRating > firstRating); - case FilterComparison.GreaterThanEqual: - return queryable.Where(s => s.Metadata.AgeRating >= firstRating); - case FilterComparison.LessThan: - return queryable.Where(s => s.Metadata.AgeRating < firstRating); - case FilterComparison.LessThanEqual: - return queryable.Where(s => s.Metadata.AgeRating <= firstRating); - case FilterComparison.Contains: - return queryable.Where(s => ratings.Contains(s.Metadata.AgeRating)); - case FilterComparison.NotContains: - return queryable.Where(s => !ratings.Contains(s.Metadata.AgeRating)); - case FilterComparison.NotEqual: - return queryable.Where(s => s.Metadata.AgeRating != firstRating); - case FilterComparison.Matches: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.AgeRating"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasAverageReadTime(this IQueryable queryable, bool condition, - FilterComparison comparison, int avgReadTime) - { - if (!condition || avgReadTime < 0) return queryable; - - switch (comparison) - { - case FilterComparison.NotEqual: - return queryable.WhereNotEqual(s => s.AvgHoursToRead, avgReadTime); - case FilterComparison.Equal: - return queryable.WhereEqual(s => s.AvgHoursToRead, avgReadTime); - case FilterComparison.GreaterThan: - return queryable.WhereGreaterThan(s => s.AvgHoursToRead, avgReadTime); - case FilterComparison.GreaterThanEqual: - return queryable.WhereGreaterThanOrEqual(s => s.AvgHoursToRead, avgReadTime); - case FilterComparison.LessThan: - return queryable.WhereLessThan(s => s.AvgHoursToRead, avgReadTime); - case FilterComparison.LessThanEqual: - return queryable.WhereLessThanOrEqual(s => s.AvgHoursToRead, avgReadTime); - case FilterComparison.Contains: - case FilterComparison.Matches: - case FilterComparison.NotContains: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.AverageReadTime"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasPublicationStatus(this IQueryable queryable, bool condition, - FilterComparison comparison, IList pubStatues) - { - if (!condition || pubStatues.Count == 0) return queryable; - - var firstStatus = pubStatues[0]; - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Metadata.PublicationStatus == firstStatus); - case FilterComparison.Contains: - return queryable.Where(s => pubStatues.Contains(s.Metadata.PublicationStatus)); - case FilterComparison.NotContains: - return queryable.Where(s => !pubStatues.Contains(s.Metadata.PublicationStatus)); - case FilterComparison.NotEqual: - return queryable.Where(s => s.Metadata.PublicationStatus != firstStatus); - case FilterComparison.MustContains: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.Matches: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.PublicationStatus"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - /// - /// - /// - /// This is more taxing on memory as the percentage calculation must be done in Memory - /// - /// - public static IQueryable HasReadingProgress(this IQueryable queryable, bool condition, - FilterComparison comparison, float readProgress, int userId) - { - if (!condition) return queryable; - - var subQuery = queryable - .Select(s => new - { - SeriesId = s.Id, - SeriesName = s.Name, - Percentage = s.Progress - .Where(p => p != null && p.AppUserId == userId) - .Sum(p => p != null ? (p.PagesRead * 1.0f / s.Pages) : 0f) * 100f - }) - .AsSplitQuery(); - - switch (comparison) - { - case FilterComparison.Equal: - subQuery = subQuery.WhereEqual(s => s.Percentage, readProgress); - break; - case FilterComparison.GreaterThan: - subQuery = subQuery.WhereGreaterThan(s => s.Percentage, readProgress); - break; - case FilterComparison.GreaterThanEqual: - subQuery = subQuery.WhereGreaterThanOrEqual(s => s.Percentage, readProgress); - break; - case FilterComparison.LessThan: - subQuery = subQuery.WhereLessThan(s => s.Percentage, readProgress); - break; - case FilterComparison.LessThanEqual: - subQuery = subQuery.WhereLessThanOrEqual(s => s.Percentage, readProgress); - break; - case FilterComparison.NotEqual: - subQuery = subQuery.WhereNotEqual(s => s.Percentage, readProgress); - break; - case FilterComparison.IsEmpty: - case FilterComparison.Matches: - case FilterComparison.Contains: - case FilterComparison.NotContains: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - - var ids = subQuery.Select(s => s.SeriesId); - return queryable.Where(s => ids.Contains(s.Id)); - } - - public static IQueryable HasAverageRating(this IQueryable queryable, bool condition, - FilterComparison comparison, float rating) - { - if (!condition) return queryable; - - var subQuery = queryable - .Where(s => s.ExternalSeriesMetadata != null) - .Include(s => s.ExternalSeriesMetadata) - .Select(s => new - { - SeriesId = s.Id, - SeriesName = s.Name, - AverageRating = s.ExternalSeriesMetadata.AverageExternalRating - }) - .AsSplitQuery() - .AsQueryable(); - - switch (comparison) - { - case FilterComparison.Equal: - subQuery = subQuery.WhereEqual(s => s.AverageRating, rating); - break; - case FilterComparison.GreaterThan: - subQuery = subQuery.WhereGreaterThan(s => s.AverageRating, rating); - break; - case FilterComparison.GreaterThanEqual: - subQuery = subQuery.WhereGreaterThanOrEqual(s => s.AverageRating, rating); - break; - case FilterComparison.LessThan: - subQuery = subQuery.WhereLessThan(s => s.AverageRating, rating); - break; - case FilterComparison.LessThanEqual: - subQuery = subQuery.WhereLessThanOrEqual(s => s.AverageRating, rating); - break; - case FilterComparison.NotEqual: - subQuery = subQuery.WhereNotEqual(s => s.AverageRating, rating); - break; - case FilterComparison.Matches: - case FilterComparison.Contains: - case FilterComparison.NotContains: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.AverageRating"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - - var ids = subQuery.Select(s => s.SeriesId); - return queryable.Where(s => ids.Contains(s.Id)); - } - - /// - /// HasReadingDate but used to filter where last reading point was TODAY() - timeDeltaDays. This allows the user - /// to build smart filters "Haven't read in a month" - /// - public static IQueryable HasReadLast(this IQueryable queryable, bool condition, - FilterComparison comparison, int timeDeltaDays, int userId) - { - if (!condition || timeDeltaDays == 0) return queryable; - - var subQuery = queryable - .Include(s => s.Progress) - .Where(s => s.Progress.Any()) - .Select(s => new - { - SeriesId = s.Id, - SeriesName = s.Name, - MaxDate = s.Progress.Where(p => p != null && p.AppUserId == userId) - .Select(p => (DateTime?) p.LastModified) - .DefaultIfEmpty() - .Max() - }) - .Where(s => s.MaxDate != null) - .AsSplitQuery() - .AsEnumerable(); - - var date = DateTime.Now.AddDays(-timeDeltaDays); - - switch (comparison) - { - case FilterComparison.Equal: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate.Equals(date)); - break; - case FilterComparison.IsAfter: - case FilterComparison.GreaterThan: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate > date); - break; - case FilterComparison.GreaterThanEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate >= date); - break; - case FilterComparison.IsBefore: - case FilterComparison.LessThan: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate < date); - break; - case FilterComparison.LessThanEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate <= date); - break; - case FilterComparison.NotEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && !s.MaxDate.Equals(date)); - break; - case FilterComparison.Matches: - case FilterComparison.Contains: - case FilterComparison.NotContains: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - - var ids = subQuery.Select(s => s.SeriesId); - return queryable.Where(s => ids.Contains(s.Id)); - } - - public static IQueryable HasReadingDate(this IQueryable queryable, bool condition, - FilterComparison comparison, DateTime? date, int userId) - { - if (!condition || !date.HasValue) return queryable; - - var subQuery = queryable - .Include(s => s.Progress) - .Where(s => s.Progress.Any()) - .Select(s => new - { - SeriesId = s.Id, - SeriesName = s.Name, - MaxDate = s.Progress.Where(p => p != null && p.AppUserId == userId) - .Select(p => (DateTime?) p.LastModified) - .DefaultIfEmpty() - .Max() - }) - .Where(s => s.MaxDate != null) - .AsSplitQuery() - .AsEnumerable(); - - switch (comparison) - { - case FilterComparison.Equal: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate.Equals(date)); - break; - case FilterComparison.IsAfter: - case FilterComparison.GreaterThan: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate > date); - break; - case FilterComparison.GreaterThanEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate >= date); - break; - case FilterComparison.IsBefore: - case FilterComparison.LessThan: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate < date); - break; - case FilterComparison.LessThanEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate <= date); - break; - case FilterComparison.NotEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && !s.MaxDate.Equals(date)); - break; - case FilterComparison.Matches: - case FilterComparison.Contains: - case FilterComparison.NotContains: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - - var ids = subQuery.Select(s => s.SeriesId); - return queryable.Where(s => ids.Contains(s.Id)); - } - - public static IQueryable HasTags(this IQueryable queryable, bool condition, - FilterComparison comparison, IList tags) - { - if (!condition || (comparison != FilterComparison.IsEmpty && tags.Count == 0)) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - case FilterComparison.Contains: - return queryable.Where(s => s.Metadata.Tags.Any(t => tags.Contains(t.Id))); - case FilterComparison.NotEqual: - case FilterComparison.NotContains: - return queryable.Where(s => s.Metadata.Tags.All(t => !tags.Contains(t.Id))); - case FilterComparison.MustContains: - // Deconstruct and do a Union of a bunch of where statements since this doesn't translate - var queries = new List>() - { - queryable - }; - queries.AddRange(tags.Select(gId => queryable.Where(s => s.Metadata.Tags.Any(p => p.Id == gId)))); - - return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Metadata.Tags.Count == 0); - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Matches: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - throw new KavitaException($"{comparison} not applicable for Series.Tags"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasPeople(this IQueryable queryable, bool condition, - FilterComparison comparison, IList people, PersonRole role) - { - if (!condition || (comparison != FilterComparison.IsEmpty && people.Count == 0)) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - case FilterComparison.Contains: - return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.PersonId) && p.Role == role)); - case FilterComparison.NotEqual: - case FilterComparison.NotContains: - return queryable.Where(s => s.Metadata.People.All(p => !people.Contains(p.PersonId) || p.Role != role)); - case FilterComparison.MustContains: - var queries = new List>() - { - queryable - }; - queries.AddRange(people.Select(personId => - queryable.Where(s => s.Metadata.People.Any(p => p.PersonId == personId && p.Role == role)))); - - return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - // Ensure no person with the given role exists - return queryable.Where(s => s.Metadata.People.All(p => p.Role != role)); - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.Matches: - throw new KavitaException($"{comparison} not applicable for Series.People"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasPeopleLegacy(this IQueryable queryable, bool condition, - FilterComparison comparison, IList people) - { - if (!condition || people.Count == 0) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - case FilterComparison.Contains: - return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.PersonId))); - case FilterComparison.NotEqual: - case FilterComparison.NotContains: - return queryable.Where(s => s.Metadata.People.All(t => !people.Contains(t.PersonId))); - case FilterComparison.MustContains: - // Deconstruct and do a Union of a bunch of where statements since this doesn't translate - var queries = new List>() - { - queryable - }; - queries.AddRange(people.Select(gId => queryable.Where(s => s.Metadata.People.Any(p => p.PersonId == gId)))); - - return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.Matches: - throw new KavitaException($"{comparison} not applicable for Series.People"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasGenre(this IQueryable queryable, bool condition, - FilterComparison comparison, IList genres) - { - if (!condition || (comparison != FilterComparison.IsEmpty && genres.Count == 0)) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - case FilterComparison.Contains: - return queryable.Where(s => s.Metadata.Genres.Any(p => genres.Contains(p.Id))); - case FilterComparison.NotEqual: - case FilterComparison.NotContains: - return queryable.Where(s => s.Metadata.Genres.All(p => !genres.Contains(p.Id))); - case FilterComparison.MustContains: - // Deconstruct and do a Union of a bunch of where statements since this doesn't translate - var queries = new List>() - { - queryable - }; - queries.AddRange(genres.Select(gId => queryable.Where(s => s.Metadata.Genres.Any(p => p.Id == gId)))); - - return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Metadata.Genres.Count == 0); - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Matches: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - throw new KavitaException($"{comparison} not applicable for Series.Genres"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasFormat(this IQueryable queryable, bool condition, - FilterComparison comparison, IList formats) - { - if (!condition || formats.Count == 0) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - case FilterComparison.Contains: - return queryable.Where(s => formats.Contains(s.Format)); - case FilterComparison.NotContains: - case FilterComparison.NotEqual: - return queryable.Where(s => !formats.Contains(s.Format)); - case FilterComparison.MustContains: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Matches: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.Format"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasCollectionTags(this IQueryable queryable, bool condition, - FilterComparison comparison, IList collectionTags, IList collectionSeries) - { - if (!condition || (comparison != FilterComparison.IsEmpty && collectionTags.Count == 0)) return queryable; - - - switch (comparison) - { - case FilterComparison.Equal: - case FilterComparison.Contains: - return queryable.Where(s => collectionSeries.Contains(s.Id)); - case FilterComparison.NotContains: - case FilterComparison.NotEqual: - return queryable.Where(s => !collectionSeries.Contains(s.Id)); - case FilterComparison.MustContains: - // // Deconstruct and do a Union of a bunch of where statements since this doesn't translate - var queries = new List>() - { - queryable - }; - queries.AddRange(collectionSeries.Select(gId => queryable.Where(s => collectionSeries.Any(p => p == s.Id)))); - - return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Collections.Count == 0); - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Matches: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - throw new KavitaException($"{comparison} not applicable for Series.CollectionTags"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasName(this IQueryable queryable, bool condition, - FilterComparison comparison, string queryString) - { - if (string.IsNullOrEmpty(queryString) || !condition) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Name.Equals(queryString) - || s.OriginalName.Equals(queryString) - || s.LocalizedName.Equals(queryString) - || s.SortName.Equals(queryString)); - case FilterComparison.BeginsWith: - return queryable.Where(s => EF.Functions.Like(s.Name, $"{queryString}%") - ||EF.Functions.Like(s.OriginalName, $"{queryString}%") - || EF.Functions.Like(s.LocalizedName, $"{queryString}%") - || EF.Functions.Like(s.SortName, $"{queryString}%")); - case FilterComparison.EndsWith: - return queryable.Where(s => EF.Functions.Like(s.Name, $"%{queryString}") - ||EF.Functions.Like(s.OriginalName, $"%{queryString}") - || EF.Functions.Like(s.LocalizedName, $"%{queryString}") - || EF.Functions.Like(s.SortName, $"%{queryString}")); - case FilterComparison.Matches: - return queryable.Where(s => EF.Functions.Like(s.Name, $"%{queryString}%") - ||EF.Functions.Like(s.OriginalName, $"%{queryString}%") - || EF.Functions.Like(s.LocalizedName, $"%{queryString}%") - || EF.Functions.Like(s.SortName, $"%{queryString}%")); - case FilterComparison.NotEqual: - return queryable.Where(s => s.Name != queryString - || s.OriginalName != queryString - || s.LocalizedName != queryString - || s.SortName != queryString); - case FilterComparison.NotContains: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Contains: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.Name"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); - } - } - - public static IQueryable HasSummary(this IQueryable queryable, bool condition, - FilterComparison comparison, string queryString) - { - if (!condition) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Metadata.Summary.Equals(queryString)); - case FilterComparison.BeginsWith: - return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"{queryString}%")); - case FilterComparison.EndsWith: - return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"%{queryString}")); - case FilterComparison.Matches: - return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"%{queryString}%")); - case FilterComparison.NotEqual: - return queryable.Where(s => s.Metadata.Summary != queryString); - case FilterComparison.IsEmpty: - return queryable.Where(s => string.IsNullOrEmpty(s.Metadata.Summary)); - case FilterComparison.NotContains: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Contains: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - throw new KavitaException($"{comparison} not applicable for Series.Metadata.Summary"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); - } - } - - public static IQueryable HasPath(this IQueryable queryable, bool condition, - FilterComparison comparison, string queryString) - { - if (!condition) return queryable; - - var normalizedPath = Parser.NormalizePath(queryString); - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.FolderPath != null && s.FolderPath.Equals(normalizedPath)); - case FilterComparison.BeginsWith: - return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"{normalizedPath}%")); - case FilterComparison.EndsWith: - return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"%{normalizedPath}")); - case FilterComparison.Matches: - return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"%{normalizedPath}%")); - case FilterComparison.NotEqual: - return queryable.Where(s => s.FolderPath != null && s.FolderPath != normalizedPath); - case FilterComparison.NotContains: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Contains: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); - } - } - - public static IQueryable HasFilePath(this IQueryable queryable, bool condition, - FilterComparison comparison, string queryString) - { - if (!condition) return queryable; - - var normalizedPath = Parser.NormalizePath(queryString); - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => - s.Volumes.Any(v => - v.Chapters.Any(c => - c.Files.Any(f => - f.FilePath != null && f.FilePath.Equals(normalizedPath) - ) - ) - ) - ); - case FilterComparison.BeginsWith: - return queryable.Where(s => - s.Volumes.Any(v => - v.Chapters.Any(c => - c.Files.Any(f => - f.FilePath != null && EF.Functions.Like(f.FilePath, $"{normalizedPath}%") - ) - ) - ) - ); - case FilterComparison.EndsWith: - return queryable.Where(s => - s.Volumes.Any(v => - v.Chapters.Any(c => - c.Files.Any(f => - f.FilePath != null && EF.Functions.Like(f.FilePath, $"%{normalizedPath}") - ) - ) - ) - ); - case FilterComparison.Matches: - return queryable.Where(s => - s.Volumes.Any(v => - v.Chapters.Any(c => - c.Files.Any(f => - f.FilePath != null && EF.Functions.Like(f.FilePath, $"%{normalizedPath}%") - ) - ) - ) - ); - case FilterComparison.NotEqual: - return queryable.Where(s => - s.Volumes.Any(v => - v.Chapters.Any(c => - c.Files.Any(f => - f.FilePath == null || !f.FilePath.Equals(normalizedPath) - ) - ) - ) - ); - case FilterComparison.NotContains: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Contains: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); - } - } - - public static IQueryable HasFileSize(this IQueryable queryable, bool condition, - FilterComparison comparison, float fileSize) - { - if (fileSize == 0f || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) == fileSize), - FilterComparison.LessThan => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) < fileSize), - FilterComparison.LessThanEqual => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) <= fileSize), - FilterComparison.GreaterThan => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) > fileSize), - FilterComparison.GreaterThanEqual => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) >= fileSize), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"), - }; - } - - -} diff --git a/API/Extensions/QueryExtensions/ProjectToExtensions.cs b/API/Extensions/QueryExtensions/ProjectToExtensions.cs deleted file mode 100644 index 067686ad1..000000000 --- a/API/Extensions/QueryExtensions/ProjectToExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ - -using System.Linq; -using AutoMapper; -using AutoMapper.QueryableExtensions; - -namespace API.Extensions.QueryExtensions; - -public static class ProjectToExtensions -{ - public static IQueryable ProjectToWithProgress( - this IQueryable queryable, - IConfigurationProvider config, - int userId) - { - return queryable.ProjectTo(config, new { userId }); - } - - // Convenience overload taking IMapper directly - public static IQueryable ProjectToWithProgress( - this IQueryable queryable, - IMapper mapper, - int userId) - { - return queryable.ProjectTo(mapper.ConfigurationProvider, new { userId }); - } -} diff --git a/API/Extensions/StringExtensions.cs b/API/Extensions/StringExtensions.cs deleted file mode 100644 index 2fdaf52a1..000000000 --- a/API/Extensions/StringExtensions.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; - -namespace API.Extensions; -#nullable enable - -public static partial class StringExtensions -{ - private static readonly Regex SentenceCaseRegex = new(@"(^[a-z])|\.\s+(.)", - RegexOptions.ExplicitCapture | RegexOptions.Compiled, - Services.Tasks.Scanner.Parser.Parser.RegexTimeout); - - public static string Sanitize(this string input) - { - if (string.IsNullOrEmpty(input)) - return string.Empty; - - // Remove all newline and control characters - var sanitized = input - .Replace(Environment.NewLine, string.Empty) - .Replace("\n", string.Empty) - .Replace("\r", string.Empty); - - // Optionally remove other potentially unwanted characters - sanitized = Regex.Replace(sanitized, @"[^\u0020-\u007E]", string.Empty); // Removes non-printable ASCII - - return sanitized.Trim(); // Trim any leading/trailing whitespace - } - - public static string SentenceCase(this string value) - { - return SentenceCaseRegex.Replace(value.ToLower(), s => s.Value.ToUpper()); - } - - /// - /// Apply normalization on the String - /// - /// - /// - public static string ToNormalized(this string? value) - { - return string.IsNullOrEmpty(value) ? string.Empty : Services.Tasks.Scanner.Parser.Parser.Normalize(value); - } - - public static float AsFloat(this string? value, float defaultValue = 0.0f) - { - return string.IsNullOrEmpty(value) ? defaultValue : float.Parse(value, CultureInfo.InvariantCulture); - } - - public static double AsDouble(this string? value, double defaultValue = 0.0f) - { - return string.IsNullOrEmpty(value) ? defaultValue : double.Parse(value, CultureInfo.InvariantCulture); - } - - public static string TrimPrefix(this string? value, string prefix) - { - if (string.IsNullOrEmpty(value)) return string.Empty; - - if (!value.StartsWith(prefix)) return value; - - return value.Substring(prefix.Length); - } - - /// - /// Censor the input string by removing all but the first and last char. - /// - /// - /// - /// If the input is an email (contains @), the domain will remain untouched - public static string Censor(this string? input) - { - if (string.IsNullOrWhiteSpace(input)) return input ?? string.Empty; - - var atIdx = input.IndexOf('@'); - if (atIdx == -1) - { - return $"{input[0]}{new string('*', input.Length - 1)}"; - } - - return input[0] + new string('*', atIdx - 1) + input[atIdx..]; - } - - /// - /// Repeat returns a string that is equal to the original string repeat n times - /// - /// String to repeat - /// Amount of times to repeat - /// - public static string Repeat(this string? input, int n) - { - return string.IsNullOrEmpty(input) ? string.Empty : string.Concat(Enumerable.Repeat(input, n)); - } - - public static IList ParseIntArray(this string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return []; - } - - return value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Select(int.Parse) - .ToList(); - } - - /// - /// Parses a human-readable file size string (e.g. "1.43 GB") into bytes. - /// - /// The input string like "1.43 GB", "4.2 KB", "512 B" - /// Byte count as long - public static long ParseHumanReadableBytes(this string input) - { - if (string.IsNullOrWhiteSpace(input)) - { - throw new ArgumentException("Input cannot be null or empty.", nameof(input)); - } - - - var match = HumanReadableBytesRegex().Match(input); - if (!match.Success) - { - throw new FormatException($"Invalid format: '{input}'"); - } - - - var value = double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); - var unit = match.Groups[2].Value.ToUpperInvariant(); - - var multiplier = unit switch - { - "B" => 1L, - "KB" => 1L << 10, - "MB" => 1L << 20, - "GB" => 1L << 30, - "TB" => 1L << 40, - "PB" => 1L << 50, - "EB" => 1L << 60, - _ => throw new FormatException($"Unknown unit: '{unit}'") - }; - - return (long)(value * multiplier); - } - - [GeneratedRegex(@"^\s*(\d+(?:\.\d+)?)\s*([KMGTPE]?B)\s*$", RegexOptions.IgnoreCase)] - private static partial Regex HumanReadableBytesRegex(); -} diff --git a/API/Helpers/Builders/MediaErrorBuilder.cs b/API/Helpers/Builders/MediaErrorBuilder.cs deleted file mode 100644 index 4d0f7f3a0..000000000 --- a/API/Helpers/Builders/MediaErrorBuilder.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.IO; -using API.Entities; -using API.Services.Tasks.Scanner.Parser; - -namespace API.Helpers.Builders; - -public class MediaErrorBuilder : IEntityBuilder -{ - private readonly MediaError _mediaError; - public MediaError Build() => _mediaError; - - public MediaErrorBuilder(string filePath) - { - _mediaError = new MediaError() - { - FilePath = Parser.NormalizePath(filePath), - Extension = Path.GetExtension(filePath).Replace(".", string.Empty).ToUpperInvariant() - }; - } - - public MediaErrorBuilder WithComment(string comment) - { - _mediaError.Comment = comment.Trim(); - return this; - } - - public MediaErrorBuilder WithDetails(string details) - { - _mediaError.Details = details.Trim(); - return this; - } -} diff --git a/API/Helpers/Builders/PlusSeriesDtoBuilder.cs b/API/Helpers/Builders/PlusSeriesDtoBuilder.cs deleted file mode 100644 index db84f33c6..000000000 --- a/API/Helpers/Builders/PlusSeriesDtoBuilder.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Linq; -using API.DTOs.Scrobbling; -using API.Entities; -using API.Extensions; -using API.Services.Plus; - -namespace API.Helpers.Builders; - -public class PlusSeriesDtoBuilder : IEntityBuilder -{ - private readonly PlusSeriesRequestDto _seriesRequestDto; - public PlusSeriesRequestDto Build() => _seriesRequestDto; - - /// - /// This must be a FULL Series - /// - /// - public PlusSeriesDtoBuilder(Series series) - { - _seriesRequestDto = new PlusSeriesRequestDto() - { - MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format), - SeriesName = series.Name, - AltSeriesName = series.LocalizedName, - AniListId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.AniListWeblinkWebsite), - MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.MalWeblinkWebsite), - GoogleBooksId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.GoogleBooksWeblinkWebsite), - MangaDexId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.MangaDexWeblinkWebsite), - VolumeCount = series.Volumes.Count, - ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial), - Year = series.Metadata.ReleaseYear - }; - } - -} diff --git a/API/Helpers/PagedList.cs b/API/Helpers/PagedList.cs deleted file mode 100644 index 4ab566e5c..000000000 --- a/API/Helpers/PagedList.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; - -namespace API.Helpers; -#nullable enable - -public class PagedList : List -{ - private PagedList(IEnumerable items, int count, int pageNumber, int pageSize) - { - CurrentPage = pageNumber; - TotalPages = (int) Math.Ceiling(count / (double) pageSize); - PageSize = pageSize; - TotalCount = count; - AddRange(items); - } - - public int CurrentPage { get; set; } - public int TotalPages { get; set; } - public int PageSize { get; set; } - public int TotalCount { get; set; } - - public static async Task> CreateAsync(IQueryable source, UserParams userParams) - { - return await CreateAsync(source, userParams.PageNumber, userParams.PageSize); - } - - public static async Task> CreateAsync(IQueryable source, int pageNumber, int pageSize) - { - // NOTE: OrderBy warning being thrown here even if query has the orderby statement - var countTask = source.CountAsync(); - var itemsTask = source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync(); - - await Task.WhenAll(countTask, itemsTask); - - return new PagedList(itemsTask.Result, countTask.Result, pageNumber, pageSize); - } - - public static PagedList Create(IEnumerable items, int totalCount, int pageNumber, int pageSize) - { - return new PagedList(items, totalCount, pageNumber, pageSize); - } -} diff --git a/API/Helpers/ParserInfoHelpers.cs b/API/Helpers/ParserInfoHelpers.cs deleted file mode 100644 index fc8d7227a..000000000 --- a/API/Helpers/ParserInfoHelpers.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner; -using API.Services.Tasks.Scanner.Parser; - -namespace API.Helpers; -#nullable enable - -public static class ParserInfoHelpers -{ - /// - /// Checks each parser info to see if there is a name match and if so, checks if the format matches the Series object. - /// This accounts for if the Series has an Unknown type and if so, considers it matching. - /// - /// - /// - /// - public static bool SeriesHasMatchingParserInfoFormat(Series series, - Dictionary> parsedSeries) - { - var format = MangaFormat.Unknown; - foreach (var pSeries in parsedSeries.Keys) - { - var name = pSeries.Name; - var normalizedName = name.ToNormalized(); - - if (normalizedName == series.NormalizedName || - normalizedName == series.Name.ToNormalized() || - name == series.Name || name == series.LocalizedName || - name == series.OriginalName || - normalizedName == series.OriginalName?.ToNormalized()) - { - format = pSeries.Format; - if (format == series.Format) - { - return true; - } - } - } - - if (series.Format == MangaFormat.Unknown) - { - return true; - } - - return format == series.Format; - } -} diff --git a/API/Properties/launchSettings.json b/API/Properties/launchSettings.json deleted file mode 100644 index 677d81685..000000000 --- a/API/Properties/launchSettings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:14778", - "sslPort": 44368 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "API": { - "commandName": "Project", - "dotnetRunMessages": "true", - "launchBrowser": false, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/API/Services/AccountService.cs b/API/Services/AccountService.cs deleted file mode 100644 index 6bffb864c..000000000 --- a/API/Services/AccountService.cs +++ /dev/null @@ -1,335 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Errors; -using API.Extensions; -using API.Helpers.Builders; -using AutoMapper; -using Kavita.Common; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -#nullable enable - -public interface IAccountService -{ - Task> ChangeUserPassword(AppUser user, string newPassword); - Task> ValidatePassword(AppUser user, string password); - Task> ValidateUsername(string? username); - Task> ValidateEmail(string email); - Task HasBookmarkPermission(AppUser? user); - Task HasDownloadPermission(AppUser? user); - Task CanChangeAgeRestriction(AppUser? user); - - /// - /// - /// - /// The user who is changing the identity - /// the user being changed - /// the provider being changed to - /// If true, user should not be updated by kavita (anymore) - /// Throws if invalid actions are being performed - Task ChangeIdentityProvider(int actingUserId, AppUser user, IdentityProvider identityProvider); - /// - /// Removes access to all libraries, then grant access to all given libraries or all libraries if the user is admin. - /// Creates side nav streams as well - /// - /// - /// - /// - /// - /// Ensure that the users SideNavStreams are loaded - /// Does NOT commit - Task UpdateLibrariesForUser(AppUser user, IList librariesIds, bool hasAdminRole); - Task> UpdateRolesForUser(AppUser user, IList roles); - /// - /// Seeds all information necessary for a new user - /// - /// - /// - Task SeedUser(AppUser user); - void AddDefaultStreamsToUser(AppUser user); - Task AddDefaultReadingProfileToUser(AppUser user); -} - -public partial class AccountService : IAccountService -{ - private readonly ILocalizationService _localizationService; - private readonly UserManager _userManager; - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IMapper _mapper; - public const string DefaultPassword = "[k.2@RZ!mxCQkJzE"; - public static readonly Regex AllowedUsernameRegex = AllowedUsernameRegexAttr(); - - - public AccountService(UserManager userManager, ILogger logger, IUnitOfWork unitOfWork, - IMapper mapper, ILocalizationService localizationService) - { - _localizationService = localizationService; - _userManager = userManager; - _logger = logger; - _unitOfWork = unitOfWork; - _mapper = mapper; - } - - public async Task> ChangeUserPassword(AppUser user, string newPassword) - { - var passwordValidationIssues = (await ValidatePassword(user, newPassword)).ToList(); - if (passwordValidationIssues.Count != 0) return passwordValidationIssues; - - var result = await _userManager.RemovePasswordAsync(user); - if (!result.Succeeded) - { - _logger.LogError("Could not update password"); - return result.Errors.Select(e => new ApiException(400, e.Code, e.Description)); - } - - result = await _userManager.AddPasswordAsync(user, newPassword); - if (result.Succeeded) return []; - - _logger.LogError("Could not update password"); - return result.Errors.Select(e => new ApiException(400, e.Code, e.Description)); - } - - public async Task> ValidatePassword(AppUser user, string password) - { - foreach (var validator in _userManager.PasswordValidators) - { - var validationResult = await validator.ValidateAsync(_userManager, user, password); - if (!validationResult.Succeeded) - { - return validationResult.Errors.Select(e => new ApiException(400, e.Code, e.Description)); - } - } - - return Array.Empty(); - } - public async Task> ValidateUsername(string? username) - { - if (string.IsNullOrWhiteSpace(username) || !AllowedUsernameRegex.IsMatch(username)) - { - return [new ApiException(400, "Invalid username")]; - } - - // Reverted because of https://go.microsoft.com/fwlink/?linkid=2129535 - if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName != null - && x.NormalizedUserName == username.ToUpper())) - { - return - [ - new(400, "Username is already taken") - ]; - } - - return []; - } - - public async Task> ValidateEmail(string email) - { - var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email); - if (user == null) return []; - - return - [ - new ApiException(400, "Email is already registered") - ]; - } - - /// - /// Does the user have the Bookmark permission or admin rights - /// - /// - /// - public async Task HasBookmarkPermission(AppUser? user) - { - if (user == null) return false; - var roles = await _userManager.GetRolesAsync(user); - - return roles.Contains(PolicyConstants.BookmarkRole) || roles.Contains(PolicyConstants.AdminRole); - } - - /// - /// Does the user have the Download permission or admin rights - /// - /// - /// - public async Task HasDownloadPermission(AppUser? user) - { - if (user == null) return false; - var roles = await _userManager.GetRolesAsync(user); - - return roles.Contains(PolicyConstants.DownloadRole) || roles.Contains(PolicyConstants.AdminRole); - } - - /// - /// Does the user have Change Restriction permission or admin rights and not Read Only - /// - /// - /// - public async Task CanChangeAgeRestriction(AppUser? user) - { - if (user == null) return false; - - var roles = await _userManager.GetRolesAsync(user); - if (roles.Contains(PolicyConstants.ReadOnlyRole)) return false; - - return roles.Contains(PolicyConstants.ChangeRestrictionRole) || roles.Contains(PolicyConstants.AdminRole); - } - - public async Task ChangeIdentityProvider(int actingUserId, AppUser user, IdentityProvider identityProvider) - { - var defaultAdminUser = await _unitOfWork.UserRepository.GetDefaultAdminUser(); - if (user.Id == defaultAdminUser.Id) - { - if (identityProvider == IdentityProvider.OpenIdConnect) - { - throw new KavitaException(await _localizationService.Translate(actingUserId, "cannot-change-identity-provider-original-user")); - } - - return false; - } - - // Allow changes if users aren't being synced - var oidcSettings = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; - if (!oidcSettings.SyncUserSettings) - { - user.IdentityProvider = identityProvider; - await _unitOfWork.CommitAsync(); - return false; - } - - // Don't allow changes to the user if they're managed by oidc, and their identity provider isn't being changed to something else - if (user.IdentityProvider == IdentityProvider.OpenIdConnect && identityProvider == IdentityProvider.OpenIdConnect) - { - throw new KavitaException(await _localizationService.Translate(actingUserId, "oidc-managed")); - } - - user.IdentityProvider = identityProvider; - await _unitOfWork.CommitAsync(); - return user.IdentityProvider == IdentityProvider.OpenIdConnect; - } - - public async Task UpdateLibrariesForUser(AppUser user, IList librariesIds, bool hasAdminRole) - { - var allLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync(LibraryIncludes.AppUser)).ToList(); - var currentLibrary = allLibraries.Where(l => l.AppUsers.Contains(user)).ToList(); - - List libraries; - if (hasAdminRole) - { - _logger.LogDebug("{UserId} is admin. Granting access to all libraries", user.Id); - libraries = allLibraries; - } - else - { - libraries = allLibraries.Where(lib => librariesIds.Contains(lib.Id)).ToList(); - } - - var toRemove = currentLibrary.Except(libraries); - var toAdd = libraries.Except(currentLibrary); - - foreach (var lib in toRemove) - { - lib.AppUsers ??= []; - lib.AppUsers.Remove(user); - user.RemoveSideNavFromLibrary(lib); - } - - foreach (var lib in toAdd) - { - lib.AppUsers ??= []; - lib.AppUsers.Add(user); - user.CreateSideNavFromLibrary(lib); - } - } - - public async Task> UpdateRolesForUser(AppUser user, IList roles) - { - var existingRoles = await _userManager.GetRolesAsync(user); - var hasAdminRole = roles.Contains(PolicyConstants.AdminRole); - if (!hasAdminRole) - { - roles.Add(PolicyConstants.PlebRole); - } - - if (existingRoles.Except(roles).Any() || roles.Except(existingRoles).Any()) - { - var roleResult = await _userManager.RemoveFromRolesAsync(user, existingRoles); - if (!roleResult.Succeeded) return roleResult.Errors; - - roleResult = await _userManager.AddToRolesAsync(user, roles); - if (!roleResult.Succeeded) return roleResult.Errors; - } - - return []; - } - - public async Task SeedUser(AppUser user) - { - AddDefaultStreamsToUser(user); - AddDefaultHighlightSlotsToUser(user); - AddAuthKeys(user); - await AddDefaultReadingProfileToUser(user); // Commits - } - - /// - /// Assign default streams - /// - /// - public void AddDefaultStreamsToUser(AppUser user) - { - foreach (var newStream in Seed.DefaultStreams.Select(_mapper.Map)) - { - user.DashboardStreams.Add(newStream); - } - - foreach (var stream in Seed.DefaultSideNavStreams.Select(_mapper.Map)) - { - user.SideNavStreams.Add(stream); - } - } - - private void AddDefaultHighlightSlotsToUser(AppUser user) - { - if (user.UserPreferences.BookReaderHighlightSlots.Any()) return; - - user.UserPreferences.BookReaderHighlightSlots = Seed.DefaultHighlightSlots.ToList(); - _unitOfWork.UserRepository.Update(user); - } - - private void AddAuthKeys(AppUser user) - { - if (user.AuthKeys.Any()) return; - - user.AuthKeys = Seed.CreateDefaultAuthKeys(); - _unitOfWork.UserRepository.Update(user); - } - - /// - /// Assign default reading profile - /// - /// - public async Task AddDefaultReadingProfileToUser(AppUser user) - { - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithName("Default Profile") - .WithKind(ReadingProfileKind.Default) - .Build(); - _unitOfWork.AppUserReadingProfileRepository.Add(profile); - await _unitOfWork.CommitAsync(); - } - - [GeneratedRegex(@"^[a-zA-Z0-9\-._@+/]*$")] - private static partial Regex AllowedUsernameRegexAttr(); -} diff --git a/API/Services/AuthKeyService.cs b/API/Services/AuthKeyService.cs deleted file mode 100644 index 4323738da..000000000 --- a/API/Services/AuthKeyService.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -public interface IAuthKeyService -{ - Task UpdateLastAccessedAsync(string authKey); -} - -public class AuthKeyService(DataContext context, ILogger logger) : IAuthKeyService -{ - public async Task UpdateLastAccessedAsync(string authKey) - { - logger.LogTrace("Updating last accessed Auth key: {AuthKey}", authKey); - await context.AppUserAuthKey - .Where(k => k.Key == authKey) - .ExecuteUpdateAsync(s => s.SetProperty(k => k.LastAccessedAtUtc, DateTime.UtcNow)); - } -} diff --git a/API/Services/Caching/AuthKeyCacheInvalidator.cs b/API/Services/Caching/AuthKeyCacheInvalidator.cs deleted file mode 100644 index 45870b4cc..000000000 --- a/API/Services/Caching/AuthKeyCacheInvalidator.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using API.Middleware; -using Microsoft.Extensions.Caching.Hybrid; - -namespace API.Services.Caching; - -public interface IAuthKeyCacheInvalidator -{ - /// - /// Invalidates the cached authentication data for a specific auth key. - /// Call this when a key is rotated or deleted. - /// - /// The actual key value (not the ID) - /// Cancellation token - Task InvalidateAsync(string keyValue, CancellationToken cancellationToken = default); -} - -public class AuthKeyCacheInvalidator(HybridCache cache) : IAuthKeyCacheInvalidator -{ - public async Task InvalidateAsync(string keyValue, CancellationToken cancellationToken = default) - { - var cacheKey = AuthKeyAuthenticationHandler.CreateCacheKey(keyValue); - await cache.RemoveAsync(cacheKey, cancellationToken); - } -} diff --git a/API/Services/CollectionTagService.cs b/API/Services/CollectionTagService.cs deleted file mode 100644 index a598c1a47..000000000 --- a/API/Services/CollectionTagService.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Collection; -using API.Entities; -using API.Extensions; -using API.Services.Plus; -using API.SignalR; -using Kavita.Common; - -namespace API.Services; -#nullable enable - -public interface ICollectionTagService -{ - Task DeleteTag(int tagId, AppUser user); - Task UpdateTag(AppUserCollectionDto dto, int userId); - Task RemoveTagFromSeries(AppUserCollection? tag, IEnumerable seriesIds); -} - - -public class CollectionTagService : ICollectionTagService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - - public CollectionTagService(IUnitOfWork unitOfWork, IEventHub eventHub) - { - _unitOfWork = unitOfWork; - _eventHub = eventHub; - } - - public async Task DeleteTag(int tagId, AppUser user) - { - var collectionTag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(tagId); - if (collectionTag == null) return true; - - user.Collections.Remove(collectionTag); - - if (!_unitOfWork.HasChanges()) return true; - - return await _unitOfWork.CommitAsync(); - } - - - public async Task UpdateTag(AppUserCollectionDto dto, int userId) - { - var existingTag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(dto.Id); - if (existingTag == null) throw new KavitaException("collection-doesnt-exist"); - if (existingTag.AppUserId != userId) throw new KavitaException("access-denied"); - - var title = dto.Title.Trim(); - if (string.IsNullOrEmpty(title)) throw new KavitaException("collection-tag-title-required"); - - // Ensure the title doesn't exist on the user's account already - if (!title.Equals(existingTag.Title) && await _unitOfWork.CollectionTagRepository.CollectionExists(dto.Title, userId)) - throw new KavitaException("collection-tag-duplicate"); - - existingTag.Items ??= []; - if (existingTag.Source == ScrobbleProvider.Kavita) - { - existingTag.Title = title; - existingTag.NormalizedTitle = dto.Title.ToNormalized(); - } - - var roles = await _unitOfWork.UserRepository.GetRoles(userId); - if (roles.Contains(PolicyConstants.AdminRole) || roles.Contains(PolicyConstants.PromoteRole)) - { - existingTag.Promoted = dto.Promoted; - } - existingTag.CoverImageLocked = dto.CoverImageLocked; - _unitOfWork.CollectionTagRepository.Update(existingTag); - - // Check if Tag has updated (Summary) - var summary = (dto.Summary ?? string.Empty).Trim(); - if (existingTag.Summary == null || !existingTag.Summary.Equals(summary)) - { - existingTag.Summary = summary; - _unitOfWork.CollectionTagRepository.Update(existingTag); - } - - // If we unlock the cover image it means reset - if (!dto.CoverImageLocked) - { - existingTag.CoverImageLocked = false; - existingTag.CoverImage = string.Empty; - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(existingTag.Id, MessageFactoryEntityTypes.Collection), false); - _unitOfWork.CollectionTagRepository.Update(existingTag); - } - - if (!_unitOfWork.HasChanges()) return true; - return await _unitOfWork.CommitAsync(); - } - - /// - /// Removes series from Collection tag. Will recalculate max age rating. - /// - /// - /// - /// - public async Task RemoveTagFromSeries(AppUserCollection? tag, IEnumerable seriesIds) - { - if (tag == null) return false; - - tag.Items ??= []; - tag.Items = tag.Items.Where(s => !seriesIds.Contains(s.Id)).ToList(); - - if (tag.Items.Count == 0) - { - _unitOfWork.CollectionTagRepository.Remove(tag); - } - - if (!_unitOfWork.HasChanges()) return true; - - var result = await _unitOfWork.CommitAsync(); - if (tag.Items.Count > 0) - { - await _unitOfWork.CollectionTagRepository.UpdateCollectionAgeRating(tag); - } - - return result; - } -} diff --git a/API/Services/DeviceService.cs b/API/Services/DeviceService.cs deleted file mode 100644 index 39bc3d890..000000000 --- a/API/Services/DeviceService.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.DTOs.Device; -using API.DTOs.Device.EmailDevice; -using API.DTOs.Email; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.Device; -using API.Helpers.Builders; -using Kavita.Common; -using Microsoft.Extensions.Logging; - -namespace API.Services; -#nullable enable - -public interface IDeviceService -{ - Task Create(CreateEmailDeviceDto dto, AppUser userWithDevices); - Task Update(UpdateEmailDeviceDto dto, AppUser userWithDevices); - Task Delete(AppUser userWithDevices, int deviceId); - Task SendTo(IReadOnlyList chapterIds, int deviceId); -} - -public class DeviceService : IDeviceService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IEmailService _emailService; - private readonly IReadingProfileService _readingProfileService; - - public DeviceService(IUnitOfWork unitOfWork, ILogger logger, IEmailService emailService, IReadingProfileService readingProfileService) - { - _unitOfWork = unitOfWork; - _logger = logger; - _emailService = emailService; - _readingProfileService = readingProfileService; - } - - public async Task Create(CreateEmailDeviceDto dto, AppUser userWithDevices) - { - try - { - userWithDevices.Devices ??= new List(); - var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Name!.Equals(dto.Name)); - if (existingDevice != null) throw new KavitaException("device-duplicate"); - - existingDevice = new DeviceBuilder(dto.Name) - .WithPlatform(dto.Platform) - .WithEmail(dto.EmailAddress) - .Build(); - - - userWithDevices.Devices.Add(existingDevice); - _unitOfWork.UserRepository.Update(userWithDevices); - - if (!_unitOfWork.HasChanges()) return existingDevice; - if (await _unitOfWork.CommitAsync()) return existingDevice; - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an error when creating your device"); - await _unitOfWork.RollbackAsync(); - } - - return null; - } - - public async Task Update(UpdateEmailDeviceDto dto, AppUser userWithDevices) - { - try - { - var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Id == dto.Id); - if (existingDevice == null) throw new KavitaException("device-not-created"); - - existingDevice.Name = dto.Name; - existingDevice.Platform = dto.Platform; - existingDevice.EmailAddress = dto.EmailAddress; - - if (!_unitOfWork.HasChanges()) return existingDevice; - if (await _unitOfWork.CommitAsync()) return existingDevice; - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an error when updating your device"); - await _unitOfWork.RollbackAsync(); - } - - return null; - } - - public async Task Delete(AppUser userWithDevices, int deviceId) - { - try - { - userWithDevices.Devices = userWithDevices.Devices.Where(d => d.Id != deviceId).ToList(); - _unitOfWork.UserRepository.Update(userWithDevices); - - await _readingProfileService.RemoveDeviceLinks(userWithDevices.Id, deviceId); - - if (!_unitOfWork.HasChanges()) return true; - if (await _unitOfWork.CommitAsync()) return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue with deleting the device, {DeviceId} for user {UserName}", deviceId, userWithDevices.UserName); - } - - return false; - } - - public async Task SendTo(IReadOnlyList chapterIds, int deviceId) - { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (!settings.IsEmailSetupForSendToDevice()) - throw new KavitaException("send-to-kavita-email"); - - var device = await _unitOfWork.DeviceRepository.GetDeviceById(deviceId); - if (device == null) throw new KavitaException("device-doesnt-exist"); - - var files = await _unitOfWork.ChapterRepository.GetFilesForChaptersAsync(chapterIds); - if (files.Any(f => f.Format is not (MangaFormat.Epub or MangaFormat.Pdf)) && device.Platform == EmailDevicePlatform.Kindle) - throw new KavitaException("send-to-permission"); - - // If the size of the files is too big - if (files.Sum(f => f.Bytes) >= settings.SmtpConfig.SizeLimit) - throw new KavitaException("send-to-size-limit"); - - - try - { - device.UpdateLastUsed(); - _unitOfWork.DeviceRepository.Update(device); - await _unitOfWork.CommitAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue updating device last used time"); - } - - var success = await _emailService.SendFilesToEmail(new SendToDto() - { - DestinationEmail = device.EmailAddress!, - FilePaths = files.Select(m => m.FilePath) - }); - - return success; - } -} diff --git a/API/Services/KoreaderService.cs b/API/Services/KoreaderService.cs deleted file mode 100644 index 4c5f82552..000000000 --- a/API/Services/KoreaderService.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Threading.Tasks; -using API.Data; -using API.DTOs.Koreader; -using API.DTOs.Progress; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Reading; -using Kavita.Common; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -#nullable enable - -public interface IKoreaderService -{ - Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId); - Task GetProgress(string bookHash, int userId); -} - -public class KoreaderService : IKoreaderService -{ - private readonly IReaderService _readerService; - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly ILogger _logger; - - public KoreaderService(IReaderService readerService, IUnitOfWork unitOfWork, ILocalizationService localizationService, ILogger logger) - { - _readerService = readerService; - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _logger = logger; - } - - /// - /// Given a Koreader hash, locate the underlying file and generate/update a progress event. - /// - /// - /// - public async Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId) - { - _logger.LogDebug("Saving Koreader progress for User ({UserId}): {KoreaderProgress}", userId, koreaderBookDto.progress.Sanitize()); - var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(koreaderBookDto.document); - if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing")); - - var userProgressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId); - if (userProgressDto == null) - { - var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(file.ChapterId, userId); - if (chapterDto == null) throw new KavitaException(await _localizationService.Translate(userId, "chapter-doesnt-exist")); - - var volumeDto = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapterDto.VolumeId); - if (volumeDto == null) throw new KavitaException(await _localizationService.Translate(userId, "volume-doesnt-exist")); - - var seriesDto = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(volumeDto.SeriesId, userId); - if (seriesDto == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - - userProgressDto = new ProgressDto() - { - PageNum = 0, // This is updated in KoreaderHelper.UpdateProgressDto - ChapterId = file.ChapterId, - VolumeId = chapterDto.VolumeId, - SeriesId = seriesDto.Id, - LibraryId = seriesDto.LibraryId - }; - } - - // Update the bookScrollId if possible - var reportedProgress = koreaderBookDto.progress; - KoreaderHelper.UpdateProgressDto(userProgressDto, koreaderBookDto.progress); - - _logger.LogDebug("Converted KOReader progress from {ProgressEncoding} to Page {PageNum} with ScrollId: {ScrollId}", reportedProgress.Sanitize(), - userProgressDto.PageNum, userProgressDto.BookScrollId?.Sanitize() ?? string.Empty); - - // Normal saving from kavita will be //body/h2[1] - await _readerService.SaveReadingProgress(userProgressDto, userId); - } - - /// - /// Returns a Koreader Dto representing current book and the progress within - /// - /// - /// - /// - public async Task GetProgress(string bookHash, int userId) - { - var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - - var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(bookHash); - if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing")); - - var progressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId); - - // Non-epubs use the pageNum as the progress. KOReader is 1-index based - var koreaderProgress = $"{progressDto?.PageNum + 1 ?? 0}"; - if (file.Format == MangaFormat.Epub) - { - koreaderProgress = KoreaderHelper.GetKoreaderPosition(progressDto); - } - - var response = new KoreaderBookDtoBuilder(bookHash) - .WithProgress(koreaderProgress) - .WithPercentage(progressDto?.PageNum, file.Pages) - .WithDeviceId(settingsDto.InstallId, userId) - .WithTimestamp(progressDto?.LastModifiedUtc) - .Build(); - - _logger.LogDebug("Responding to KOReader with Page {PageNum}, Scroll Id: {ScrollId}, and Progress: {Progress}", - progressDto?.PageNum, response.progress.Sanitize(), response.percentage); - - - return response; - } -} diff --git a/API/Services/MediaConversionService.cs b/API/Services/MediaConversionService.cs deleted file mode 100644 index 4220b065e..000000000 --- a/API/Services/MediaConversionService.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Comparators; -using API.Data; -using API.Entities.Enums; -using API.Extensions; -using API.SignalR; -using Hangfire; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -public interface IMediaConversionService -{ - [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] - Task ConvertAllBookmarkToEncoding(); - [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] - Task ConvertAllCoversToEncoding(); - [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] - Task ConvertAllManagedMediaToEncodingFormat(); - - Task SaveAsEncodingFormat(string imageDirectory, string filename, string targetFolder, - EncodeFormat encodeFormat); -} - -public class MediaConversionService : IMediaConversionService -{ - public const string Name = "MediaConversionService"; - public static readonly string[] ConversionMethods = ["ConvertAllBookmarkToEncoding", "ConvertAllCoversToEncoding", "ConvertAllManagedMediaToEncodingFormat"]; - private readonly IUnitOfWork _unitOfWork; - private readonly IImageService _imageService; - private readonly IEventHub _eventHub; - private readonly IDirectoryService _directoryService; - private readonly ILogger _logger; - - public MediaConversionService(IUnitOfWork unitOfWork, IImageService imageService, IEventHub eventHub, - IDirectoryService directoryService, ILogger logger) - { - _unitOfWork = unitOfWork; - _imageService = imageService; - _eventHub = eventHub; - _directoryService = directoryService; - _logger = logger; - } - - /// - /// Converts all Kavita managed media (bookmarks, covers, favicons, etc) to the saved target encoding. - /// Do not invoke anyway except via Hangfire. - /// - /// This is a long-running job - /// - [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] - public async Task ConvertAllManagedMediaToEncodingFormat() - { - await ConvertAllBookmarkToEncoding(); - await ConvertAllCoversToEncoding(); - await CoverAllFaviconsToEncoding(); - - } - - /// - /// This is a long-running job that will convert all bookmarks into a format that is not PNG. Do not invoke anyway except via Hangfire. - /// - [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] - public async Task ConvertAllBookmarkToEncoding() - { - var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; - var encodeFormat = - (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; - - if (encodeFormat == EncodeFormat.PNG) - { - _logger.LogError("Cannot convert media to PNG"); - return; - } - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started)); - var bookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync()) - .Where(b => !b.FileName.EndsWith(encodeFormat.GetExtension())).ToList(); - - var count = 1F; - foreach (var bookmark in bookmarks) - { - bookmark.FileName = await SaveAsEncodingFormat(bookmarkDirectory, bookmark.FileName, - BookmarkService.BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId), encodeFormat); - _unitOfWork.UserRepository.Update(bookmark); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertBookmarksProgressEvent(count / bookmarks.Count, ProgressEventType.Updated)); - count++; - } - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended)); - - _logger.LogInformation("[MediaConversionService] Converted bookmarks to {Format}", encodeFormat); - } - - /// - /// This is a long-running job that will convert all covers into WebP. Do not invoke anyway except via Hangfire. - /// - [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] - public async Task ConvertAllCoversToEncoding() - { - var coverDirectory = _directoryService.CoverImageDirectory; - var encodeFormat = - (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; - - if (encodeFormat == EncodeFormat.PNG) - { - _logger.LogError("Cannot convert media to PNG"); - return; - } - - _logger.LogInformation("[MediaConversionService] Starting conversion of all covers to {Format}", encodeFormat); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(0F, ProgressEventType.Started)); - - var chapterCovers = await _unitOfWork.ChapterRepository.GetAllChaptersWithCoversInDifferentEncoding(encodeFormat); - var customSeriesCovers = await _unitOfWork.SeriesRepository.GetAllWithCoversInDifferentEncoding(encodeFormat); - var seriesCovers = await _unitOfWork.SeriesRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, false); - var nonCustomOrConvertedVolumeCovers = await _unitOfWork.VolumeRepository.GetAllWithCoversInDifferentEncoding(encodeFormat); - - var readingListCovers = await _unitOfWork.ReadingListRepository.GetAllWithCoversInDifferentEncoding(encodeFormat); - var libraryCovers = await _unitOfWork.LibraryRepository.GetAllWithCoversInDifferentEncoding(encodeFormat); - var collectionCovers = await _unitOfWork.CollectionTagRepository.GetAllWithCoversInDifferentEncoding(encodeFormat); - - var totalCount = chapterCovers.Count + seriesCovers.Count + readingListCovers.Count + - libraryCovers.Count + collectionCovers.Count + nonCustomOrConvertedVolumeCovers.Count + customSeriesCovers.Count; - - var count = 1F; - _logger.LogInformation("[MediaConversionService] Starting conversion of chapters"); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(0, ProgressEventType.Started)); - _logger.LogInformation("[MediaConversionService] Starting conversion of libraries"); - foreach (var library in libraryCovers) - { - if (string.IsNullOrEmpty(library.CoverImage)) continue; - - var newFile = await SaveAsEncodingFormat(coverDirectory, library.CoverImage, coverDirectory, encodeFormat); - library.CoverImage = Path.GetFileName(newFile); - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated)); - count++; - } - - _logger.LogInformation("[MediaConversionService] Starting conversion of reading lists"); - foreach (var readingList in readingListCovers) - { - if (string.IsNullOrEmpty(readingList.CoverImage)) continue; - - var newFile = await SaveAsEncodingFormat(coverDirectory, readingList.CoverImage, coverDirectory, encodeFormat); - readingList.CoverImage = Path.GetFileName(newFile); - _unitOfWork.ReadingListRepository.Update(readingList); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated)); - count++; - } - - _logger.LogInformation("[MediaConversionService] Starting conversion of collections"); - foreach (var collection in collectionCovers) - { - if (string.IsNullOrEmpty(collection.CoverImage)) continue; - - var newFile = await SaveAsEncodingFormat(coverDirectory, collection.CoverImage, coverDirectory, encodeFormat); - collection.CoverImage = Path.GetFileName(newFile); - _unitOfWork.CollectionTagRepository.Update(collection); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated)); - count++; - } - - _logger.LogInformation("[MediaConversionService] Starting conversion of chapters"); - foreach (var chapter in chapterCovers) - { - if (string.IsNullOrEmpty(chapter.CoverImage)) continue; - - var newFile = await SaveAsEncodingFormat(coverDirectory, chapter.CoverImage, coverDirectory, encodeFormat); - chapter.CoverImage = Path.GetFileName(newFile); - _unitOfWork.ChapterRepository.Update(chapter); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated)); - count++; - } - - // Now null out all series and volumes that aren't webp or custom - _logger.LogInformation("[MediaConversionService] Starting conversion of volumes"); - foreach (var volume in nonCustomOrConvertedVolumeCovers) - { - if (string.IsNullOrEmpty(volume.CoverImage)) continue; - volume.CoverImage = volume.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; - _unitOfWork.VolumeRepository.Update(volume); - await _unitOfWork.CommitAsync(); - } - - _logger.LogInformation("[MediaConversionService] Starting conversion of series"); - foreach (var series in customSeriesCovers) - { - if (string.IsNullOrEmpty(series.CoverImage)) continue; - - var newFile = await SaveAsEncodingFormat(coverDirectory, series.CoverImage, coverDirectory, encodeFormat); - series.CoverImage = string.IsNullOrEmpty(newFile) ? - series.CoverImage.Replace(Path.GetExtension(series.CoverImage), encodeFormat.GetExtension()) : Path.GetFileName(newFile); - - _unitOfWork.SeriesRepository.Update(series); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated)); - count++; - } - - foreach (var series in seriesCovers) - { - if (string.IsNullOrEmpty(series.CoverImage)) continue; - series.CoverImage = series.GetCoverImage(); - if (series.CoverImage == null) - { - _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); - } - _unitOfWork.SeriesRepository.Update(series); - await _unitOfWork.CommitAsync(); - } - - // Get all volumes and remap their covers - - // Get all series and remap their covers - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(1F, ProgressEventType.Ended)); - - _logger.LogInformation("[MediaConversionService] Converted covers to {Format}", encodeFormat); - } - - private async Task CoverAllFaviconsToEncoding() - { - var encodeFormat = - (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; - - if (encodeFormat == EncodeFormat.PNG) - { - _logger.LogError("Cannot convert media to PNG"); - return; - } - - _logger.LogInformation("[MediaConversionService] Starting conversion of favicons to {Format}", encodeFormat); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started)); - var pngFavicons = _directoryService.GetFiles(_directoryService.FaviconDirectory) - .Where(b => !b.EndsWith(encodeFormat.GetExtension())). - ToList(); - - var count = 1F; - foreach (var file in pngFavicons) - { - await SaveAsEncodingFormat(_directoryService.FaviconDirectory, _directoryService.FileSystem.FileInfo.New(file).Name, _directoryService.FaviconDirectory, - encodeFormat); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertBookmarksProgressEvent(count / pngFavicons.Count, ProgressEventType.Updated)); - count++; - } - - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended)); - - _logger.LogInformation("[MediaConversionService] Converted favicons to {Format}", encodeFormat); - } - - - /// - /// Converts an image file, deletes original and returns the new path back - /// - /// Full Path to where files are stored - /// The file to convert - /// Full path to where files should be stored or any stem - /// Encoding Format - /// - public async Task SaveAsEncodingFormat(string imageDirectory, string filename, string targetFolder, EncodeFormat encodeFormat) - { - // This must be Public as it's used in via Hangfire as a background task - var fullSourcePath = _directoryService.FileSystem.Path.Join(imageDirectory, filename); - var fullTargetDirectory = fullSourcePath.Replace(new FileInfo(filename).Name, string.Empty); - - var newFilename = string.Empty; - _logger.LogDebug("Converting {Source} image into {Encoding} at {Target}", fullSourcePath, encodeFormat, fullTargetDirectory); - - if (!File.Exists(fullSourcePath)) - { - _logger.LogError("Requested to convert {File} but it doesn't exist", fullSourcePath); - return newFilename; - } - - try - { - // Convert target file to format then delete original target file - try - { - var targetFile = await _imageService.ConvertToEncodingFormat(fullSourcePath, fullTargetDirectory, encodeFormat); - var targetName = new FileInfo(targetFile).Name; - newFilename = Path.Join(targetFolder, targetName); - _directoryService.DeleteFiles(new[] {fullSourcePath}); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not convert image {FilePath} to {Format}", filename, encodeFormat); - newFilename = filename; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not convert image to {Format}", encodeFormat); - } - - return newFilename; - } - -} diff --git a/API/Services/MediaErrorService.cs b/API/Services/MediaErrorService.cs deleted file mode 100644 index 2c0a1df68..000000000 --- a/API/Services/MediaErrorService.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Threading.Tasks; -using API.Data; -using API.Helpers.Builders; -using Hangfire; - -namespace API.Services; - -public enum MediaErrorProducer -{ - BookService = 0, - ArchiveService = 1 - -} - -public interface IMediaErrorService -{ - void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, string details); - void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, Exception ex); - Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, string details); - Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, Exception ex); -} - -public class MediaErrorService : IMediaErrorService -{ - private readonly IUnitOfWork _unitOfWork; - - public MediaErrorService(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - - - public void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, Exception ex) - { - // TODO: Localize all these messages - // To avoid overhead on commits, do async. We don't need to wait. - BackgroundJob.Enqueue(() => ReportMediaIssueAsync(filename, producer, errorMessage, ex.Message)); - } - - public void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, string details) - { - // To avoid overhead on commits, do async. We don't need to wait. - BackgroundJob.Enqueue(() => ReportMediaIssueAsync(filename, producer, errorMessage, details)); - } - - public async Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, Exception ex) - { - await ReportMediaIssueAsync(filename, producer, errorMessage, ex.Message); - } - - public async Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, string details) - { - var error = new MediaErrorBuilder(filename) - .WithComment(errorMessage) - .WithDetails(details) - .Build(); - - if (await _unitOfWork.MediaErrorRepository.ExistsAsync(error)) - { - return; - } - - - _unitOfWork.MediaErrorRepository.Attach(error); - await _unitOfWork.CommitAsync(); - } - -} diff --git a/API/Services/RatingService.cs b/API/Services/RatingService.cs deleted file mode 100644 index f0ec485bd..000000000 --- a/API/Services/RatingService.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using API.Data; -using API.DTOs; -using API.Entities; -using API.Entities.User; -using API.Services.Plus; -using Hangfire; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -public interface IRatingService -{ - /// - /// Updates the users' rating for a given series - /// - /// Should include ratings - /// - /// - Task UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto); - - /// - /// Updates the users' rating for a given chapter - /// - /// Should include ratings - /// chapterId must be set - /// - Task UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto); -} - -public class RatingService: IRatingService -{ - - private readonly IUnitOfWork _unitOfWork; - private readonly IScrobblingService _scrobblingService; - private readonly ILogger _logger; - - public RatingService(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, ILogger logger) - { - _unitOfWork = unitOfWork; - _scrobblingService = scrobblingService; - _logger = logger; - } - - public async Task UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto) - { - var userRating = - await _unitOfWork.UserRepository.GetUserRatingAsync(updateRatingDto.SeriesId, user.Id) ?? - new AppUserRating(); - - try - { - userRating.Rating = Math.Clamp(updateRatingDto.UserRating, 0f, 5f); - userRating.HasBeenRated = true; - userRating.SeriesId = updateRatingDto.SeriesId; - - if (userRating.Id == 0) - { - user.Ratings ??= new List(); - user.Ratings.Add(userRating); - } - - _unitOfWork.UserRepository.Update(user); - - if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) - { - BackgroundJob.Enqueue(() => - _scrobblingService.ScrobbleRatingUpdate(user.Id, updateRatingDto.SeriesId, - userRating.Rating)); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception saving rating"); - } - - await _unitOfWork.RollbackAsync(); - user.Ratings?.Remove(userRating); - - return false; - } - - public async Task UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto) - { - if (updateRatingDto.ChapterId == null) - { - return false; - } - - var userRating = - await _unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, updateRatingDto.ChapterId.Value) ?? - new AppUserChapterRating(); - - try - { - userRating.Rating = Math.Clamp(updateRatingDto.UserRating, 0f, 5f); - userRating.HasBeenRated = true; - userRating.SeriesId = updateRatingDto.SeriesId; - userRating.ChapterId = updateRatingDto.ChapterId.Value; - - if (userRating.Id == 0) - { - user.ChapterRatings ??= new List(); - user.ChapterRatings.Add(userRating); - } - - _unitOfWork.UserRepository.Update(user); - - await _unitOfWork.CommitAsync(); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception saving rating"); - } - - await _unitOfWork.RollbackAsync(); - user.ChapterRatings?.Remove(userRating); - - return false; - } - -} diff --git a/API/Services/Store/UserContext.cs b/API/Services/Store/UserContext.cs deleted file mode 100644 index 27650d063..000000000 --- a/API/Services/Store/UserContext.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.Entities.Progress; -using Kavita.Common; - -namespace API.Services.Store; -#nullable enable - -public interface IUserContext -{ - /// - /// Gets the current authenticated user's ID. - /// Returns null if user is not authenticated or on [AllowAnonymous] endpoint. - /// - int? GetUserId(); - - /// - /// Gets the current authenticated user's ID. - /// Throws KavitaException if user is not authenticated. - /// - int GetUserIdOrThrow(); - - /// - /// Gets the current authenticated user's username. - /// Returns null if user is not authenticated. - /// - /// Warning! Username's can contain .. and /, do not use folders or filenames explicitly with the Username - string? GetUsername(); - /// - /// The Roles associated with the Authenticated user - /// - IReadOnlyList Roles { get; } - /// - /// Returns true if the current user is authenticated. - /// - bool IsAuthenticated { get; } - /// - /// Gets the authentication method used (JWT, Auth Key, OIDC). - /// - AuthenticationType GetAuthenticationType(); - - - bool HasRole(string role); - bool HasAnyRole(params string[] roles); - bool HasAllRoles(params string[] roles); -} - -public class UserContext : IUserContext -{ - private int? _userId; - private string? _username; - private AuthenticationType _authType; - private List _roles = new(); - - public int? GetUserId() => _userId; - - public int GetUserIdOrThrow() - { - // TODO: Refactor this to use ProblemDetails and handle appropriately - return _userId ?? throw new KavitaException("User is not authenticated"); - } - - public string? GetUsername() => _username; - - public AuthenticationType GetAuthenticationType() => _authType; - - public bool IsAuthenticated { get; private set; } - public IReadOnlyList Roles => _roles.AsReadOnly(); - - // Internal method used by middleware to set context - internal void SetUserContext(int userId, string username, AuthenticationType authType, IEnumerable roles) - { - _userId = userId; - _username = username; - _authType = authType; - IsAuthenticated = true; - _roles = roles?.ToList() ?? []; - } - - internal void Clear() - { - _userId = null; - _username = null; - _authType = AuthenticationType.Unknown; - IsAuthenticated = false; - _roles.Clear(); - } - - public bool HasRole(string role) - { - return _roles.Any(r => r.Equals(role, StringComparison.OrdinalIgnoreCase)); - } - - public bool HasAnyRole(params string[] roles) - { - return roles.Any(HasRole); - } - - public bool HasAllRoles(params string[] roles) - { - return roles.All(HasRole); - } -} diff --git a/API/Services/StreamService.cs b/API/Services/StreamService.cs deleted file mode 100644 index 068143df6..000000000 --- a/API/Services/StreamService.cs +++ /dev/null @@ -1,435 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Dashboard; -using API.DTOs.SideNav; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.SignalR; -using Kavita.Common; -using Kavita.Common.Helpers; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -/// -/// For SideNavStream and DashboardStream manipulation -/// -public interface IStreamService -{ - Task> GetDashboardStreams(int userId, bool visibleOnly = true); - Task> GetSidenavStreams(int userId, bool visibleOnly = true); - Task> GetExternalSources(int userId); - Task CreateDashboardStreamFromSmartFilter(int userId, int smartFilterId); - Task UpdateDashboardStream(int userId, DashboardStreamDto dto); - Task UpdateDashboardStreamPosition(int userId, UpdateStreamPositionDto dto); - Task UpdateSideNavStreamBulk(int userId, BulkUpdateSideNavStreamVisibilityDto dto); - Task CreateSideNavStreamFromSmartFilter(int userId, int smartFilterId); - Task CreateSideNavStreamFromExternalSource(int userId, int externalSourceId); - Task UpdateSideNavStream(int userId, SideNavStreamDto dto); - Task UpdateSideNavStreamPosition(int userId, UpdateStreamPositionDto dto); - Task CreateExternalSource(int userId, ExternalSourceDto dto); - Task UpdateExternalSource(int userId, ExternalSourceDto dto); - Task DeleteExternalSource(int userId, int externalSourceId); - Task DeleteSideNavSmartFilterStream(int userId, int sideNavStreamId); - Task DeleteDashboardSmartFilterStream(int userId, int dashboardStreamId); - Task RenameSmartFilterStreams(AppUserSmartFilter smartFilter); -} - -public class StreamService : IStreamService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - private readonly ILocalizationService _localizationService; - private readonly ILogger _logger; - - public StreamService(IUnitOfWork unitOfWork, IEventHub eventHub, ILocalizationService localizationService, ILogger logger) - { - _unitOfWork = unitOfWork; - _eventHub = eventHub; - _localizationService = localizationService; - _logger = logger; - } - - public async Task> GetDashboardStreams(int userId, bool visibleOnly = true) - { - return await _unitOfWork.UserRepository.GetDashboardStreams(userId, visibleOnly); - } - - public async Task> GetSidenavStreams(int userId, bool visibleOnly = true) - { - return await _unitOfWork.UserRepository.GetSideNavStreams(userId, visibleOnly); - } - - public async Task> GetExternalSources(int userId) - { - return await _unitOfWork.AppUserExternalSourceRepository.GetExternalSources(userId); - } - - public async Task CreateDashboardStreamFromSmartFilter(int userId, int smartFilterId) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.DashboardStreams); - if (user == null) throw new KavitaException(await _localizationService.Translate(userId, "no-user")); - - var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId); - if (smartFilter == null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist")); - - var stream = user.DashboardStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); - if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-already-in-use")); - - var maxOrder = user!.DashboardStreams.Max(d => d.Order); - var createdStream = new AppUserDashboardStream() - { - Name = smartFilter.Name, - IsProvided = false, - StreamType = DashboardStreamType.SmartFilter, - Visible = true, - Order = maxOrder + 1, - SmartFilter = smartFilter - }; - - user.DashboardStreams.Add(createdStream); - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - - var ret = new DashboardStreamDto() - { - Id = createdStream.Id, - Name = createdStream.Name, - IsProvided = createdStream.IsProvided, - Visible = createdStream.Visible, - Order = createdStream.Order, - SmartFilterEncoded = smartFilter.Filter, - StreamType = createdStream.StreamType - }; - - await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id), - userId); - - return ret; - } - - public async Task UpdateDashboardStream(int userId, DashboardStreamDto dto) - { - var stream = await _unitOfWork.UserRepository.GetDashboardStream(dto.Id); - if (stream == null) throw new KavitaException(await _localizationService.Translate(userId, "dashboard-stream-doesnt-exist")); - stream.Visible = dto.Visible; - - _unitOfWork.UserRepository.Update(stream); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(userId), - userId); - } - - public async Task UpdateDashboardStreamPosition(int userId, UpdateStreamPositionDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, - AppUserIncludes.DashboardStreams); - var stream = user?.DashboardStreams.FirstOrDefault(d => d.Id == dto.Id); - if (stream == null) - { - throw new KavitaException(await _localizationService.Translate(userId, "dashboard-stream-doesnt-exist")); - } - - if (stream.Order == dto.ToPosition) return; - - var list = user!.DashboardStreams.OrderBy(s => s.Order).ToList(); - OrderableHelper.ReorderItems(list, stream.Id, dto.ToPosition); - user.DashboardStreams = list; - - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - if (!stream.Visible) return; - await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id), - user.Id); - } - - public async Task UpdateSideNavStreamBulk(int userId, BulkUpdateSideNavStreamVisibilityDto dto) - { - var streams = await _unitOfWork.UserRepository.GetDashboardStreamsByIds(dto.Ids); - foreach (var stream in streams) - { - stream.Visible = dto.Visibility; - _unitOfWork.UserRepository.Update(stream); - } - - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), - userId); - } - - public async Task CreateSideNavStreamFromSmartFilter(int userId, int smartFilterId) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.SideNavStreams); - if (user == null) throw new KavitaException(await _localizationService.Translate(userId, "no-user")); - - var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId); - if (smartFilter == null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist")); - - var stream = user.SideNavStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); - if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-already-in-use")); - - var maxOrder = user!.SideNavStreams.Max(d => d.Order); - var createdStream = new AppUserSideNavStream() - { - Name = smartFilter.Name, - IsProvided = false, - StreamType = SideNavStreamType.SmartFilter, - Visible = true, - Order = maxOrder + 1, - SmartFilter = smartFilter - }; - - user.SideNavStreams.Add(createdStream); - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - - var ret = new SideNavStreamDto() - { - Id = createdStream.Id, - Name = createdStream.Name, - IsProvided = createdStream.IsProvided, - Visible = createdStream.Visible, - Order = createdStream.Order, - SmartFilterEncoded = smartFilter.Filter, - StreamType = createdStream.StreamType - }; - - - await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), - userId); - return ret; - } - - public async Task CreateSideNavStreamFromExternalSource(int userId, int externalSourceId) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.SideNavStreams); - if (user == null) throw new KavitaException(await _localizationService.Translate(userId, "no-user")); - - var externalSource = await _unitOfWork.AppUserExternalSourceRepository.GetById(externalSourceId); - if (externalSource == null) throw new KavitaException(await _localizationService.Translate(userId, "external-source-doesnt-exist")); - - var stream = user?.SideNavStreams.FirstOrDefault(d => d.ExternalSourceId == externalSourceId); - if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "external-source-already-in-use")); - - var maxOrder = user!.SideNavStreams.Max(d => d.Order); - var createdStream = new AppUserSideNavStream() - { - Name = externalSource.Name, - IsProvided = false, - StreamType = SideNavStreamType.ExternalSource, - Visible = true, - Order = maxOrder + 1, - ExternalSourceId = externalSource.Id - }; - - user.SideNavStreams.Add(createdStream); - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - - var ret = new SideNavStreamDto() - { - Name = createdStream.Name, - IsProvided = createdStream.IsProvided, - Visible = createdStream.Visible, - Order = createdStream.Order, - StreamType = createdStream.StreamType, - ExternalSource = new ExternalSourceDto() - { - Host = externalSource.Host, - Id = externalSource.Id, - Name = externalSource.Name, - ApiKey = externalSource.ApiKey - } - }; - - - await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), - userId); - return ret; - } - - public async Task UpdateSideNavStream(int userId, SideNavStreamDto dto) - { - var stream = await _unitOfWork.UserRepository.GetSideNavStream(dto.Id); - if (stream == null) - throw new KavitaException(await _localizationService.Translate(userId, "sidenav-stream-doesnt-exist")); - - stream.Visible = dto.Visible; - - _unitOfWork.UserRepository.Update(stream); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), - userId); - } - - public async Task UpdateSideNavStreamPosition(int userId, UpdateStreamPositionDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, - AppUserIncludes.SideNavStreams); - var stream = user?.SideNavStreams.FirstOrDefault(d => d.Id == dto.Id); - if (stream == null) throw new KavitaException(await _localizationService.Translate(userId, "sidenav-stream-doesnt-exist")); - - if (stream.Order == dto.ToPosition) return; - - var list = user!.SideNavStreams.OrderBy(s => s.Order).ToList(); - - var wantedPosition = dto.ToPosition; - if (!dto.PositionIncludesInvisible) - { - var visibleItems = list.Where(i => i.Visible).ToList(); - if (dto.ToPosition < 0 || dto.ToPosition >= visibleItems.Count) return; - - var itemAtWantedPosition = visibleItems[dto.ToPosition]; - wantedPosition = list.IndexOf(itemAtWantedPosition); - } - - OrderableHelper.ReorderItems(list, stream.Id, wantedPosition); - user.SideNavStreams = list; - - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - if (!stream.Visible) return; - await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), - userId); - } - - public async Task CreateExternalSource(int userId, ExternalSourceDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, - AppUserIncludes.ExternalSources); - if (user == null) throw new KavitaException("not-authenticated"); - - if (user.ExternalSources.Any(s => s.Host == dto.Host)) - { - throw new KavitaException("external-source-already-exists"); - } - - if (string.IsNullOrEmpty(dto.ApiKey) || string.IsNullOrEmpty(dto.Name)) throw new KavitaException("external-source-required"); - if (!UrlHelper.StartsWithHttpOrHttps(dto.Host)) throw new KavitaException("external-source-host-format"); - - - var newSource = new AppUserExternalSource() - { - Name = dto.Name, - Host = UrlHelper.EnsureEndsWithSlash( - UrlHelper.EnsureStartsWithHttpOrHttps(dto.Host)), - ApiKey = dto.ApiKey - }; - user.ExternalSources.Add(newSource); - - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - - dto.Id = newSource.Id; - - return dto; - } - - public async Task UpdateExternalSource(int userId, ExternalSourceDto dto) - { - var source = await _unitOfWork.AppUserExternalSourceRepository.GetById(dto.Id); - if (source == null) throw new KavitaException("external-source-doesnt-exist"); - if (source.AppUserId != userId) throw new KavitaException("external-source-doesnt-exist"); - - if (string.IsNullOrEmpty(dto.ApiKey) || string.IsNullOrEmpty(dto.Host) || string.IsNullOrEmpty(dto.Name)) throw new KavitaException("external-source-required"); - - source.Host = UrlHelper.EnsureEndsWithSlash( - UrlHelper.EnsureStartsWithHttpOrHttps(dto.Host)); - source.ApiKey = dto.ApiKey; - source.Name = dto.Name; - - _unitOfWork.AppUserExternalSourceRepository.Update(source); - await _unitOfWork.CommitAsync(); - - dto.Host = source.Host; - return dto; - } - - public async Task DeleteExternalSource(int userId, int externalSourceId) - { - var source = await _unitOfWork.AppUserExternalSourceRepository.GetById(externalSourceId); - if (source == null) throw new KavitaException("external-source-doesnt-exist"); - if (source.AppUserId != userId) throw new KavitaException("external-source-doesnt-exist"); - - _unitOfWork.AppUserExternalSourceRepository.Delete(source); - - // Find all SideNav's with this source and delete them as well - var streams2 = await _unitOfWork.UserRepository.GetSideNavStreamWithExternalSource(externalSourceId); - _unitOfWork.UserRepository.Delete(streams2); - - await _unitOfWork.CommitAsync(); - } - - public async Task DeleteSideNavSmartFilterStream(int userId, int sideNavStreamId) - { - try - { - var stream = await _unitOfWork.UserRepository.GetSideNavStream(sideNavStreamId); - if (stream == null) throw new KavitaException("sidenav-stream-doesnt-exist"); - - if (stream.AppUserId != userId) throw new KavitaException("sidenav-stream-doesnt-exist"); - - - if (stream.StreamType != SideNavStreamType.SmartFilter) - { - throw new KavitaException("sidenav-stream-only-delete-smart-filter"); - } - - _unitOfWork.UserRepository.Delete(stream); - - await _unitOfWork.CommitAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception deleting SideNav Smart Filter Stream: {FilterId}", sideNavStreamId); - throw; - } - } - - public async Task DeleteDashboardSmartFilterStream(int userId, int dashboardStreamId) - { - try - { - var stream = await _unitOfWork.UserRepository.GetDashboardStream(dashboardStreamId); - if (stream == null) throw new KavitaException("dashboard-stream-doesnt-exist"); - - if (stream.AppUserId != userId) throw new KavitaException("dashboard-stream-doesnt-exist"); - - if (stream.StreamType != DashboardStreamType.SmartFilter) - { - throw new KavitaException("dashboard-stream-only-delete-smart-filter"); - } - - _unitOfWork.UserRepository.Delete(stream); - - await _unitOfWork.CommitAsync(); - } catch (Exception ex) - { - _logger.LogError(ex, "There was an exception deleting Dashboard Smart Filter Stream: {FilterId}", dashboardStreamId); - throw; - } - } - - public async Task RenameSmartFilterStreams(AppUserSmartFilter smartFilter) - { - var sideNavStreams = await _unitOfWork.UserRepository.GetSideNavStreamWithFilter(smartFilter.Id); - var dashboardStreams = await _unitOfWork.UserRepository.GetDashboardStreamWithFilter(smartFilter.Id); - - foreach (var sideNavStream in sideNavStreams) - { - sideNavStream.Name = smartFilter.Name; - } - - foreach (var dashboardStream in dashboardStreams) - { - dashboardStream.Name = smartFilter.Name; - } - - await _unitOfWork.CommitAsync(); - } -} diff --git a/API/Services/Tasks/BackupService.cs b/API/Services/Tasks/BackupService.cs deleted file mode 100644 index f45c1c05e..000000000 --- a/API/Services/Tasks/BackupService.cs +++ /dev/null @@ -1,311 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Entities.Enums; -using API.Logging; -using API.SignalR; -using Hangfire; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Services.Tasks; -#nullable enable - -public interface IBackupService -{ - Task BackupDatabase(); - /// - /// Returns a list of all log files for Kavita - /// - /// If file rolling is enabled. Defaults to True. - /// - IEnumerable GetLogFiles(bool rollFiles = LogLevelOptions.LogRollingEnabled); -} -public class BackupService : IBackupService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IDirectoryService _directoryService; - private readonly IEventHub _eventHub; - - private readonly IList _backupFiles; - - public BackupService(ILogger logger, IUnitOfWork unitOfWork, - IDirectoryService directoryService, IEventHub eventHub) - { - _unitOfWork = unitOfWork; - _logger = logger; - _directoryService = directoryService; - _eventHub = eventHub; - - _backupFiles = - [ - "appsettings.json" - ]; - } - - /// - /// Returns a list of all log files for Kavita - /// - /// If file rolling is enabled. Defaults to True. - /// - public IEnumerable GetLogFiles(bool rollFiles = LogLevelOptions.LogRollingEnabled) - { - var multipleFileRegex = rollFiles ? @"\d*" : string.Empty; - var fi = _directoryService.FileSystem.FileInfo.New(LogLevelOptions.LogFile); - - var files = rollFiles - ? _directoryService.GetFiles(_directoryService.LogDirectory, - $@"{_directoryService.FileSystem.Path.GetFileNameWithoutExtension(fi.Name)}{multipleFileRegex}\.log") - : [_directoryService.FileSystem.Path.Join(_directoryService.LogDirectory, "kavita.log")]; - return files; - } - - /// - /// Will back up anything that needs to be backed up. This includes logs, setting files, bare minimum cover images (just locked and first cover). - /// - [AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)] - public async Task BackupDatabase() - { - _logger.LogInformation("Beginning backup of Database at {BackupTime}", DateTime.Now); - var backupDirectory = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value; - - _logger.LogDebug("Backing up to {BackupDirectory}", backupDirectory); - if (!_directoryService.ExistOrCreate(backupDirectory)) - { - _logger.LogCritical("Could not write to {BackupDirectory}; aborting backup", backupDirectory); - await _eventHub.SendMessageAsync(MessageFactory.Error, - MessageFactory.ErrorEvent("Backup Service Error",$"Could not write to {backupDirectory}; aborting backup")); - return; - } - - await SendProgress(0F, "Started backup"); - await SendProgress(0.1F, "Copying core files"); - - var dateString = $"{DateTime.UtcNow.ToShortDateString()}_{DateTime.UtcNow:s}Z".Replace("/", "_").Replace(":", "_"); - var zipPath = _directoryService.FileSystem.Path.Join(backupDirectory, $"kavita_backup_v{BuildInfo.Version}_{dateString}.zip"); - - if (File.Exists(zipPath)) - { - _logger.LogCritical("{ZipFile} already exists, aborting", zipPath); - await _eventHub.SendMessageAsync(MessageFactory.Error, - MessageFactory.ErrorEvent("Backup Service Error",$"{zipPath} already exists, aborting")); - return; - } - - var tempDirectory = Path.Join(_directoryService.TempDirectory, dateString); - _directoryService.ExistOrCreate(tempDirectory); - _directoryService.ClearDirectory(tempDirectory); - - await SendProgress(0.1F, "Backing up database"); - await BackupDatabaseFile(tempDirectory); - - await SendProgress(0.15F, "Copying config files"); - _directoryService.CopyFilesToDirectory( - _backupFiles.Select(file => _directoryService.FileSystem.Path.Join(_directoryService.ConfigDirectory, file)), tempDirectory); - - // Copy any csv's as those are used for manual migrations - _directoryService.CopyFilesToDirectory( - _directoryService.GetFilesWithCertainExtensions(_directoryService.ConfigDirectory, @"\.csv"), tempDirectory); - - await SendProgress(0.2F, "Copying logs"); - CopyLogsToBackupDirectory(tempDirectory); - - await SendProgress(0.25F, "Copying cover images"); - await CopyCoverImagesToBackupDirectory(tempDirectory); - - await SendProgress(0.35F, "Copying templates images"); - CopyTemplatesToBackupDirectory(tempDirectory); - - await SendProgress(0.5F, "Copying bookmarks"); - await CopyBookmarksToBackupDirectory(tempDirectory); - - await SendProgress(0.6F, "Copying Fonts"); - CopyFontsToBackupDirectory(tempDirectory); - - await SendProgress(0.75F, "Copying themes"); - CopyThemesToBackupDirectory(tempDirectory); - - await SendProgress(0.85F, "Copying favicons"); - CopyFaviconsToBackupDirectory(tempDirectory); - - try - { - await ZipFile.CreateFromDirectoryAsync(tempDirectory, zipPath); - } - catch (AggregateException ex) - { - _logger.LogError(ex, "There was an issue when archiving library backup"); - } - - _directoryService.ClearAndDeleteDirectory(tempDirectory); - _logger.LogInformation("Database backup completed"); - await SendProgress(1F, "Completed backup"); - } - - private void CopyLogsToBackupDirectory(string tempDirectory) - { - var files = GetLogFiles(); - _directoryService.CopyFilesToDirectory(files, _directoryService.FileSystem.Path.Join(tempDirectory, "logs")); - } - - /// - /// Creates a backup of the SQLite database using VACUUM INTO command. - /// This method safely backs up the database while it's in use, without locking issues. - /// - /// The directory where the backup file will be created - private async Task BackupDatabaseFile(string tempDirectory) - { - var backupPath = _directoryService.FileSystem.Path.Join(tempDirectory, "kavita.db"); - - // Validate the backup path to prevent SQL injection - // The path must not contain single quotes which could break the SQL command - if (backupPath.Contains('\'')) - { - throw new ArgumentException("Backup path contains invalid characters", nameof(tempDirectory)); - } - - try - { - // Use VACUUM INTO to create a safe backup of the database while it's running - // This creates a consistent snapshot without locking the main database - // Note: VACUUM INTO requires a literal path and cannot use SQL parameters - #pragma warning disable EF1002 // The backup path is validated above to not contain SQL injection characters - await _unitOfWork.DataContext.Database.ExecuteSqlRawAsync($"VACUUM INTO '{backupPath}'"); - #pragma warning restore EF1002 - _logger.LogDebug("Database backup created successfully at {BackupPath}", backupPath); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to create database backup using VACUUM INTO at {BackupPath}", backupPath); - throw new InvalidOperationException($"Failed to create database backup at {backupPath}", ex); - } - } - - private void CopyFaviconsToBackupDirectory(string tempDirectory) - { - _directoryService.CopyDirectoryToDirectory(_directoryService.FaviconDirectory, _directoryService.FileSystem.Path.Join(tempDirectory, "favicons")); - } - - private void CopyTemplatesToBackupDirectory(string tempDirectory) - { - _directoryService.CopyDirectoryToDirectory(_directoryService.TemplateDirectory, _directoryService.FileSystem.Path.Join(tempDirectory, "templates")); - } - - private async Task CopyCoverImagesToBackupDirectory(string tempDirectory) - { - var outputTempDir = Path.Join(tempDirectory, "covers"); - _directoryService.ExistOrCreate(outputTempDir); - - try - { - var seriesImages = await _unitOfWork.SeriesRepository.GetLockedCoverImagesAsync(); - _directoryService.CopyFilesToDirectory( - seriesImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - - var collectionTags = await _unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync(); - _directoryService.CopyFilesToDirectory( - collectionTags.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - - var chapterImages = await _unitOfWork.ChapterRepository.GetCoverImagesForLockedChaptersAsync(); - _directoryService.CopyFilesToDirectory( - chapterImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - - var volumeImages = await _unitOfWork.VolumeRepository.GetCoverImagesForLockedVolumesAsync(); - _directoryService.CopyFilesToDirectory( - volumeImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - - var libraryImages = await _unitOfWork.LibraryRepository.GetAllCoverImagesAsync(); - _directoryService.CopyFilesToDirectory( - libraryImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - - var readingListImages = await _unitOfWork.ReadingListRepository.GetAllCoverImagesAsync(); - _directoryService.CopyFilesToDirectory( - readingListImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - } - catch (IOException) - { - // Swallow exception. This can be a duplicate cover being copied as chapter and volumes can share same file. - } - - if (!_directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) - { - _directoryService.ClearAndDeleteDirectory(outputTempDir); - } - } - - private async Task CopyBookmarksToBackupDirectory(string tempDirectory) - { - var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; - - var outputTempDir = Path.Join(tempDirectory, "bookmarks"); - _directoryService.ExistOrCreate(outputTempDir); - - try - { - _directoryService.CopyDirectoryToDirectory(bookmarkDirectory, outputTempDir); - } - catch (IOException) - { - // Swallow exception. - } - - if (!_directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) - { - _directoryService.ClearAndDeleteDirectory(outputTempDir); - } - } - - private void CopyFontsToBackupDirectory(string tempDirectory) - { - var outputTempDir = Path.Join(tempDirectory, "fonts"); - _directoryService.ExistOrCreate(outputTempDir); - - try - { - _directoryService.CopyDirectoryToDirectory(_directoryService.EpubFontDirectory, outputTempDir); - } - catch (IOException ex) - { - _logger.LogWarning(ex, "Failed to copy fonts to backup directory '{OutputTempDir}'. Fonts will not be included in the backup.", outputTempDir); - } - - if (!_directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) - { - _directoryService.ClearAndDeleteDirectory(outputTempDir); - } - } - - private void CopyThemesToBackupDirectory(string tempDirectory) - { - var outputTempDir = Path.Join(tempDirectory, "themes"); - _directoryService.ExistOrCreate(outputTempDir); - - try - { - _directoryService.CopyDirectoryToDirectory(_directoryService.SiteThemeDirectory, outputTempDir); - } - catch (IOException) - { - // Swallow exception. - } - - if (!_directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) - { - _directoryService.ClearAndDeleteDirectory(outputTempDir); - } - } - - private async Task SendProgress(float progress, string subtitle) - { - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.BackupDatabaseProgressEvent(progress, subtitle)); - } - -} diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs deleted file mode 100644 index 1884805bc..000000000 --- a/API/Services/Tasks/CleanupService.cs +++ /dev/null @@ -1,449 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Filtering; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; -using Hangfire; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Services.Tasks; -#nullable enable - -public interface ICleanupService -{ - Task Cleanup(); - Task CleanupDbEntries(); - Task CleanupCacheAndTempDirectories(); - void CleanupCacheDirectory(); - Task DeleteSeriesCoverImages(); - Task DeleteChapterCoverImages(); - Task DeleteTagCoverImages(); - Task CleanupBackups(); - Task CleanupLogs(); - void CleanupTemp(); - Task EnsureChapterProgressIsCapped(); - /// - /// Responsible to remove Series from Want To Read when user's have fully read the series and the series has Publication Status of Completed or Cancelled. - /// - /// - Task CleanupWantToRead(); - - Task ConsolidateProgress(); - - Task CleanupMediaErrors(); - -} -/// -/// Cleans up after operations on reoccurring basis -/// -public class CleanupService : ICleanupService -{ - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - private readonly IDirectoryService _directoryService; - - public CleanupService(ILogger logger, - IUnitOfWork unitOfWork, IEventHub eventHub, - IDirectoryService directoryService) - { - _logger = logger; - _unitOfWork = unitOfWork; - _eventHub = eventHub; - _directoryService = directoryService; - } - - - /// - /// Cleans up Temp, cache, deleted cover images, and old database backups - /// - [AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail, DelaysInSeconds = [120, 300, 300])] - public async Task Cleanup() - { - if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToEncoding", [], - TaskScheduler.DefaultQueue, true) || - TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllBookmarkToEncoding", [], - TaskScheduler.DefaultQueue, true)) - { - _logger.LogInformation("Cleanup put on hold as a media conversion in progress"); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ErrorEvent("Cleanup", "Cleanup put on hold as a media conversion in progress")); - return; - } - - _logger.LogInformation("Starting Cleanup"); - - // TODO: Why do I have clear temp directory then immediately do it again? - var cleanupSteps = new List<(Func, string)> - { - (() => Task.Run(() => _directoryService.ClearDirectory(_directoryService.TempDirectory)), "Cleaning temp directory"), - (CleanupCacheAndTempDirectories, "Cleaning cache and temp directories"), - (CleanupBackups, "Cleaning old database backups"), - (ConsolidateProgress, "Consolidating Progress Events"), - (CleanupMediaErrors, "Consolidating Media Errors"), - (CleanupDbEntries, "Cleaning abandoned database rows"), // Cleanup DB before removing files linked to DB entries - (DeleteSeriesCoverImages, "Cleaning deleted series cover images"), - (DeleteChapterCoverImages, "Cleaning deleted chapter cover images"), - (() => Task.WhenAll(DeleteTagCoverImages(), DeleteReadingListCoverImages(), DeletePersonCoverImages()), "Cleaning deleted cover images"), - (CleanupLogs, "Cleaning old logs"), - (EnsureChapterProgressIsCapped, "Cleaning progress events that exceed 100%") - }; - - await SendProgress(0F, "Starting cleanup"); - - for (var i = 0; i < cleanupSteps.Count; i++) - { - var (method, subtitle) = cleanupSteps[i]; - var progress = (float)(i + 1) / (cleanupSteps.Count + 1); - - _logger.LogInformation("{Message}", subtitle); - await method(); - await SendProgress(progress, subtitle); - } - - await SendProgress(1F, "Cleanup finished"); - _logger.LogInformation("Cleanup finished"); - } - - /// - /// Cleans up abandon rows in the DB - /// - public async Task CleanupDbEntries() - { - await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); - await _unitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated(); - await _unitOfWork.GenreRepository.RemoveAllGenreNoLongerAssociated(); - await _unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(); - await _unitOfWork.ReadingListRepository.RemoveReadingListsWithoutSeries(); - } - - private async Task SendProgress(float progress, string subtitle) - { - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.CleanupProgressEvent(progress, subtitle)); - } - - /// - /// Removes all series images that are not in the database. They must follow filename pattern. - /// - public async Task DeleteSeriesCoverImages() - { - var images = await _unitOfWork.SeriesRepository.GetAllCoverImagesAsync(); - var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.SeriesCoverImageRegex); - _directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file)))); - } - - /// - /// Removes all chapter/volume images that are not in the database. They must follow filename pattern. - /// - public async Task DeleteChapterCoverImages() - { - var images = await _unitOfWork.ChapterRepository.GetAllCoverImagesAsync(); - var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.ChapterCoverImageRegex); - _directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file)))); - } - - /// - /// Removes all collection tag images that are not in the database. They must follow filename pattern. - /// - public async Task DeleteTagCoverImages() - { - var images = await _unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync(); - var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.CollectionTagCoverImageRegex); - _directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file)))); - } - - /// - /// Removes all reading list images that are not in the database. They must follow filename pattern. - /// - public async Task DeleteReadingListCoverImages() - { - var images = await _unitOfWork.ReadingListRepository.GetAllCoverImagesAsync(); - var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.ReadingListCoverImageRegex); - _directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file)))); - } - - /// - /// Remove all person cover images no longer associated with a person in the database - /// - public async Task DeletePersonCoverImages() - { - var images = await _unitOfWork.PersonRepository.GetAllCoverImagesAsync(); - var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.PersonCoverImageRegex); - _directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file)))); - } - - /// - /// Removes all files and directories in the cache and temp directory - /// - public Task CleanupCacheAndTempDirectories() - { - _logger.LogInformation("Performing cleanup of Cache & Temp directories"); - _directoryService.ExistOrCreate(_directoryService.CacheDirectory); - _directoryService.ExistOrCreate(_directoryService.TempDirectory); - - try - { - _directoryService.ClearDirectory(_directoryService.CacheDirectory); - _directoryService.ClearDirectory(_directoryService.TempDirectory); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); - } - - _logger.LogInformation("Cache and temp directory purged"); - - return Task.CompletedTask; - } - - public void CleanupCacheDirectory() - { - _logger.LogInformation("Performing cleanup of Cache directories"); - _directoryService.ExistOrCreate(_directoryService.CacheDirectory); - - try - { - _directoryService.ClearDirectory(_directoryService.CacheDirectory); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); - } - - _logger.LogInformation("Cache directory purged"); - } - - /// - /// Removes Database backups older than configured total backups. If all backups are older than total backups days, only the latest is kept. - /// - public async Task CleanupBackups() - { - var dayThreshold = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).TotalBackups; - _logger.LogInformation("Beginning cleanup of Database backups at {Time}", DateTime.Now); - var backupDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value; - if (!_directoryService.Exists(backupDirectory)) return; - - var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold)); - var allBackups = _directoryService.GetFiles(backupDirectory).ToList(); - var expiredBackups = allBackups.Select(filename => _directoryService.FileSystem.FileInfo.New(filename)) - .Where(f => f.CreationTime < deltaTime) - .ToList(); - - if (expiredBackups.Count == allBackups.Count) - { - _logger.LogInformation("All expired backups are older than {Threshold} days. Removing all but last backup", dayThreshold); - var toDelete = expiredBackups.OrderByDescending(f => f.CreationTime).ToList(); - _directoryService.DeleteFiles(toDelete.Take(toDelete.Count - 1).Select(f => f.FullName)); - } - else - { - _directoryService.DeleteFiles(expiredBackups.Select(f => f.FullName)); - } - _logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now); - } - - /// - /// Find any progress events that have duplicate, find the highest page read event, then copy over information from that and delete others, to leave one. - /// - public async Task ConsolidateProgress() - { - _logger.LogInformation("Consolidating Progress Events"); - // AppUserProgress - var allProgress = await _unitOfWork.AppUserProgressRepository.GetAllProgress(); - - // Group by the unique identifiers that would make a progress entry unique - var duplicateGroups = allProgress - .GroupBy(p => new - { - p.AppUserId, - p.ChapterId, - }) - .Where(g => g.Count() > 1); - - foreach (var group in duplicateGroups) - { - // Find the entry with the highest pages read - var highestProgress = group - .OrderByDescending(p => p.PagesRead) - .ThenByDescending(p => p.LastModifiedUtc) - .First(); - - // Get the duplicate entries to remove (all except the highest progress) - var duplicatesToRemove = group - .Where(p => p.Id != highestProgress.Id) - .ToList(); - - // Copy over any non-null BookScrollId if the highest progress entry doesn't have one - if (string.IsNullOrEmpty(highestProgress.BookScrollId)) - { - var firstValidScrollId = duplicatesToRemove - .FirstOrDefault(p => !string.IsNullOrEmpty(p.BookScrollId)) - ?.BookScrollId; - - if (firstValidScrollId != null) - { - highestProgress.BookScrollId = firstValidScrollId; - highestProgress.MarkModified(); - } - } - - // Remove the duplicates - foreach (var duplicate in duplicatesToRemove) - { - _unitOfWork.AppUserProgressRepository.Remove(duplicate); - } - } - - // Save changes - await _unitOfWork.CommitAsync(); - } - - /// - /// Scans through Media Error and removes any entries that have been fixed and are within the DB (proper files where wordcount/pagecount > 0) - /// - public async Task CleanupMediaErrors() - { - try - { - List errorStrings = ["This archive cannot be read or not supported", "File format not supported"]; - var mediaErrors = await _unitOfWork.MediaErrorRepository.GetAllErrorsAsync(errorStrings); - _logger.LogInformation("Beginning consolidation of {Count} Media Errors", mediaErrors.Count); - - var pathToErrorMap = mediaErrors - .GroupBy(me => Parser.NormalizePath(me.FilePath)) - .ToDictionary( - group => group.Key, - group => group.ToList() // The same file can be duplicated (rare issue when network drives die out midscan) - ); - - var normalizedPaths = pathToErrorMap.Keys.ToList(); - - // Find all files that are valid - var validFiles = await _unitOfWork.DataContext.MangaFile - .Where(f => normalizedPaths.Contains(f.FilePath) && f.Pages > 0) - .Select(f => f.FilePath) - .ToListAsync(); - - var removalCount = 0; - foreach (var validFilePath in validFiles) - { - if (!pathToErrorMap.TryGetValue(validFilePath, out var mediaError)) continue; - - _unitOfWork.MediaErrorRepository.Remove(mediaError); - removalCount++; - } - - await _unitOfWork.CommitAsync(); - - _logger.LogInformation("Finished consolidation of {Count} Media Errors, Removed: {RemovalCount}", - mediaErrors.Count, removalCount); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception consolidating media errors"); - } - } - - public async Task CleanupLogs() - { - _logger.LogInformation("Performing cleanup of logs directory"); - var dayThreshold = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).TotalLogs; - var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold)); - var allLogs = _directoryService.GetFiles(_directoryService.LogDirectory).ToList(); - var expiredLogs = allLogs.Select(filename => _directoryService.FileSystem.FileInfo.New(filename)) - .Where(f => f.CreationTime < deltaTime) - .ToList(); - - if (expiredLogs.Count == allLogs.Count) - { - _logger.LogInformation("All expired backups are older than {Threshold} days. Removing all but last backup", dayThreshold); - var toDelete = expiredLogs.OrderBy(f => f.CreationTime).ToList(); - _directoryService.DeleteFiles(toDelete.Take(toDelete.Count - 1).Select(f => f.FullName)); - } - else - { - _directoryService.DeleteFiles(expiredLogs.Select(f => f.FullName)); - } - _logger.LogInformation("Finished cleanup of logs at {Time}", DateTime.Now); - } - - public void CleanupTemp() - { - _logger.LogInformation("Performing cleanup of Temp directory"); - _directoryService.ExistOrCreate(_directoryService.TempDirectory); - - try - { - _directoryService.ClearDirectory(_directoryService.TempDirectory); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); - } - - _logger.LogInformation("Temp directory purged"); - } - - /// - /// Ensures that each chapter's progress (pages read) is capped at the total pages. This can get out of sync when a chapter is replaced after being read with one with lower page count. - /// - /// - public async Task EnsureChapterProgressIsCapped() - { - _logger.LogInformation("Cleaning up any progress rows that exceed chapter page count"); - await _unitOfWork.AppUserProgressRepository.UpdateAllProgressThatAreMoreThanChapterPages(); - _logger.LogInformation("Cleaning up any progress rows that exceed chapter page count - complete"); - } - - /// - /// This does not cleanup any Series that are not Completed or Cancelled - /// - public async Task CleanupWantToRead() - { - _logger.LogInformation("Performing cleanup of Series that are Completed and have been fully read that are in Want To Read list"); - - var libraryIds = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Select(l => l.Id).ToList(); - var filter = new FilterDto() - { - PublicationStatus = new List() - { - PublicationStatus.Completed, - PublicationStatus.Cancelled - }, - Libraries = libraryIds, - ReadStatus = new ReadStatus() - { - Read = true, - InProgress = false, - NotRead = false - } - }; - foreach (var user in await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.WantToRead)) - { - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(0, user.Id, new UserParams(), filter); - var seriesIds = series.Select(s => s.Id).ToList(); - if (seriesIds.Count == 0) continue; - - user.WantToRead ??= new List(); - user.WantToRead = user.WantToRead.Where(s => !seriesIds.Contains(s.SeriesId)).ToList(); - _unitOfWork.UserRepository.Update(user); - } - - if (_unitOfWork.HasChanges()) - { - await _unitOfWork.CommitAsync(); - } - - _logger.LogInformation("Performing cleanup of Series that are Completed and have been fully read that are in Want To Read list, completed"); - } -} diff --git a/API/SignalR/EventHub.cs b/API/SignalR/EventHub.cs deleted file mode 100644 index 2df394fe9..000000000 --- a/API/SignalR/EventHub.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Linq; -using System.Collections.Generic; -using System.Threading.Tasks; -using API.SignalR.Presence; -using Microsoft.AspNetCore.SignalR; - -namespace API.SignalR; - -/// -/// Responsible for ushering events to the UI and allowing simple DI hook to send data -/// -public interface IEventHub -{ - Task SendMessageAsync(string method, SignalRMessage message, bool onlyAdmins = true); - Task SendMessageToAsync(string method, SignalRMessage message, int userId); -} - -public class EventHub : IEventHub -{ - private readonly IHubContext _messageHub; - private readonly IPresenceTracker _presenceTracker; - - public EventHub(IHubContext messageHub, IPresenceTracker presenceTracker) - { - _messageHub = messageHub; - _presenceTracker = presenceTracker; - - // TODO: When sending a message, queue the message up and on re-connect, reply the queued messages. Queue messages expire on a rolling basis (rolling array) - } - - public async Task SendMessageAsync(string method, SignalRMessage message, bool onlyAdmins = true) - { - // TODO: If libraryId and NOT onlyAdmins, then perform RBS check before sending the event - - var users = _messageHub.Clients.All; - if (onlyAdmins) - { - var admins = await _presenceTracker.GetOnlineAdminIds(); - users = _messageHub.Clients.Users(admins.Select(i => i.ToString()).ToArray()); - } - - - await users.SendAsync(method, message); - } - - /// - /// Sends a message directly to a user if they are connected - /// - /// - /// - /// - /// - public async Task SendMessageToAsync(string method, SignalRMessage message, int userId) - { - await _messageHub.Clients.Users(new List() {userId + string.Empty}).SendAsync(method, message); - } - -} diff --git a/API/redo-migration.sh b/API/redo-migration.sh deleted file mode 100755 index a1ef82bc0..000000000 --- a/API/redo-migration.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -migrations=($(ls -1 Data/Migrations/*.cs 2>/dev/null | grep -v "Designer.cs" | grep -v "Snapshot.cs" | sort -r)) - -if [ ${#migrations[@]} -lt 2 ]; then - echo "Error: Need at least 2 migrations to redo" - exit 1 -fi - -second_last=$(basename "${migrations[1]}" .cs) -last=$(basename "${migrations[0]}" .cs) -last_name=$(echo "$last" | sed 's/^[0-9]*_//') - -new_name=${1:-$last_name} - -echo "Rolling back to: $second_last" -echo "Removing $last_name and re-adding as $new_name" -read -p "Continue? (y/N) " -n 1 -r -echo "" - -if [[ $REPLY =~ ^[Yy]$ ]]; then - dotnet ef database update "$second_last" && \ - dotnet ef migrations remove && \ - dotnet ef migrations add "$new_name" -else - echo "Cancelled" - exit 0 -fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7bb7bd79d..ce51ce4ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,14 +26,17 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit - `npm install -g @angular/cli` 5. Start the frontend - `npm run start` -6. Build the project in Visual Studio/Rider, Setting startup project to `API` +6. Build the project in Visual Studio/Rider, Setting startup project to `Kavita.Server (Server)` 7. Debug the project in Visual Studio/Rider 8. Open http://localhost:4200 9. (Deployment only) Run build.sh and pass the Runtime Identifier for your OS or just build.sh for all supported RIDs. ### Debugging on Device ### -- Update `IP` constant in `Web/UI/src/environments/environment.ts` to your dev machine's ip instead of `localhost`. +- Run `npm run start-proxy` instead to have the Angular application proxy the requests to the backend. +### Apple users + +The backend may fail to start due to port 5000 already being in use. To fix this, temporally turn off AirPlay Receiver in System Settings. You can re-enable it later, it will bind to a different port. You may need to do this again after an update or reboot. ### Contributing Code ### - If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Kareadita/Kavita/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first) @@ -61,7 +64,7 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit ### Swagger API ### If you just want to play with Swagger, you can just -- cd Kavita/API +- cd Kavita/Kavita.Server - dotnet run -c Debug - Go to http://localhost:5000/swagger/index.html diff --git a/Dockerfile b/Dockerfile index 04e13304f..057137da6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ FROM ubuntu:noble COPY --from=copytask /Kavita /kavita COPY --from=copytask /files/wwwroot /kavita/wwwroot -COPY API/config/appsettings.json /tmp/config/appsettings.json +COPY Kavita.Server/config/appsettings.json /tmp/config/appsettings.json #Installs program dependencies ENV DEBIAN_FRONTEND=noninteractive diff --git a/Kavita.API/Attributes/SkipDeviceTrackingAttribute.cs b/Kavita.API/Attributes/SkipDeviceTrackingAttribute.cs new file mode 100644 index 000000000..7263ef8a0 --- /dev/null +++ b/Kavita.API/Attributes/SkipDeviceTrackingAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace Kavita.API.Attributes; + +/// +/// Attribute to skip device tracking on specific endpoints. +/// Use for high-frequency endpoints where device tracking adds unnecessary overhead. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class SkipDeviceTrackingAttribute : Attribute; diff --git a/Kavita.API/Database/IDataContext.cs b/Kavita.API/Database/IDataContext.cs new file mode 100644 index 000000000..455e3211e --- /dev/null +++ b/Kavita.API/Database/IDataContext.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities; +using Kavita.Models.Entities.History; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.Scrobble; +using Kavita.Models.Entities.User; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Kavita.API.Database; + + +public interface IDataContext : IDisposable +{ + + DatabaseFacade Database { get; } + + DbSet Users { get; } + DbSet Library { get; } + DbSet Series { get; } + DbSet Chapter { get; } + DbSet Volume { get; } + DbSet AppUser { get; } + DbSet MangaFile { get; } + DbSet AppUserProgresses { get; } + DbSet AppUserRating { get; } + DbSet ServerSetting { get; } + DbSet AppUserPreferences { get; } + DbSet SeriesMetadata { get; } + DbSet SeriesMetadataTag { get; } + DbSet GenreSeriesMetadata { get; } + + [Obsolete("Use AppUserCollection")] + DbSet CollectionTag { get; } + + DbSet AppUserBookmark { get; } + DbSet ReadingList { get; } + DbSet ReadingListItem { get; } + DbSet Person { get; } + DbSet PersonAlias { get; } + DbSet Genre { get; } + DbSet Tag { get; } + DbSet SiteTheme { get; } + DbSet SeriesRelation { get; } + DbSet FolderPath { get; } + DbSet Device { get; } + DbSet ServerStatistics { get; } + DbSet MediaError { get; } + DbSet ScrobbleEvent { get; } + DbSet ScrobbleError { get; } + DbSet ScrobbleHold { get; } + DbSet AppUserOnDeckRemoval { get; } + DbSet AppUserTableOfContent { get; } + DbSet AppUserSmartFilter { get; } + DbSet AppUserDashboardStream { get; } + DbSet AppUserSideNavStream { get; } + DbSet AppUserExternalSource { get; } + DbSet ExternalReview { get; } + DbSet ExternalRating { get; } + DbSet ExternalSeriesMetadata { get; } + DbSet ExternalRecommendation { get; } + DbSet ManualMigrationHistory { get; } + + [Obsolete("Use IsBlacklisted field on Series")] + DbSet SeriesBlacklist { get; } + + DbSet AppUserCollection { get; } + DbSet ChapterPeople { get; } + DbSet SeriesMetadataPeople { get; } + DbSet EmailHistory { get; } + DbSet MetadataSettings { get; } + DbSet MetadataFieldMapping { get; } + DbSet AppUserChapterRating { get; } + DbSet AppUserReadingProfiles { get; } + DbSet AppUserAnnotation { get; } + DbSet EpubFont { get; } + DbSet AppUserReadingSession { get; } + DbSet AppUserReadingSessionActivityData { get; } + DbSet AppUserReadingHistory { get; } + DbSet ClientDevice { get; } + DbSet ClientDeviceHistory { get; } + DbSet AppUserAuthKey { get; } + + // Change Tracking and Saving + ChangeTracker ChangeTracker { get; } + int SaveChanges(); + int SaveChanges(bool acceptAllChangesOnSuccess); + Task SaveChangesAsync(CancellationToken cancellationToken = default); + Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default); + + EntityEntry Entry(TEntity entity) where TEntity : class; +} diff --git a/Kavita.API/Database/IUnitOfWork.cs b/Kavita.API/Database/IUnitOfWork.cs new file mode 100644 index 000000000..6c352282d --- /dev/null +++ b/Kavita.API/Database/IUnitOfWork.cs @@ -0,0 +1,42 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Repositories; + +namespace Kavita.API.Database; + +public interface IUnitOfWork +{ + IDataContext DataContext { get; } + ISeriesRepository SeriesRepository { get; } + IUserRepository UserRepository { get; } + ILibraryRepository LibraryRepository { get; } + IVolumeRepository VolumeRepository { get; } + ISettingsRepository SettingsRepository { get; } + IAppUserProgressRepository AppUserProgressRepository { get; } + ICollectionTagRepository CollectionTagRepository { get; } + IChapterRepository ChapterRepository { get; } + IReadingListRepository ReadingListRepository { get; } + ISeriesMetadataRepository SeriesMetadataRepository { get; } + IPersonRepository PersonRepository { get; } + IGenreRepository GenreRepository { get; } + ITagRepository TagRepository { get; } + ISiteThemeRepository SiteThemeRepository { get; } + IMangaFileRepository MangaFileRepository { get; } + IDeviceRepository DeviceRepository { get; } + IMediaErrorRepository MediaErrorRepository { get; } + IScrobbleRepository ScrobbleRepository { get; } + IUserTableOfContentRepository UserTableOfContentRepository { get; } + IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; } + IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } + IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } + IEmailHistoryRepository EmailHistoryRepository { get; } + IAppUserReadingProfileRepository AppUserReadingProfileRepository { get; } + IAnnotationRepository AnnotationRepository { get; } + IEpubFontRepository EpubFontRepository { get; } + IReadingSessionRepository ReadingSessionRepository { get; } + IClientDeviceRepository ClientDeviceRepository { get; } + bool Commit(); + Task CommitAsync(CancellationToken ct = default); + bool HasChanges(); + Task RollbackAsync(CancellationToken ct = default); +} diff --git a/API/Errors/ApiException.cs b/Kavita.API/Errors/ApiException.cs similarity index 67% rename from API/Errors/ApiException.cs rename to Kavita.API/Errors/ApiException.cs index 60d93729c..85147985d 100644 --- a/API/Errors/ApiException.cs +++ b/Kavita.API/Errors/ApiException.cs @@ -1,4 +1,3 @@ -namespace API.Errors; +namespace Kavita.API.Errors; -#nullable enable public record ApiException(int Status, string? Message = null, string? Details = null); diff --git a/API/Exceptions/OpdsException.cs b/Kavita.API/Errors/OpdsException.cs similarity index 80% rename from API/Exceptions/OpdsException.cs rename to Kavita.API/Errors/OpdsException.cs index 0267628a2..3e8187105 100644 --- a/API/Exceptions/OpdsException.cs +++ b/Kavita.API/Errors/OpdsException.cs @@ -1,8 +1,6 @@ -using System; -using API.Controllers; -using API.Services; +using System; -namespace API.Exceptions; +namespace Kavita.API.Errors; /// /// Should be caught in and ONLY used in diff --git a/Kavita.API/Kavita.API.csproj b/Kavita.API/Kavita.API.csproj new file mode 100644 index 000000000..63fa90831 --- /dev/null +++ b/Kavita.API/Kavita.API.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + disable + enable + + + + + + + + + + + + + + + + diff --git a/Kavita.API/Repositories/IAnnotationRepository.cs b/Kavita.API/Repositories/IAnnotationRepository.cs new file mode 100644 index 000000000..47474d572 --- /dev/null +++ b/Kavita.API/Repositories/IAnnotationRepository.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Annotations; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +public interface IAnnotationRepository +{ + void Attach(AppUserAnnotation annotation); + void Update(AppUserAnnotation annotation); + void Remove(AppUserAnnotation annotation); + void Remove(IEnumerable annotations); + Task GetAnnotationDto(int id, CancellationToken ct = default); + Task GetAnnotation(int id, CancellationToken ct = default); + Task> GetAllAnnotations(CancellationToken ct = default); + Task> GetAnnotations(int userId, IList ids, CancellationToken ct = default); + Task> GetFullAnnotationsByUserIdAsync(int userId, CancellationToken ct = default); + Task> GetFullAnnotations(int userId, IList annotationIds, CancellationToken ct = default); + Task> GetAnnotationDtos(int userId, BrowseAnnotationFilterDto filter, UserParams userParams, CancellationToken ct = default); + Task> GetSeriesWithAnnotations(int userId, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IAppUserExternalSourceRepository.cs b/Kavita.API/Repositories/IAppUserExternalSourceRepository.cs new file mode 100644 index 000000000..b6c59f943 --- /dev/null +++ b/Kavita.API/Repositories/IAppUserExternalSourceRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +public interface IAppUserExternalSourceRepository +{ + void Update(AppUserExternalSource source); + void Delete(AppUserExternalSource source); + Task GetById(int externalSourceId, CancellationToken ct = default); + Task> GetExternalSources(int userId, CancellationToken ct = default); + Task ExternalSourceExists(int userId, string name, string host, string apiKey, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IAppUserProgressRepository.cs b/Kavita.API/Repositories/IAppUserProgressRepository.cs new file mode 100644 index 000000000..6032fb6d2 --- /dev/null +++ b/Kavita.API/Repositories/IAppUserProgressRepository.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; + +namespace Kavita.API.Repositories; + +public interface IAppUserProgressRepository +{ + void Update(AppUserProgress userProgress); + void Remove(AppUserProgress userProgress); + Task CleanupAbandonedChapters(CancellationToken ct = default); + Task UserHasProgress(LibraryType libraryType, int userId, CancellationToken ct = default); + Task GetUserProgressAsync(int chapterId, int userId, CancellationToken ct = default); + Task HasAnyProgressOnSeriesAsync(int seriesId, int userId, CancellationToken ct = default); + Task> GetUserProgressForSeriesAsync(int seriesId, int userId, CancellationToken ct = default); + Task> GetAllProgress(CancellationToken ct = default); + Task GetLatestProgress(CancellationToken ct = default); + Task GetUserProgressDtoAsync(int chapterId, int userId, CancellationToken ct = default); + Task AnyUserProgressForSeriesAsync(int seriesId, int userId, CancellationToken ct = default); + Task GetHighestFullyReadChapterForSeries(int seriesId, int userId, CancellationToken ct = default); + Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId, CancellationToken ct = default); + Task GetLatestProgressForSeries(int seriesId, int userId, CancellationToken ct = default); + Task GetLatestProgressForVolume(int volumeId, int userId, CancellationToken ct = default); + Task GetLatestProgressForChapter(int chapterId, int userId, CancellationToken ct = default); + Task GetFirstProgressForSeries(int seriesId, int userId, CancellationToken ct = default); + Task GetFirstProgressForUser(int userId, CancellationToken ct = default); + Task UpdateAllProgressThatAreMoreThanChapterPages(CancellationToken ct = default); + Task> GetUserProgressForChapter(int chapterId, int userId = 0, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IAppUserReadingProfileRepository.cs b/Kavita.API/Repositories/IAppUserReadingProfileRepository.cs new file mode 100644 index 000000000..657bfd8ca --- /dev/null +++ b/Kavita.API/Repositories/IAppUserReadingProfileRepository.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +public interface IAppUserReadingProfileRepository +{ + /// + /// Returns the reading profile to use for the given series + /// + /// + /// + /// + /// + /// + /// + /// + Task GetProfileForSeries(int userId, int libraryId, int seriesId, int? activeDeviceId = null, bool skipImplicit = false, CancellationToken ct = default); + + /// + /// Get all profiles assigned to a library + /// + /// + /// + /// + /// + Task> GetProfilesForLibrary(int userId, int libraryId, CancellationToken ct = default); + + /// + /// Return the profile if it belongs to the user + /// + /// + /// + /// + /// + Task GetUserProfile(int userId, int profileId, CancellationToken ct = default); + + /// + /// Returns all reading profiles for the user + /// + /// + /// + /// + /// + Task> GetProfilesForUser(int userId, bool skipImplicit = false, CancellationToken ct = default); + + /// + /// Returns all reading profiles for the user + /// + /// + /// + /// + /// + Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false, CancellationToken ct = default); + + /// + /// Is there a user reading profile with this name (normalized)? + /// + /// + /// + /// + /// + Task IsProfileNameInUse(int userId, string name, CancellationToken ct = default); + + void Add(AppUserReadingProfile readingProfile); + void Update(AppUserReadingProfile readingProfile); + void Remove(AppUserReadingProfile readingProfile); + void RemoveRange(IEnumerable readingProfiles); +} diff --git a/Kavita.API/Repositories/IAppUserSmartFilterRepository.cs b/Kavita.API/Repositories/IAppUserSmartFilterRepository.cs new file mode 100644 index 000000000..6b8049e36 --- /dev/null +++ b/Kavita.API/Repositories/IAppUserSmartFilterRepository.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +public interface IAppUserSmartFilterRepository +{ + void Update(AppUserSmartFilter filter); + void Attach(AppUserSmartFilter filter); + void Delete(AppUserSmartFilter filter); + Task> GetAllDtosByUserId(int userId, CancellationToken ct = default); + Task> GetPagedDtosByUserIdAsync(int userId, UserParams userParams, CancellationToken ct = default); + Task GetById(int smartFilterId, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IChapterRepository.cs b/Kavita.API/Repositories/IChapterRepository.cs new file mode 100644 index 000000000..dccbb8fff --- /dev/null +++ b/Kavita.API/Repositories/IChapterRepository.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; + +namespace Kavita.API.Repositories; + +[Flags] +public enum ChapterIncludes +{ + None = 1 << 0, + Volumes = 1 << 1, + Files = 1 << 2, + People = 1 << 3, + Genres = 1 << 4, + Tags = 1 << 5, + ExternalReviews = 1 << 6, + ExternalRatings = 1 << 7 +} + +public interface IChapterRepository +{ + void Update(Chapter chapter); + void Remove(Chapter chapter); + void Remove(IList chapters); + Task> GetChaptersByIdsAsync(IList chapterIds, ChapterIncludes includes = ChapterIncludes.None, CancellationToken ct = default); + Task GetChapterInfoDtoAsync(int chapterId, CancellationToken ct = default); + Task GetChapterTotalPagesAsync(int chapterId, CancellationToken ct = default); + Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files, CancellationToken ct = default); + Task GetChapterDtoAsync(int chapterId, int userId, CancellationToken ct = default); + Task> GetChapterDtoByIdsAsync(IEnumerable chapterIds, int userId, CancellationToken ct = default); + Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files, CancellationToken ct = default); + Task> GetFilesForChapterAsync(int chapterId, CancellationToken ct = default); + Task> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None, CancellationToken ct = default); + Task> GetChapterDtosAsync(int volumeId, int userId, CancellationToken ct = default); + Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds, CancellationToken ct = default); + Task GetFilesizeForChapterAsync(int chapterId, CancellationToken ct = default); + Task> GetFilesizeForChaptersAsync(IList chapterIds, CancellationToken ct = default); + Task GetChapterCoverImageAsync(int chapterId, CancellationToken ct = default); + Task> GetAllCoverImagesAsync(CancellationToken ct = default); + Task> GetAllChaptersWithCoversInDifferentEncoding(EncodeFormat format, CancellationToken ct = default); + Task> GetCoverImagesForLockedChaptersAsync(CancellationToken ct = default); + IQueryable GetChaptersForSeries(int seriesId, CancellationToken ct = default); + Task> GetAllChaptersForSeries(int seriesId, CancellationToken ct = default); + Task GetAverageUserRating(int chapterId, int userId, CancellationToken ct = default); + Task> GetExternalChapterReviewDtos(int chapterId, CancellationToken ct = default); + Task> GetExternalChapterReview(int chapterId, CancellationToken ct = default); + Task> GetExternalChapterRatingDtos(int chapterId, CancellationToken ct = default); + Task> GetExternalChapterRatings(int chapterId, CancellationToken ct = default); + Task GetCurrentlyReadingChapterAsync(int seriesId, int userId, CancellationToken ct = default); + Task GetFirstChapterForSeriesAsync(int seriesId, int userId, CancellationToken ct = default); + Task GetFirstChapterForVolumeAsync(int volumeId, int userId, CancellationToken ct = default); + Task> GetChapterDtosAsync(IEnumerable chapterIds, int userId, CancellationToken ct = default); + Task GetSeriesIdForChapter(int chapterId, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IClientDeviceRepository.cs b/Kavita.API/Repositories/IClientDeviceRepository.cs new file mode 100644 index 000000000..722176ee3 --- /dev/null +++ b/Kavita.API/Repositories/IClientDeviceRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +public interface IClientDeviceRepository +{ + Task GetClientDeviceById(int id, int userId, CancellationToken cancellationToken = default); + Task GetClientDeviceByClientFingerprint(int userId, string uiFingerprint, CancellationToken cancellationToken); + Task> GetUserDevicesAsync(int userId, bool includeInactive = false, CancellationToken cancellationToken = default); + Task> GetUserDeviceDtosAsync(int userId, bool includeInactive = false, CancellationToken cancellationToken = default); + Task> GetAllUserDeviceDtos(bool includeInactive = false, CancellationToken cancellationToken = default); +} diff --git a/Kavita.API/Repositories/ICollectionTagRepository.cs b/Kavita.API/Repositories/ICollectionTagRepository.cs new file mode 100644 index 000000000..fb9c84e07 --- /dev/null +++ b/Kavita.API/Repositories/ICollectionTagRepository.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +[Flags] +public enum CollectionTagIncludes +{ + None = 1 << 0, + SeriesMetadata = 1 << 1, + SeriesMetadataWithSeries = 1 << 2 +} + +[Flags] +public enum CollectionIncludes +{ + None = 1 << 0, + Series = 1 << 1, +} + +public interface ICollectionTagRepository +{ + void Remove(AppUserCollection tag); + void Update(AppUserCollection tag); + Task GetCoverImageAsync(int collectionTagId, CancellationToken ct = default); + Task GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default); + Task RemoveCollectionsWithoutSeries(CancellationToken ct = default); + Task GetCollectionDtoAsync(int collectionId, int userId, CancellationToken ct = default); + + Task> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default); + + /// + /// Returns all of the user's collections with the option of other user's promoted + /// + /// + /// + /// + /// + Task> GetCollectionDtosAsync(int userId, bool includePromoted = false, CancellationToken ct = default); + Task> GetCollectionDtosPagedAsync(int userId, UserParams userParams, bool includePromoted = false, CancellationToken ct = default); + Task> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false, CancellationToken ct = default); + + Task> GetAllCoverImagesAsync(CancellationToken ct = default); + Task CollectionExists(string title, int userId, CancellationToken ct = default); + Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, CancellationToken ct = default); + Task> GetRandomCoverImagesAsync(int collectionId, CancellationToken ct = default); + Task> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default); + Task UpdateCollectionAgeRating(AppUserCollection tag, CancellationToken ct = default); + Task> GetCollectionsByIds(IEnumerable tags, CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default); + Task> GetAllCollectionsForSyncing(DateTime expirationTime, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IDeviceRepository.cs b/Kavita.API/Repositories/IDeviceRepository.cs new file mode 100644 index 000000000..5b0ad240a --- /dev/null +++ b/Kavita.API/Repositories/IDeviceRepository.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface IDeviceRepository +{ + void Update(Device device); + Task> GetDevicesForUserAsync(int userId, CancellationToken ct = default); + Task GetDeviceById(int deviceId, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IEmailHistoryRepository.cs b/Kavita.API/Repositories/IEmailHistoryRepository.cs new file mode 100644 index 000000000..b2e4d1a29 --- /dev/null +++ b/Kavita.API/Repositories/IEmailHistoryRepository.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Email; + +namespace Kavita.API.Repositories; + +public interface IEmailHistoryRepository +{ + Task> GetEmailDtos(UserParams userParams, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IEpubFontRepository.cs b/Kavita.API/Repositories/IEpubFontRepository.cs new file mode 100644 index 000000000..80cf7bc9d --- /dev/null +++ b/Kavita.API/Repositories/IEpubFontRepository.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Font; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface IEpubFontRepository +{ + void Add(EpubFont font); + void Remove(EpubFont font); + void Update(EpubFont font); + Task> GetFontDtosAsync(CancellationToken ct = default); + Task GetFontDtoAsync(int fontId, CancellationToken ct = default); + Task GetFontDtoByNameAsync(string name, CancellationToken ct = default); + Task> GetFontsAsync(CancellationToken ct = default); + Task GetFontAsync(int fontId, CancellationToken ct = default); + Task IsFontInUseAsync(int fontId, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IExternalSeriesMetadataRepository.cs b/Kavita.API/Repositories/IExternalSeriesMetadataRepository.cs new file mode 100644 index 000000000..2a5f3168f --- /dev/null +++ b/Kavita.API/Repositories/IExternalSeriesMetadataRepository.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.KavitaPlus.Manage; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Metadata; + +namespace Kavita.API.Repositories; + +public interface IExternalSeriesMetadataRepository +{ + void Attach(ExternalSeriesMetadata metadata); + void Attach(ExternalRating rating); + void Attach(ExternalReview review); + void Remove(IEnumerable? reviews); + void Remove(IEnumerable? ratings); + void Remove(IEnumerable? recommendations); + void Remove(ExternalSeriesMetadata metadata); + Task GetExternalSeriesMetadata(int seriesId, CancellationToken ct = default); + Task NeedsDataRefresh(int seriesId, CancellationToken ct = default); + Task GetSeriesDetailPlusDto(int seriesId, CancellationToken ct = default); + Task LinkRecommendationsToSeries(Series series, CancellationToken ct = default); + Task IsBlacklistedSeries(int seriesId, CancellationToken ct = default); + Task> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false, CancellationToken ct = default); + Task> GetAllSeries(ManageMatchFilterDto filter, UserParams userParams, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IGenreRepository.cs b/Kavita.API/Repositories/IGenreRepository.cs new file mode 100644 index 000000000..b0bdf5895 --- /dev/null +++ b/Kavita.API/Repositories/IGenreRepository.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface IGenreRepository +{ + void Attach(Genre genre); + void Remove(Genre genre); + Task FindByNameAsync(string genreName, CancellationToken ct = default); + Task> GetAllGenresAsync(CancellationToken ct = default); + Task> GetAllGenresByNamesAsync(IEnumerable normalizedNames, CancellationToken ct = default); + Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false, CancellationToken ct = default); + Task> GetAllGenreDtosForLibrariesAsync(int userId, IList? libraryIds = null, QueryContext context = QueryContext.None, CancellationToken ct = default); + Task GetCountAsync(CancellationToken ct = default); + Task GetRandomGenre(CancellationToken ct = default); + Task GetGenreById(int id, CancellationToken ct = default); + Task> GetAllGenresNotInListAsync(ICollection genreNames, CancellationToken ct = default); + Task> GetBrowseableGenre(int userId, UserParams userParams, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/ILibraryRepository.cs b/Kavita.API/Repositories/ILibraryRepository.cs new file mode 100644 index 000000000..148e0102b --- /dev/null +++ b/Kavita.API/Repositories/ILibraryRepository.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.JumpBar; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Repositories; + +[Flags] +public enum LibraryIncludes +{ + None = 1 << 0, + Series = 1 << 1, + AppUser = 1 << 2, + Folders = 1 << 3, + FileTypes = 1 << 4, + ExcludePatterns = 1 << 5 +} + +public interface ILibraryRepository +{ + void Add(Library library); + void Update(Library library); + void Delete(Library? library); + Task> GetLibraryDtosAsync(CancellationToken ct = default); + Task GetLibraryDtoByIdAsync(int libraryId, CancellationToken ct = default); + Task GetLiteLibraryDtoByIdAsync(int libraryId, CancellationToken ct = default); + Task LibraryExists(string libraryName, CancellationToken ct = default); + Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None, CancellationToken ct = default); + Task> GetLibraryDtosForUsernameAsync(string userName, CancellationToken ct = default); + Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, bool track = true, CancellationToken ct = default); + Task> GetLibrariesForUserIdAsync(int userId, CancellationToken ct = default); + Task> GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None, CancellationToken ct = default); + Task GetLibraryTypeAsync(int libraryId, CancellationToken ct = default); + Task GetLibraryTypeBySeriesIdAsync(int seriesId, CancellationToken ct = default); + Task> GetLibraryForIdsAsync(IEnumerable libraryIds, LibraryIncludes includes = LibraryIncludes.None, CancellationToken ct = default); + Task GetTotalFiles(CancellationToken ct = default); + IEnumerable GetJumpBarAsync(int libraryId, CancellationToken ct = default); + Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds, CancellationToken ct = default); + Task> GetAllLanguagesForLibrariesAsync(List? libraryIds, CancellationToken ct = default); + IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds, CancellationToken ct = default); + Task DoAnySeriesFoldersMatch(IEnumerable folders, CancellationToken ct = default); + Task GetLibraryCoverImageAsync(int libraryId, CancellationToken ct = default); + Task> GetAllCoverImagesAsync(CancellationToken ct = default); + Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, CancellationToken ct = default); + Task GetAllowsScrobblingBySeriesId(int seriesId, CancellationToken ct = default); + + Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IMangaFileRepository.cs b/Kavita.API/Repositories/IMangaFileRepository.cs new file mode 100644 index 000000000..6fbf6b46c --- /dev/null +++ b/Kavita.API/Repositories/IMangaFileRepository.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface IMangaFileRepository +{ + void Update(MangaFile file); + Task> GetAllWithMissingExtension(CancellationToken ct = default); + Task GetByKoreaderHash(string hash, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IMediaErrorRepository.cs b/Kavita.API/Repositories/IMediaErrorRepository.cs new file mode 100644 index 000000000..b99b88604 --- /dev/null +++ b/Kavita.API/Repositories/IMediaErrorRepository.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.MediaErrors; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface IMediaErrorRepository +{ + void Attach(MediaError error); + void Remove(MediaError error); + void Remove(IList errors); + Task Find(string filename, CancellationToken ct = default); + Task> GetAllErrorDtosAsync(CancellationToken ct = default); + Task ExistsAsync(MediaError error, CancellationToken ct = default); + Task DeleteAll(CancellationToken ct = default); + Task> GetAllErrorsAsync(IList comments, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IPersonRepository.cs b/Kavita.API/Repositories/IPersonRepository.cs new file mode 100644 index 000000000..79c62ded1 --- /dev/null +++ b/Kavita.API/Repositories/IPersonRepository.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.DTOs.Person; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; + +namespace Kavita.API.Repositories; + +[Flags] +public enum PersonIncludes +{ + None = 1 << 0, + Aliases = 1 << 1, + ChapterPeople = 1 << 2, + SeriesPeople = 1 << 3, + + All = Aliases | ChapterPeople | SeriesPeople, +} + +public interface IPersonRepository +{ + void Attach(Person person); + void Attach(IEnumerable person); + void Remove(Person person); + void Remove(ChapterPeople person); + void Remove(SeriesMetadataPeople person); + void Update(Person person); + + Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default); + Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None, CancellationToken ct = default); + Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None, CancellationToken ct = default); + Task RemoveAllPeopleNoLongerAssociated(CancellationToken ct = default); + Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, PersonIncludes includes = PersonIncludes.None, CancellationToken ct = default); + + Task GetCoverImageAsync(int personId, CancellationToken ct = default); + Task> GetAllCoverImagesAsync(CancellationToken ct = default); + Task GetCoverImageByNameAsync(string name, CancellationToken ct = default); + Task> GetRolesForPersonByName(int personId, int userId, CancellationToken ct = default); + Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams, CancellationToken ct = default); + Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None, CancellationToken ct = default); + Task GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default); + + /// + /// Returns a person matched on a normalized name or alias + /// + /// + /// + /// + /// + Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default); + Task IsNameUnique(string name, CancellationToken ct = default); + + Task> GetSeriesKnownFor(int personId, int userId, CancellationToken ct = default); + Task> GetChaptersForPersonByRole(int personId, int userId, PersonRole role, CancellationToken ct = default); + + /// + /// Returns all people with a matching name, or alias + /// + /// + /// + /// + /// + Task> GetPeopleByNames(List normalizedNames, PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default); + Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default); + + Task> SearchPeople(string searchQuery, PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default); + + Task AnyAliasExist(string alias, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IReadingListRepository.cs b/Kavita.API/Repositories/IReadingListRepository.cs new file mode 100644 index 000000000..569ad3d90 --- /dev/null +++ b/Kavita.API/Repositories/IReadingListRepository.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Repositories; + +[Flags] +public enum ReadingListIncludes +{ + None = 1 << 0, + Items = 1 << 1, + ItemChapter = 1 << 2, +} + +public interface IReadingListRepository +{ + void Remove(ReadingListItem item); + void Add(ReadingList list); + void BulkRemove(IEnumerable items); + void Update(ReadingList list); + + Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true, CancellationToken ct = default); + Task GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None, CancellationToken ct = default); + Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId, UserParams? userParams = null, CancellationToken ct = default); + Task GetReadingListDtoByIdAsync(int readingListId, int userId, CancellationToken ct = default); + Task GetReadingListDtoByTitleAsync(int userId, string title, CancellationToken ct = default); + Task> GetReadingListItemsByIdAsync(int readingListId, CancellationToken ct = default); + Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, + bool includePromoted, CancellationToken ct = default); + Task> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, + bool includePromoted, CancellationToken ct = default); + Task Count(CancellationToken ct = default); + Task GetCoverImageAsync(int readingListId, CancellationToken ct = default); + Task> GetRandomCoverImagesAsync(int readingListId, CancellationToken ct = default); + Task> GetAllCoverImagesAsync(CancellationToken ct = default); + Task ReadingListExists(string name, int? readingListId = null, CancellationToken ct = default); + Task ReadingListExistsForUser(string name, int userId, CancellationToken ct = default); + IEnumerable GetReadingListPeopleAsync(int readingListId, PersonRole role, CancellationToken ct = default); + Task GetReadingListAllPeopleAsync(int readingListId, CancellationToken ct = default); + Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, CancellationToken ct = default); + Task RemoveReadingListsWithoutSeries(CancellationToken ct = default); + Task GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items, CancellationToken ct = default); + Task> GetReadingListsByIds(IList ids, ReadingListIncludes includes = ReadingListIncludes.Items, CancellationToken ct = default); + Task> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items, CancellationToken ct = default); + Task GetReadingListInfoAsync(int readingListId, CancellationToken ct = default); + Task AnyUserReadingProgressAsync(int readingListId, int userId, CancellationToken ct = default); + Task GetContinueReadingPoint(int readingListId, int userId, CancellationToken ct = default); + Task GetReadingListItemCountAsync(int readingListId, int userId, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IReadingSessionRepository.cs b/Kavita.API/Repositories/IReadingSessionRepository.cs new file mode 100644 index 000000000..1037b7fed --- /dev/null +++ b/Kavita.API/Repositories/IReadingSessionRepository.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Progress; + +namespace Kavita.API.Repositories; + +public interface IReadingSessionRepository +{ + Task> GetAllReadingSessionAsync(bool isActiveOnly = true, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IScrobbleRepository.cs b/Kavita.API/Repositories/IScrobbleRepository.cs new file mode 100644 index 000000000..32e820e8c --- /dev/null +++ b/Kavita.API/Repositories/IScrobbleRepository.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Scrobble; + +namespace Kavita.API.Repositories; + +public interface IScrobbleRepository +{ + void Attach(ScrobbleEvent evt); + void Attach(ScrobbleError error); + void Remove(ScrobbleEvent evt); + void Remove(IEnumerable events); + void Remove(IEnumerable errors); + void Update(ScrobbleEvent evt); + Task> GetByEvent(ScrobbleEventType type, bool isProcessed = false, CancellationToken ct = default); + Task> GetProcessedEvents(int daysAgo, CancellationToken ct = default); + Task Exists(int userId, int seriesId, ScrobbleEventType eventType, CancellationToken ct = default); + Task> GetScrobbleErrors(CancellationToken ct = default); + Task> GetAllScrobbleErrorsForSeries(int seriesId, CancellationToken ct = default); + Task ClearScrobbleErrors(CancellationToken ct = default); + Task HasErrorForSeries(int seriesId, CancellationToken ct = default); + + /// + /// Get all events for a specific user and type + /// + /// + /// + /// + /// If true, only returned not processed events + /// + /// + Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false, CancellationToken ct = default); + Task> GetUserEventsForSeries(int userId, int seriesId, CancellationToken ct = default); + + /// + /// Return the events with given ids, when belonging to the passed user + /// + /// + /// + /// + /// + Task> GetUserEvents(int userId, IList scrobbleEventIds, CancellationToken ct = default); + Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination, CancellationToken ct = default); + Task> GetAllEventsForSeries(int seriesId, CancellationToken ct = default); + Task> GetAllEventsWithSeriesIds(IEnumerable seriesIds, CancellationToken ct = default); + Task> GetEvents(CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/ISeriesMetadataRepository.cs b/Kavita.API/Repositories/ISeriesMetadataRepository.cs new file mode 100644 index 000000000..d5fe50cbc --- /dev/null +++ b/Kavita.API/Repositories/ISeriesMetadataRepository.cs @@ -0,0 +1,8 @@ +using Kavita.Models.Entities.Metadata; + +namespace Kavita.API.Repositories; + +public interface ISeriesMetadataRepository +{ + void Update(SeriesMetadata seriesMetadata); +} diff --git a/Kavita.API/Repositories/ISeriesRepository.cs b/Kavita.API/Repositories/ISeriesRepository.cs new file mode 100644 index 000000000..19972d6ac --- /dev/null +++ b/Kavita.API/Repositories/ISeriesRepository.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.Search; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.User; +using Kavita.Models.Misc; +using Kavita.Models.Parser; + +namespace Kavita.API.Repositories; + +[Flags] +public enum SeriesIncludes +{ + None = 1 << 0, + Volumes = 1 << 1, + /// + /// This will include all necessary includes + /// + Metadata = 1 << 2, + Related = 1 << 3, + Library = 1 << 4, + Chapters = 1 << 5, + ExternalReviews = 1 << 6, + ExternalRatings = 1 << 7, + ExternalRecommendations = 1 << 8, + ExternalMetadata = 1 << 9, + + ExternalData = ExternalMetadata | ExternalReviews | ExternalRatings | ExternalRecommendations, +} + +/// +/// For complex queries, Library has certain restrictions where the library should not be included in results. +/// This enum dictates which field to use for the lookup. +/// +public enum QueryContext +{ + None = 1, + Search = 2, + [Obsolete("Use Dashboard")] + Recommended = 3, + Dashboard = 4, +} + +public interface ISeriesRepository +{ + void Add(Series series); + void Attach(SeriesRelation relation); + void Update(Series series); + void Update(SeriesMetadata seriesMetadata); + void Remove(Series series); + void Remove(IEnumerable series); + Task DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format, CancellationToken ct = default); + + /// + /// Adds user information like progress, ratings, etc + /// + /// + /// + /// Pagination info + /// Filtering/Sorting to apply + /// + /// + Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter, CancellationToken ct = default); + + /// + /// Does not add user information like progress, ratings, etc. + /// + /// + /// + /// + /// + /// Includes Files in the Search + /// + /// + Task SearchSeries(int userId, bool isAdmin, IList libraryIds, string searchQuery, bool includeChapterAndFiles = true, CancellationToken ct = default); + Task> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default); + Task GetSeriesDtoByIdAsync(int seriesId, int userId, CancellationToken ct = default); + Task GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata, CancellationToken ct = default); + Task> GetSeriesDtoByIdsAsync(IEnumerable seriesIds, AppUser user, CancellationToken ct = default); + Task> GetSeriesByIdsAsync(IList seriesIds, bool fullSeries = true, CancellationToken ct = default); + Task GetChapterIdsForSeriesAsync(IList seriesIds, CancellationToken ct = default); + Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds, CancellationToken ct = default); + Task GetFilesizeForSeriesAsync(int seriesId, CancellationToken ct = default); + Task> GetFilesizeForMultipleSeriesAsync(IList seriesIds, CancellationToken ct = default); + Task GetSeriesCoverImageAsync(int seriesId, CancellationToken ct = default); + Task> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto? filter, CancellationToken ct = default); + Task> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter, CancellationToken ct = default); + Task> GetRecentlyAddedV2(int userId, UserParams userParams, FilterV2Dto filter, CancellationToken ct = default); + Task GetSeriesMetadata(int seriesId, CancellationToken ct = default); + Task> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams, CancellationToken ct = default); + Task> GetFilesForSeries(int seriesId, CancellationToken ct = default); + Task> GetSeriesDtoForIdsAsync(IEnumerable seriesIds, int userId, CancellationToken ct = default); + Task> GetAllCoverImagesAsync(CancellationToken ct = default); + Task> GetLockedCoverImagesAsync(CancellationToken ct = default); + Task> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams, CancellationToken ct = default); + Task GetFullSeriesForSeriesIdAsync(int seriesId, CancellationToken ct = default); + Task GetChunkInfo(int libraryId = 0, CancellationToken ct = default); + Task> GetRecentlyUpdatedSeries(int userId, UserParams? userParams, CancellationToken ct = default); + Task GetRelatedSeries(int userId, int seriesId, CancellationToken ct = default); + Task> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind, CancellationToken ct = default); + Task> GetQuickReads(int userId, int libraryId, UserParams userParams, CancellationToken ct = default); + Task> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams, CancellationToken ct = default); + Task> GetHighlyRated(int userId, int libraryId, UserParams userParams, CancellationToken ct = default); + Task> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams, CancellationToken ct = default); + Task> GetRediscover(int userId, int libraryId, UserParams userParams, CancellationToken ct = default); + Task GetSeriesForMangaFile(int mangaFileId, int userId, CancellationToken ct = default); + Task GetSeriesForChapter(int chapterId, int userId, CancellationToken ct = default); + Task> GetWantToReadForUserAsync(int userId, UserParams userParams, FilterDto filter, CancellationToken ct = default); + Task> GetWantToReadForUserV2Async(int userId, UserParams userParams, FilterV2Dto filter, CancellationToken ct = default); + Task> GetWantToReadForUserAsync(int userId, CancellationToken ct = default); + Task IsSeriesInWantToRead(int userId, int seriesId, CancellationToken ct = default); + Task GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default); + Task GetSeriesThatContainsLowestFolderPath(string path, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default); + Task> GetAllSeriesByNameAsync(IList normalizedNames, + int userId, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default); + Task GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true, CancellationToken ct = default); + Task GetSeriesByAnyName(IList names, IList formats, + int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default); + Task GetSeriesByAnyName(string seriesName, string localizedName, IList formats, int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default); + public Task> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId, + MangaFormat format, CancellationToken ct = default); + Task> RemoveSeriesNotInList(IList seenSeries, int libraryId, CancellationToken ct = default); + Task>> GetFolderPathMap(int libraryId, CancellationToken ct = default); + Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds, CancellationToken ct = default); + Task> GetSeriesMetadataForIds(IEnumerable seriesIds, CancellationToken ct = default); + Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true, CancellationToken ct = default); + Task GetSeriesDtoByNamesAndMetadataIds(IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl, CancellationToken ct = default); + Task GetAverageUserRating(int seriesId, int userId, CancellationToken ct = default); + Task RemoveFromOnDeck(int seriesId, int userId, CancellationToken ct = default); + Task ClearOnDeckRemoval(int seriesId, int userId, CancellationToken ct = default); + Task> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None, CancellationToken ct = default); + Task GetPlusSeriesDto(int seriesId, CancellationToken ct = default); + Task MatchSeries(ExternalSeriesDetailDto externalSeries, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/ISettingsRepository.cs b/Kavita.API/Repositories/ISettingsRepository.cs new file mode 100644 index 000000000..e70bec252 --- /dev/null +++ b/Kavita.API/Repositories/ISettingsRepository.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; + +namespace Kavita.API.Repositories; + +public interface ISettingsRepository +{ + void Update(ServerSetting settings); + void Update(MetadataSettings settings); + void Remove(ServerSetting setting); + void RemoveRange(List fieldMappings); + Task GetSettingsDtoAsync(CancellationToken ct = default); + Task GetSettingAsync(ServerSettingKey key, CancellationToken ct = default); + Task> GetSettingsAsync(CancellationToken ct = default); + Task GetExternalSeriesMetadata(int seriesId, CancellationToken ct = default); + Task GetMetadataSettings(CancellationToken ct = default); + Task GetMetadataSettingDto(CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/ISiteThemeRepository.cs b/Kavita.API/Repositories/ISiteThemeRepository.cs new file mode 100644 index 000000000..9a8c6bbc7 --- /dev/null +++ b/Kavita.API/Repositories/ISiteThemeRepository.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface ISiteThemeRepository +{ + void Add(SiteTheme theme); + void Remove(SiteTheme theme); + void Update(SiteTheme siteTheme); + Task> GetThemeDtos(); + Task GetThemeDto(int themeId); + Task GetThemeDtoByName(string themeName); + Task GetDefaultTheme(); + Task> GetThemes(); + Task GetTheme(int themeId); + Task IsThemeInUse(int themeId); +} diff --git a/Kavita.API/Repositories/ITagRepository.cs b/Kavita.API/Repositories/ITagRepository.cs new file mode 100644 index 000000000..0bd075f30 --- /dev/null +++ b/Kavita.API/Repositories/ITagRepository.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface ITagRepository +{ + void Attach(Tag tag); + void Remove(Tag tag); + Task> GetAllTagsAsync(CancellationToken ct = default); + Task> GetAllTagsByNameAsync(IEnumerable normalizedNames, CancellationToken ct = default); + Task> GetAllTagDtosAsync(int userId, CancellationToken ct = default); + Task RemoveAllTagNoLongerAssociated(CancellationToken ct = default); + Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null, CancellationToken ct = default); + Task> GetAllTagsNotInListAsync(ICollection tags, CancellationToken ct = default); + Task> GetBrowseableTag(int userId, UserParams userParams, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IUserRepository.cs b/Kavita.API/Repositories/IUserRepository.cs new file mode 100644 index 000000000..d5940aebb --- /dev/null +++ b/Kavita.API/Repositories/IUserRepository.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.KavitaPlus.Account; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +[Flags] +public enum AppUserIncludes +{ + None = 1, + Progress = 1 << 1, + Bookmarks = 1 << 2, + ReadingLists = 1 << 3, + Ratings = 1 << 4, + UserPreferences = 1 << 5, + WantToRead = 1 << 6, + ReadingListsWithItems = 1 << 7, + Devices = 1 << 8, + ScrobbleHolds = 1 << 9, + SmartFilters = 1 << 10, + DashboardStreams = 1 << 11, + SideNavStreams = 1 << 12, + ExternalSources = 1 << 13, + Collections = 1 << 14, + ChapterRatings = 1 << 15, + AuthKeys = 1 << 16 +} + +public interface IUserRepository +{ + #region Synchronous CRUD + void Add(AppUserAuthKey key); + void Add(AppUserBookmark bookmark); + void Add(AppUser bookmark); + void Update(AppUser user); + void Update(AppUserPreferences preferences); + void Update(AppUserBookmark bookmark); + void Update(AppUserDashboardStream stream); + void Update(AppUserSideNavStream stream); + void Delete(AppUser? user); + void Delete(AppUserAuthKey? key); + void Delete(AppUserBookmark bookmark); + void Delete(IEnumerable streams); + void Delete(AppUserDashboardStream stream); + void Delete(IEnumerable streams); + void Delete(AppUserSideNavStream stream); + #endregion + + #region User Retrieval + Task> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true, CancellationToken ct = default); + Task> GetAdminUsersAsync(CancellationToken ct = default); + Task IsUserAdminAsync(AppUser? user, CancellationToken ct = default); + Task> GetRoles(int userId, CancellationToken ct = default); + Task> GetRolesByAuthKey(string? apiKey, CancellationToken ct = default); + Task GetUserDtoByAuthKeyAsync(string authKey, CancellationToken ct = default); + Task GetUserIdByAuthKeyAsync(string authKey, CancellationToken ct = default); + Task GetUserDtoById(int userId, CancellationToken ct = default); + Task GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None, CancellationToken ct = default); + Task GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None, CancellationToken ct = default); + Task GetUserByAuthKey(string authKey, AppUserIncludes includeFlags = AppUserIncludes.None, CancellationToken ct = default); + Task GetUserIdByUsernameAsync(string username, CancellationToken ct = default); + Task GetUserByEmailAsync(string email, AppUserIncludes includes = AppUserIncludes.None, CancellationToken ct = default); + Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true, CancellationToken ct = default); + Task GetUserByConfirmationToken(string token, CancellationToken ct = default); + Task GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None, CancellationToken ct = default); + Task> GetUserTokenInfo(CancellationToken ct = default); + Task GetUserByDeviceEmail(string deviceEmail, CancellationToken ct = default); + Task GetByOidcId(string? oidcId, AppUserIncludes includes = AppUserIncludes.None, CancellationToken ct = default); + Task UpdateUserAsActive(int userId, CancellationToken ct = default); + #endregion + + #region Ratings & Reviews + Task GetUserRatingAsync(int seriesId, int userId, CancellationToken ct = default); + Task GetUserChapterRatingAsync(int userId, int chapterId, CancellationToken ct = default); + Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId, CancellationToken ct = default); + Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId, CancellationToken ct = default); + Task> GetSeriesWithRatings(int userId, CancellationToken ct = default); + Task> GetSeriesWithReviews(int userId, CancellationToken ct = default); + Task> GetAllReviewsForUser(int userId, int requestingUserId, string? query = null, float? ratingFilter = null, CancellationToken ct = default); + #endregion + + #region Bookmarks + Task> GetBookmarkDtosForSeries(int userId, int seriesId, CancellationToken ct = default); + Task> GetBookmarkDtosForVolume(int userId, int volumeId, CancellationToken ct = default); + Task> GetBookmarkDtosForChapter(int userId, int chapterId, CancellationToken ct = default); + Task> GetAllBookmarkDtos(int userId, FilterV2Dto filter, CancellationToken ct = default); + Task> GetAllBookmarksAsync(CancellationToken ct = default); + Task GetBookmarkForPage(int page, int chapterId, int imageOffset, int userId, CancellationToken ct = default); + Task GetBookmarkAsync(int bookmarkId, CancellationToken ct = default); + Task> GetAllBookmarksByIds(IList bookmarkIds, CancellationToken ct = default); + #endregion + + #region Preferences & Settings + Task GetPreferencesAsync(string username, CancellationToken ct = default); + Task> GetAllPreferencesByThemeAsync(int themeId, CancellationToken ct = default); + Task> GetAllPreferencesByFontAsync(string fontName, CancellationToken ct = default); + Task GetLocale(int userId, CancellationToken ct = default); + Task GetSocialPreferencesForUser(int userId, CancellationToken ct = default); + Task GetPreferencesForUser(int userId, CancellationToken ct = default); + Task GetOpdsPreferences(int userId, CancellationToken ct = default); + #endregion + + #region Permissions + Task HasAccessToLibrary(int libraryId, int userId, CancellationToken ct = default); + Task HasAccessToSeries(int userId, int seriesId, CancellationToken ct = default); + Task HasAccessToVolume(int userId, int volumeId, CancellationToken ct = default); + Task HasAccessToChapter(int userId, int chapterId, CancellationToken ct = default); + Task HasAccessToPerson(int userId, int personId, CancellationToken ct = default); + Task HasAccessToReadingList(int userId, int readingListId, CancellationToken ct = default); + #endregion + + #region Scrobbling & Holds + Task HasHoldOnSeries(int userId, int seriesId, CancellationToken ct = default); + Task> GetHolds(int userId, CancellationToken ct = default); + #endregion + + #region Streams (Dashboard & SideNav) + Task> GetDashboardStreams(int userId, bool visibleOnly = false, CancellationToken ct = default); + Task> GetAllDashboardStreams(CancellationToken ct = default); + Task GetDashboardStream(int streamId, CancellationToken ct = default); + Task> GetDashboardStreamWithFilter(int filterId, CancellationToken ct = default); + Task> GetSideNavStreams(int userId, bool visibleOnly = false, CancellationToken ct = default); + Task GetSideNavStream(int streamId, CancellationToken ct = default); + Task GetSideNavStreamWithUser(int streamId, CancellationToken ct = default); + Task> GetSideNavStreamWithFilter(int filterId, CancellationToken ct = default); + Task> GetSideNavStreamsByLibraryId(int libraryId, CancellationToken ct = default); + Task> GetSideNavStreamWithExternalSource(int externalSourceId, CancellationToken ct = default); + Task> GetDashboardStreamsByIds(IList streamIds, CancellationToken ct = default); + #endregion + + #region Annotations + Task> GetAnnotations(int userId, int chapterId, CancellationToken ct = default); + Task> GetAnnotationsByPage(int userId, int chapterId, int pageNum, CancellationToken ct = default); + Task GetAnnotationDtoById(int userId, int annotationId, CancellationToken ct = default); + Task> GetAnnotationDtosBySeries(int userId, int seriesId, CancellationToken ct = default); + #endregion + + #region Images & Media + Task GetCoverImageAsync(int userId, CancellationToken ct = default); + Task GetPersonCoverImageAsync(int personId, CancellationToken ct = default); + #endregion + + #region Auth Keys + Task> GetAuthKeysForUserId(int userId, CancellationToken ct = default); + Task> GetAllAuthKeysDtosWithExpiration(CancellationToken ct = default); + Task GetAuthKeyById(int authKeyId, CancellationToken ct = default); + Task GetAuthKeyExpiration(string authKey, int userId, CancellationToken ct = default); + #endregion +} diff --git a/Kavita.API/Repositories/IUserTableOfContentRepository.cs b/Kavita.API/Repositories/IUserTableOfContentRepository.cs new file mode 100644 index 000000000..37771a03e --- /dev/null +++ b/Kavita.API/Repositories/IUserTableOfContentRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +public interface IUserTableOfContentRepository +{ + void Attach(AppUserTableOfContent toc); + void Remove(AppUserTableOfContent toc); + Task IsUnique(int userId, int chapterId, int page, string title); + Task> GetPersonalToC(int userId, int chapterId); + Task> GetPersonalToCForPage(int userId, int chapterId, int page); + Task Get(int userId, int chapterId, int pageNum, string title); +} diff --git a/Kavita.API/Repositories/IVolumeRepository.cs b/Kavita.API/Repositories/IVolumeRepository.cs new file mode 100644 index 000000000..b78ff110f --- /dev/null +++ b/Kavita.API/Repositories/IVolumeRepository.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Repositories; + +[Flags] +public enum VolumeIncludes +{ + None = 1 << 0, + Chapters = 1 << 1, + People = 1 << 2, + Tags = 1 << 3, + /// + /// This will include Chapters by default + /// + Files = 1 << 4 +} + +public interface IVolumeRepository +{ + void Add(Volume volume); + void Update(Volume volume); + void Remove(Volume volume); + void Remove(IList volumes); + Task> GetFilesForVolume(int volumeId, CancellationToken ct = default); + Task GetVolumeCoverImageAsync(int volumeId, CancellationToken ct = default); + Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds, CancellationToken ct = default); + Task> GetVolumesDtoAsync(int seriesId, int userId, VolumeIncludes includes = VolumeIncludes.Chapters, CancellationToken ct = default); + Task GetVolumeByIdAsync(int volumeId, VolumeIncludes includes = VolumeIncludes.Files, CancellationToken ct = default); + Task GetVolumeDtoAsync(int volumeId, int userId, CancellationToken ct = default); + Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false, CancellationToken ct = default); + Task> GetVolumes(int seriesId, CancellationToken ct = default); + Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None, CancellationToken ct = default); + Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, CancellationToken ct = default); + Task> GetCoverImagesForLockedVolumesAsync(CancellationToken ct = default); + Task GetFilesizeForVolumeAsync(int volumeId, CancellationToken ct = default); + Task> GetFilesizeForVolumesAsync(IList volumeIds, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Helpers/ICacheHelper.cs b/Kavita.API/Services/Helpers/ICacheHelper.cs new file mode 100644 index 000000000..f2248ae0a --- /dev/null +++ b/Kavita.API/Services/Helpers/ICacheHelper.cs @@ -0,0 +1,18 @@ +using System; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Interfaces; + +namespace Kavita.API.Services.Helpers; + +public interface ICacheHelper +{ + bool ShouldUpdateCoverImage(string coverPath, MangaFile? firstFile, DateTime chapterCreated, + bool forceUpdate = false, + bool isCoverLocked = false); + + bool CoverImageExists(string path); + + bool IsFileUnmodifiedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile? firstFile); + bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile? firstFile); + +} diff --git a/Kavita.API/Services/IAccountService.cs b/Kavita.API/Services/IAccountService.cs new file mode 100644 index 000000000..76e608fa4 --- /dev/null +++ b/Kavita.API/Services/IAccountService.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Errors; +using Kavita.Common; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Microsoft.AspNetCore.Identity; + +namespace Kavita.API.Services; + +public interface IAccountService +{ + Task> ChangeUserPassword(AppUser user, string newPassword, CancellationToken ct = default); + Task> ValidatePassword(AppUser user, string password, CancellationToken ct = default); + Task> ValidateUsername(string? username, CancellationToken ct = default); + Task> ValidateEmail(string email, CancellationToken ct = default); + Task CanChangeAgeRestriction(AppUser? user, CancellationToken ct = default); + + /// + /// + /// + /// The user who is changing the identity + /// the user being changed + /// the provider being changed to + /// + /// If true, user should not be updated by kavita (anymore) + /// Throws if invalid actions are being performed + Task ChangeIdentityProvider(int actingUserId, AppUser user, IdentityProvider identityProvider, CancellationToken ct = default); + + /// + /// Removes access to all libraries, then grant access to all given libraries or all libraries if the user is admin. + /// Creates side nav streams as well + /// + /// + /// + /// + /// + /// + /// Ensure that the users SideNavStreams are loaded + /// Does NOT commit + Task UpdateLibrariesForUser(AppUser user, IList librariesIds, bool hasAdminRole, CancellationToken ct = default); + Task> UpdateRolesForUser(AppUser user, IList roles, CancellationToken ct = default); + + /// + /// Seeds all information necessary for a new user + /// + /// + /// + /// + Task SeedUser(AppUser user, CancellationToken ct = default); + void AddDefaultStreamsToUser(AppUser user, CancellationToken ct = default); + Task AddDefaultReadingProfileToUser(AppUser user, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IAnnotationService.cs b/Kavita.API/Services/IAnnotationService.cs new file mode 100644 index 000000000..4cc903591 --- /dev/null +++ b/Kavita.API/Services/IAnnotationService.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Reader; + +namespace Kavita.API.Services; + +public interface IAnnotationService +{ + Task CreateAnnotation(int userId, AnnotationDto dto, CancellationToken ct = default); + Task UpdateAnnotation(int userId, AnnotationDto dto, CancellationToken ct = default); + + /// + /// Export all annotations for a user, or optionally specify which annotation exactly + /// + /// + /// + /// + /// + Task ExportAnnotations(int userId, IList? annotationIds = null, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IArchiveService.cs b/Kavita.API/Services/IArchiveService.cs new file mode 100644 index 000000000..8eb5683da --- /dev/null +++ b/Kavita.API/Services/IArchiveService.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.IO.Compression; +using System.Threading.Tasks; +using Kavita.Common; +using Kavita.Models.DTOs.Archive; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; + +namespace Kavita.API.Services; + +public interface IArchiveService +{ + void ExtractArchive(string archivePath, string extractPath); + int GetNumberOfPagesFromArchive(string archivePath); + string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format, CoverImageSize size = CoverImageSize.Default); + bool IsValidArchive(string archivePath); + ComicInfo? GetComicInfo(string archivePath); + ArchiveLibrary CanOpen(string archivePath); + bool ArchiveNeedsFlattening(ZipArchive archive); + /// + /// Creates a zip file form the listed files and outputs to the temp folder. This will combine into one zip of multiple zips. + /// + /// List of files to be zipped up. Should be full file paths. + /// Temp folder name to use for preparing the files. Will be created and deleted + /// Path to the temp zip + /// + string CreateZipForDownload(IEnumerable files, string tempFolder); + + /// + /// Creates a zip file form the listed files and outputs to the temp folder. This will extract each archive and combine them into one zip. + /// + /// List of files to be zipped up. Should be full file paths. + /// Temp folder name to use for preparing the files. Will be created and deleted + /// + /// Path to the temp zip + /// + string CreateZipFromFoldersForDownload(IList files, string tempFolder, Func, Task> progressCallback); +} diff --git a/Kavita.API/Services/IAuthKeyService.cs b/Kavita.API/Services/IAuthKeyService.cs new file mode 100644 index 000000000..a5413239c --- /dev/null +++ b/Kavita.API/Services/IAuthKeyService.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services; + +public interface IAuthKeyService +{ + Task UpdateLastAccessedAsync(string authKey, CancellationToken ct = default); + + /// + /// Invalidates the cached authentication data for a specific auth key. + /// Call this when a key is rotated or deleted. + /// + /// The actual key value (not the ID) + /// Cancellation token + Task InvalidateAsync(string keyValue, CancellationToken cancellationToken = default); + + string CreateCacheKey(string keyValue); +} diff --git a/Kavita.API/Services/IBackupService.cs b/Kavita.API/Services/IBackupService.cs new file mode 100644 index 000000000..0b71d53e0 --- /dev/null +++ b/Kavita.API/Services/IBackupService.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services; + +public interface IBackupService +{ + public const string LogFile = "config/logs/kavita.log"; + + Task BackupDatabase(CancellationToken ct = default); + /// + /// Returns a list of all log files for Kavita + /// + /// If file rolling is enabled. Defaults to True. + /// + IEnumerable GetLogFiles(bool rollFiles = true); +} diff --git a/Kavita.API/Services/IBookService.cs b/Kavita.API/Services/IBookService.cs new file mode 100644 index 000000000..40b819a70 --- /dev/null +++ b/Kavita.API/Services/IBookService.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using VersOne.Epub; + +namespace Kavita.API.Services; + +public interface IBookService +{ + int GetNumberOfPages(string filePath); + string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + ComicInfo? GetComicInfo(string filePath); + ParserInfo? ParseInfo(string filePath); + + /// + /// Scopes styles to .reading-section and replaces img src to the passed apiBase + /// + /// + /// + /// If the stylesheetHtml contains Import statements, when scoping the filename, scope needs to be wrt filepath. + /// Book Reference, needed for if you expect Import statements + /// + /// + Task ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book, CancellationToken ct = default); + /// + /// Extracts a PDF file's pages as images to a target directory + /// + /// This method relies on Docnet which has explicit patches from Kavita for ARM support. This should only be used with Tachiyomi + /// + /// Where the files will be extracted to. If doesn't exist, will be created. + void ExtractPdfImages(string fileFilePath, string targetDirectory); + Task> GenerateTableOfContents(Chapter chapter, CancellationToken ct = default); + /// + /// This returns a single page within the epub book. All html will be rewritten to be scoped within our reader, + /// all css is scoped, etc. + /// + /// + /// The requested page + /// The chapterId + /// The path to the cached epub file + /// The API base for Kavita, to rewrite urls to so we load though our endpoint + /// + /// + /// + /// Full epub HTML Page, scoped to Kavita's reader + /// All exceptions throw this + Task GetBookPage(int userId, int page, int chapterId, string cachedEpubPath, string baseUrl, List ptocBookmarks, List annotations, CancellationToken ct = default); + Task> CreateKeyToPageMappingAsync(EpubBookRef book, CancellationToken ct = default); + Task?> GetWordCountsPerPage(string bookFilePath, CancellationToken ct = default); + Task GetWordCountBetweenXPaths(string bookFilePath, string startXpath, int startPage, string endXpath, int endPage, CancellationToken ct = default); + Task CopyImageToTempFromBook(int chapterId, BookmarkDto bookmarkDto, string cachedBookPath, CancellationToken ct = default); + Task GetResourceAsync(string bookFilePath, string requestedKey, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IBookmarkService.cs b/Kavita.API/Services/IBookmarkService.cs new file mode 100644 index 000000000..109ef38c6 --- /dev/null +++ b/Kavita.API/Services/IBookmarkService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface IBookmarkService +{ + Task DeleteBookmarkFiles(IEnumerable bookmarks, CancellationToken ct = default); + Task BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark, CancellationToken ct = default); + Task RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, CancellationToken ct = default); + Task> GetBookmarkFilesById(IEnumerable bookmarkIds, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/ICacheService.cs b/Kavita.API/Services/ICacheService.cs new file mode 100644 index 000000000..75fc3df9d --- /dev/null +++ b/Kavita.API/Services/ICacheService.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities; + +namespace Kavita.API.Services; + +public interface ICacheService +{ + /// + /// Ensures the cache is created for the given chapter and if not, will create it. Should be called before any other + /// cache operations (except cleanup). + /// + /// + /// Extracts a PDF into images for a different reading experience + /// + /// Chapter for the passed chapterId. Side-effect from ensuring cache. + Task Ensure(int chapterId, bool extractPdfToImages = false, CancellationToken ct = default); + /// + /// Clears cache directory of all volumes. This can be invoked from deleting a library or a series. + /// + /// Volumes that belong to that library. Assume the library might have been deleted before this invocation. + void CleanupChapters(IEnumerable chapterIds); + void CleanupBookmarks(IEnumerable seriesIds); + string GetCachedPagePath(int chapterId, int page); + string GetCachePath(int chapterId); + string GetBookmarkCachePath(int seriesId); + IEnumerable GetCachedPages(int chapterId); + IEnumerable GetCachedFileDimensions(string cachePath); + string GetCachedBookmarkPagePath(int seriesId, int page); + string GetCachedFile(Chapter chapter); + string GetCachedFile(int chapterId, string firstFilePath); + public void ExtractChapterFiles(string extractPath, IReadOnlyList files, bool extractPdfImages = false); + Task CacheBookmarkForSeries(int userId, int seriesId, CancellationToken ct = default); + void CleanupBookmarkCache(int seriesId); +} diff --git a/Kavita.API/Services/ICleanupService.cs b/Kavita.API/Services/ICleanupService.cs new file mode 100644 index 000000000..888f60f42 --- /dev/null +++ b/Kavita.API/Services/ICleanupService.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services; + +public interface ICleanupService +{ + Task Cleanup(CancellationToken ct = default); + Task CleanupDbEntries(CancellationToken ct = default); + Task CleanupCacheAndTempDirectories(CancellationToken ct = default); + void CleanupCacheDirectory(); + Task DeleteSeriesCoverImages(CancellationToken ct = default); + Task DeleteChapterCoverImages(CancellationToken ct = default); + Task DeleteTagCoverImages(CancellationToken ct = default); + Task CleanupBackups(CancellationToken ct = default); + Task CleanupLogs(CancellationToken ct = default); + void CleanupTemp(); + Task EnsureChapterProgressIsCapped(CancellationToken ct = default); + /// + /// Responsible to remove Series from Want To Read when user's have fully read the series and the series has Publication Status of Completed or Cancelled. + /// + /// + Task CleanupWantToRead(CancellationToken ct = default); + + Task ConsolidateProgress(CancellationToken ct = default); + + Task CleanupMediaErrors(CancellationToken ct = default); + +} diff --git a/Kavita.API/Services/IClientDeviceService.cs b/Kavita.API/Services/IClientDeviceService.cs new file mode 100644 index 000000000..014961a40 --- /dev/null +++ b/Kavita.API/Services/IClientDeviceService.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Device.ClientDevice; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface IClientDeviceService +{ + Task IdentifyOrRegisterDeviceAsync(int userId, ClientInfoData clientInfo, string? uiFingerprint, CancellationToken cancellationToken = default); + Task RenameDeviceAsync(int userId, int deviceId, string newName, CancellationToken ct = default); + Task DeleteDeviceAsync(int userId, int deviceId, CancellationToken ct = default); + Task UpdateFriendlyNameAsync(int userId, UpdateClientDeviceNameDto dto, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IClientInfoAccessor.cs b/Kavita.API/Services/IClientInfoAccessor.cs new file mode 100644 index 000000000..56d07f2b3 --- /dev/null +++ b/Kavita.API/Services/IClientInfoAccessor.cs @@ -0,0 +1,22 @@ +using Kavita.Models.Entities.Progress; + +namespace Kavita.API.Services; + +/// +/// Provides access to client information for the current request. +/// This service captures details about the client making the request including +/// browser info, device type, authentication method, etc. +/// +public interface IClientInfoAccessor +{ + /// + /// Gets the client information for the current request. + /// Returns null if called outside an HTTP request context (e.g., background jobs). + /// + ClientInfoData? Current { get; } + string? CurrentUiFingerprint { get; } + /// + /// Client Device PK + /// + int? CurrentDeviceId { get; } +} diff --git a/Kavita.API/Services/ICollectionTagService.cs b/Kavita.API/Services/ICollectionTagService.cs new file mode 100644 index 000000000..0a25c27b1 --- /dev/null +++ b/Kavita.API/Services/ICollectionTagService.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface ICollectionTagService +{ + Task DeleteTag(int tagId, AppUser user, CancellationToken ct = default); + Task UpdateTag(AppUserCollectionDto dto, int userId, CancellationToken ct = default); + /// + /// Removes series from Collection tag. Will recalculate max age rating. + /// + /// + /// + /// + /// + Task RemoveTagFromSeries(AppUserCollection? tag, IEnumerable seriesIds, CancellationToken ct = default); + + Task GenerateCollectionCoverImage(int collectionId); +} diff --git a/Kavita.API/Services/IDeviceService.cs b/Kavita.API/Services/IDeviceService.cs new file mode 100644 index 000000000..498094b7e --- /dev/null +++ b/Kavita.API/Services/IDeviceService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface IDeviceService +{ + Task Create(CreateEmailDeviceDto dto, AppUser userWithDevices, CancellationToken ct = default); + Task Update(UpdateEmailDeviceDto dto, AppUser userWithDevices, CancellationToken ct = default); + Task Delete(AppUser userWithDevices, int deviceId, CancellationToken ct = default); + Task SendTo(IReadOnlyList chapterIds, int deviceId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IDeviceTrackingService.cs b/Kavita.API/Services/IDeviceTrackingService.cs new file mode 100644 index 000000000..77a6e5b87 --- /dev/null +++ b/Kavita.API/Services/IDeviceTrackingService.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities.Progress; + +namespace Kavita.API.Services; + +public interface IDeviceTrackingService +{ + Task TrackDeviceAsync(int userId, ClientInfoData clientInfo, string? uiFingerprint, CancellationToken ct); + Task ClearDeviceCacheAsync(int deviceId); + Task ClearUserDeviceCachesAsync(int userId); +} diff --git a/Kavita.API/Services/IDirectoryService.cs b/Kavita.API/Services/IDirectoryService.cs new file mode 100644 index 000000000..324100ac6 --- /dev/null +++ b/Kavita.API/Services/IDirectoryService.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.System; +using Kavita.Models.Entities.Enums; +using Microsoft.Extensions.Logging; + +namespace Kavita.API.Services; + +public interface IDirectoryService +{ + IFileSystem FileSystem { get; } + string CacheDirectory { get; } + string CoverImageDirectory { get; } + string LogDirectory { get; } + string TempDirectory { get; } + string ConfigDirectory { get; } + string SiteThemeDirectory { get; } + string FaviconDirectory { get; } + string LocalizationDirectory { get; } + string CustomizedTemplateDirectory { get; } + string TemplateDirectory { get; } + string PublisherDirectory { get; } + /// + /// Used for caching documents that may need to stay on disk for more than a day + /// + string LongTermCacheDirectory { get; } + /// + /// Original BookmarkDirectory. Only used for resetting directory. Use for actual path. + /// + string BookmarkDirectory { get; } + /// + /// Used for random files needed, like images to check against, list of countries, etc + /// + string AssetsDirectory { get; } + string EpubFontDirectory { get; } + string BackupDirectory { get; } + + /// + /// Lists out top-level folders for a given directory. Filters out System and Hidden folders. + /// + /// Absolute path of directory to scan. + /// List of folder names + IEnumerable ListDirectory(string rootPath); + Task ReadFileAsync(string path); + bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, string prepend = ""); + bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, IList newFilenames); + bool Exists(string directory); + void CopyFileToDirectory(string fullFilePath, string targetDirectory); + int TraverseTreeParallelForEach(string root, Action action, string searchPattern, ILogger logger); + bool IsDriveMounted(string path); + bool IsDirectoryEmpty(string path); + long GetTotalSize(IEnumerable paths); + void ClearDirectory(string directoryPath); + void ClearAndDeleteDirectory(string directoryPath); + string[] GetFilesWithExtension(string path, string searchPatternExpression = ""); + bool CopyDirectoryToDirectory(string? sourceDirName, string destDirName, string searchPattern = ""); + Dictionary FindHighestDirectoriesFromFiles(IEnumerable libraryFolders, + IList filePaths); + string? FindLowestDirectoriesFromFiles(IList libraryFolders, + IList filePaths); + IEnumerable GetFoldersTillRoot(string rootPath, string fullPath); + IEnumerable GetFiles(string path, string fileNameRegex = "", SearchOption searchOption = SearchOption.TopDirectoryOnly); + bool ExistOrCreate(string directoryPath); + void DeleteFiles(IEnumerable files); + void CopyFile(string sourcePath, string destinationPath, bool overwrite = true); + void RemoveNonImages(string directoryName); + void Flatten(string directoryName); + Task CheckWriteAccess(string directoryName); + IEnumerable GetFilesWithCertainExtensions(string path, + string searchPatternExpression = "", + SearchOption searchOption = SearchOption.TopDirectoryOnly); + IEnumerable GetDirectories(string folderPath); + IEnumerable GetDirectories(string folderPath, GlobMatcher? matcher); + IEnumerable GetAllDirectories(string folderPath, GlobMatcher? matcher = null); + string GetParentDirectoryName(string fileOrFolder); + IList ScanFiles(string folderPath, string fileTypes, GlobMatcher? matcher = null, SearchOption searchOption = SearchOption.AllDirectories); + DateTime GetLastWriteTime(string folderPath); +} diff --git a/Kavita.API/Services/IDownloadService.cs b/Kavita.API/Services/IDownloadService.cs new file mode 100644 index 000000000..f61333bd0 --- /dev/null +++ b/Kavita.API/Services/IDownloadService.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using Kavita.Models.Entities; + +namespace Kavita.API.Services; + +public interface IDownloadService +{ + Tuple GetFirstFileDownload(IEnumerable files); + string GetContentTypeFromFile(string filepath); +} diff --git a/Kavita.API/Services/IEmailService.cs b/Kavita.API/Services/IEmailService.cs new file mode 100644 index 000000000..407f8754f --- /dev/null +++ b/Kavita.API/Services/IEmailService.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Email; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Microsoft.AspNetCore.Http; + +namespace Kavita.API.Services; + +public interface IEmailService +{ + Task SendInviteEmail(ConfirmationEmailDto data); + Task SendForgotPasswordEmail(PasswordResetEmailDto dto); + Task SendFilesToEmail(SendToDto data); + Task SendTestEmail(string adminEmail); + Task SendEmailChangeEmail(ConfirmationEmailDto data); + bool IsValidEmail(string email); + + Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, + bool withHost = true); + + Task SendTokenExpiredEmail(int userId, ScrobbleProvider provider); + Task SendTokenExpiringSoonEmail(int userId, ScrobbleProvider provider); + Task SendAuthKeyExpiredEmail(int userId, IList keys); + Task SendAuthKeyExpiringSoonEmail(int userId, IList keys); + Task SendKavitaPlusDebug(); +} diff --git a/API/Helpers/Formatting/LocalizedNamingContext.cs b/Kavita.API/Services/IEntityNamingService.cs similarity index 52% rename from API/Helpers/Formatting/LocalizedNamingContext.cs rename to Kavita.API/Services/IEntityNamingService.cs index 7cabd6377..d7957db6c 100644 --- a/API/Helpers/Formatting/LocalizedNamingContext.cs +++ b/Kavita.API/Services/IEntityNamingService.cs @@ -1,11 +1,51 @@ -using System.Threading.Tasks; -using API.DTOs; -using API.DTOs.ReadingLists; -using API.Entities.Enums; -using API.Services; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities.Enums; -namespace API.Helpers.Formatting; -#nullable enable +namespace Kavita.API.Services; + +/// +/// Provides consistent, testable naming for series, volumes, and chapters across the application. +/// All methods are pure functions with no side effects. +/// +public interface IEntityNamingService +{ + /// + /// Formats a chapter title based on library type and chapter metadata. + /// + string FormatChapterTitle(LibraryType libraryType, ChapterDto chapter, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); + + /// + /// Formats a chapter title from raw values. + /// + string FormatChapterTitle(LibraryType libraryType, bool isSpecial, string range, string? title, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null, bool withHash = true); + + /// + /// Formats a volume name based on library type and volume metadata. + /// + string? FormatVolumeName(LibraryType libraryType, VolumeDto volume, string? volumeLabel = null); + /// + /// Builds a full display title for a chapter within a series/volume context. + /// Used for OPDS feeds, reading lists, etc. + /// + string BuildFullTitle(LibraryType libraryType, SeriesDto series, VolumeDto? volume, ChapterDto chapter, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); + /// + /// Builds a display title for a chapter within its volume context. + /// Used when series context is not needed (e.g., reading history within a series grouping). + /// + string BuildChapterTitle(LibraryType libraryType, VolumeDto volume, ChapterDto chapter, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); + /// + /// Formats a reading list item title based on the item's metadata. + /// Handles the unique naming conventions for reading list display. + /// + string FormatReadingListItemTitle(ReadingListItemDto item, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); + + /// + /// Formats a reading list item title from raw values. + /// + string FormatReadingListItemTitle( LibraryType libraryType, MangaFormat format, string? chapterNumber, string? volumeNumber, string? chapterTitleName, bool isSpecial, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); +} /// /// Pre-fetched localized labels for entity naming. diff --git a/Kavita.API/Services/IFileService.cs b/Kavita.API/Services/IFileService.cs new file mode 100644 index 000000000..063375144 --- /dev/null +++ b/Kavita.API/Services/IFileService.cs @@ -0,0 +1,12 @@ +using System; +using System.IO.Abstractions; + +namespace Kavita.API.Services; + +public interface IFileService +{ + IFileSystem GetFileSystem(); + bool HasFileBeenModifiedSince(string filePath, DateTime time); + bool Exists(string filePath); + bool ValidateSha(string filepath, string sha); +} diff --git a/Kavita.API/Services/IFontService.cs b/Kavita.API/Services/IFontService.cs new file mode 100644 index 000000000..fa4428f7a --- /dev/null +++ b/Kavita.API/Services/IFontService.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities; + +namespace Kavita.API.Services; + +public interface IFontService +{ + Task CreateFontFromFileAsync(string path, CancellationToken ct = default); + Task Delete(int fontId, CancellationToken ct = default); + Task CreateFontFromUrl(string url, CancellationToken ct = default); + Task IsFontInUse(int fontId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IImageService.cs b/Kavita.API/Services/IImageService.cs new file mode 100644 index 000000000..c9cf430ab --- /dev/null +++ b/Kavita.API/Services/IImageService.cs @@ -0,0 +1,61 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; + +namespace Kavita.API.Services; + +public interface IImageService +{ + void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1); + string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size); + + /// + /// Creates a Thumbnail version of a base64 image + /// + /// base64 encoded image + /// + /// Convert and save as encoding format + /// Width of thumbnail + /// If null, will write to + /// File name with extension of the file. + string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320, string? targetDirectory = null); + /// + /// Writes out a thumbnail by stream input + /// + /// + /// + /// + /// + /// + string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + /// + /// Writes out a thumbnail by file path input + /// + /// + /// + /// + /// + /// + string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + + /// + /// Converts the passed image to encoding and outputs it in the same directory + /// + /// Full path to the image to convert + /// Where to output the file + /// Encoding Format + /// + /// File of written encoded image + Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat, CancellationToken ct = default); + + /// + /// Performs I/O to determine if the file is a valid Image + /// + /// + /// + /// + Task IsImage(string filePath, CancellationToken ct = default); + void UpdateColorScape(IHasCoverImage entity); +} diff --git a/Kavita.API/Services/IKoreaderService.cs b/Kavita.API/Services/IKoreaderService.cs new file mode 100644 index 000000000..2ec3f6ba6 --- /dev/null +++ b/Kavita.API/Services/IKoreaderService.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Koreader; + +namespace Kavita.API.Services; + +public interface IKoreaderService +{ + Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId, CancellationToken ct = default); + Task GetProgress(string bookHash, int userId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/ILocalizationService.cs b/Kavita.API/Services/ILocalizationService.cs new file mode 100644 index 000000000..40a9a8bdf --- /dev/null +++ b/Kavita.API/Services/ILocalizationService.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Models.DTOs; + +namespace Kavita.API.Services; + +public interface ILocalizationService +{ + Task Get(string locale, string key, params object[] args); + Task Translate(int userId, string key, params object[] args); + IEnumerable GetLocales(); +} diff --git a/Kavita.API/Services/ILoggingService.cs b/Kavita.API/Services/ILoggingService.cs new file mode 100644 index 000000000..cbd105de0 --- /dev/null +++ b/Kavita.API/Services/ILoggingService.cs @@ -0,0 +1,6 @@ +namespace Kavita.API.Services; + +public interface ILoggingService +{ + void SwitchLogLevel(string level); +} diff --git a/Kavita.API/Services/IMediaConversionService.cs b/Kavita.API/Services/IMediaConversionService.cs new file mode 100644 index 000000000..bbca39626 --- /dev/null +++ b/Kavita.API/Services/IMediaConversionService.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services; + +public interface IMediaConversionService +{ + [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] + Task ConvertAllBookmarkToEncoding(CancellationToken ct = default); + [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] + Task ConvertAllCoversToEncoding(CancellationToken ct = default); + [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] + Task ConvertAllManagedMediaToEncodingFormat(CancellationToken ct = default); + + Task SaveAsEncodingFormat(string imageDirectory, string filename, string targetFolder, + EncodeFormat encodeFormat); +} diff --git a/Kavita.API/Services/IMediaErrorService.cs b/Kavita.API/Services/IMediaErrorService.cs new file mode 100644 index 000000000..3e65b2448 --- /dev/null +++ b/Kavita.API/Services/IMediaErrorService.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services; + +public interface IMediaErrorService +{ + void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, string details); + void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, Exception ex); + Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, string details, CancellationToken ct = default); + Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, Exception ex, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IMetadataService.cs b/Kavita.API/Services/IMetadataService.cs new file mode 100644 index 000000000..3ccdbbfa4 --- /dev/null +++ b/Kavita.API/Services/IMetadataService.cs @@ -0,0 +1,35 @@ +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services; + +public interface IMetadataService +{ + /// + /// Recalculates cover images for all entities in a library. + /// + /// + /// + /// + /// + [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false, bool forceColorScape = false, CancellationToken ct = default); + + /// + /// Performs a forced refresh of cover images just for a series, and it's nested entities + /// + /// + /// + /// + /// Overrides any cache logic and forces execution + /// + /// + Task GenerateCoversForSeries(ServerSettingDto serverSetting, int libraryId, int seriesId, bool forceUpdate = true, bool forceColorScape = true, CancellationToken ct = default); + Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false, bool forceColorScape = true, CancellationToken ct = default); + Task RemoveAbandonedMetadataKeys(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IOidcService.cs b/Kavita.API/Services/IOidcService.cs new file mode 100644 index 000000000..a7e2a8803 --- /dev/null +++ b/Kavita.API/Services/IOidcService.cs @@ -0,0 +1,38 @@ +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common; +using Kavita.Models.Entities.User; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http; + +namespace Kavita.API.Services; + +public interface IOidcService +{ + /// + /// Returns the user authenticated with OpenID Connect + /// + /// + /// + /// + /// + /// if any requirements aren't met + Task LoginOrCreate(HttpRequest request, ClaimsPrincipal principal, CancellationToken ct = default); + + /// + /// Refresh the token inside the cookie when it's close to expiring. And sync the user + /// + /// + /// + /// + /// If the token is refreshed successfully, updates the last active time of the suer + Task RefreshCookieToken(CookieValidatePrincipalContext ctx, CancellationToken ct = default); + + /// + /// Remove from all users + /// + /// + /// + Task ClearOidcIds(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IOpdsService.cs b/Kavita.API/Services/IOpdsService.cs new file mode 100644 index 000000000..40c7bc999 --- /dev/null +++ b/Kavita.API/Services/IOpdsService.cs @@ -0,0 +1,32 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.OPDS; +using Kavita.Models.DTOs.OPDS.Requests; + +namespace Kavita.API.Services; + +public interface IOpdsService +{ + Task GetCatalogue(OpdsCatalogueRequest request, CancellationToken ct = default); + Task GetSmartFilters(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetLibraries(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetWantToRead(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetCollections(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetReadingLists(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetRecentlyAdded(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetRecentlyUpdated(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetOnDeck(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + + Task GetMoreInGenre(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default); + Task GetSeriesFromSmartFilter(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default); + Task GetSeriesFromCollection(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default); + Task GetSeriesFromLibrary(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default); + Task GetReadingListItems(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default); + Task GetSeriesDetail(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default); + Task GetItemsFromVolume(OpdsItemsFromCompoundEntityIdsRequest request, CancellationToken ct = default); + Task GetItemsFromChapter(OpdsItemsFromCompoundEntityIdsRequest request, CancellationToken ct = default); + + Task Search(OpdsSearchRequest request, CancellationToken ct = default); + + string SerializeXml(Feed? feed); +} diff --git a/Kavita.API/Services/IPersonService.cs b/Kavita.API/Services/IPersonService.cs new file mode 100644 index 000000000..66e7f2547 --- /dev/null +++ b/Kavita.API/Services/IPersonService.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities.Person; + +namespace Kavita.API.Services; + +public interface IPersonService +{ + /// + /// Adds src as an alias to dst, this is a destructive operation + /// + /// Merged person + /// Remaining person + /// + /// The entities passed as arguments **must** include all relations + /// + Task MergePeopleAsync(Person src, Person dst, CancellationToken ct = default); + + /// + /// Adds the alias to the person, requires that the aliases are not shared with anyone else + /// + /// This method does NOT commit changes + /// + /// + /// + /// + Task UpdatePersonAliasesAsync(Person person, IList aliases, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IRatingService.cs b/Kavita.API/Services/IRatingService.cs new file mode 100644 index 000000000..df8046df8 --- /dev/null +++ b/Kavita.API/Services/IRatingService.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface IRatingService +{ + /// + /// Updates the users' rating for a given series + /// + /// Should include ratings + /// + /// + /// + Task UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto, CancellationToken ct = default); + + /// + /// Updates the users' rating for a given chapter + /// + /// Should include ratings + /// chapterId must be set + /// + /// + Task UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IReadingItemService.cs b/Kavita.API/Services/IReadingItemService.cs new file mode 100644 index 000000000..2fa6c7f80 --- /dev/null +++ b/Kavita.API/Services/IReadingItemService.cs @@ -0,0 +1,12 @@ +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; + +namespace Kavita.API.Services; + +public interface IReadingItemService +{ + int GetNumberOfPages(string filePath, MangaFormat format); + string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1); + ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata); +} diff --git a/Kavita.API/Services/ISeriesService.cs b/Kavita.API/Services/ISeriesService.cs new file mode 100644 index 000000000..68f98fcf6 --- /dev/null +++ b/Kavita.API/Services/ISeriesService.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.SeriesDetail; + +namespace Kavita.API.Services; + +public interface ISeriesService +{ + Task GetSeriesDetail(int seriesId, int userId, CancellationToken ct = default); + Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto, CancellationToken ct = default); + Task DeleteMultipleSeries(IList seriesIds, CancellationToken ct = default); + Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto, CancellationToken ct = default); + Task GetRelatedSeries(int userId, int seriesId, CancellationToken ct = default); + Task GetEstimatedChapterCreationDate(int seriesId, int userId, CancellationToken ct = default); + Task> GetCurrentlyReading(int userId, int requestingUserId, UserParams userParams, CancellationToken ct = default); + Task> GetProfilePrivacyStatements(int userId, int requestingUserId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/ISettingsService.cs b/Kavita.API/Services/ISettingsService.cs new file mode 100644 index 000000000..3c42f3f55 --- /dev/null +++ b/Kavita.API/Services/ISettingsService.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Settings; + +namespace Kavita.API.Services; + +public interface ISettingsService +{ + Task UpdateMetadataSettings(MetadataSettingsDto dto, CancellationToken ct = default); + + /// + /// Update , , , + /// with data from the given dto. + /// + /// + /// + /// + /// + Task ImportFieldMappings(FieldMappingsDto dto, ImportSettingsDto settings, CancellationToken ct = default); + Task UpdateSettings(ServerSettingDto updateSettingsDto, CancellationToken ct = default); + + /// + /// Check if the server can reach the authority at the given uri + /// + /// + /// + /// + Task IsValidAuthority(string authority, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IStatisticService.cs b/Kavita.API/Services/IStatisticService.cs new file mode 100644 index 000000000..74a925a16 --- /dev/null +++ b/Kavita.API/Services/IStatisticService.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.Statistics; +using Kavita.Models.DTOs.Stats; +using Kavita.Models.DTOs.Stats.V3.ClientDevice; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services; + +public interface IStatisticService +{ + Task GetServerStatistics(CancellationToken ct = default); + Task GetUserReadStatistics(int userId, IList libraryIds, CancellationToken ct = default); + Task>> GetYearCount(CancellationToken ct = default); + Task>> GetTopYears(CancellationToken ct = default); + Task> GetPopularDecades(CancellationToken ct = default); + Task>> GetPopularLibraries(CancellationToken ct = default); + Task>> GetPopularSeries(CancellationToken ct = default); + Task>> GetPopularReadingList(int take = 5, CancellationToken ct = default); + Task>> GetPopularGenres(CancellationToken ct = default); + Task>> GetPopularTags(CancellationToken ct = default); + Task>> GetPopularPerson(PersonRole role, CancellationToken ct = default); + Task>> GetPublicationCount(CancellationToken ct = default); + Task>> GetMangaFormatCount(CancellationToken ct = default); + Task GetFileBreakdown(CancellationToken ct = default); + Task> GetTopUsers(int days, CancellationToken ct = default); + Task> GetReadingHistory(int userId, CancellationToken ct = default); + Task>> ReadCountByDay(int userId = 0, int days = 0, CancellationToken ct = default); + Task>> ReadCounts(StatsFilterDto filter, int userId = 0, CancellationToken ct = default); + Task>> GetDayBreakdown(int userId = 0, CancellationToken ct = default); + Task>> GetPagesReadCountByYear(int userId = 0, CancellationToken ct = default); + Task>> GetWordsReadCountByYear(int userId = 0, CancellationToken ct = default); + Task UpdateServerStatistics(CancellationToken ct = default); + Task> GetFilesByExtension(string fileExtension, CancellationToken ct = default); + Task GetClientTypeBreakdown(DateTime fromDateUtc, CancellationToken ct = default); + Task>> GetDeviceTypeCounts(DateTime fromDateUtc, CancellationToken ct = default); + Task GetReadingActivityGraphData(StatsFilterDto filter, int userId, int year, int requestingUserId, CancellationToken ct = default); + Task GetReadingPaceForUser(StatsFilterDto filter, int userId, int year, bool booksOnly, int requestingUserId, CancellationToken ct = default); + Task> GetGenreBreakdownForUser(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task> GetTagBreakdownForUser(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task GetPageSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task GetWordSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task>> GetReadsPerMonth(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task> GetMostReadAuthors(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task GetTotalReads(int userId, int requestingUserId, CancellationToken ct = default); + Task GetTimeReadingByHour(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task GetUserStatBar(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task> GetMostActiveUsers(StatsFilterDto filter, CancellationToken ct = default); + Task>> GetFilesAddedOverTime(CancellationToken ct = default); + Task> GetReadingHistoryItems(StatsFilterDto filter, UserParams userParams, int userId, int requestingUserId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IStatsService.cs b/Kavita.API/Services/IStatsService.cs new file mode 100644 index 000000000..2f8d8ce56 --- /dev/null +++ b/Kavita.API/Services/IStatsService.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Stats; + +namespace Kavita.API.Services; + +public interface IStatsService +{ + Task Send(CancellationToken ct = default); + Task GetServerInfoSlim(CancellationToken ct = default); + Task SendCancellation(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IStreamService.cs b/Kavita.API/Services/IStreamService.cs new file mode 100644 index 000000000..385906ccf --- /dev/null +++ b/Kavita.API/Services/IStreamService.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +/// +/// For SideNavStream and DashboardStream manipulation +/// +public interface IStreamService +{ + Task> GetDashboardStreams(int userId, bool visibleOnly = true, CancellationToken ct = default); + Task> GetSidenavStreams(int userId, bool visibleOnly = true, CancellationToken ct = default); + Task> GetExternalSources(int userId, CancellationToken ct = default); + Task CreateDashboardStreamFromSmartFilter(int userId, int smartFilterId, CancellationToken ct = default); + Task UpdateDashboardStream(int userId, DashboardStreamDto dto, CancellationToken ct = default); + Task UpdateDashboardStreamPosition(int userId, UpdateStreamPositionDto dto, CancellationToken ct = default); + Task UpdateSideNavStreamBulk(int userId, BulkUpdateSideNavStreamVisibilityDto dto, CancellationToken ct = default); + Task CreateSideNavStreamFromSmartFilter(int userId, int smartFilterId, CancellationToken ct = default); + Task CreateSideNavStreamFromExternalSource(int userId, int externalSourceId, CancellationToken ct = default); + Task UpdateSideNavStream(int userId, SideNavStreamDto dto, CancellationToken ct = default); + Task UpdateSideNavStreamPosition(int userId, UpdateStreamPositionDto dto, CancellationToken ct = default); + Task CreateExternalSource(int userId, ExternalSourceDto dto, CancellationToken ct = default); + Task UpdateExternalSource(int userId, ExternalSourceDto dto, CancellationToken ct = default); + Task DeleteExternalSource(int userId, int externalSourceId, CancellationToken ct = default); + Task DeleteSideNavSmartFilterStream(int userId, int sideNavStreamId, CancellationToken ct = default); + Task DeleteDashboardSmartFilterStream(int userId, int dashboardStreamId, CancellationToken ct = default); + Task RenameSmartFilterStreams(AppUserSmartFilter smartFilter, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/ITachiyomiService.cs b/Kavita.API/Services/ITachiyomiService.cs new file mode 100644 index 000000000..d9a279801 --- /dev/null +++ b/Kavita.API/Services/ITachiyomiService.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface ITachiyomiService +{ + /// + /// Gets the latest chapter/volume read. + /// + /// + /// + /// + /// Due to how Tachiyomi works we need a hack to properly return both chapters and volumes. + /// If its a chapter, return the chapterDto as is. + /// If it's a volume, the volume number gets returned in the 'Number' attribute of a chapterDto encoded. + /// The volume number gets divided by 10,000 because that's how Tachiyomi interprets volumes + Task GetLatestChapter(int seriesId, int userId, CancellationToken ct = default); + /// + /// Marks every chapter and volume that is sorted below the passed number as Read. This will not mark any specials as read. + /// Passed number will also be marked as read + /// + /// + /// + /// Can also be a Tachiyomi encoded volume number + /// + Task MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/ITaskScheduler.cs b/Kavita.API/Services/ITaskScheduler.cs new file mode 100644 index 000000000..3fd9d95e1 --- /dev/null +++ b/Kavita.API/Services/ITaskScheduler.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services; + +public interface ITaskScheduler +{ + Task ScheduleTasks(CancellationToken cancellationToken = default); + Task ScheduleStatsTasks(CancellationToken cancellationToken = default); + void ScheduleUpdaterTasks(); + Task ScheduleKavitaPlusTasks(CancellationToken cancellationToken = default); + void ScanFolder(string folderPath, string originalPath, TimeSpan delay); + void ScanFolder(string folderPath, bool abortOnNoSeriesMatch = false); + Task ScanLibrary(int libraryId, bool force = false); + Task ScanLibraries(bool force = false); + void CleanupChapters(int[] chapterIds); + void RefreshMetadata(int libraryId, bool forceUpdate = true, bool forceColorscape = true); + Task RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false, bool forceColorscape = false); + Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = false); + void AnalyzeFilesForSeries(int libraryId, int seriesId, bool forceUpdate = false); + void CancelStatsTasks(); + Task RunStatCollection(); + void ConvertAllCoversToEncoding(); + Task CleanupDbEntries(); + Task CheckForUpdate(CancellationToken cancellationToken = default); +} diff --git a/Kavita.API/Services/IThemeService.cs b/Kavita.API/Services/IThemeService.cs new file mode 100644 index 000000000..2db945551 --- /dev/null +++ b/Kavita.API/Services/IThemeService.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities; + +namespace Kavita.API.Services; + +public interface IThemeService +{ + Task GetContent(int themeId, CancellationToken ct = default); + Task UpdateDefault(int themeId, CancellationToken ct = default); + + /// + /// Browse theme repo for themes to download + /// + /// + /// + Task> GetDownloadableThemes(CancellationToken ct = default); + + Task DownloadRepoTheme(DownloadableSiteThemeDto dto, CancellationToken ct = default); + Task DeleteTheme(int siteThemeId, CancellationToken ct = default); + Task CreateThemeFromFile(string tempFile, string username, CancellationToken ct = default); + Task SyncThemes(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/ITokenService.cs b/Kavita.API/Services/ITokenService.cs new file mode 100644 index 000000000..1f09ea75a --- /dev/null +++ b/Kavita.API/Services/ITokenService.cs @@ -0,0 +1,14 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Account; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface ITokenService +{ + Task CreateToken(AppUser user, CancellationToken ct = default); + Task ValidateRefreshToken(TokenRequestDto request, CancellationToken ct = default); + Task CreateRefreshToken(AppUser user, CancellationToken ct = default); + Task GetJwtFromUser(AppUser user, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IVersionUpdaterService.cs b/Kavita.API/Services/IVersionUpdaterService.cs new file mode 100644 index 000000000..5a5839512 --- /dev/null +++ b/Kavita.API/Services/IVersionUpdaterService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Update; + +namespace Kavita.API.Services; + +public interface IVersionUpdaterService +{ + Task CheckForUpdate(CancellationToken ct = default); + Task PushUpdate(UpdateNotificationDto update, CancellationToken ct = default); + Task> GetAllReleases(int count = 0, CancellationToken ct = default); + Task GetNumberOfReleasesBehind(bool stableOnly = false, CancellationToken ct = default); + void BustGithubCache(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Metadata/ICoverDbService.cs b/Kavita.API/Services/Metadata/ICoverDbService.cs new file mode 100644 index 000000000..c7f649c1c --- /dev/null +++ b/Kavita.API/Services/Metadata/ICoverDbService.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services.Metadata; + +public interface ICoverDbService +{ + Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat, CancellationToken ct = default); + Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat, CancellationToken ct = default); + Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, CancellationToken ct = default); + Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url, CancellationToken ct = default); + Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false, bool chooseBetterImage = true, CancellationToken ct = default); + Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, bool chooseBetterImage = false, CancellationToken ct = default); + Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false, CancellationToken ct = default); + Task SetUserCoverByUrl(int userId, string url, bool fromBase64 = true, bool chooseBetterImage = false, CancellationToken ct = default); + Task SetUserCoverByUrl(AppUser user, string url, bool fromBase64 = true, bool chooseBetterImage = false, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Metadata/IWordCountAnalyzerService.cs b/Kavita.API/Services/Metadata/IWordCountAnalyzerService.cs new file mode 100644 index 000000000..6c991dafe --- /dev/null +++ b/Kavita.API/Services/Metadata/IWordCountAnalyzerService.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; +using Hangfire; + +namespace Kavita.API.Services.Metadata; + +public interface IWordCountAnalyzerService +{ + [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] + [AutomaticRetry(Attempts = 2, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + Task ScanLibrary(int libraryId, bool forceUpdate = false, CancellationToken ct = default); + Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Plus/IExternalMetadataService.cs b/Kavita.API/Services/Plus/IExternalMetadataService.cs new file mode 100644 index 000000000..6b25ff789 --- /dev/null +++ b/Kavita.API/Services/Plus/IExternalMetadataService.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Metadata.Matching; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services.Plus; + +public interface IExternalMetadataService +{ + public static readonly HashSet NonEligibleLibraryTypes = [LibraryType.Comic, LibraryType.Book, LibraryType.Image]; + + /// + /// Retrieves Metadata about a Recommended External Series + /// + /// + /// + /// + /// + /// + /// + Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId, CancellationToken ct = default); + + /// + /// Returns Series Detail data from Kavita+ - Review, Recs, Ratings + /// + /// + /// + /// + /// + Task GetSeriesDetailPlus(int seriesId, LibraryType libraryType, CancellationToken ct = default); + /// + /// This is a task that runs on a schedule and slowly fetches data from Kavita+ to keep + /// data in the DB non-stale and fetched. + /// + /// To avoid blasting Kavita+ API, this only processes 25 records. The goal is to slowly build out/refresh the data + /// + Task FetchExternalDataTask(CancellationToken ct = default); + + /// + /// This is an entry point and provides a level of protection against calling upstream API. Will only allow 100 new + /// series to fetch data within a day and enqueues background jobs at certain times to fetch that data. + /// + /// + /// + /// + /// If the fetch was made + Task FetchSeriesMetadata(int seriesId, LibraryType libraryType, CancellationToken ct = default); + + Task> GetStacksForUser(int userId, CancellationToken ct = default); + + /// + /// Returns the match results for a Series from UI Flow + /// + /// + /// Will extract alternative names like Localized name, year will send as ReleaseYear but fallback to Comic Vine syntax if applicable + /// + /// + /// + /// + Task> MatchSeries(MatchSeriesDto dto, CancellationToken ct = default); + + /// + /// This will override any sort of matching that was done prior and force it to be what the user Selected + /// + /// + /// + /// + /// + /// + Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId, CancellationToken ct = default); + + /// + /// Sets a series to Don't Match and removes all previously cached + /// + /// + /// + /// + Task UpdateSeriesDontMatch(int seriesId, bool dontMatch, CancellationToken ct = default); + + /// + /// Given external metadata from Kavita+, write as much as possible to the Kavita series as possible + /// + /// + /// + /// + /// + Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Plus/IKavitaPlusApiService.cs b/Kavita.API/Services/Plus/IKavitaPlusApiService.cs new file mode 100644 index 000000000..39ba753ca --- /dev/null +++ b/Kavita.API/Services/Plus/IKavitaPlusApiService.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Metadata.Matching; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services.Plus; + +/// +/// All Http requests to K+ should be contained in this service, the service will not handle any errors. +/// This is expected from the caller. +/// +public interface IKavitaPlusApiService +{ + Task HasTokenExpired(string license, string token, ScrobbleProvider provider, CancellationToken ct = default); + Task GetRateLimit(string license, string token, CancellationToken ct = default); + Task PostScrobbleUpdate(ScrobbleDto data, string license, CancellationToken ct = default); + Task> GetMalStacks(string malUsername, string license, CancellationToken ct = default); + Task> MatchSeries(MatchSeriesRequestDto request, CancellationToken ct = default); + Task GetSeriesDetail(PlusSeriesRequestDto request, CancellationToken ct = default); + Task GetSeriesDetailById(ExternalMetadataIdsDto request, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Plus/ILicenseService.cs b/Kavita.API/Services/Plus/ILicenseService.cs new file mode 100644 index 000000000..bb2033209 --- /dev/null +++ b/Kavita.API/Services/Plus/ILicenseService.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.KavitaPlus.License; + +namespace Kavita.API.Services.Plus; + +public interface ILicenseService +{ + //Task ValidateLicenseStatus(); + Task RemoveLicense(CancellationToken ct = default); + Task AddLicense(string license, string email, string? discordId, CancellationToken ct = default); + Task HasActiveLicense(bool forceCheck = false, CancellationToken ct = default); + Task HasActiveSubscription(string? license, CancellationToken ct = default); + Task ResetLicense(string license, string email, CancellationToken ct = default); + Task GetLicenseInfo(bool forceCheck = false, CancellationToken ct = default); + Task ResendWelcomeEmail(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Plus/IScrobblingService.cs b/Kavita.API/Services/Plus/IScrobblingService.cs new file mode 100644 index 000000000..71867312a --- /dev/null +++ b/Kavita.API/Services/Plus/IScrobblingService.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services.Plus; + +public interface IScrobblingService +{ + /// + /// An automated job that will run against all user's tokens and validate if they are still active + /// + /// + /// This service can validate without license check as the task which calls will be guarded + /// + Task CheckExternalAccessTokens(CancellationToken ct = default); + + /// + /// Checks if the token has expired with , if it has double checks with K+, + /// otherwise return false. + /// + /// + /// + /// + /// + /// Returns true if there is no license present + Task HasTokenExpired(int userId, ScrobbleProvider provider, CancellationToken ct = default); + + /// + /// Create, or update a non-processed, event, for the given series + /// + /// + /// + /// + /// + /// + Task ScrobbleRatingUpdate(int userId, int seriesId, float rating, CancellationToken ct = default); + + /// + /// NOP, until hardcover support has been worked out + /// + /// + /// + /// + /// + /// + /// + Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody, CancellationToken ct = default); + + /// + /// Create, or update a non-processed, event, for the given series + /// + /// + /// + /// + /// + Task ScrobbleReadingUpdate(int userId, int seriesId, CancellationToken ct = default); + + /// + /// Creates an or for + /// the given series + /// + /// + /// + /// + /// + /// + /// Only the result of both WantToRead types is send to K+ + Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead, CancellationToken ct = default); + + /// + /// Removed all processed events that are at least 7 days old + /// + /// + /// + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + public Task ClearProcessedEvents(CancellationToken ct = default); + + /// + /// Makes K+ requests for all non-processed events until rate limits are reached + /// + /// + /// + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + Task ProcessUpdatesSinceLastSync(CancellationToken ct = default); + + Task CreateEventsFromExistingHistory(int userId = 0, CancellationToken ct = default); + Task CreateEventsFromExistingHistoryForSeries(int seriesId, CancellationToken ct = default); + Task ClearEventsForSeries(int userId, int seriesId, CancellationToken ct = default); +} + +public static class ScrobblingHelper +{ + public const string AniListWeblinkWebsite = "https://anilist.co/manga/"; + public const string MalWeblinkWebsite = "https://myanimelist.net/manga/"; + public const string MalStaffWebsite = "https://myanimelist.net/people/"; + public const string MalCharacterWebsite = "https://myanimelist.net/character/"; + public const string GoogleBooksWeblinkWebsite = "https://books.google.com/books?id="; + public const string MangaDexWeblinkWebsite = "https://mangadex.org/title/"; + public const string AniListStaffWebsite = "https://anilist.co/staff/"; + public const string AniListCharacterWebsite = "https://anilist.co/character/"; + public const string HardcoverStaffWebsite = "https://hardcover.app/authors/"; + + private static readonly Dictionary WeblinkExtractionMap = new() + { + {AniListWeblinkWebsite, 0}, + {MalWeblinkWebsite, 0}, + {GoogleBooksWeblinkWebsite, 0}, + {MangaDexWeblinkWebsite, 0}, + {AniListStaffWebsite, 0}, + {AniListCharacterWebsite, 0}, + }; + + private static bool IsAniListReviewValid(string reviewTitle, string reviewBody) + { + return string.IsNullOrEmpty(reviewTitle) || string.IsNullOrEmpty(reviewBody) || (reviewTitle.Length < 2200 || + reviewTitle.Length > 120 || + reviewTitle.Length < 20); + } + + public static long? GetMalId(Series series) + { + var malId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite); + return malId ?? 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); + 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) + { + return id is null or 0 ? string.Empty : $"{url}{id}/"; + } + +} diff --git a/Kavita.API/Services/Plus/ISmartCollectionSyncService.cs b/Kavita.API/Services/Plus/ISmartCollectionSyncService.cs new file mode 100644 index 000000000..85ce8351b --- /dev/null +++ b/Kavita.API/Services/Plus/ISmartCollectionSyncService.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services.Plus; + +/// +/// Responsible to synchronize Collection series from non-Kavita sources +/// +public interface ISmartCollectionSyncService +{ + /// + /// Synchronize all collections + /// + /// + /// + Task Sync(CancellationToken ct = default); + + /// + /// Synchronize a collection + /// + /// + /// + /// + Task Sync(int collectionId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Plus/IWantToReadSyncService.cs b/Kavita.API/Services/Plus/IWantToReadSyncService.cs new file mode 100644 index 000000000..8c394fa5c --- /dev/null +++ b/Kavita.API/Services/Plus/IWantToReadSyncService.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services.Plus; + +public interface IWantToReadSyncService +{ + Task Sync(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Reading/IReaderService.cs b/Kavita.API/Services/Reading/IReaderService.cs new file mode 100644 index 000000000..a59c5e30e --- /dev/null +++ b/Kavita.API/Services/Reading/IReaderService.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services.Reading; + +public interface IReaderService +{ + public const float MinWordsPerHour = 10260F; + public const float MaxWordsPerHour = 30000F; + public const float MinPagesPerMinute = 3.33F; + public const float MaxPagesPerMinute = 2.75F; + public const float AvgWordsPerHour = (MaxWordsPerHour + MinWordsPerHour) / 2F; + public const float AvgPagesPerMinute = (MaxPagesPerMinute + MinPagesPerMinute) / 2F; //3.04 + + Task MarkSeriesAsRead(AppUser user, int seriesId); + Task MarkSeriesAsUnread(AppUser user, int seriesId); + Task MarkChaptersAsRead(AppUser user, int seriesId, IList chapters); + Task MarkChaptersAsUnread(AppUser user, int seriesId, IList chapters); + Task SaveReadingProgress(ProgressDto progressDto, int userId); + int CapPageToChapter(Chapter chapter, int page); + Task GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId); + Task GetPrevChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId); + Task GetContinuePoint(int seriesId, int userId); + Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber); + Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber); + IDictionary GetPairs(IEnumerable dimensions); + Task GetThumbnail(Chapter chapter, int pageNum, IEnumerable cachedImages); + Task CheckSeriesForReRead(int userId, int seriesId, int libraryId); + Task CheckVolumeForReRead(int userId, int volumeId, int seriesId, int libraryId); + Task CheckChapterForReRead(int userId, int chapterId, int seriesId, int libraryId); +} diff --git a/Kavita.API/Services/Reading/IReadingHistoryService.cs b/Kavita.API/Services/Reading/IReadingHistoryService.cs new file mode 100644 index 000000000..52aa77453 --- /dev/null +++ b/Kavita.API/Services/Reading/IReadingHistoryService.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services.Reading; + +public interface IReadingHistoryService +{ + Task AggregateYesterdaysActivity(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Reading/IReadingListService.cs b/Kavita.API/Services/Reading/IReadingListService.cs new file mode 100644 index 000000000..32a4864c4 --- /dev/null +++ b/Kavita.API/Services/Reading/IReadingListService.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.ReadingLists.CBL; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services.Reading; + +public interface IReadingListService +{ + Task CreateReadingListForUser(AppUser userWithReadingList, string title); + Task UpdateReadingList(ReadingList readingList, UpdateReadingListDto dto); + Task RemoveFullyReadItems(int readingListId, AppUser user); + Task UpdateReadingListItemPosition(UpdateReadingListPosition dto); + Task DeleteReadingListItem(UpdateReadingListPosition dto); + 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 ValidateCblFile(int userId, CblReadingList cblReading, bool useComicLibraryMatching = false); + Task CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false, bool useComicLibraryMatching = false); + Task CalculateStartAndEndDates(ReadingList readingListWithItems); + /// + /// This is expected to be called from ProcessSeries and has the Full Series present. Will generate on the default admin user. + /// + /// + /// + /// + Task CreateReadingListsFromSeries(Series series, Library library); + + Task CreateReadingListsFromSeries(int libraryId, int seriesId); + Task GenerateReadingListCoverImage(int readingListId); + /// + /// Check, and update if needed, all reading lists' AgeRating who contain the passed series + /// + /// The series whose age rating is being updated + /// The new (uncommited) age rating of the series + /// + /// This method does not commit changes + Task UpdateReadingListAgeRatingForSeries(int seriesId, AgeRating ageRating); + + Task> GetReadingListItems(int readingListId, int userId, UserParams? userParams = null); + Task GetContinueReadingPoint(int readingListId, int userId); +} diff --git a/Kavita.API/Services/Reading/IReadingProfileService.cs b/Kavita.API/Services/Reading/IReadingProfileService.cs new file mode 100644 index 000000000..eda139aac --- /dev/null +++ b/Kavita.API/Services/Reading/IReadingProfileService.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Common; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services.Reading; + +public interface IReadingProfileService +{ + /// + /// Returns the ReadingProfile that should be applied to the given series, walks up the tree. + /// Series (Implicit) -> Series (User) -> Library (User) -> Default + /// + /// + /// + /// + /// + /// + /// + Task GetReadingProfileDtoForSeries(int userId, int libraryId, int seriesId, int? activeDeviceId, bool skipImplicit = false); + + /// + /// Creates a new reading profile for a user. Name must be unique per user + /// + /// + /// + /// + Task CreateReadingProfile(int userId, UserReadingProfileDto dto); + /// + /// Given an implicit profile, promotes it to a profile of kind , then removes + /// all links to the series this implicit profile was created for from other reading profiles (if the device id matches + /// if given) + /// + /// + /// + /// + /// + Task PromoteImplicitProfile(int userId, int profileId, int? activeDeviceId); + + /// + /// Updates the implicit reading profile for a series, creates one if none exists + /// + /// + /// + /// + /// + /// + /// + Task UpdateImplicitReadingProfile(int userId, int libraryId, int seriesId, UserReadingProfileDto dto, int? activeDeviceId); + + /// + /// Updates the non-implicit reading profile for the given series, and removes implicit profiles + /// + /// + /// + /// + /// + /// + /// + Task UpdateParent(int userId, int libraryId, int seriesId, UserReadingProfileDto dto, int? activeDeviceId); + + /// + /// Updates a given reading profile for a user + /// + /// + /// + /// + /// Does not update connected series and libraries + Task UpdateReadingProfile(int userId, UserReadingProfileDto dto); + + /// + /// Deletes a given profile for a user + /// + /// + /// + /// + /// + /// The default profile for the user cannot be deleted + Task DeleteReadingProfile(int userId, int profileId); + + /// + /// Binds the reading profile to the series, and remove the implicit RP from the series if it exists + /// + /// + /// + /// + /// + Task SetSeriesProfiles(int userId, List profileIds, int seriesId); + + /// + /// Binds the reading profile to many series, and remove the implicit RP from the series if it exists + /// + /// + /// + /// + /// + Task BulkSetSeriesProfiles(int userId, List profileIds, List seriesIds); + + /// + /// Remove all reading profiles bound to the series + /// + /// + /// + /// + Task ClearSeriesProfile(int userId, int seriesId); + + /// + /// Bind the reading profile to the library + /// + /// + /// + /// + /// + Task SetLibraryProfiles(int userId, List profileIds, int libraryId); + + /// + /// Remove the reading profile bound to the library, if it exists + /// + /// + /// + /// + Task ClearLibraryProfile(int userId, int libraryId); + + /// + /// Returns the all bound Reading Profile to a Library + /// + /// + /// + /// + Task> GetReadingProfileDtosForLibrary(int userId, int libraryId); + + /// + /// Returns the all bound Reading Profile to a Series + /// + /// + /// + /// + Task> GetReadingProfileDtosForSeries(int userId, int seriesId); + + /// + /// Set the assigned devices for the given reading profile. Then removes all duplicate links, ensuring each series + /// and library only has one profile per device + /// + /// + /// + /// + /// + Task SetProfileDevices(int userId, int profileId, List deviceIds); + + /// + /// Remove device ids from all profiles, does **NOT** commit + /// + /// + /// + /// + Task RemoveDeviceLinks(int userId, int deviceId); +} diff --git a/Kavita.API/Services/Reading/IReadingSessionService.cs b/Kavita.API/Services/Reading/IReadingSessionService.cs new file mode 100644 index 000000000..64bf8fd7c --- /dev/null +++ b/Kavita.API/Services/Reading/IReadingSessionService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Progress; + +namespace Kavita.API.Services.Reading; + +public interface IReadingSessionService +{ + Task UpdateProgress(int userId, ProgressDto progressDto, ClientInfoData? clientInfo, int? deviceId); +} diff --git a/Kavita.API/Services/Scanner/ILibraryWatcher.cs b/Kavita.API/Services/Scanner/ILibraryWatcher.cs new file mode 100644 index 000000000..df7b19401 --- /dev/null +++ b/Kavita.API/Services/Scanner/ILibraryWatcher.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; + +namespace Kavita.API.Services.Scanner; + +public interface ILibraryWatcher +{ + /// + /// Start watching all library folders + /// + /// + Task StartWatching(); + /// + /// Stop watching all folders + /// + void StopWatching(); + /// + /// Essentially stops then starts watching. Useful if there is a change in folders or libraries + /// + /// + Task RestartWatching(); +} diff --git a/Kavita.API/Services/Scanner/IProcessSeries.cs b/Kavita.API/Services/Scanner/IProcessSeries.cs new file mode 100644 index 000000000..2f6b4232c --- /dev/null +++ b/Kavita.API/Services/Scanner/IProcessSeries.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.Entities; +using Kavita.Models.Parser; + +namespace Kavita.API.Services.Scanner; + +public sealed record ProcessSeriesArgs +{ + public required Library Library { get; init; } + public required int TotalToProcess { get; init; } + public required int LeftToProcess { get; init; } + public bool ForceUpdate { get; init; } = false; +} + +public interface IProcessSeries +{ + Task ProcessSeriesAsync(MetadataSettingsDto settings, IList parsedInfos, ProcessSeriesArgs args); +} diff --git a/Kavita.API/Services/Scanner/IScannerService.cs b/Kavita.API/Services/Scanner/IScannerService.cs new file mode 100644 index 000000000..f9604b6b0 --- /dev/null +++ b/Kavita.API/Services/Scanner/IScannerService.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using Hangfire; +using Kavita.Common.Constants; +using Kavita.Models.Constants; + +namespace Kavita.API.Services.Scanner; + +public interface IScannerService +{ + /// + /// Given a library id, scans folders for said library. Parses files and generates DB updates. Will overwrite + /// cover images if forceUpdate is true. + /// + /// Library to scan against + /// Don't perform optimization checks, defaults to false + [Queue(TaskSchedulerConstants.ScanQueue)] + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + Task ScanLibrary(int libraryId, bool forceUpdate = false, bool isSingleScan = true); + + [Queue(TaskSchedulerConstants.ScanQueue)] + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + Task ScanLibraries(bool forceUpdate = false); + + [Queue(TaskSchedulerConstants.ScanQueue)] + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + Task ScanSeries(int seriesId, bool bypassFolderOptimizationChecks = true); + + Task ScanFolder(string folder, string originalPath, bool abortOnNoSeriesMatch = false); + Task AnalyzeFiles(); + +} diff --git a/Kavita.API/Services/SignalR/IEventHub.cs b/Kavita.API/Services/SignalR/IEventHub.cs new file mode 100644 index 000000000..fb6c1e4f8 --- /dev/null +++ b/Kavita.API/Services/SignalR/IEventHub.cs @@ -0,0 +1,14 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.SignalR; + +namespace Kavita.API.Services.SignalR; + +/// +/// Responsible for ushering events to the UI and allowing simple DI hook to send data +/// +public interface IEventHub +{ + Task SendMessageAsync(string method, SignalRMessage message, bool onlyAdmins = true, CancellationToken ct = default); + Task SendMessageToAsync(string method, SignalRMessage message, int userId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/SignalR/IPresenceTracker.cs b/Kavita.API/Services/SignalR/IPresenceTracker.cs new file mode 100644 index 000000000..be8e0cec1 --- /dev/null +++ b/Kavita.API/Services/SignalR/IPresenceTracker.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Kavita.API.Services.SignalR; + +public interface IPresenceTracker +{ + Task UserConnected(int userId, string connectionId); + Task UserDisconnected(int userId, string connectionId); + Task GetOnlineAdminIds(); + /// + /// Returns ids for users that are not admin + /// + /// + Task GetOnlineUserIds(); + Task> GetConnectionsForUser(int userId); +} diff --git a/Kavita.API/Store/IUserContext.cs b/Kavita.API/Store/IUserContext.cs new file mode 100644 index 000000000..8d0099f56 --- /dev/null +++ b/Kavita.API/Store/IUserContext.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Kavita.Models.Entities.Progress; + +namespace Kavita.API.Store; + +public interface IUserContext +{ + /// + /// Gets the current authenticated user's ID. + /// Returns null if user is not authenticated or on [AllowAnonymous] endpoint. + /// + int? GetUserId(); + + /// + /// Gets the current authenticated user's ID. + /// Throws KavitaException if user is not authenticated. + /// + int GetUserIdOrThrow(); + + /// + /// Gets the current authenticated user's username. + /// Returns null if user is not authenticated. + /// + /// Warning! Username's can contain .. and /, do not use folders or filenames explicitly with the Username + string? GetUsername(); + /// + /// The Roles associated with the Authenticated user + /// + IReadOnlyList Roles { get; } + /// + /// Returns true if the current user is authenticated. + /// + bool IsAuthenticated { get; } + /// + /// Gets the authentication method used (JWT, Auth Key, OIDC). + /// + AuthenticationType GetAuthenticationType(); + + + bool HasRole(string role); + bool HasAnyRole(params string[] roles); + bool HasAllRoles(params string[] roles); +} diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/Kavita.Benchmark/ArchiveServiceBenchmark.cs similarity index 97% rename from API.Benchmark/ArchiveServiceBenchmark.cs rename to Kavita.Benchmark/ArchiveServiceBenchmark.cs index 0d13623c2..2eb2307b4 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/Kavita.Benchmark/ArchiveServiceBenchmark.cs @@ -1,17 +1,16 @@ -using System; -using System.IO; -using System.IO.Abstractions; -using Microsoft.Extensions.Logging.Abstractions; -using API.Services; +using System.IO.Abstractions; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; +using Kavita.API.Services; +using Kavita.Services; +using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Processing; -namespace API.Benchmark; +namespace Kavita.Benchmark; [StopOnFirstError] [MemoryDiagnoser] diff --git a/API.Benchmark/CleanTitleBenchmark.cs b/Kavita.Benchmark/CleanTitleBenchmark.cs similarity index 63% rename from API.Benchmark/CleanTitleBenchmark.cs rename to Kavita.Benchmark/CleanTitleBenchmark.cs index 96c57a466..120c76c80 100644 --- a/API.Benchmark/CleanTitleBenchmark.cs +++ b/Kavita.Benchmark/CleanTitleBenchmark.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; -using System.IO; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Kavita.Services.Scanner; -namespace API.Benchmark; +namespace Kavita.Benchmark; [MemoryDiagnoser] public class CleanTitleBenchmarks @@ -17,7 +16,7 @@ public class CleanTitleBenchmarks { foreach (var name in _names) { - Services.Tasks.Scanner.Parser.Parser.CleanTitle(name, true); + Parser.CleanTitle(name, true); } } } diff --git a/API.Benchmark/Data/AesopsFables.epub b/Kavita.Benchmark/Data/AesopsFables.epub similarity index 100% rename from API.Benchmark/Data/AesopsFables.epub rename to Kavita.Benchmark/Data/AesopsFables.epub diff --git a/API.Benchmark/Data/Comics.txt b/Kavita.Benchmark/Data/Comics.txt similarity index 100% rename from API.Benchmark/Data/Comics.txt rename to Kavita.Benchmark/Data/Comics.txt diff --git a/API.Benchmark/Data/SeriesNamesForNormalization.txt b/Kavita.Benchmark/Data/SeriesNamesForNormalization.txt similarity index 100% rename from API.Benchmark/Data/SeriesNamesForNormalization.txt rename to Kavita.Benchmark/Data/SeriesNamesForNormalization.txt diff --git a/Kavita.Benchmark/Kavita.Benchmark.csproj b/Kavita.Benchmark/Kavita.Benchmark.csproj new file mode 100644 index 000000000..4afe15da2 --- /dev/null +++ b/Kavita.Benchmark/Kavita.Benchmark.csproj @@ -0,0 +1,44 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + PreserveNewest + + + + + + Always + + + + + Data + Always + + + + + PreserveNewest + + + + + + + + + diff --git a/API.Benchmark/KoreaderHashBenchmark.cs b/Kavita.Benchmark/KoreaderHashBenchmark.cs similarity index 91% rename from API.Benchmark/KoreaderHashBenchmark.cs rename to Kavita.Benchmark/KoreaderHashBenchmark.cs index c0abfd2ad..ffd94adae 100644 --- a/API.Benchmark/KoreaderHashBenchmark.cs +++ b/Kavita.Benchmark/KoreaderHashBenchmark.cs @@ -1,10 +1,9 @@ -using API.Helpers.Builders; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; -using System; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Builders; -namespace API.Benchmark +namespace Kavita.Benchmark { [StopOnFirstError] [MemoryDiagnoser] diff --git a/API.Benchmark/ParserBenchmarks.cs b/Kavita.Benchmark/ParserBenchmarks.cs similarity index 94% rename from API.Benchmark/ParserBenchmarks.cs rename to Kavita.Benchmark/ParserBenchmarks.cs index 0dabc560b..4e2d28110 100644 --- a/API.Benchmark/ParserBenchmarks.cs +++ b/Kavita.Benchmark/ParserBenchmarks.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; -namespace API.Benchmark; +namespace Kavita.Benchmark; [MemoryDiagnoser] [Orderer(SummaryOrderPolicy.FastestToSlowest)] diff --git a/API.Benchmark/Program.cs b/Kavita.Benchmark/Program.cs similarity index 93% rename from API.Benchmark/Program.cs rename to Kavita.Benchmark/Program.cs index 76ed97c70..a8f15c3a0 100644 --- a/API.Benchmark/Program.cs +++ b/Kavita.Benchmark/Program.cs @@ -1,6 +1,6 @@ using BenchmarkDotNet.Running; -namespace API.Benchmark; +namespace Kavita.Benchmark; /// /// To build this, cd into API.Benchmark directory and run diff --git a/API.Benchmark/TestBenchmark.cs b/Kavita.Benchmark/TestBenchmark.cs similarity index 89% rename from API.Benchmark/TestBenchmark.cs rename to Kavita.Benchmark/TestBenchmark.cs index 511d250aa..2365683a0 100644 --- a/API.Benchmark/TestBenchmark.cs +++ b/Kavita.Benchmark/TestBenchmark.cs @@ -1,12 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.DTOs; -using API.Extensions; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs; +using Kavita.Services.Extensions; -namespace API.Benchmark; +namespace Kavita.Benchmark; /// /// This is used as a scratchpad for testing diff --git a/API.Tests/Extensions/EnumerableExtensionsTests.cs b/Kavita.Common.Tests/Extensions/EnumerableExtensionsTests.cs similarity index 84% rename from API.Tests/Extensions/EnumerableExtensionsTests.cs rename to Kavita.Common.Tests/Extensions/EnumerableExtensionsTests.cs index bdd3433ae..deb048093 100644 --- a/API.Tests/Extensions/EnumerableExtensionsTests.cs +++ b/Kavita.Common.Tests/Extensions/EnumerableExtensionsTests.cs @@ -1,11 +1,6 @@ -using System.Collections.Generic; -using System.Linq; -using API.Data.Misc; -using API.Entities.Enums; -using API.Extensions; -using Xunit; +using Kavita.Common.Extensions; -namespace API.Tests.Extensions; +namespace Kavita.Common.Tests.Extensions; public class EnumerableExtensionsTests { @@ -135,33 +130,4 @@ public class EnumerableExtensionsTests i++; } } - - [Theory] - [InlineData(true, 2)] - [InlineData(false, 1)] - public void RestrictAgainstAgeRestriction_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) - { - var items = new List() - { - new RecentlyAddedSeries() - { - AgeRating = AgeRating.Teen, - }, - new RecentlyAddedSeries() - { - AgeRating = AgeRating.Unknown, - }, - new RecentlyAddedSeries() - { - AgeRating = AgeRating.X18Plus, - }, - }; - - var filtered = items.RestrictAgainstAgeRestriction(new AgeRestriction() - { - AgeRating = AgeRating.Teen, - IncludeUnknowns = includeUnknowns - }); - Assert.Equal(expectedCount, filtered.Count()); - } } diff --git a/API.Tests/Extensions/PathExtensionsTests.cs b/Kavita.Common.Tests/Extensions/PathExtensionsTests.cs similarity index 81% rename from API.Tests/Extensions/PathExtensionsTests.cs rename to Kavita.Common.Tests/Extensions/PathExtensionsTests.cs index bdc752a92..fff2ed966 100644 --- a/API.Tests/Extensions/PathExtensionsTests.cs +++ b/Kavita.Common.Tests/Extensions/PathExtensionsTests.cs @@ -1,8 +1,6 @@ -using System.IO; -using Xunit; -using API.Extensions; +using Kavita.Common.Extensions; -namespace API.Tests.Extensions; +namespace Kavita.Common.Tests.Extensions; public class PathExtensionsTests { diff --git a/API.Tests/Extensions/VersionExtensionTests.cs b/Kavita.Common.Tests/Extensions/VersionExtensionTests.cs similarity index 95% rename from API.Tests/Extensions/VersionExtensionTests.cs rename to Kavita.Common.Tests/Extensions/VersionExtensionTests.cs index aee295370..ac6dee29e 100644 --- a/API.Tests/Extensions/VersionExtensionTests.cs +++ b/Kavita.Common.Tests/Extensions/VersionExtensionTests.cs @@ -1,8 +1,6 @@ -using System; -using API.Extensions; -using Xunit; +using Kavita.Common.Extensions; -namespace API.Tests.Extensions; +namespace Kavita.Common.Tests.Extensions; public class VersionHelperTests { diff --git a/API.Tests/Converters/CronConverterTests.cs b/Kavita.Common.Tests/Helpers/CronConverterTests.cs similarity index 83% rename from API.Tests/Converters/CronConverterTests.cs rename to Kavita.Common.Tests/Helpers/CronConverterTests.cs index 5568c89d0..86eeb3e62 100644 --- a/API.Tests/Converters/CronConverterTests.cs +++ b/Kavita.Common.Tests/Helpers/CronConverterTests.cs @@ -1,8 +1,7 @@ -using API.Helpers.Converters; -using Xunit; +using Kavita.Common.Helpers; + +namespace Kavita.Common.Tests.Helpers; -namespace API.Tests.Converters; -#nullable enable public class CronConverterTests { [Theory] diff --git a/Kavita.Common.Tests/Helpers/HtmlHelperTests.cs b/Kavita.Common.Tests/Helpers/HtmlHelperTests.cs new file mode 100644 index 000000000..b609a2823 --- /dev/null +++ b/Kavita.Common.Tests/Helpers/HtmlHelperTests.cs @@ -0,0 +1,134 @@ +using Kavita.Common.Helpers; + +namespace Kavita.Common.Tests.Helpers; + +public class HtmlHelperTests +{ + #region GetCharacters Tests + + [Fact] + public void GetCharacters_WithNullBody_ReturnsNull() + { + + string body = null; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetCharacters_WithEmptyBody_ReturnsEmptyString() + { + + var body = string.Empty; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void GetCharacters_WithNoTextNodes_ReturnsEmptyString() + { + + const string body = "
"; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void GetCharacters_WithLessCharactersThanLimit_ReturnsFullText() + { + + var body = "

This is a short review.

"; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Equal("This is a short review.…", result); + } + + [Fact] + public void GetCharacters_WithMoreCharactersThanLimit_TruncatesText() + { + + var body = "

" + new string('a', 200) + "

"; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Equal(new string('a', 175) + "…", result); + Assert.Equal(176, result.Length); // 175 characters + ellipsis + } + + [Fact] + public void GetCharacters_IgnoresScriptTags() + { + + const string body = "

Visible text

"; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Equal("Visible text…", result); + Assert.DoesNotContain("hidden", result); + } + + [Fact] + public void GetCharacters_RemovesMarkdownSymbols() + { + + const string body = "

This is **bold** and _italic_ text with [link](url).

"; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Equal("This is bold and italic text with link.…", result); + } + + [Fact] + public void GetCharacters_HandlesComplexMarkdownAndHtml() + { + + const string body = """ + +
+

# Header

+

This is ~~strikethrough~~ and __underlined__ text

+

~~~code block~~~

+

+++highlighted+++

+

img123(image.jpg)

+
+ """; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.DoesNotContain("~~", result); + Assert.DoesNotContain("__", result); + Assert.DoesNotContain("~~~", result); + Assert.DoesNotContain("+++", result); + Assert.DoesNotContain("img123(", result); + Assert.Contains("Header", result); + Assert.Contains("strikethrough", result); + Assert.Contains("underlined", result); + Assert.Contains("code block", result); + Assert.Contains("highlighted", result); + } + + #endregion +} diff --git a/API.Tests/Helpers/RandfHelper.cs b/Kavita.Common.Tests/Helpers/RandfHelper.cs similarity index 96% rename from API.Tests/Helpers/RandfHelper.cs rename to Kavita.Common.Tests/Helpers/RandfHelper.cs index 01a6a2df5..9ca02b912 100644 --- a/API.Tests/Helpers/RandfHelper.cs +++ b/Kavita.Common.Tests/Helpers/RandfHelper.cs @@ -1,11 +1,10 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -namespace API.Tests.Helpers; +namespace Kavita.Common.Tests.Helpers; +/// +/// This is not a test class, but a helper to help you write test that require random interactions +/// public static class RandfHelper { private static readonly Random Random = new (); diff --git a/API.Tests/Helpers/RateLimiterTests.cs b/Kavita.Common.Tests/Helpers/RateLimiterTests.cs similarity index 93% rename from API.Tests/Helpers/RateLimiterTests.cs rename to Kavita.Common.Tests/Helpers/RateLimiterTests.cs index bf7827106..f7c58e130 100644 --- a/API.Tests/Helpers/RateLimiterTests.cs +++ b/Kavita.Common.Tests/Helpers/RateLimiterTests.cs @@ -1,9 +1,6 @@ -using System; -using System.Threading.Tasks; -using API.Helpers; -using Xunit; +using Kavita.Common.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Common.Tests.Helpers; public class RateLimiterTests { diff --git a/API.Tests/Helpers/StringHelperTests.cs b/Kavita.Common.Tests/Helpers/StringHelperTests.cs similarity index 96% rename from API.Tests/Helpers/StringHelperTests.cs rename to Kavita.Common.Tests/Helpers/StringHelperTests.cs index 8f845c9b0..9a3d11d5c 100644 --- a/API.Tests/Helpers/StringHelperTests.cs +++ b/Kavita.Common.Tests/Helpers/StringHelperTests.cs @@ -1,7 +1,6 @@ -using API.Helpers; -using Xunit; +using Kavita.Common.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Common.Tests.Helpers; public class StringHelperTests { diff --git a/Kavita.Common.Tests/Kavita.Common.Tests.csproj b/Kavita.Common.Tests/Kavita.Common.Tests.csproj new file mode 100644 index 000000000..6ac641f8e --- /dev/null +++ b/Kavita.Common.Tests/Kavita.Common.Tests.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + \ No newline at end of file diff --git a/API/Constants/Headers.cs b/Kavita.Common/Constants/Headers.cs similarity index 93% rename from API/Constants/Headers.cs rename to Kavita.Common/Constants/Headers.cs index 7f44b30ac..df8d590b8 100644 --- a/API/Constants/Headers.cs +++ b/Kavita.Common/Constants/Headers.cs @@ -1,4 +1,4 @@ -namespace API.Constants; +namespace Kavita.Common.Constants; public static class Headers { diff --git a/API/Extensions/ClaimsPrincipalExtensions.cs b/Kavita.Common/Extensions/ClaimsPrincipalExtensions.cs similarity index 89% rename from API/Extensions/ClaimsPrincipalExtensions.cs rename to Kavita.Common/Extensions/ClaimsPrincipalExtensions.cs index ea1cb5355..c83a694d0 100644 --- a/API/Extensions/ClaimsPrincipalExtensions.cs +++ b/Kavita.Common/Extensions/ClaimsPrincipalExtensions.cs @@ -1,11 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; -using Kavita.Common; -using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; -namespace API.Extensions; -#nullable enable +namespace Kavita.Common.Extensions; public static class ClaimsPrincipalExtensions { diff --git a/API/Extensions/DateTimeExtensions.cs b/Kavita.Common/Extensions/DateTimeExtensions.cs similarity index 94% rename from API/Extensions/DateTimeExtensions.cs rename to Kavita.Common/Extensions/DateTimeExtensions.cs index 89c930afc..43a945396 100644 --- a/API/Extensions/DateTimeExtensions.cs +++ b/Kavita.Common/Extensions/DateTimeExtensions.cs @@ -1,7 +1,6 @@ -using System; +using System; -namespace API.Extensions; -#nullable enable +namespace Kavita.Common.Extensions; public static class DateTimeExtensions { diff --git a/API/Extensions/DoubleExtensions.cs b/Kavita.Common/Extensions/DoubleExtensions.cs similarity index 92% rename from API/Extensions/DoubleExtensions.cs rename to Kavita.Common/Extensions/DoubleExtensions.cs index 3deb37ffb..d66e56943 100644 --- a/API/Extensions/DoubleExtensions.cs +++ b/Kavita.Common/Extensions/DoubleExtensions.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace API.Extensions; +namespace Kavita.Common.Extensions; public static class DoubleExtensions { diff --git a/Kavita.Common/Extensions/EnumExtensions.cs b/Kavita.Common/Extensions/EnumExtensions.cs index e672d8050..83f8c2fe7 100644 --- a/Kavita.Common/Extensions/EnumExtensions.cs +++ b/Kavita.Common/Extensions/EnumExtensions.cs @@ -1,4 +1,6 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; +using System.Reflection; namespace Kavita.Common.Extensions; @@ -17,4 +19,38 @@ public static class EnumExtensions return attributes is {Length: > 0} ? attributes[0].Description : value.ToString(); } + + /// + /// Extension on Enum.TryParse which also tried matching on the description attribute + /// + /// if a match was found + /// First tries Enum.TryParse then fall back to the more expensive operation + public static bool TryParse(string? value, out TEnum result) where TEnum : struct, Enum + { + result = default; + + if (string.IsNullOrEmpty(value)) + { + return false; + } + + if (Enum.TryParse(value, out result)) + { + return true; + } + + foreach (var field in typeof(TEnum).GetFields(BindingFlags.Public | BindingFlags.Static)) + { + var description = field.GetCustomAttribute()?.Description; + + if (!string.IsNullOrEmpty(description) && + string.Equals(description, value, StringComparison.OrdinalIgnoreCase)) + { + result = (TEnum)field.GetValue(null)!; + return true; + } + } + + return false; + } } diff --git a/Kavita.Common/Extensions/EnumerableExtensions.cs b/Kavita.Common/Extensions/EnumerableExtensions.cs new file mode 100644 index 000000000..b8e0ba40e --- /dev/null +++ b/Kavita.Common/Extensions/EnumerableExtensions.cs @@ -0,0 +1,68 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Kavita.Common.Extensions; + +public static class EnumerableExtensions +{ + private static readonly Regex Regex = new Regex(@"\d+", RegexOptions.Compiled, TimeSpan.FromMilliseconds(500)); + + /// + /// A natural sort implementation + /// + /// IEnumerable to process + /// Function that produces a string. Does not support null values + /// Defaults to CurrentCulture + /// + /// Sorted Enumerable + public static IEnumerable OrderByNatural(this IEnumerable items, Func selector, StringComparer? stringComparer = null) + { + var list = items.ToList(); + var maxDigits = list + .SelectMany(i => Regex.Matches(selector(i)) + .Select(digitChunk => (int?)digitChunk.Value.Length)) + .Max() ?? 0; + + return list.OrderBy(i => Regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture); + } + + /// + /// + extension(IList source) + { + /// + /// Safety net around Max, returning the default value if the source contains no elements + /// + /// + /// + /// + /// + public TResult? MaxOrDefault(Func selector, + TResult? defaultValue) + { + return source.Count == 0 ? defaultValue : source.Max(selector); + } + + /// + /// Safety wrapper around Min, returning the default value if the source has no elements + /// + /// + /// + /// + /// + public TResult? MinOrDefault(Func selector, + TResult? defaultValue) + { + return source.Count == 0 ? defaultValue : source.Min(selector); + } + } + + public static IEnumerable WhereNotNull(this IEnumerable source) + where TSource : class + { + return source.Where(item => item != null)!; + } +} diff --git a/API/Extensions/FloatExtensions.cs b/Kavita.Common/Extensions/FloatExtensions.cs similarity index 91% rename from API/Extensions/FloatExtensions.cs rename to Kavita.Common/Extensions/FloatExtensions.cs index 6fa553239..d9facfe20 100644 --- a/API/Extensions/FloatExtensions.cs +++ b/Kavita.Common/Extensions/FloatExtensions.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace API.Extensions; +namespace Kavita.Common.Extensions; public static class FloatExtensions { diff --git a/API/Extensions/FlurlExtensions.cs b/Kavita.Common/Extensions/FlurlExtensions.cs similarity index 93% rename from API/Extensions/FlurlExtensions.cs rename to Kavita.Common/Extensions/FlurlExtensions.cs index a26e53914..e385cdc66 100644 --- a/API/Extensions/FlurlExtensions.cs +++ b/Kavita.Common/Extensions/FlurlExtensions.cs @@ -1,15 +1,13 @@ using System; -using API.Constants; using System.Linq; using System.Threading.Tasks; using Flurl.Http; -using Kavita.Common; +using Kavita.Common.Constants; using Kavita.Common.EnvironmentInfo; -using Microsoft.Net.Http.Headers; using Microsoft.AspNetCore.StaticFiles; +using Microsoft.Net.Http.Headers; -namespace API.Extensions; -#nullable enable +namespace Kavita.Common.Extensions; public static class FlurlExtensions { @@ -34,8 +32,7 @@ public static class FlurlExtensions return null; } - // TODO: Move to new Headers class after merge with progress branch - var contentTypeHeader = headResponse.Headers.FirstOrDefault("Content-Type"); + var contentTypeHeader = headResponse.Headers.FirstOrDefault(HeaderNames.ContentType); if (string.IsNullOrEmpty(contentTypeHeader)) { return null; diff --git a/API/Extensions/ImageExtensions.cs b/Kavita.Common/Extensions/ImageExtensions.cs similarity index 99% rename from API/Extensions/ImageExtensions.cs rename to Kavita.Common/Extensions/ImageExtensions.cs index 5779b18ec..58bfea11d 100644 --- a/API/Extensions/ImageExtensions.cs +++ b/Kavita.Common/Extensions/ImageExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using Image = SixLabors.ImageSharp.Image; -namespace API.Extensions; +namespace Kavita.Common.Extensions; public static class ImageExtensions { diff --git a/Kavita.Common/Extensions/PathExtensions.cs b/Kavita.Common/Extensions/PathExtensions.cs index 904589630..b04290241 100644 --- a/Kavita.Common/Extensions/PathExtensions.cs +++ b/Kavita.Common/Extensions/PathExtensions.cs @@ -4,8 +4,11 @@ namespace Kavita.Common.Extensions; public static class PathExtensions { - public static string GetParentDirectory(string filePath) + public static string GetFullPathWithoutExtension(this string filepath) { - return Path.GetDirectoryName(filePath); + if (string.IsNullOrEmpty(filepath)) return filepath; + var extension = Path.GetExtension(filepath); + if (string.IsNullOrEmpty(extension)) return filepath; + return Path.GetFullPath(filepath.Replace(extension, string.Empty)); } } diff --git a/Kavita.Common/Extensions/StringExtensions.cs b/Kavita.Common/Extensions/StringExtensions.cs new file mode 100644 index 000000000..333b1c169 --- /dev/null +++ b/Kavita.Common/Extensions/StringExtensions.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Kavita.Common.Extensions; +#nullable enable + +public static partial class StringExtensions +{ + private static readonly Regex SentenceCaseRegex = new(@"(^[a-z])|\.\s+(.)", + RegexOptions.ExplicitCapture | RegexOptions.Compiled, + TimeSpan.FromMilliseconds(500)); + + /// + /// Normalize everything within Kavita. Some characters don't fall under Unicode, like full-width characters and need to be + /// added on a case-by-case basis. + /// + private static readonly Regex NormalizeRegex = new(@"[^\p{L}0-9\+!*!+]", + RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(500)); + + extension(string input) + { + public string Sanitize() + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + + // Remove all newline and control characters + var sanitized = input + .Replace(Environment.NewLine, string.Empty) + .Replace("\n", string.Empty) + .Replace("\r", string.Empty); + + // Optionally remove other potentially unwanted characters + sanitized = Regex.Replace(sanitized, @"[^\u0020-\u007E]", string.Empty); // Removes non-printable ASCII + + return sanitized.Trim(); // Trim any leading/trailing whitespace + } + + public string SentenceCase() + { + return SentenceCaseRegex.Replace(input.ToLower(), s => s.Value.ToUpper()); + } + } + + /// + extension(string? value) + { + /// + /// Apply normalization on the String + /// + /// + public string ToNormalized() + { + return string.IsNullOrEmpty(value) ? string.Empty : NormalizeRegex.Replace(value, string.Empty).Trim().ToLower(); + } + + /// + /// Normalizes the slashes in a path to be + /// + /// /manga/1\1 -> /manga/1/1 + /// + public string NormalizePath() + { + return string.IsNullOrEmpty(value) ? string.Empty : value.Replace('\\', Path.AltDirectorySeparatorChar) + .Replace(@"//", Path.AltDirectorySeparatorChar + string.Empty); + } + + public float AsFloat(float defaultValue = 0.0f) + { + return string.IsNullOrEmpty(value) ? defaultValue : float.Parse(value, CultureInfo.InvariantCulture); + } + + public double AsDouble(double defaultValue = 0.0f) + { + return string.IsNullOrEmpty(value) ? defaultValue : double.Parse(value, CultureInfo.InvariantCulture); + } + + public string TrimPrefix(string prefix) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + + if (!value.StartsWith(prefix)) return value; + + return value.Substring(prefix.Length); + } + + /// + /// Censor the input string by removing all but the first and last char. + /// + /// + /// If the input is an email (contains @), the domain will remain untouched + public string Censor() + { + if (string.IsNullOrWhiteSpace(value)) return value ?? string.Empty; + + var atIdx = value.IndexOf('@'); + if (atIdx == -1) + { + return $"{value[0]}{new string('*', value.Length - 1)}"; + } + + return value[0] + new string('*', atIdx - 1) + value[atIdx..]; + } + + /// + /// Repeat returns a string that is equal to the original string repeat n times + /// + /// Amount of times to repeat + /// + public string Repeat(int n) + { + return string.IsNullOrEmpty(value) ? string.Empty : string.Concat(Enumerable.Repeat(value, n)); + } + } + + extension(string value) + { + /// + /// Splits the string by the given separator. While cleaning out entries and removing duplicates + /// + /// + /// + public IList SplitBy(char separator) + { + if (string.IsNullOrEmpty(value)) + { + return ImmutableList.Empty; + } + + return value.Split(separator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .DistinctBy(s => s.ToNormalized()) + .ToList(); + } + + public IList ParseIntArray() + { + if (string.IsNullOrWhiteSpace(value)) + { + return []; + } + + return value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(int.Parse) + .ToList(); + } + + /// + /// Parses a human-readable file size string (e.g. "1.43 GB") into bytes. + /// + /// Byte count as long + /// The input string like "1.43 GB", "4.2 KB", "512 B" + public long ParseHumanReadableBytes() + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Input cannot be null or empty.", nameof(value)); + } + + + var match = HumanReadableBytesRegex().Match(value); + if (!match.Success) + { + throw new FormatException($"Invalid format: '{value}'"); + } + + + var value1 = double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + var unit = match.Groups[2].Value.ToUpperInvariant(); + + var multiplier = unit switch + { + "B" => 1L, + "KB" => 1L << 10, + "MB" => 1L << 20, + "GB" => 1L << 30, + "TB" => 1L << 40, + "PB" => 1L << 50, + "EB" => 1L << 60, + _ => throw new FormatException($"Unknown unit: '{unit}'") + }; + + return (long)(value1 * multiplier); + } + } + + [GeneratedRegex(@"^\s*(\d+(?:\.\d+)?)\s*([KMGTPE]?B)\s*$", RegexOptions.IgnoreCase)] + private static partial Regex HumanReadableBytesRegex(); +} diff --git a/API/Extensions/VersionExtensions.cs b/Kavita.Common/Extensions/VersionExtensions.cs similarity index 88% rename from API/Extensions/VersionExtensions.cs rename to Kavita.Common/Extensions/VersionExtensions.cs index 1877b48b1..d6f7b7d76 100644 --- a/API/Extensions/VersionExtensions.cs +++ b/Kavita.Common/Extensions/VersionExtensions.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace API.Extensions; +namespace Kavita.Common.Extensions; public static class VersionExtensions { diff --git a/API/Helpers/AuthKeyHelper.cs b/Kavita.Common/Helpers/AuthKeyHelper.cs similarity index 95% rename from API/Helpers/AuthKeyHelper.cs rename to Kavita.Common/Helpers/AuthKeyHelper.cs index 94a6e04d2..211a6932e 100644 --- a/API/Helpers/AuthKeyHelper.cs +++ b/Kavita.Common/Helpers/AuthKeyHelper.cs @@ -1,7 +1,7 @@ using System; using System.Security.Cryptography; -namespace API.Helpers; +namespace Kavita.Common.Helpers; public static class AuthKeyHelper { diff --git a/API/Helpers/Converters/CronConverter.cs b/Kavita.Common/Helpers/CronConverter.cs similarity index 77% rename from API/Helpers/Converters/CronConverter.cs rename to Kavita.Common/Helpers/CronConverter.cs index f1f0ebc1b..ff8f94cb8 100644 --- a/API/Helpers/Converters/CronConverter.cs +++ b/Kavita.Common/Helpers/CronConverter.cs @@ -1,17 +1,16 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Hangfire; -namespace API.Helpers.Converters; -#nullable enable +namespace Kavita.Common.Helpers; public static class CronConverter { - public static readonly IEnumerable Options = new [] - { + public static readonly IEnumerable Options = + [ "disabled", "daily", - "weekly", - }; + "weekly" + ]; /// /// Converts to Cron Notation /// diff --git a/API/Helpers/DayOfWeekHelper.cs b/Kavita.Common/Helpers/DayOfWeekHelper.cs similarity index 89% rename from API/Helpers/DayOfWeekHelper.cs rename to Kavita.Common/Helpers/DayOfWeekHelper.cs index 10cdb4170..c12fc02c3 100644 --- a/API/Helpers/DayOfWeekHelper.cs +++ b/Kavita.Common/Helpers/DayOfWeekHelper.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace API.Helpers; +namespace Kavita.Common.Helpers; public static class DayOfWeekHelper { diff --git a/API/Helpers/ReviewHelper.cs b/Kavita.Common/Helpers/HtmlHelper.cs similarity index 62% rename from API/Helpers/ReviewHelper.cs rename to Kavita.Common/Helpers/HtmlHelper.cs index a5b9f35d3..3c0f00ab3 100644 --- a/API/Helpers/ReviewHelper.cs +++ b/Kavita.Common/Helpers/HtmlHelper.cs @@ -1,51 +1,16 @@ -using System; -using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using API.DTOs.SeriesDetail; using HtmlAgilityPack; -namespace API.Helpers; +namespace Kavita.Common.Helpers; -public static class ReviewHelper +#nullable enable + +public static class HtmlHelper { private const int BodyTextLimit = 175; - public static IEnumerable SelectSpectrumOfReviews(IList reviews) - { - IList externalReviews; - var totalReviews = reviews.Count; - if (totalReviews > 10) - { - var stepSize = Math.Max((totalReviews - 4) / 8, 1); - - var selectedReviews = new List() - { - reviews[0], - reviews[1], - }; - for (var i = 2; i < totalReviews - 2; i += stepSize) - { - selectedReviews.Add(reviews[i]); - - if (selectedReviews.Count >= 8) - break; - } - - selectedReviews.Add(reviews[totalReviews - 2]); - selectedReviews.Add(reviews[totalReviews - 1]); - - externalReviews = selectedReviews; - } - else - { - externalReviews = reviews; - } - - return externalReviews.OrderByDescending(r => r.Score); - } - - public static string GetCharacters(string body) + public static string? GetCharacters(string? body) { if (string.IsNullOrEmpty(body)) return body; @@ -54,6 +19,7 @@ public static class ReviewHelper var textNodes = doc.DocumentNode.SelectNodes("//text()[not(parent::script)]"); if (textNodes == null) return string.Empty; + var plainText = string.Join(" ", textNodes .Select(node => node.InnerText) .Where(s => !s.Equals("\n"))); @@ -84,5 +50,4 @@ public static class ReviewHelper return plainText + "…"; } - } diff --git a/API/Helpers/JwtHelper.cs b/Kavita.Common/Helpers/JwtHelper.cs similarity index 96% rename from API/Helpers/JwtHelper.cs rename to Kavita.Common/Helpers/JwtHelper.cs index c4dc99125..4fe2cf4d0 100644 --- a/API/Helpers/JwtHelper.cs +++ b/Kavita.Common/Helpers/JwtHelper.cs @@ -1,7 +1,7 @@ using System; using System.IdentityModel.Tokens.Jwt; -namespace API.Helpers; +namespace Kavita.Common.Helpers; public static class JwtHelper { diff --git a/API/Helpers/NumberHelper.cs b/Kavita.Common/Helpers/NumberHelper.cs similarity index 84% rename from API/Helpers/NumberHelper.cs rename to Kavita.Common/Helpers/NumberHelper.cs index 906e405cc..06c41ab2b 100644 --- a/API/Helpers/NumberHelper.cs +++ b/Kavita.Common/Helpers/NumberHelper.cs @@ -1,4 +1,4 @@ -namespace API.Helpers; +namespace Kavita.Common.Helpers; #nullable enable public static class NumberHelper diff --git a/Kavita.Common/Helpers/PagedList.cs b/Kavita.Common/Helpers/PagedList.cs new file mode 100644 index 000000000..d633dd126 --- /dev/null +++ b/Kavita.Common/Helpers/PagedList.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Kavita.Common.Helpers; + +public class PagedList : List +{ + private PagedList(IEnumerable items, int count, int pageNumber, int pageSize) + { + CurrentPage = pageNumber; + TotalPages = (int) Math.Ceiling(count / (double) pageSize); + PageSize = pageSize; + TotalCount = count; + AddRange(items); + } + + public int CurrentPage { get; set; } + public int TotalPages { get; set; } + public int PageSize { get; set; } + public int TotalCount { get; set; } + + public static PagedList Create(IEnumerable items, int totalCount, int pageNumber, int pageSize) + { + return new PagedList(items, totalCount, pageNumber, pageSize); + } +} diff --git a/API/Helpers/PaginationHeader.cs b/Kavita.Common/Helpers/PaginationHeader.cs similarity index 91% rename from API/Helpers/PaginationHeader.cs rename to Kavita.Common/Helpers/PaginationHeader.cs index b11c5ecd4..bfa5b3e8d 100644 --- a/API/Helpers/PaginationHeader.cs +++ b/Kavita.Common/Helpers/PaginationHeader.cs @@ -1,5 +1,4 @@ -namespace API.Helpers; -#nullable enable +namespace Kavita.Common.Helpers; public class PaginationHeader { diff --git a/API/Helpers/RateLimiter.cs b/Kavita.Common/Helpers/RateLimiter.cs similarity index 98% rename from API/Helpers/RateLimiter.cs rename to Kavita.Common/Helpers/RateLimiter.cs index ffdcadc9c..735a96a94 100644 --- a/API/Helpers/RateLimiter.cs +++ b/Kavita.Common/Helpers/RateLimiter.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Threading; -namespace API.Helpers; +namespace Kavita.Common.Helpers; public class RateLimiter(int maxRequests, TimeSpan duration, bool refillBetween = true) { diff --git a/API/Helpers/StringHelper.cs b/Kavita.Common/Helpers/StringHelper.cs similarity index 98% rename from API/Helpers/StringHelper.cs rename to Kavita.Common/Helpers/StringHelper.cs index 0a20910c5..e34a019b7 100644 --- a/API/Helpers/StringHelper.cs +++ b/Kavita.Common/Helpers/StringHelper.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace API.Helpers; +namespace Kavita.Common.Helpers; #nullable enable public static partial class StringHelper diff --git a/API/Helpers/UserParams.cs b/Kavita.Common/Helpers/UserParams.cs similarity index 96% rename from API/Helpers/UserParams.cs rename to Kavita.Common/Helpers/UserParams.cs index 24faf6a8d..ff6a57feb 100644 --- a/API/Helpers/UserParams.cs +++ b/Kavita.Common/Helpers/UserParams.cs @@ -1,4 +1,4 @@ -namespace API.Helpers; +namespace Kavita.Common.Helpers; #nullable enable /// diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 436c0f0b4..c91ef05a5 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -9,15 +9,23 @@ + - - - + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + \ No newline at end of file diff --git a/API.Tests/AbstractDbTest.cs b/Kavita.Database.Tests/AbstractDbTest.cs similarity index 81% rename from API.Tests/AbstractDbTest.cs rename to Kavita.Database.Tests/AbstractDbTest.cs index 79fa075c8..7962c27a6 100644 --- a/API.Tests/AbstractDbTest.cs +++ b/Kavita.Database.Tests/AbstractDbTest.cs @@ -1,23 +1,20 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.AutoMapper; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; using AutoMapper; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Models.AutoMapper; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using NSubstitute; using Xunit.Abstractions; -namespace API.Tests; -#nullable enable +namespace Kavita.Database.Tests; public abstract class AbstractDbTest(ITestOutputHelper testOutputHelper): AbstractFsTest, IAsyncDisposable { @@ -27,6 +24,9 @@ public abstract class AbstractDbTest(ITestOutputHelper testOutputHelper): Abstra protected async Task<(IUnitOfWork, DataContext, IMapper)> CreateDatabase() { + + GlobalConfiguration.Configuration.UseInMemoryStorage(); + // Dispose any previous connection if CreateDatabase is called multiple times if (_connection != null) { @@ -36,9 +36,8 @@ public abstract class AbstractDbTest(ITestOutputHelper testOutputHelper): Abstra _connection = new SqliteConnection("Filename=:memory:"); await _connection.OpenAsync(); - var contextOptions = new DbContextOptionsBuilder() - .UseSqlite(_connection) - .EnableSensitiveDataLogging() + var contextOptions = ((DbContextOptionsBuilder)new DbContextOptionsBuilder() + .UseSqlite(_connection)).EnableSensitiveDataLogging() .Options; _context = new DataContext(contextOptions); @@ -54,20 +53,21 @@ public abstract class AbstractDbTest(ITestOutputHelper testOutputHelper): Abstra }); var mapper = config.CreateMapper(); - GlobalConfiguration.Configuration.UseInMemoryStorage(); - var unitOfWork = new UnitOfWork(_context, mapper, null); + var unitOfWork = new UnitOfWork(_context, mapper, null!); _context.ChangeTracker.Clear(); return (unitOfWork, _context, mapper); } - private async Task SeedDb(DataContext context) + private async Task SeedDb(DataContext context) { try { - var filesystem = CreateFileSystem(); - await Seed.SeedSettings(context, new DirectoryService(Substitute.For>(), filesystem)); + var directoryService = Substitute.For(); + directoryService.BackupDirectory.Returns(BackupDirectory); + + await Seed.SeedSettings(context, directoryService); var setting = await context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync(); setting.Value = CacheDirectory; @@ -92,19 +92,18 @@ public abstract class AbstractDbTest(ITestOutputHelper testOutputHelper): Abstra await context.SaveChangesAsync(); await Seed.SeedMetadataSettings(context); - - return true; } catch (Exception ex) { testOutputHelper.WriteLine($"[SeedDb] Error: {ex.Message} \n{ex.StackTrace}"); - return false; + throw; } } /// /// Add a role to an existing User. Commits. /// + /// /// /// protected static async Task AddUserWithRole(DataContext context, int userId, string roleName) diff --git a/API.Tests/AbstractFsTest.cs b/Kavita.Database.Tests/AbstractFsTest.cs similarity index 90% rename from API.Tests/AbstractFsTest.cs rename to Kavita.Database.Tests/AbstractFsTest.cs index 0c6a0e262..4afc27e2d 100644 --- a/API.Tests/AbstractFsTest.cs +++ b/Kavita.Database.Tests/AbstractFsTest.cs @@ -1,14 +1,13 @@ using System.IO; using System.IO.Abstractions.TestingHelpers; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Common.Extensions; -namespace API.Tests; -#nullable enable +namespace Kavita.Database.Tests; public abstract class AbstractFsTest { - protected static readonly string Root = Parser.NormalizePath(Path.GetPathRoot(Directory.GetCurrentDirectory())); + protected static readonly string Root = Path.GetPathRoot(Directory.GetCurrentDirectory()).NormalizePath(); protected static readonly string ConfigDirectory = Root + "kavita/config/"; protected static readonly string CacheDirectory = ConfigDirectory + "cache/"; protected static readonly string CacheLongDirectory = ConfigDirectory + "cache-long/"; diff --git a/API.Tests/Extensions/QueryableExtensionsTests.cs b/Kavita.Database.Tests/Extensions/QueryableExtensionsTests.cs similarity index 98% rename from API.Tests/Extensions/QueryableExtensionsTests.cs rename to Kavita.Database.Tests/Extensions/QueryableExtensionsTests.cs index 15e02430a..9c112585c 100644 --- a/API.Tests/Extensions/QueryableExtensionsTests.cs +++ b/Kavita.Database.Tests/Extensions/QueryableExtensionsTests.cs @@ -1,17 +1,16 @@ using System.Collections.Generic; using System.Linq; -using API.Data.Misc; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; -using API.Entities.Metadata; -using API.Entities.Person; -using API.Entities.User; -using API.Extensions.QueryExtensions; -using API.Helpers.Builders; +using Kavita.Database.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; using Xunit; -namespace API.Tests.Extensions; +namespace Kavita.Database.Tests.Extensions; public class QueryableExtensionsTests { diff --git a/Kavita.Database.Tests/Kavita.Database.Tests.csproj b/Kavita.Database.Tests/Kavita.Database.Tests.csproj new file mode 100644 index 000000000..f15cc7299 --- /dev/null +++ b/Kavita.Database.Tests/Kavita.Database.Tests.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + disable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + \ No newline at end of file diff --git a/Kavita.Database.Tests/Repositories/ExternalSeriesMetadataRepositoryTests.cs b/Kavita.Database.Tests/Repositories/ExternalSeriesMetadataRepositoryTests.cs new file mode 100644 index 000000000..f7b6e58ba --- /dev/null +++ b/Kavita.Database.Tests/Repositories/ExternalSeriesMetadataRepositoryTests.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Xunit; +using Xunit.Abstractions; + +namespace Kavita.Database.Tests.Repositories; + +public class ExternalSeriesMetadataRepositoryTests(ITestOutputHelper outputHelper) : AbstractDbTest(outputHelper) +{ + [Fact] + public async Task NeedsDataRefresh_WhenValidUntilIsInThePast_ReturnsTrue() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(-1) // expired yesterday + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.NeedsDataRefresh(series.Id); + + Assert.True(result); + } + + [Fact] + public async Task NeedsDataRefresh_WhenValidUntilIsInTheFuture_ReturnsFalse() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(7) // valid for another week + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.NeedsDataRefresh(series.Id); + + Assert.False(result); + } + + [Fact] + public async Task NeedsDataRefresh_OnlyChecksRequestedSeries() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .WithSeries(new SeriesBuilder("series1").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series0 = context.Series.First(s => s.Name == "series0"); + var series1 = context.Series.First(s => s.Name == "series1"); + + context.ExternalSeriesMetadata.AddRange( + new ExternalSeriesMetadata { SeriesId = series0.Id, ValidUntilUtc = DateTime.UtcNow.AddDays(-1) }, + new ExternalSeriesMetadata { SeriesId = series1.Id, ValidUntilUtc = DateTime.UtcNow.AddDays(7) } + ); + await context.SaveChangesAsync(); + + var staleSeries = await unitOfWork.ExternalSeriesMetadataRepository.NeedsDataRefresh(series0.Id); + var freshSeries = await unitOfWork.ExternalSeriesMetadataRepository.NeedsDataRefresh(series1.Id); + + Assert.True(staleSeries); + Assert.False(freshSeries); + } + + [Fact] + public async Task GetSeriesDetailPlusDto_WithRatings_ReturnsAllRatings() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(7), + ExternalRatings = new List + { + new() { Provider = ScrobbleProvider.AniList, AverageScore = 85, FavoriteCount = 1000 }, + new() { Provider = ScrobbleProvider.Mal, AverageScore = 90, FavoriteCount = 2000 } + } + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(series.Id); + + Assert.NotNull(result); + Assert.Equal(2, result.Ratings?.Count()); + } + + [Fact] + public async Task GetSeriesDetailPlusDto_WithReviews_ReturnsReviewsSortedByScoreDescending() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(7), + ExternalReviews = new List + { + new() { Score = 60, Body = "Decent", Username = "user1", Provider = ScrobbleProvider.AniList, BodyJustText = string.Empty}, + new() { Score = 95, Body = "Excellent", Username = "user2", Provider = ScrobbleProvider.AniList, BodyJustText = string.Empty }, + new() { Score = 80, Body = "Good", Username = "user3", Provider = ScrobbleProvider.AniList, BodyJustText = string.Empty } + } + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(series.Id); + + Assert.NotNull(result); + var reviews = result.Reviews.ToList(); + Assert.Equal(3, reviews.Count); + Assert.Equal(95, reviews[0].Score); + Assert.Equal(80, reviews[1].Score); + Assert.Equal(60, reviews[2].Score); + Assert.All(reviews, r => Assert.True(r.IsExternal)); + } + + [Fact] + public async Task GetSeriesDetailPlusDto_WithRecommendations_SplitsOwnedAndExternalCorrectly() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .WithSeries(new SeriesBuilder("owned-rec-series").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + var ownedRecSeries = context.Series.First(s => s.Name == "owned-rec-series"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(7), + ExternalRecommendations = new List + { + // owned — has a SeriesId pointing to an existing series + new() { SeriesId = ownedRecSeries.Id, Name = ownedRecSeries.Name, Provider = ScrobbleProvider.AniList, CoverUrl = string.Empty, Url = string.Empty}, + // external — no SeriesId (not in library) + new() { SeriesId = null, Name = "External Rec 1", Provider = ScrobbleProvider.AniList, CoverUrl = string.Empty, Url = string.Empty }, + new() { SeriesId = null, Name = "External Rec 2", Provider = ScrobbleProvider.AniList, CoverUrl = string.Empty, Url = string.Empty } + } + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(series.Id); + + Assert.NotNull(result); + Assert.NotNull(result.Recommendations); + + Assert.Single(result.Recommendations.OwnedSeries); + Assert.Equal(ownedRecSeries.Name, result.Recommendations.OwnedSeries[0].Name); + + Assert.Equal(2, result.Recommendations.ExternalSeries.Count()); + Assert.Contains(result.Recommendations.ExternalSeries, r => r.Name == "External Rec 1"); + Assert.Contains(result.Recommendations.ExternalSeries, r => r.Name == "External Rec 2"); + } + + [Fact] + public async Task GetSeriesDetailPlusDto_WithNoRatingsOrReviews_ReturnsEmptyCollections() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(7), + ExternalRatings = new List(), + ExternalReviews = new List(), + ExternalRecommendations = new List() + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(series.Id); + + Assert.NotNull(result); + Assert.Empty(result.Ratings ?? []); + Assert.Empty(result.Reviews); + Assert.Empty(result.Recommendations?.OwnedSeries ?? []); + Assert.Empty(result.Recommendations?.ExternalSeries ?? []); + } + + [Fact] + public async Task GetSeriesDetailPlusDto_OwnedRecommendations_AreSortedBySortNameAscending() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .WithSeries(new SeriesBuilder("Charlie").Build()) + .WithSeries(new SeriesBuilder("Alpha").Build()) + .WithSeries(new SeriesBuilder("Bravo").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + var charlie = context.Series.First(s => s.Name == "Charlie"); + var alpha = context.Series.First(s => s.Name == "Alpha"); + var bravo = context.Series.First(s => s.Name == "Bravo"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(7), + ExternalRecommendations = new List + { + new() { SeriesId = charlie.Id, Name = "Charlie", Provider = ScrobbleProvider.AniList, CoverUrl = string.Empty, Url = string.Empty }, + new() { SeriesId = alpha.Id, Name = "Alpha", Provider = ScrobbleProvider.AniList, CoverUrl = string.Empty, Url = string.Empty }, + new() { SeriesId = bravo.Id, Name = "Bravo", Provider = ScrobbleProvider.AniList, CoverUrl = string.Empty, Url = string.Empty } + } + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(series.Id); + + Assert.NotNull(result); + var owned = result.Recommendations?.OwnedSeries ?? []; + Assert.Equal(3, owned.Count); + Assert.Equal("Alpha", owned[0].Name); + Assert.Equal("Bravo", owned[1].Name); + Assert.Equal("Charlie", owned[2].Name); + } +} diff --git a/API.Tests/Repository/GenreRepositoryTests.cs b/Kavita.Database.Tests/Repositories/GenreRepositoryTests.cs similarity index 97% rename from API.Tests/Repository/GenreRepositoryTests.cs rename to Kavita.Database.Tests/Repositories/GenreRepositoryTests.cs index 5ad42809a..426640b92 100644 --- a/API.Tests/Repository/GenreRepositoryTests.cs +++ b/Kavita.Database.Tests/Repositories/GenreRepositoryTests.cs @@ -2,16 +2,17 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Metadata.Browse; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; using Xunit; using Xunit.Abstractions; -namespace API.Tests.Repository; +namespace Kavita.Database.Tests.Repositories; public class GenreRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Repository/PersonRepositoryTests.cs b/Kavita.Database.Tests/Repositories/PersonRepositoryTests.cs similarity index 95% rename from API.Tests/Repository/PersonRepositoryTests.cs rename to Kavita.Database.Tests/Repositories/PersonRepositoryTests.cs index b6297bfe2..a2003e429 100644 --- a/API.Tests/Repository/PersonRepositoryTests.cs +++ b/Kavita.Database.Tests/Repositories/PersonRepositoryTests.cs @@ -1,19 +1,21 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Metadata.Browse; -using API.DTOs.Metadata.Browse.Requests; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Helpers; -using API.Helpers.Builders; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; using Xunit; using Xunit.Abstractions; -namespace API.Tests.Repository; +namespace Kavita.Database.Tests.Repositories; public class PersonRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -237,19 +239,19 @@ public class PersonRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTe var ageChapterRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, fullAccess.Id); Assert.Equal(3, sharedSeriesRoles.Count()); Assert.Equal(6, chapterRoles.Count()); - Assert.Single(ageChapterRoles); + Assert.Single((IEnumerable)ageChapterRoles); var restrictedRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, restrictedAccess.Id); var restrictedChapterRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, restrictedAccess.Id); var restrictedAgePersonChapterRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, restrictedAccess.Id); Assert.Equal(2, restrictedRoles.Count()); Assert.Equal(4, restrictedChapterRoles.Count()); - Assert.Single(restrictedAgePersonChapterRoles); + Assert.Single((IEnumerable)restrictedAgePersonChapterRoles); var restrictedAgeRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, restrictedAgeAccess.Id); var restrictedAgeChapterRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, restrictedAgeAccess.Id); var restrictedAgeAgePersonChapterRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, restrictedAgeAccess.Id); - Assert.Single(restrictedAgeRoles); + Assert.Single((IEnumerable)restrictedAgeRoles); Assert.Equal(2, restrictedAgeChapterRoles.Count()); // Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up Assert.Empty(restrictedAgeAgePersonChapterRoles); @@ -294,10 +296,10 @@ public class PersonRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTe Assert.Equal(2, series.Count()); series = await unitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, restrictedAgeAccess.Id); - Assert.Single(series); + Assert.Single((IEnumerable)series); series = await unitOfWork.PersonRepository.GetSeriesKnownFor(lib1SeriesPerson.Id, restrictedAgeAccess.Id); - Assert.Single(series); + Assert.Single((IEnumerable)series); } [Fact] @@ -312,7 +314,7 @@ public class PersonRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTe var chapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, fullAccess.Id, PersonRole.Colorist); var restrictedChapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, restrictedAccess.Id, PersonRole.Colorist); var restrictedAgeChapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, restrictedAgeAccess.Id, PersonRole.Colorist); - Assert.Single(chapters); + Assert.Single((IEnumerable)chapters); Assert.Empty(restrictedChapters); Assert.Empty(restrictedAgeChapters); @@ -320,16 +322,16 @@ public class PersonRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTe chapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, fullAccess.Id, PersonRole.Imprint); restrictedChapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, restrictedAccess.Id, PersonRole.Imprint); restrictedAgeChapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, restrictedAgeAccess.Id, PersonRole.Imprint); - Assert.Single(chapters); - Assert.Single(restrictedChapters); + Assert.Single((IEnumerable)chapters); + Assert.Single((IEnumerable)restrictedChapters); Assert.Empty(restrictedAgeChapters); // Lib1 - not age restricted series chapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, fullAccess.Id, PersonRole.Team); restrictedChapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, restrictedAccess.Id, PersonRole.Team); restrictedAgeChapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, restrictedAgeAccess.Id, PersonRole.Team); - Assert.Single(chapters); - Assert.Single(restrictedChapters); - Assert.Single(restrictedAgeChapters); + Assert.Single((IEnumerable)chapters); + Assert.Single((IEnumerable)restrictedChapters); + Assert.Single((IEnumerable)restrictedAgeChapters); } } diff --git a/API.Tests/Repository/SeriesRepositoryTests.cs b/Kavita.Database.Tests/Repositories/SeriesRepositoryTests.cs similarity index 95% rename from API.Tests/Repository/SeriesRepositoryTests.cs rename to Kavita.Database.Tests/Repositories/SeriesRepositoryTests.cs index fd58badb0..4151e14aa 100644 --- a/API.Tests/Repository/SeriesRepositoryTests.cs +++ b/Kavita.Database.Tests/Repositories/SeriesRepositoryTests.cs @@ -1,13 +1,12 @@ using System.Threading.Tasks; -using API.Data; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Helpers.Builders; +using Kavita.API.Database; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; using Xunit; using Xunit.Abstractions; -namespace API.Tests.Repository; -#nullable enable +namespace Kavita.Database.Tests.Repositories; public class SeriesRepositoryTests(ITestOutputHelper testOutputHelper) : AbstractDbTest(testOutputHelper) { diff --git a/API.Tests/Repository/TagRepositoryTests.cs b/Kavita.Database.Tests/Repositories/TagRepositoryTests.cs similarity index 97% rename from API.Tests/Repository/TagRepositoryTests.cs rename to Kavita.Database.Tests/Repositories/TagRepositoryTests.cs index af4ac7cea..f1f747747 100644 --- a/API.Tests/Repository/TagRepositoryTests.cs +++ b/Kavita.Database.Tests/Repositories/TagRepositoryTests.cs @@ -2,17 +2,18 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Metadata.Browse; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Helpers; -using API.Helpers.Builders; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; using Xunit; using Xunit.Abstractions; -namespace API.Tests.Repository; +namespace Kavita.Database.Tests.Repositories; public class TagRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API/Helpers/Converters/AnnotationFilterFieldValueConverter.cs b/Kavita.Database/Converters/AnnotationFilterFieldValueConverter.cs similarity index 88% rename from API/Helpers/Converters/AnnotationFilterFieldValueConverter.cs rename to Kavita.Database/Converters/AnnotationFilterFieldValueConverter.cs index 86e188c5c..f3a3d9fc2 100644 --- a/API/Helpers/Converters/AnnotationFilterFieldValueConverter.cs +++ b/Kavita.Database/Converters/AnnotationFilterFieldValueConverter.cs @@ -1,8 +1,8 @@ using System; -using API.DTOs.Filtering.v2; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Filtering.v2; -namespace API.Helpers.Converters; +namespace Kavita.Database.Converters; public static class AnnotationFilterFieldValueConverter { diff --git a/API/Helpers/Converters/FilterFieldValueConverter.cs b/Kavita.Database/Converters/FilterFieldValueConverter.cs similarity index 96% rename from API/Helpers/Converters/FilterFieldValueConverter.cs rename to Kavita.Database/Converters/FilterFieldValueConverter.cs index 4755392a9..31b01ce01 100644 --- a/API/Helpers/Converters/FilterFieldValueConverter.cs +++ b/Kavita.Database/Converters/FilterFieldValueConverter.cs @@ -1,12 +1,11 @@ -using System; +using System; using System.Globalization; using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.Enums; -namespace API.Helpers.Converters; -#nullable enable +namespace Kavita.Database.Converters; public static class FilterFieldValueConverter { diff --git a/API/Helpers/Converters/PersonFilterFieldValueConverter.cs b/Kavita.Database/Converters/PersonFilterFieldValueConverter.cs similarity index 80% rename from API/Helpers/Converters/PersonFilterFieldValueConverter.cs rename to Kavita.Database/Converters/PersonFilterFieldValueConverter.cs index 822ce105a..08ec45331 100644 --- a/API/Helpers/Converters/PersonFilterFieldValueConverter.cs +++ b/Kavita.Database/Converters/PersonFilterFieldValueConverter.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.Enums; -namespace API.Helpers.Converters; +namespace Kavita.Database.Converters; public static class PersonFilterFieldValueConverter { @@ -20,7 +20,7 @@ public static class PersonFilterFieldValueConverter }; } - private static IList ParsePersonRoles(string value) + private static List ParsePersonRoles(string value) { if (string.IsNullOrEmpty(value)) return []; diff --git a/API/Data/DataContext.cs b/Kavita.Database/DataContext.cs similarity index 97% rename from API/Data/DataContext.cs rename to Kavita.Database/DataContext.cs index 9f7d560c8..08046ab67 100644 --- a/API/Data/DataContext.cs +++ b/Kavita.Database/DataContext.cs @@ -1,35 +1,34 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using API.DTOs.Progress; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.User; -using API.Entities.Enums.UserPreferences; -using API.Entities.History; -using API.Entities.Interfaces; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Entities.Progress; -using API.Entities.Scrobble; -using API.Entities.User; -using API.Extensions; -using Hangfire.Storage.SQLite.Entities; +using Kavita.API.Database; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.History; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.Scrobble; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; -namespace API.Data; +namespace Kavita.Database; public sealed class DataContext : IdentityDbContext, AppUserRole, IdentityUserLogin, - IdentityRoleClaim, IdentityUserToken>, IDataProtectionKeyContext + IdentityRoleClaim, IdentityUserToken>, IDataProtectionKeyContext, IDataContext { public DataContext(DbContextOptions options) : base(options) { @@ -105,7 +104,6 @@ public sealed class DataContext : IdentityDbContext() .HasOne(pt => pt.Series) .WithMany(p => p.Relations) diff --git a/Kavita.Database/Extensions/ApplicationServiceExtensions.cs b/Kavita.Database/Extensions/ApplicationServiceExtensions.cs new file mode 100644 index 000000000..618c041aa --- /dev/null +++ b/Kavita.Database/Extensions/ApplicationServiceExtensions.cs @@ -0,0 +1,42 @@ +using Kavita.API.Database; +using Kavita.Common.EnvironmentInfo; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using NeoSmart.Caching.Sqlite; + +namespace Kavita.Database.Extensions; + +public static class ApplicationServiceExtensions +{ + public static void AddKavitaDatabases(this IServiceCollection services) + { + services.AddSqLite(); + + services.AddScoped(); + services.AddScoped(); + + // Store keys inside database, such that cookies can be decrypted between container restarts + services.AddDataProtection() + .PersistKeysToDbContext() + .SetApplicationName(BuildInfo.AppName); + } + + private static void AddSqLite(this IServiceCollection services) + { + services.AddSqliteCache("config/cache.db"); + + services.AddDbContextPool(options => + { + options.UseSqlite("Data source=config/kavita.db", builder => + { + builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); + }); + options.EnableDetailedErrors(); + options.EnableSensitiveDataLogging(); + options.ConfigureWarnings(warnings => + warnings.Ignore(RelationalEventId.PendingModelChangesWarning)); + }); + } +} diff --git a/API/Extensions/QueryExtensions/AuthKeyQueryExtensions.cs b/Kavita.Database/Extensions/AuthKeyQueryExtensions.cs similarity index 76% rename from API/Extensions/QueryExtensions/AuthKeyQueryExtensions.cs rename to Kavita.Database/Extensions/AuthKeyQueryExtensions.cs index a6954323b..85e847439 100644 --- a/API/Extensions/QueryExtensions/AuthKeyQueryExtensions.cs +++ b/Kavita.Database/Extensions/AuthKeyQueryExtensions.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.Linq; -using API.Entities.User; +using Kavita.Models.Entities.User; -namespace API.Extensions.QueryExtensions; +namespace Kavita.Database.Extensions; public static class AuthKeyQueryExtensions { @@ -11,3 +11,4 @@ public static class AuthKeyQueryExtensions return queryable.Where(k => k.ExpiresAtUtc == null || k.ExpiresAtUtc > DateTime.UtcNow); } } + diff --git a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs b/Kavita.Database/Extensions/BookmarkSortExtensions.cs similarity index 84% rename from API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs rename to Kavita.Database/Extensions/BookmarkSortExtensions.cs index 030517dbf..88ab6af4a 100644 --- a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs +++ b/Kavita.Database/Extensions/BookmarkSortExtensions.cs @@ -1,18 +1,12 @@ -using System.Linq; -using API.DTOs.Filtering; -using API.Entities; +using System.Linq; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions.Filtering; -#nullable enable +namespace Kavita.Database.Extensions; -public class BookmarkSeriesPair -{ - public AppUserBookmark Bookmark { get; init; } = null!; - public Series Series { get; init; } = null!; -} - -public static class BookmarkSort +public static class BookmarkSortExtensions { /// /// Applies the correct sort based on diff --git a/API/Extensions/QueryExtensions/ChapterQueryExtensions.cs b/Kavita.Database/Extensions/ChapterQueryExtensions.cs similarity index 61% rename from API/Extensions/QueryExtensions/ChapterQueryExtensions.cs rename to Kavita.Database/Extensions/ChapterQueryExtensions.cs index 44a2f2a68..256168c10 100644 --- a/API/Extensions/QueryExtensions/ChapterQueryExtensions.cs +++ b/Kavita.Database/Extensions/ChapterQueryExtensions.cs @@ -1,9 +1,10 @@ -using System.Linq; -using API.Entities; -using API.Services.Tasks.Scanner.Parser; +using System.Linq; +using Kavita.Common.Constants; +using Kavita.Models.Constants; +using Kavita.Models.Entities; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions; +namespace Kavita.Database.Extensions; public static class ChapterQueryExtensions { @@ -13,11 +14,11 @@ public static class ChapterQueryExtensions .Include(c => c.Volume) .OrderBy(c => // Priority 1: Regular volumes (not loose-leaf, not special) - c.Volume.MinNumber == Parser.LooseLeafVolumeNumber || - c.Volume.MinNumber == Parser.SpecialVolumeNumber ? 1 : 0) + c.Volume.MinNumber == ParserConstants.LooseLeafVolumeNumber || + c.Volume.MinNumber == ParserConstants.SpecialVolumeNumber ? 1 : 0) .ThenBy(c => // Priority 2: Loose leaf over specials - c.Volume.MinNumber == Parser.SpecialVolumeNumber ? 1 : 0) + c.Volume.MinNumber == ParserConstants.SpecialVolumeNumber ? 1 : 0) // Priority 3: Non-special chapters .ThenBy(c => c.IsSpecial ? 1 : 0) .ThenBy(c => c.Volume.MinNumber) diff --git a/API/Extensions/DataContextExtensions.cs b/Kavita.Database/Extensions/DataContextExtensions.cs similarity index 92% rename from API/Extensions/DataContextExtensions.cs rename to Kavita.Database/Extensions/DataContextExtensions.cs index abf076861..d58660b7b 100644 --- a/API/Extensions/DataContextExtensions.cs +++ b/Kavita.Database/Extensions/DataContextExtensions.cs @@ -1,11 +1,10 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace API.Extensions; +namespace Kavita.Database.Extensions; public static class DataContextExtensions { - public static PropertyBuilder HasJsonConversion(this PropertyBuilder builder, TProperty def = default) { return builder.HasConversion( @@ -13,5 +12,4 @@ public static class DataContextExtensions v => JsonSerializer.Deserialize(v, JsonSerializerOptions.Default) ?? def ); } - } diff --git a/API/Extensions/QueryExtensions/Filtering/ActivityFilter.cs b/Kavita.Database/Extensions/Filters/ActivityFilter.cs similarity index 92% rename from API/Extensions/QueryExtensions/Filtering/ActivityFilter.cs rename to Kavita.Database/Extensions/Filters/ActivityFilter.cs index d2c8f9641..cfadd107b 100644 --- a/API/Extensions/QueryExtensions/Filtering/ActivityFilter.cs +++ b/Kavita.Database/Extensions/Filters/ActivityFilter.cs @@ -1,12 +1,11 @@ using System.Linq; -using API.DTOs.Statistics; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; -using API.Entities.Progress; -using API.Entities.User; +using Kavita.Models.DTOs.Statistics; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; -namespace API.Extensions.QueryExtensions.Filtering; +namespace Kavita.Database.Extensions.Filters; public static class ActivityFilter { diff --git a/Kavita.Database/Extensions/Filters/AnnotationFilter.cs b/Kavita.Database/Extensions/Filters/AnnotationFilter.cs new file mode 100644 index 000000000..311008e93 --- /dev/null +++ b/Kavita.Database/Extensions/Filters/AnnotationFilter.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Kavita.Common; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.User; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Extensions.Filters; + +public static class AnnotationFilter +{ + + extension(IQueryable queryable) + { + public IQueryable IsOwnedBy(bool condition, + FilterComparison comparison, IList ownerIds) + { + if (ownerIds.Count == 0 || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.AppUserId == ownerIds[0]), + FilterComparison.Contains => queryable.Where(a => ownerIds.Contains(a.AppUserId)), + FilterComparison.NotContains => queryable.Where(a => !ownerIds.Contains(a.AppUserId)), + FilterComparison.NotEqual => queryable.Where(a => a.AppUserId != ownerIds[0]), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable IsInLibrary(bool condition, + FilterComparison comparison, IList libraryIds) + { + if (libraryIds.Count == 0 || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.LibraryId == libraryIds[0]), + FilterComparison.Contains => queryable.Where(a => libraryIds.Contains(a.LibraryId)), + FilterComparison.NotContains => queryable.Where(a => !libraryIds.Contains(a.LibraryId)), + FilterComparison.NotEqual => queryable.Where(a => a.LibraryId != libraryIds[0]), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable HasSeries(bool condition, + FilterComparison comparison, IList seriesIds) + { + if (seriesIds.Count == 0 || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.SeriesId == seriesIds[0]), + FilterComparison.Contains => queryable.Where(a => seriesIds.Contains(a.SeriesId)), + FilterComparison.NotContains => queryable.Where(a => !seriesIds.Contains(a.SeriesId)), + FilterComparison.NotEqual => queryable.Where(a => a.SeriesId != seriesIds[0]), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable IsUsingHighlights(bool condition, + FilterComparison comparison, IList highlightSlotIdxs) + { + if (highlightSlotIdxs.Count == 0 || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.SelectedSlotIndex == highlightSlotIdxs[0]), + FilterComparison.Contains => queryable.Where(a => highlightSlotIdxs.Contains(a.SelectedSlotIndex)), + FilterComparison.NotContains => queryable.Where(a => !highlightSlotIdxs.Contains(a.SelectedSlotIndex)), + FilterComparison.NotEqual => queryable.Where(a => a.SelectedSlotIndex != highlightSlotIdxs[0]), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable HasSelected(bool condition, + FilterComparison comparison, string value) + { + if (string.IsNullOrEmpty(value) || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.SelectedText == value), + FilterComparison.NotEqual => queryable.Where(a => a.SelectedText != value), + FilterComparison.BeginsWith => queryable.Where(a => EF.Functions.Like(a.SelectedText, $"{value}%")), + FilterComparison.EndsWith => queryable.Where(a => EF.Functions.Like(a.SelectedText, $"%{value}")), + FilterComparison.Matches => queryable.Where(a => EF.Functions.Like(a.SelectedText, $"%{value}%")), + FilterComparison.GreaterThan or + FilterComparison.GreaterThanEqual or + FilterComparison.LessThan or + FilterComparison.LessThanEqual or + FilterComparison.Contains or + FilterComparison.MustContains or + FilterComparison.NotContains or + FilterComparison.IsBefore or + FilterComparison.IsAfter or + FilterComparison.IsInLast or + FilterComparison.IsNotInLast or + FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.SelectedText"), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable HasCommented(bool condition, + FilterComparison comparison, string value) + { + if (string.IsNullOrEmpty(value) || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.CommentPlainText == value), + FilterComparison.NotEqual => queryable.Where(a => a.CommentPlainText != value), + FilterComparison.BeginsWith => queryable.Where(a => EF.Functions.Like(a.CommentPlainText, $"{value}%")), + FilterComparison.EndsWith => queryable.Where(a => EF.Functions.Like(a.CommentPlainText, $"%{value}")), + FilterComparison.Matches => queryable.Where(a => EF.Functions.Like(a.CommentPlainText, $"%{value}%")), + FilterComparison.GreaterThan or + FilterComparison.GreaterThanEqual or + FilterComparison.LessThan or + FilterComparison.LessThanEqual or + FilterComparison.Contains or + FilterComparison.MustContains or + FilterComparison.NotContains or + FilterComparison.IsBefore or + FilterComparison.IsAfter or + FilterComparison.IsInLast or + FilterComparison.IsNotInLast or + FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.CommentPlainText"), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable HasLikes(bool condition, + FilterComparison comparison, int value) + { + if (!condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.Likes.Count == value), + FilterComparison.NotEqual => queryable.Where(a => a.Likes.Count != value), + FilterComparison.GreaterThan => queryable.Where(a => a.Likes.Count > value), + FilterComparison.GreaterThanEqual => queryable.Where(a => a.Likes.Count >= value), + FilterComparison.LessThan => queryable.Where(a => a.Likes.Count < value), + FilterComparison.LessThanEqual => queryable.Where(a => a.Likes.Count <= value), + FilterComparison.BeginsWith or + FilterComparison.EndsWith or + FilterComparison.Matches or + FilterComparison.Contains or + FilterComparison.MustContains or + FilterComparison.NotContains or + FilterComparison.IsBefore or + FilterComparison.IsAfter or + FilterComparison.IsInLast or + FilterComparison.IsNotInLast or + FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.Likes"), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable IsLikedBy(bool condition, + FilterComparison comparison, IList value) + { + if (value.Count == 0 || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.Likes.Contains(value[0])), + FilterComparison.NotEqual => queryable.Where(a => a!.Likes.Contains(value[0])), + FilterComparison.Contains => queryable.Where(a => a.Likes.Any(value.Contains)), + FilterComparison.NotContains => queryable.Where(a => !a.Likes.Any(value.Contains)), + FilterComparison.GreaterThan or + FilterComparison.GreaterThanEqual or + FilterComparison.LessThan or + FilterComparison.LessThanEqual or + FilterComparison.BeginsWith or + FilterComparison.EndsWith or + FilterComparison.Matches or + FilterComparison.MustContains or + FilterComparison.IsBefore or + FilterComparison.IsAfter or + FilterComparison.IsInLast or + FilterComparison.IsNotInLast or + FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.Likes"), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + } +} diff --git a/API/Extensions/QueryExtensions/Filtering/PersonFilter.cs b/Kavita.Database/Extensions/Filters/PersonFilter.cs similarity index 97% rename from API/Extensions/QueryExtensions/Filtering/PersonFilter.cs rename to Kavita.Database/Extensions/Filters/PersonFilter.cs index c36164d9d..ba447c48b 100644 --- a/API/Extensions/QueryExtensions/Filtering/PersonFilter.cs +++ b/Kavita.Database/Extensions/Filters/PersonFilter.cs @@ -1,13 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; -using API.Entities.Person; using Kavita.Common; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions.Filtering; +namespace Kavita.Database.Extensions.Filters; public static class PersonFilter { diff --git a/Kavita.Database/Extensions/Filters/SeriesFilter.cs b/Kavita.Database/Extensions/Filters/SeriesFilter.cs new file mode 100644 index 000000000..f31b2815b --- /dev/null +++ b/Kavita.Database/Extensions/Filters/SeriesFilter.cs @@ -0,0 +1,944 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Extensions.Filters; + +public static class SeriesFilter +{ + private const float FloatingPointTolerance = 0.001f; + + extension(IQueryable queryable) + { + public IQueryable HasLanguage(bool condition, + FilterComparison comparison, IList languages) + { + if (languages.Count == 0 || !condition) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Metadata.Language.Equals(languages[0])); + case FilterComparison.Contains: + return queryable.Where(s => languages.Contains(s.Metadata.Language)); + case FilterComparison.MustContains: + return queryable.Where(s => languages.All(s2 => s2.Equals(s.Metadata.Language))); + case FilterComparison.NotContains: + return queryable.Where(s => !languages.Contains(s.Metadata.Language)); + case FilterComparison.NotEqual: + return queryable.Where(s => !s.Metadata.Language.Equals(languages[0])); + case FilterComparison.Matches: + return queryable.Where(s => EF.Functions.Like(s.Metadata.Language, $"{languages[0]}%")); + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.IsEmpty: + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasReleaseYear(bool condition, + FilterComparison comparison, int? releaseYear) + { + if (!condition || releaseYear == null) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Metadata.ReleaseYear == releaseYear); + case FilterComparison.GreaterThan: + case FilterComparison.IsAfter: + return queryable.Where(s => s.Metadata.ReleaseYear > releaseYear); + case FilterComparison.GreaterThanEqual: + return queryable.Where(s => s.Metadata.ReleaseYear >= releaseYear); + case FilterComparison.LessThan: + case FilterComparison.IsBefore: + return queryable.Where(s => s.Metadata.ReleaseYear < releaseYear); + case FilterComparison.LessThanEqual: + return queryable.Where(s => s.Metadata.ReleaseYear <= releaseYear); + case FilterComparison.IsInLast: + return queryable.Where(s => s.Metadata.ReleaseYear >= DateTime.Now.Year - (int) releaseYear); + case FilterComparison.IsNotInLast: + return queryable.Where(s => s.Metadata.ReleaseYear < DateTime.Now.Year - (int) releaseYear); + case FilterComparison.IsEmpty: + return queryable.Where(s => s.Metadata.ReleaseYear == 0); + case FilterComparison.Matches: + case FilterComparison.Contains: + case FilterComparison.NotContains: + case FilterComparison.NotEqual: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.MustContains: + throw new KavitaException($"{comparison} not applicable for Series.ReleaseYear"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasRating(bool condition, + FilterComparison comparison, float rating, int userId) + { + if (rating < 0 || !condition || userId <= 0) return queryable; + + // AppUserRating stores a 5-digit number. + rating = Math.Clamp(rating, 0f, 5f); + + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) <= FloatingPointTolerance && r.AppUserId == userId)); + case FilterComparison.GreaterThan: + return queryable.Where(s => s.Ratings.Any(r => r.Rating > rating && r.AppUserId == userId)); + case FilterComparison.GreaterThanEqual: + return queryable.Where(s => s.Ratings.Any(r => r.Rating >= rating && r.AppUserId == userId)); + case FilterComparison.LessThan: + return queryable.Where(s => s.Ratings.Any(r => r.Rating < rating && r.AppUserId == userId)); + case FilterComparison.LessThanEqual: + return queryable.Where(s => s.Ratings.Any(r => r.Rating <= rating && r.AppUserId == userId)); + case FilterComparison.NotEqual: + return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) >= FloatingPointTolerance && r.AppUserId == userId)); + case FilterComparison.IsEmpty: + return queryable.Where(s => s.Ratings.All(r => r.AppUserId != userId)); + case FilterComparison.Contains: + case FilterComparison.Matches: + case FilterComparison.NotContains: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + throw new KavitaException($"{comparison} not applicable for Series.Rating"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasAgeRating(bool condition, + FilterComparison comparison, IList ratings) + { + if (!condition || ratings.Count == 0) return queryable; + + var firstRating = ratings[0]; + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Metadata.AgeRating == firstRating); + case FilterComparison.GreaterThan: + return queryable.Where(s => s.Metadata.AgeRating > firstRating); + case FilterComparison.GreaterThanEqual: + return queryable.Where(s => s.Metadata.AgeRating >= firstRating); + case FilterComparison.LessThan: + return queryable.Where(s => s.Metadata.AgeRating < firstRating); + case FilterComparison.LessThanEqual: + return queryable.Where(s => s.Metadata.AgeRating <= firstRating); + case FilterComparison.Contains: + return queryable.Where(s => ratings.Contains(s.Metadata.AgeRating)); + case FilterComparison.NotContains: + return queryable.Where(s => !ratings.Contains(s.Metadata.AgeRating)); + case FilterComparison.NotEqual: + return queryable.Where(s => s.Metadata.AgeRating != firstRating); + case FilterComparison.Matches: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.AgeRating"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasAverageReadTime(bool condition, + FilterComparison comparison, int avgReadTime) + { + if (!condition || avgReadTime < 0) return queryable; + + switch (comparison) + { + case FilterComparison.NotEqual: + return queryable.WhereNotEqual(s => s.AvgHoursToRead, avgReadTime); + case FilterComparison.Equal: + return queryable.WhereEqual(s => s.AvgHoursToRead, avgReadTime); + case FilterComparison.GreaterThan: + return queryable.WhereGreaterThan(s => s.AvgHoursToRead, avgReadTime); + case FilterComparison.GreaterThanEqual: + return queryable.WhereGreaterThanOrEqual(s => s.AvgHoursToRead, avgReadTime); + case FilterComparison.LessThan: + return queryable.WhereLessThan(s => s.AvgHoursToRead, avgReadTime); + case FilterComparison.LessThanEqual: + return queryable.WhereLessThanOrEqual(s => s.AvgHoursToRead, avgReadTime); + case FilterComparison.Contains: + case FilterComparison.Matches: + case FilterComparison.NotContains: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.AverageReadTime"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasPublicationStatus(bool condition, + FilterComparison comparison, IList pubStatues) + { + if (!condition || pubStatues.Count == 0) return queryable; + + var firstStatus = pubStatues[0]; + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Metadata.PublicationStatus == firstStatus); + case FilterComparison.Contains: + return queryable.Where(s => pubStatues.Contains(s.Metadata.PublicationStatus)); + case FilterComparison.NotContains: + return queryable.Where(s => !pubStatues.Contains(s.Metadata.PublicationStatus)); + case FilterComparison.NotEqual: + return queryable.Where(s => s.Metadata.PublicationStatus != firstStatus); + case FilterComparison.MustContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.Matches: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.PublicationStatus"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + /// + /// + /// + /// This is more taxing on memory as the percentage calculation must be done in Memory + /// + /// + public IQueryable HasReadingProgress(bool condition, + FilterComparison comparison, float readProgress, int userId) + { + if (!condition) return queryable; + + var subQuery = queryable + .Select(s => new + { + SeriesId = s.Id, + SeriesName = s.Name, + Percentage = s.Progress + .Where(p => p != null && p.AppUserId == userId) + .Sum(p => p != null ? (p.PagesRead * 1.0f / s.Pages) : 0f) * 100f + }) + .AsSplitQuery(); + + switch (comparison) + { + case FilterComparison.Equal: + subQuery = subQuery.WhereEqual(s => s.Percentage, readProgress); + break; + case FilterComparison.GreaterThan: + subQuery = subQuery.WhereGreaterThan(s => s.Percentage, readProgress); + break; + case FilterComparison.GreaterThanEqual: + subQuery = subQuery.WhereGreaterThanOrEqual(s => s.Percentage, readProgress); + break; + case FilterComparison.LessThan: + subQuery = subQuery.WhereLessThan(s => s.Percentage, readProgress); + break; + case FilterComparison.LessThanEqual: + subQuery = subQuery.WhereLessThanOrEqual(s => s.Percentage, readProgress); + break; + case FilterComparison.NotEqual: + subQuery = subQuery.WhereNotEqual(s => s.Percentage, readProgress); + break; + case FilterComparison.IsEmpty: + case FilterComparison.Matches: + case FilterComparison.Contains: + case FilterComparison.NotContains: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + + var ids = subQuery.Select(s => s.SeriesId); + return queryable.Where(s => ids.Contains(s.Id)); + } + + public IQueryable HasAverageRating(bool condition, + FilterComparison comparison, float rating) + { + if (!condition) return queryable; + + var subQuery = queryable + .Where(s => s.ExternalSeriesMetadata != null) + .Include(s => s.ExternalSeriesMetadata) + .Select(s => new + { + SeriesId = s.Id, + SeriesName = s.Name, + AverageRating = s.ExternalSeriesMetadata.AverageExternalRating + }) + .AsSplitQuery() + .AsQueryable(); + + switch (comparison) + { + case FilterComparison.Equal: + subQuery = subQuery.WhereEqual(s => s.AverageRating, rating); + break; + case FilterComparison.GreaterThan: + subQuery = subQuery.WhereGreaterThan(s => s.AverageRating, rating); + break; + case FilterComparison.GreaterThanEqual: + subQuery = subQuery.WhereGreaterThanOrEqual(s => s.AverageRating, rating); + break; + case FilterComparison.LessThan: + subQuery = subQuery.WhereLessThan(s => s.AverageRating, rating); + break; + case FilterComparison.LessThanEqual: + subQuery = subQuery.WhereLessThanOrEqual(s => s.AverageRating, rating); + break; + case FilterComparison.NotEqual: + subQuery = subQuery.WhereNotEqual(s => s.AverageRating, rating); + break; + case FilterComparison.Matches: + case FilterComparison.Contains: + case FilterComparison.NotContains: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.AverageRating"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + + var ids = subQuery.Select(s => s.SeriesId); + return queryable.Where(s => ids.Contains(s.Id)); + } + + /// + /// HasReadingDate but used to filter where last reading point was TODAY() - timeDeltaDays. This allows the user + /// to build smart filters "Haven't read in a month" + /// + public IQueryable HasReadLast(bool condition, + FilterComparison comparison, int timeDeltaDays, int userId) + { + if (!condition || timeDeltaDays == 0) return queryable; + + var subQuery = queryable + .Include(s => s.Progress) + .Where(s => s.Progress.Any()) + .Select(s => new + { + SeriesId = s.Id, + SeriesName = s.Name, + MaxDate = s.Progress.Where(p => p != null && p.AppUserId == userId) + .Select(p => (DateTime?) p.LastModified) + .DefaultIfEmpty() + .Max() + }) + .Where(s => s.MaxDate != null) + .AsSplitQuery() + .AsEnumerable(); + + var date = DateTime.Now.AddDays(-timeDeltaDays); + + switch (comparison) + { + case FilterComparison.Equal: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate.Equals(date)); + break; + case FilterComparison.IsAfter: + case FilterComparison.GreaterThan: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate > date); + break; + case FilterComparison.GreaterThanEqual: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate >= date); + break; + case FilterComparison.IsBefore: + case FilterComparison.LessThan: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate < date); + break; + case FilterComparison.LessThanEqual: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate <= date); + break; + case FilterComparison.NotEqual: + subQuery = subQuery.Where(s => s.MaxDate != null && !s.MaxDate.Equals(date)); + break; + case FilterComparison.Matches: + case FilterComparison.Contains: + case FilterComparison.NotContains: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + + var ids = subQuery.Select(s => s.SeriesId); + return queryable.Where(s => ids.Contains(s.Id)); + } + + public IQueryable HasReadingDate(bool condition, + FilterComparison comparison, DateTime? date, int userId) + { + if (!condition || !date.HasValue) return queryable; + + var subQuery = queryable + .Include(s => s.Progress) + .Where(s => s.Progress.Any()) + .Select(s => new + { + SeriesId = s.Id, + SeriesName = s.Name, + MaxDate = s.Progress.Where(p => p != null && p.AppUserId == userId) + .Select(p => (DateTime?) p.LastModified) + .DefaultIfEmpty() + .Max() + }) + .Where(s => s.MaxDate != null) + .AsSplitQuery() + .AsEnumerable(); + + switch (comparison) + { + case FilterComparison.Equal: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate.Equals(date)); + break; + case FilterComparison.IsAfter: + case FilterComparison.GreaterThan: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate > date); + break; + case FilterComparison.GreaterThanEqual: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate >= date); + break; + case FilterComparison.IsBefore: + case FilterComparison.LessThan: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate < date); + break; + case FilterComparison.LessThanEqual: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate <= date); + break; + case FilterComparison.NotEqual: + subQuery = subQuery.Where(s => s.MaxDate != null && !s.MaxDate.Equals(date)); + break; + case FilterComparison.Matches: + case FilterComparison.Contains: + case FilterComparison.NotContains: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + + var ids = subQuery.Select(s => s.SeriesId); + return queryable.Where(s => ids.Contains(s.Id)); + } + + public IQueryable HasTags(bool condition, + FilterComparison comparison, IList tags) + { + if (!condition || (comparison != FilterComparison.IsEmpty && tags.Count == 0)) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + case FilterComparison.Contains: + return queryable.Where(s => s.Metadata.Tags.Any(t => tags.Contains(t.Id))); + case FilterComparison.NotEqual: + case FilterComparison.NotContains: + return queryable.Where(s => s.Metadata.Tags.All(t => !tags.Contains(t.Id))); + case FilterComparison.MustContains: + // Deconstruct and do a Union of a bunch of where statements since this doesn't translate + var queries = new List>() + { + queryable + }; + queries.AddRange(tags.Select(gId => queryable.Where(s => s.Metadata.Tags.Any(p => p.Id == gId)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); + case FilterComparison.IsEmpty: + return queryable.Where(s => s.Metadata.Tags.Count == 0); + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Matches: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + throw new KavitaException($"{comparison} not applicable for Series.Tags"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasPeople(bool condition, + FilterComparison comparison, IList people, PersonRole role) + { + if (!condition || (comparison != FilterComparison.IsEmpty && people.Count == 0)) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + case FilterComparison.Contains: + return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.PersonId) && p.Role == role)); + case FilterComparison.NotEqual: + case FilterComparison.NotContains: + return queryable.Where(s => s.Metadata.People.All(p => !people.Contains(p.PersonId) || p.Role != role)); + case FilterComparison.MustContains: + var queries = new List>() + { + queryable + }; + queries.AddRange(people.Select(personId => + queryable.Where(s => s.Metadata.People.Any(p => p.PersonId == personId && p.Role == role)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); + case FilterComparison.IsEmpty: + // Ensure no person with the given role exists + return queryable.Where(s => s.Metadata.People.All(p => p.Role != role)); + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.Matches: + throw new KavitaException($"{comparison} not applicable for Series.People"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasPeopleLegacy(bool condition, + FilterComparison comparison, IList people) + { + if (!condition || people.Count == 0) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + case FilterComparison.Contains: + return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.PersonId))); + case FilterComparison.NotEqual: + case FilterComparison.NotContains: + return queryable.Where(s => s.Metadata.People.All(t => !people.Contains(t.PersonId))); + case FilterComparison.MustContains: + // Deconstruct and do a Union of a bunch of where statements since this doesn't translate + var queries = new List>() + { + queryable + }; + queries.AddRange(people.Select(gId => queryable.Where(s => s.Metadata.People.Any(p => p.PersonId == gId)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); + case FilterComparison.IsEmpty: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.Matches: + throw new KavitaException($"{comparison} not applicable for Series.People"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasGenre(bool condition, + FilterComparison comparison, IList genres) + { + if (!condition || (comparison != FilterComparison.IsEmpty && genres.Count == 0)) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + case FilterComparison.Contains: + return queryable.Where(s => s.Metadata.Genres.Any(p => genres.Contains(p.Id))); + case FilterComparison.NotEqual: + case FilterComparison.NotContains: + return queryable.Where(s => s.Metadata.Genres.All(p => !genres.Contains(p.Id))); + case FilterComparison.MustContains: + // Deconstruct and do a Union of a bunch of where statements since this doesn't translate + var queries = new List>() + { + queryable + }; + queries.AddRange(genres.Select(gId => queryable.Where(s => s.Metadata.Genres.Any(p => p.Id == gId)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); + case FilterComparison.IsEmpty: + return queryable.Where(s => s.Metadata.Genres.Count == 0); + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Matches: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + throw new KavitaException($"{comparison} not applicable for Series.Genres"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasFormat(bool condition, + FilterComparison comparison, IList formats) + { + if (!condition || formats.Count == 0) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + case FilterComparison.Contains: + return queryable.Where(s => formats.Contains(s.Format)); + case FilterComparison.NotContains: + case FilterComparison.NotEqual: + return queryable.Where(s => !formats.Contains(s.Format)); + case FilterComparison.MustContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Matches: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.Format"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasCollectionTags(bool condition, + FilterComparison comparison, IList collectionTags, IList collectionSeries) + { + if (!condition || (comparison != FilterComparison.IsEmpty && collectionTags.Count == 0)) return queryable; + + + switch (comparison) + { + case FilterComparison.Equal: + case FilterComparison.Contains: + return queryable.Where(s => collectionSeries.Contains(s.Id)); + case FilterComparison.NotContains: + case FilterComparison.NotEqual: + return queryable.Where(s => !collectionSeries.Contains(s.Id)); + case FilterComparison.MustContains: + // // Deconstruct and do a Union of a bunch of where statements since this doesn't translate + var queries = new List>() + { + queryable + }; + queries.AddRange(collectionSeries.Select(gId => queryable.Where(s => collectionSeries.Any(p => p == s.Id)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); + case FilterComparison.IsEmpty: + return queryable.Where(s => s.Collections.Count == 0); + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Matches: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + throw new KavitaException($"{comparison} not applicable for Series.CollectionTags"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasName(bool condition, + FilterComparison comparison, string queryString) + { + if (string.IsNullOrEmpty(queryString) || !condition) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Name.Equals(queryString) + || s.OriginalName.Equals(queryString) + || s.LocalizedName.Equals(queryString) + || s.SortName.Equals(queryString)); + case FilterComparison.BeginsWith: + return queryable.Where(s => EF.Functions.Like(s.Name, $"{queryString}%") + ||EF.Functions.Like(s.OriginalName, $"{queryString}%") + || EF.Functions.Like(s.LocalizedName, $"{queryString}%") + || EF.Functions.Like(s.SortName, $"{queryString}%")); + case FilterComparison.EndsWith: + return queryable.Where(s => EF.Functions.Like(s.Name, $"%{queryString}") + ||EF.Functions.Like(s.OriginalName, $"%{queryString}") + || EF.Functions.Like(s.LocalizedName, $"%{queryString}") + || EF.Functions.Like(s.SortName, $"%{queryString}")); + case FilterComparison.Matches: + return queryable.Where(s => EF.Functions.Like(s.Name, $"%{queryString}%") + ||EF.Functions.Like(s.OriginalName, $"%{queryString}%") + || EF.Functions.Like(s.LocalizedName, $"%{queryString}%") + || EF.Functions.Like(s.SortName, $"%{queryString}%")); + case FilterComparison.NotEqual: + return queryable.Where(s => s.Name != queryString + || s.OriginalName != queryString + || s.LocalizedName != queryString + || s.SortName != queryString); + case FilterComparison.NotContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Contains: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.Name"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); + } + } + + public IQueryable HasSummary(bool condition, + FilterComparison comparison, string queryString) + { + if (!condition) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Metadata.Summary.Equals(queryString)); + case FilterComparison.BeginsWith: + return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"{queryString}%")); + case FilterComparison.EndsWith: + return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"%{queryString}")); + case FilterComparison.Matches: + return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"%{queryString}%")); + case FilterComparison.NotEqual: + return queryable.Where(s => s.Metadata.Summary != queryString); + case FilterComparison.IsEmpty: + return queryable.Where(s => string.IsNullOrEmpty(s.Metadata.Summary)); + case FilterComparison.NotContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Contains: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + throw new KavitaException($"{comparison} not applicable for Series.Metadata.Summary"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); + } + } + + public IQueryable HasPath(bool condition, + FilterComparison comparison, string queryString) + { + if (!condition) return queryable; + + var normalizedPath = queryString.NormalizePath(); + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.FolderPath != null && s.FolderPath.Equals(normalizedPath)); + case FilterComparison.BeginsWith: + return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"{normalizedPath}%")); + case FilterComparison.EndsWith: + return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"%{normalizedPath}")); + case FilterComparison.Matches: + return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"%{normalizedPath}%")); + case FilterComparison.NotEqual: + return queryable.Where(s => s.FolderPath != null && s.FolderPath != normalizedPath); + case FilterComparison.NotContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Contains: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); + } + } + + public IQueryable HasFilePath(bool condition, + FilterComparison comparison, string queryString) + { + if (!condition) return queryable; + + var normalizedPath = queryString.NormalizePath(); + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath != null && f.FilePath.Equals(normalizedPath) + ) + ) + ) + ); + case FilterComparison.BeginsWith: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath != null && EF.Functions.Like(f.FilePath, $"{normalizedPath}%") + ) + ) + ) + ); + case FilterComparison.EndsWith: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath != null && EF.Functions.Like(f.FilePath, $"%{normalizedPath}") + ) + ) + ) + ); + case FilterComparison.Matches: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath != null && EF.Functions.Like(f.FilePath, $"%{normalizedPath}%") + ) + ) + ) + ); + case FilterComparison.NotEqual: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath == null || !f.FilePath.Equals(normalizedPath) + ) + ) + ) + ); + case FilterComparison.NotContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Contains: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); + } + } + + public IQueryable HasFileSize(bool condition, + FilterComparison comparison, float fileSize) + { + if (fileSize == 0f || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) == fileSize), + FilterComparison.LessThan => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) < fileSize), + FilterComparison.LessThanEqual => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) <= fileSize), + FilterComparison.GreaterThan => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) > fileSize), + FilterComparison.GreaterThanEqual => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) >= fileSize), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"), + }; + } + } +} diff --git a/API/Extensions/QueryExtensions/IncludesExtensions.cs b/Kavita.Database/Extensions/IncludesExtensions.cs similarity index 98% rename from API/Extensions/QueryExtensions/IncludesExtensions.cs rename to Kavita.Database/Extensions/IncludesExtensions.cs index 113dfb8f4..c0354d949 100644 --- a/API/Extensions/QueryExtensions/IncludesExtensions.cs +++ b/Kavita.Database/Extensions/IncludesExtensions.cs @@ -1,11 +1,11 @@ -using System.Linq; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Person; +using System.Linq; +using Kavita.API.Repositories; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions; -#nullable enable +namespace Kavita.Database.Extensions; /// /// All extensions against IQueryable that enables the dynamic including based on bitwise flag pattern diff --git a/Kavita.Database/Extensions/PagedListExtensions.cs b/Kavita.Database/Extensions/PagedListExtensions.cs new file mode 100644 index 000000000..fd44d6623 --- /dev/null +++ b/Kavita.Database/Extensions/PagedListExtensions.cs @@ -0,0 +1,29 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Extensions; + +public static class PagedListExtensions +{ + extension(PagedList pagedList) + { + public static async Task> CreateAsync(IQueryable source, UserParams userParams, CancellationToken ct = default) + { + return await PagedList.CreateAsync(source, userParams.PageNumber, userParams.PageSize, ct); + } + + public static async Task> CreateAsync(IQueryable source, int pageNumber, int pageSize, CancellationToken ct = default) + { + // NOTE: OrderBy warning being thrown here even if query has the orderby statement + var countTask = source.CountAsync(ct); + var itemsTask = source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync(ct); + + await Task.WhenAll(countTask, itemsTask); + + return PagedList.Create(itemsTask.Result, countTask.Result, pageNumber, pageSize); + } + } +} diff --git a/Kavita.Database/Extensions/ProjectToExtensions.cs b/Kavita.Database/Extensions/ProjectToExtensions.cs new file mode 100644 index 000000000..18769b7f8 --- /dev/null +++ b/Kavita.Database/Extensions/ProjectToExtensions.cs @@ -0,0 +1,25 @@ +using System.Linq; +using AutoMapper; +using AutoMapper.QueryableExtensions; + +namespace Kavita.Database.Extensions; + +public static class ProjectToExtensions +{ + extension(IQueryable queryable) + { + public IQueryable ProjectToWithProgress(IConfigurationProvider config, + int userId) + { + return queryable.ProjectTo(config, new { userId }); + } + + // Convenience overload taking IMapper directly + public IQueryable ProjectToWithProgress(IMapper mapper, + int userId) + { + return queryable.ProjectTo(mapper.ConfigurationProvider, new { userId }); + } + } +} + diff --git a/API/Extensions/QueryExtensions/QueryableExtensions.cs b/Kavita.Database/Extensions/QueryableExtensions.cs similarity index 97% rename from API/Extensions/QueryExtensions/QueryableExtensions.cs rename to Kavita.Database/Extensions/QueryableExtensions.cs index d433b6831..36fdc01de 100644 --- a/API/Extensions/QueryExtensions/QueryableExtensions.cs +++ b/Kavita.Database/Extensions/QueryableExtensions.cs @@ -3,19 +3,18 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; -using API.Data.Misc; -using API.Data.Repositories; -using API.DTOs.Annotations; -using API.DTOs.Filtering; -using API.DTOs.KavitaPlus.Manage; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Entities.Scrobble; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.Annotations; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.KavitaPlus.Manage; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.Scrobble; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions; -#nullable enable +namespace Kavita.Database.Extensions; public static class QueryableExtensions { diff --git a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs b/Kavita.Database/Extensions/RestrictByAgeExtensions.cs similarity index 75% rename from API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs rename to Kavita.Database/Extensions/RestrictByAgeExtensions.cs index 038899a40..0951c2325 100644 --- a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs +++ b/Kavita.Database/Extensions/RestrictByAgeExtensions.cs @@ -1,13 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using API.Data.Misc; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Entities.User; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; -namespace API.Extensions.QueryExtensions; -#nullable enable +namespace Kavita.Database.Extensions; /// /// Responsible for restricting Entities based on an AgeRestriction @@ -83,7 +81,7 @@ public static class RestrictByAgeExtensions } /// - /// Returns all Genres where any of the linked Series/Chapters are less than or equal to restriction age rating + /// Returns all Genres where any of the linked Series/Chapters are less than or equal to the restriction age rating /// /// /// @@ -178,80 +176,81 @@ public static class RestrictByAgeExtensions return q; } - private static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction, int userId) + /// + extension(IQueryable queryable) { - if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; - var q = queryable.Where(a => a.Series.Metadata.AgeRating <= restriction.AgeRating || a.AppUserId == userId); - - if (!restriction.IncludeUnknowns) + private IQueryable RestrictAgainstAgeRestriction(AgeRestriction restriction, int userId) { - return q.Where(a => a.Series.Metadata.AgeRating != AgeRating.Unknown || a.AppUserId == userId); + if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; + var q = queryable.Where(a => a.Series.Metadata.AgeRating <= restriction.AgeRating || a.AppUserId == userId); + + if (!restriction.IncludeUnknowns) + { + return q.Where(a => a.Series.Metadata.AgeRating != AgeRating.Unknown || a.AppUserId == userId); + } + + return q; } - return q; - } - - // TODO: After updating to .net 10, leverage new Complex Data type queries to inline all db operations here - /// - /// Filter annotations by social preferences of users - /// - /// - /// - /// List of user preferences for every user on the server - /// - public static IQueryable RestrictBySocialPreferences(this IQueryable queryable, int userId, IList userPreferences) - { - var preferencesById = userPreferences.ToDictionary(p => p.AppUserId, p => p.SocialPreferences); - var socialPreferences = preferencesById[userId]; - - if (socialPreferences.ViewOtherAnnotations) + /// + /// Filter annotations by social preferences of users + /// + /// + /// List of user preferences for every user on the server + /// + public IQueryable RestrictBySocialPreferences(int userId, IList userPreferences) { - // We are unable to do dictionary lookups in Sqlite; This means we need to translate them to X IN Y. - var sharingUserIds = userPreferences - .Where(p => p.SocialPreferences.ShareAnnotations) - .Select(p => p.AppUserId) - .ToHashSet(); + var preferencesById = userPreferences.ToDictionary(p => p.AppUserId, p => p.SocialPreferences); + var socialPreferences = preferencesById[userId]; - // Only include the users' annotations, or those of users that are sharing - queryable = queryable.Where(a => a.AppUserId == userId || sharingUserIds.Contains(a.AppUserId)); - - // For other users' annotation - foreach (var sharingUserId in sharingUserIds.Where(id => id != userId)) + if (socialPreferences.ViewOtherAnnotations) { - // Filter out libs if enabled - var libs = preferencesById[sharingUserId].SocialLibraries; - if (libs.Count > 0) - { - queryable = queryable.Where(a => a.AppUserId != sharingUserId || libs.Contains(a.LibraryId)); - } + // We are unable to do dictionary lookups in Sqlite; This means we need to translate them to X IN Y. + var sharingUserIds = userPreferences + .Where(p => p.SocialPreferences.ShareAnnotations) + .Select(p => p.AppUserId) + .ToHashSet(); - // Filter on age rating - var ageRating = preferencesById[sharingUserId].SocialMaxAgeRating; - var includeUnknowns = preferencesById[sharingUserId].SocialIncludeUnknowns; - if (ageRating != AgeRating.NotApplicable) + // Only include the users' annotations or those of users that are sharing + queryable = queryable.Where(a => a.AppUserId == userId || sharingUserIds.Contains(a.AppUserId)); + + // For other users' annotation + foreach (var sharingUserId in sharingUserIds.Where(id => id != userId)) { - queryable = queryable.Where(a => a.AppUserId != sharingUserId || a.Series.Metadata.AgeRating <= ageRating) - .WhereIf(!includeUnknowns, - a => a.AppUserId != sharingUserId || a.Series.Metadata.AgeRating != AgeRating.Unknown); + // Filter out libs if enabled + var libs = preferencesById[sharingUserId].SocialLibraries; + if (libs.Count > 0) + { + queryable = queryable.Where(a => a.AppUserId != sharingUserId || libs.Contains(a.LibraryId)); + } + + // Filter on age rating + var ageRating = preferencesById[sharingUserId].SocialMaxAgeRating; + var includeUnknowns = preferencesById[sharingUserId].SocialIncludeUnknowns; + if (ageRating != AgeRating.NotApplicable) + { + queryable = queryable.Where(a => a.AppUserId != sharingUserId || a.Series.Metadata.AgeRating <= ageRating) + .WhereIf(!includeUnknowns, + a => a.AppUserId != sharingUserId || a.Series.Metadata.AgeRating != AgeRating.Unknown); + } } } - } - else - { - queryable = queryable.Where(a => a.AppUserId == userId); - } - - return queryable - .WhereIf(socialPreferences.SocialLibraries.Count > 0, - a => a.AppUserId == userId || socialPreferences.SocialLibraries.Contains(a.LibraryId)) - .RestrictAgainstAgeRestriction(new AgeRestriction + else { - AgeRating = socialPreferences.SocialMaxAgeRating, - IncludeUnknowns = socialPreferences.SocialIncludeUnknowns, - }, userId); + queryable = queryable.Where(a => a.AppUserId == userId); + } + + return queryable + .WhereIf(socialPreferences.SocialLibraries.Count > 0, + a => a.AppUserId == userId || socialPreferences.SocialLibraries.Contains(a.LibraryId)) + .RestrictAgainstAgeRestriction(new AgeRestriction + { + AgeRating = socialPreferences.SocialMaxAgeRating, + IncludeUnknowns = socialPreferences.SocialIncludeUnknowns, + }, userId); + } } - // TODO: After updating to .net 10, leverage new Complex Data type queries to inline all db operations here /// /// Filter user reviews social preferences of users /// @@ -299,7 +298,6 @@ public static class RestrictByAgeExtensions }, userId); } - // TODO: After updating to .net 10, leverage new Complex Data type queries to inline all db operations here /// /// Filter user chapter reviews social preferences of users /// diff --git a/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs b/Kavita.Database/Extensions/RestrictByLibraryExtensions.cs similarity index 89% rename from API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs rename to Kavita.Database/Extensions/RestrictByLibraryExtensions.cs index 9ec1b8621..bece6474a 100644 --- a/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs +++ b/Kavita.Database/Extensions/RestrictByLibraryExtensions.cs @@ -1,12 +1,11 @@ -using System.Linq; -using API.Entities; -using API.Entities.Person; +using System.Linq; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Person; -namespace API.Extensions.QueryExtensions; +namespace Kavita.Database.Extensions; public static class RestrictByLibraryExtensions { - public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) { return query.Where(p => diff --git a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs b/Kavita.Database/Extensions/SearchQueryableExtensions.cs similarity index 93% rename from API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs rename to Kavita.Database/Extensions/SearchQueryableExtensions.cs index 173e3dedc..e8aff4e32 100644 --- a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs +++ b/Kavita.Database/Extensions/SearchQueryableExtensions.cs @@ -1,13 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using API.Data.Misc; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Metadata; -using API.Entities.Person; +using Kavita.API.Repositories; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions.Filtering; +namespace Kavita.Database.Extensions; public static class SearchQueryableExtensions { diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs b/Kavita.Database/Extensions/SeriesSortExtensions.cs similarity index 91% rename from API/Extensions/QueryExtensions/Filtering/SeriesSort.cs rename to Kavita.Database/Extensions/SeriesSortExtensions.cs index 8f0e9a364..7f798e958 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs +++ b/Kavita.Database/Extensions/SeriesSortExtensions.cs @@ -1,17 +1,17 @@ using System.Linq; -using API.DTOs.Filtering; -using API.Entities; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.Entities; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions.Filtering; -#nullable enable +namespace Kavita.Database.Extensions; -public static class SeriesSort +public static class SeriesSortExtensions { /// /// Applies the correct sort based on /// /// + /// /// /// public static IQueryable Sort(this IQueryable query, int userId, SortOptions? sortOptions) diff --git a/API/Extensions/QueryExtensions/StatisticsQueryExtensions.cs b/Kavita.Database/Extensions/StatisticsQueryExtensions.cs similarity index 74% rename from API/Extensions/QueryExtensions/StatisticsQueryExtensions.cs rename to Kavita.Database/Extensions/StatisticsQueryExtensions.cs index 11a2b7bdf..93da861d1 100644 --- a/API/Extensions/QueryExtensions/StatisticsQueryExtensions.cs +++ b/Kavita.Database/Extensions/StatisticsQueryExtensions.cs @@ -1,12 +1,12 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; -using API.Entities.Progress; +using Kavita.Models.Entities.Progress; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions; +namespace Kavita.Database.Extensions; public class IdCount { @@ -19,7 +19,7 @@ public class IdCount /// public static class StatisticsQueryExtensions { - public static async Task> GetTopCounts( this IQueryable query, Expression> keySelector, int? take = null) + public static async Task> GetTopCounts(this IQueryable query, Expression> keySelector, int? take = null) { var result = query .GroupBy(keySelector) diff --git a/Kavita.Database/Kavita.Database.csproj b/Kavita.Database/Kavita.Database.csproj new file mode 100644 index 000000000..c621ee196 --- /dev/null +++ b/Kavita.Database/Kavita.Database.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + disable + enable + + + + + + + + + + + + + + + + + + diff --git a/API/Data/Migrations/20201213205325_AddUser.Designer.cs b/Kavita.Database/Migrations/20201213205325_AddUser.Designer.cs similarity index 96% rename from API/Data/Migrations/20201213205325_AddUser.Designer.cs rename to Kavita.Database/Migrations/20201213205325_AddUser.Designer.cs index 565d03517..8e72096fb 100644 --- a/API/Data/Migrations/20201213205325_AddUser.Designer.cs +++ b/Kavita.Database/Migrations/20201213205325_AddUser.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20201213205325_AddUser")] diff --git a/API/Data/Migrations/20201213205325_AddUser.cs b/Kavita.Database/Migrations/20201213205325_AddUser.cs similarity index 97% rename from API/Data/Migrations/20201213205325_AddUser.cs rename to Kavita.Database/Migrations/20201213205325_AddUser.cs index 4429111b1..57322fdbb 100644 --- a/API/Data/Migrations/20201213205325_AddUser.cs +++ b/Kavita.Database/Migrations/20201213205325_AddUser.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddUser : Migration { diff --git a/API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs b/Kavita.Database/Migrations/20201215195007_AddedLibrary.Designer.cs similarity index 98% rename from API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs rename to Kavita.Database/Migrations/20201215195007_AddedLibrary.Designer.cs index 4a657771e..fc14610f7 100644 --- a/API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs +++ b/Kavita.Database/Migrations/20201215195007_AddedLibrary.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20201215195007_AddedLibrary")] diff --git a/API/Data/Migrations/20201215195007_AddedLibrary.cs b/Kavita.Database/Migrations/20201215195007_AddedLibrary.cs similarity index 98% rename from API/Data/Migrations/20201215195007_AddedLibrary.cs rename to Kavita.Database/Migrations/20201215195007_AddedLibrary.cs index f1c4adf56..1c0a688be 100644 --- a/API/Data/Migrations/20201215195007_AddedLibrary.cs +++ b/Kavita.Database/Migrations/20201215195007_AddedLibrary.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddedLibrary : Migration { diff --git a/API/Data/Migrations/20201218173135_ManyToManyLibraries.Designer.cs b/Kavita.Database/Migrations/20201218173135_ManyToManyLibraries.Designer.cs similarity index 98% rename from API/Data/Migrations/20201218173135_ManyToManyLibraries.Designer.cs rename to Kavita.Database/Migrations/20201218173135_ManyToManyLibraries.Designer.cs index 98af0c730..4a1b042b6 100644 --- a/API/Data/Migrations/20201218173135_ManyToManyLibraries.Designer.cs +++ b/Kavita.Database/Migrations/20201218173135_ManyToManyLibraries.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20201218173135_ManyToManyLibraries")] diff --git a/API/Data/Migrations/20201218173135_ManyToManyLibraries.cs b/Kavita.Database/Migrations/20201218173135_ManyToManyLibraries.cs similarity index 99% rename from API/Data/Migrations/20201218173135_ManyToManyLibraries.cs rename to Kavita.Database/Migrations/20201218173135_ManyToManyLibraries.cs index e7d2cb39b..4a7586add 100644 --- a/API/Data/Migrations/20201218173135_ManyToManyLibraries.cs +++ b/Kavita.Database/Migrations/20201218173135_ManyToManyLibraries.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ManyToManyLibraries : Migration { diff --git a/API/Data/Migrations/20201221141047_IdentityAdded.Designer.cs b/Kavita.Database/Migrations/20201221141047_IdentityAdded.Designer.cs similarity index 99% rename from API/Data/Migrations/20201221141047_IdentityAdded.Designer.cs rename to Kavita.Database/Migrations/20201221141047_IdentityAdded.Designer.cs index 0836f6f4a..b42232860 100644 --- a/API/Data/Migrations/20201221141047_IdentityAdded.Designer.cs +++ b/Kavita.Database/Migrations/20201221141047_IdentityAdded.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20201221141047_IdentityAdded")] diff --git a/API/Data/Migrations/20201221141047_IdentityAdded.cs b/Kavita.Database/Migrations/20201221141047_IdentityAdded.cs similarity index 99% rename from API/Data/Migrations/20201221141047_IdentityAdded.cs rename to Kavita.Database/Migrations/20201221141047_IdentityAdded.cs index ee9dd15b2..7e77010b6 100644 --- a/API/Data/Migrations/20201221141047_IdentityAdded.cs +++ b/Kavita.Database/Migrations/20201221141047_IdentityAdded.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class IdentityAdded : Migration { diff --git a/API/Data/Migrations/20201224155621_MiscCleanup.Designer.cs b/Kavita.Database/Migrations/20201224155621_MiscCleanup.Designer.cs similarity index 99% rename from API/Data/Migrations/20201224155621_MiscCleanup.Designer.cs rename to Kavita.Database/Migrations/20201224155621_MiscCleanup.Designer.cs index 8ae8c597a..38894dbeb 100644 --- a/API/Data/Migrations/20201224155621_MiscCleanup.Designer.cs +++ b/Kavita.Database/Migrations/20201224155621_MiscCleanup.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20201224155621_MiscCleanup")] diff --git a/API/Data/Migrations/20201224155621_MiscCleanup.cs b/Kavita.Database/Migrations/20201224155621_MiscCleanup.cs similarity index 97% rename from API/Data/Migrations/20201224155621_MiscCleanup.cs rename to Kavita.Database/Migrations/20201224155621_MiscCleanup.cs index 78e66aea8..f2ead54d9 100644 --- a/API/Data/Migrations/20201224155621_MiscCleanup.cs +++ b/Kavita.Database/Migrations/20201224155621_MiscCleanup.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MiscCleanup : Migration { diff --git a/API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs b/Kavita.Database/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs similarity index 99% rename from API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs rename to Kavita.Database/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs index 5cf25a225..5bb9643a9 100644 --- a/API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs +++ b/Kavita.Database/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20201229190216_SeriesAndVolumeEntities")] diff --git a/API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.cs b/Kavita.Database/Migrations/20201229190216_SeriesAndVolumeEntities.cs similarity index 99% rename from API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.cs rename to Kavita.Database/Migrations/20201229190216_SeriesAndVolumeEntities.cs index 5b4302ba3..bfa689b92 100644 --- a/API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.cs +++ b/Kavita.Database/Migrations/20201229190216_SeriesAndVolumeEntities.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesAndVolumeEntities : Migration { diff --git a/API/Data/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs b/Kavita.Database/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs similarity index 99% rename from API/Data/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs rename to Kavita.Database/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs index a1a54360f..57d044b29 100644 --- a/API/Data/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs +++ b/Kavita.Database/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210101180935_AddedCoverImageToSeries")] diff --git a/API/Data/Migrations/20210101180935_AddedCoverImageToSeries.cs b/Kavita.Database/Migrations/20210101180935_AddedCoverImageToSeries.cs similarity index 94% rename from API/Data/Migrations/20210101180935_AddedCoverImageToSeries.cs rename to Kavita.Database/Migrations/20210101180935_AddedCoverImageToSeries.cs index 45e0fdc41..690015930 100644 --- a/API/Data/Migrations/20210101180935_AddedCoverImageToSeries.cs +++ b/Kavita.Database/Migrations/20210101180935_AddedCoverImageToSeries.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddedCoverImageToSeries : Migration { diff --git a/API/Data/Migrations/20210102165536_EntityTimestamps.Designer.cs b/Kavita.Database/Migrations/20210102165536_EntityTimestamps.Designer.cs similarity index 99% rename from API/Data/Migrations/20210102165536_EntityTimestamps.Designer.cs rename to Kavita.Database/Migrations/20210102165536_EntityTimestamps.Designer.cs index de4910b51..2cd579f6c 100644 --- a/API/Data/Migrations/20210102165536_EntityTimestamps.Designer.cs +++ b/Kavita.Database/Migrations/20210102165536_EntityTimestamps.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210102165536_EntityTimestamps")] diff --git a/API/Data/Migrations/20210102165536_EntityTimestamps.cs b/Kavita.Database/Migrations/20210102165536_EntityTimestamps.cs similarity index 98% rename from API/Data/Migrations/20210102165536_EntityTimestamps.cs rename to Kavita.Database/Migrations/20210102165536_EntityTimestamps.cs index 2ed6041f0..4fa0ef673 100644 --- a/API/Data/Migrations/20210102165536_EntityTimestamps.cs +++ b/Kavita.Database/Migrations/20210102165536_EntityTimestamps.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class EntityTimestamps : Migration { diff --git a/API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs b/Kavita.Database/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs similarity index 99% rename from API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs rename to Kavita.Database/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs index 1102111fc..9a20fe065 100644 --- a/API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs +++ b/Kavita.Database/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210102173326_VolumeNumberRefactor")] diff --git a/API/Data/Migrations/20210102173326_VolumeNumberRefactor.cs b/Kavita.Database/Migrations/20210102173326_VolumeNumberRefactor.cs similarity index 96% rename from API/Data/Migrations/20210102173326_VolumeNumberRefactor.cs rename to Kavita.Database/Migrations/20210102173326_VolumeNumberRefactor.cs index 21cc8d42c..24265650b 100644 --- a/API/Data/Migrations/20210102173326_VolumeNumberRefactor.cs +++ b/Kavita.Database/Migrations/20210102173326_VolumeNumberRefactor.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class VolumeNumberRefactor : Migration { diff --git a/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs b/Kavita.Database/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs similarity index 99% rename from API/Data/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs rename to Kavita.Database/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs index 4288a9878..8e9c3ceba 100644 --- a/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs +++ b/Kavita.Database/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210103201043_RemoveUserIsAdmin")] diff --git a/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.cs b/Kavita.Database/Migrations/20210103201043_RemoveUserIsAdmin.cs similarity index 94% rename from API/Data/Migrations/20210103201043_RemoveUserIsAdmin.cs rename to Kavita.Database/Migrations/20210103201043_RemoveUserIsAdmin.cs index 826159fbb..a9e0e0fca 100644 --- a/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.cs +++ b/Kavita.Database/Migrations/20210103201043_RemoveUserIsAdmin.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class RemoveUserIsAdmin : Migration { diff --git a/API/Data/Migrations/20210103230812_SeriesCoverImage.Designer.cs b/Kavita.Database/Migrations/20210103230812_SeriesCoverImage.Designer.cs similarity index 99% rename from API/Data/Migrations/20210103230812_SeriesCoverImage.Designer.cs rename to Kavita.Database/Migrations/20210103230812_SeriesCoverImage.Designer.cs index 03f94a6a2..3ad597825 100644 --- a/API/Data/Migrations/20210103230812_SeriesCoverImage.Designer.cs +++ b/Kavita.Database/Migrations/20210103230812_SeriesCoverImage.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210103230812_SeriesCoverImage")] diff --git a/API/Data/Migrations/20210103230812_SeriesCoverImage.cs b/Kavita.Database/Migrations/20210103230812_SeriesCoverImage.cs similarity index 96% rename from API/Data/Migrations/20210103230812_SeriesCoverImage.cs rename to Kavita.Database/Migrations/20210103230812_SeriesCoverImage.cs index 9436cbdcf..24c81a886 100644 --- a/API/Data/Migrations/20210103230812_SeriesCoverImage.cs +++ b/Kavita.Database/Migrations/20210103230812_SeriesCoverImage.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesCoverImage : Migration { diff --git a/API/Data/Migrations/20210104011624_VolumeCoverImage.Designer.cs b/Kavita.Database/Migrations/20210104011624_VolumeCoverImage.Designer.cs similarity index 99% rename from API/Data/Migrations/20210104011624_VolumeCoverImage.Designer.cs rename to Kavita.Database/Migrations/20210104011624_VolumeCoverImage.Designer.cs index 437daca24..b479e98ba 100644 --- a/API/Data/Migrations/20210104011624_VolumeCoverImage.Designer.cs +++ b/Kavita.Database/Migrations/20210104011624_VolumeCoverImage.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210104011624_VolumeCoverImage")] diff --git a/API/Data/Migrations/20210104011624_VolumeCoverImage.cs b/Kavita.Database/Migrations/20210104011624_VolumeCoverImage.cs similarity index 94% rename from API/Data/Migrations/20210104011624_VolumeCoverImage.cs rename to Kavita.Database/Migrations/20210104011624_VolumeCoverImage.cs index 49bc17fea..108858368 100644 --- a/API/Data/Migrations/20210104011624_VolumeCoverImage.cs +++ b/Kavita.Database/Migrations/20210104011624_VolumeCoverImage.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class VolumeCoverImage : Migration { diff --git a/API/Data/Migrations/20210109205034_CacheMetadata.Designer.cs b/Kavita.Database/Migrations/20210109205034_CacheMetadata.Designer.cs similarity index 99% rename from API/Data/Migrations/20210109205034_CacheMetadata.Designer.cs rename to Kavita.Database/Migrations/20210109205034_CacheMetadata.Designer.cs index 66b17bf30..8acd11aaa 100644 --- a/API/Data/Migrations/20210109205034_CacheMetadata.Designer.cs +++ b/Kavita.Database/Migrations/20210109205034_CacheMetadata.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210109205034_CacheMetadata")] diff --git a/API/Data/Migrations/20210109205034_CacheMetadata.cs b/Kavita.Database/Migrations/20210109205034_CacheMetadata.cs similarity index 97% rename from API/Data/Migrations/20210109205034_CacheMetadata.cs rename to Kavita.Database/Migrations/20210109205034_CacheMetadata.cs index 476591e15..a12c93303 100644 --- a/API/Data/Migrations/20210109205034_CacheMetadata.cs +++ b/Kavita.Database/Migrations/20210109205034_CacheMetadata.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CacheMetadata : Migration { diff --git a/API/Data/Migrations/20210111231840_VolumePages.Designer.cs b/Kavita.Database/Migrations/20210111231840_VolumePages.Designer.cs similarity index 99% rename from API/Data/Migrations/20210111231840_VolumePages.Designer.cs rename to Kavita.Database/Migrations/20210111231840_VolumePages.Designer.cs index f351a04e1..db1dc5f1c 100644 --- a/API/Data/Migrations/20210111231840_VolumePages.Designer.cs +++ b/Kavita.Database/Migrations/20210111231840_VolumePages.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210111231840_VolumePages")] diff --git a/API/Data/Migrations/20210111231840_VolumePages.cs b/Kavita.Database/Migrations/20210111231840_VolumePages.cs similarity index 94% rename from API/Data/Migrations/20210111231840_VolumePages.cs rename to Kavita.Database/Migrations/20210111231840_VolumePages.cs index c9b36b03a..b96b58ecc 100644 --- a/API/Data/Migrations/20210111231840_VolumePages.cs +++ b/Kavita.Database/Migrations/20210111231840_VolumePages.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class VolumePages : Migration { diff --git a/API/Data/Migrations/20210114214506_UserProgress.Designer.cs b/Kavita.Database/Migrations/20210114214506_UserProgress.Designer.cs similarity index 99% rename from API/Data/Migrations/20210114214506_UserProgress.Designer.cs rename to Kavita.Database/Migrations/20210114214506_UserProgress.Designer.cs index cd7e5a53b..b64bcbe83 100644 --- a/API/Data/Migrations/20210114214506_UserProgress.Designer.cs +++ b/Kavita.Database/Migrations/20210114214506_UserProgress.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210114214506_UserProgress")] diff --git a/API/Data/Migrations/20210114214506_UserProgress.cs b/Kavita.Database/Migrations/20210114214506_UserProgress.cs similarity index 98% rename from API/Data/Migrations/20210114214506_UserProgress.cs rename to Kavita.Database/Migrations/20210114214506_UserProgress.cs index 6d966fbdc..f22582bad 100644 --- a/API/Data/Migrations/20210114214506_UserProgress.cs +++ b/Kavita.Database/Migrations/20210114214506_UserProgress.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class UserProgress : Migration { diff --git a/API/Data/Migrations/20210117180406_ReadStatusModifications.Designer.cs b/Kavita.Database/Migrations/20210117180406_ReadStatusModifications.Designer.cs similarity index 99% rename from API/Data/Migrations/20210117180406_ReadStatusModifications.Designer.cs rename to Kavita.Database/Migrations/20210117180406_ReadStatusModifications.Designer.cs index d4133c335..36c45de23 100644 --- a/API/Data/Migrations/20210117180406_ReadStatusModifications.Designer.cs +++ b/Kavita.Database/Migrations/20210117180406_ReadStatusModifications.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210117180406_ReadStatusModifications")] diff --git a/API/Data/Migrations/20210117180406_ReadStatusModifications.cs b/Kavita.Database/Migrations/20210117180406_ReadStatusModifications.cs similarity index 99% rename from API/Data/Migrations/20210117180406_ReadStatusModifications.cs rename to Kavita.Database/Migrations/20210117180406_ReadStatusModifications.cs index d852d8843..d346cceb3 100644 --- a/API/Data/Migrations/20210117180406_ReadStatusModifications.cs +++ b/Kavita.Database/Migrations/20210117180406_ReadStatusModifications.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadStatusModifications : Migration { diff --git a/API/Data/Migrations/20210117181421_SeriesPages.Designer.cs b/Kavita.Database/Migrations/20210117181421_SeriesPages.Designer.cs similarity index 99% rename from API/Data/Migrations/20210117181421_SeriesPages.Designer.cs rename to Kavita.Database/Migrations/20210117181421_SeriesPages.Designer.cs index 8caa3acc1..b8f218fcd 100644 --- a/API/Data/Migrations/20210117181421_SeriesPages.Designer.cs +++ b/Kavita.Database/Migrations/20210117181421_SeriesPages.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210117181421_SeriesPages")] diff --git a/API/Data/Migrations/20210117181421_SeriesPages.cs b/Kavita.Database/Migrations/20210117181421_SeriesPages.cs similarity index 94% rename from API/Data/Migrations/20210117181421_SeriesPages.cs rename to Kavita.Database/Migrations/20210117181421_SeriesPages.cs index 97ee23b1b..43cbf2a93 100644 --- a/API/Data/Migrations/20210117181421_SeriesPages.cs +++ b/Kavita.Database/Migrations/20210117181421_SeriesPages.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesPages : Migration { diff --git a/API/Data/Migrations/20210119213837_AppUserRatingAndReviews.Designer.cs b/Kavita.Database/Migrations/20210119213837_AppUserRatingAndReviews.Designer.cs similarity index 99% rename from API/Data/Migrations/20210119213837_AppUserRatingAndReviews.Designer.cs rename to Kavita.Database/Migrations/20210119213837_AppUserRatingAndReviews.Designer.cs index e68e9e11b..183cb172e 100644 --- a/API/Data/Migrations/20210119213837_AppUserRatingAndReviews.Designer.cs +++ b/Kavita.Database/Migrations/20210119213837_AppUserRatingAndReviews.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210119213837_AppUserRatingAndReviews")] diff --git a/API/Data/Migrations/20210119213837_AppUserRatingAndReviews.cs b/Kavita.Database/Migrations/20210119213837_AppUserRatingAndReviews.cs similarity index 97% rename from API/Data/Migrations/20210119213837_AppUserRatingAndReviews.cs rename to Kavita.Database/Migrations/20210119213837_AppUserRatingAndReviews.cs index 98db3af2b..52b4a01ac 100644 --- a/API/Data/Migrations/20210119213837_AppUserRatingAndReviews.cs +++ b/Kavita.Database/Migrations/20210119213837_AppUserRatingAndReviews.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AppUserRatingAndReviews : Migration { diff --git a/API/Data/Migrations/20210121180051_AddedServerSettings.Designer.cs b/Kavita.Database/Migrations/20210121180051_AddedServerSettings.Designer.cs similarity index 99% rename from API/Data/Migrations/20210121180051_AddedServerSettings.Designer.cs rename to Kavita.Database/Migrations/20210121180051_AddedServerSettings.Designer.cs index 23894ae47..7b3959795 100644 --- a/API/Data/Migrations/20210121180051_AddedServerSettings.Designer.cs +++ b/Kavita.Database/Migrations/20210121180051_AddedServerSettings.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210121180051_AddedServerSettings")] diff --git a/API/Data/Migrations/20210121180051_AddedServerSettings.cs b/Kavita.Database/Migrations/20210121180051_AddedServerSettings.cs similarity index 96% rename from API/Data/Migrations/20210121180051_AddedServerSettings.cs rename to Kavita.Database/Migrations/20210121180051_AddedServerSettings.cs index 98fb77452..3cf0ca26d 100644 --- a/API/Data/Migrations/20210121180051_AddedServerSettings.cs +++ b/Kavita.Database/Migrations/20210121180051_AddedServerSettings.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddedServerSettings : Migration { diff --git a/API/Data/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs b/Kavita.Database/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs similarity index 99% rename from API/Data/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs rename to Kavita.Database/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs index 8072786e9..ccde420a1 100644 --- a/API/Data/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs +++ b/Kavita.Database/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210121215532_ServerSettingsAdjustment")] diff --git a/API/Data/Migrations/20210121215532_ServerSettingsAdjustment.cs b/Kavita.Database/Migrations/20210121215532_ServerSettingsAdjustment.cs similarity index 97% rename from API/Data/Migrations/20210121215532_ServerSettingsAdjustment.cs rename to Kavita.Database/Migrations/20210121215532_ServerSettingsAdjustment.cs index 6c1f1b268..5f340d3cd 100644 --- a/API/Data/Migrations/20210121215532_ServerSettingsAdjustment.cs +++ b/Kavita.Database/Migrations/20210121215532_ServerSettingsAdjustment.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ServerSettingsAdjustment : Migration { diff --git a/API/Data/Migrations/20210122165809_ServerSettingsChange.Designer.cs b/Kavita.Database/Migrations/20210122165809_ServerSettingsChange.Designer.cs similarity index 99% rename from API/Data/Migrations/20210122165809_ServerSettingsChange.Designer.cs rename to Kavita.Database/Migrations/20210122165809_ServerSettingsChange.Designer.cs index f277eae77..543811494 100644 --- a/API/Data/Migrations/20210122165809_ServerSettingsChange.Designer.cs +++ b/Kavita.Database/Migrations/20210122165809_ServerSettingsChange.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210122165809_ServerSettingsChange")] diff --git a/API/Data/Migrations/20210122165809_ServerSettingsChange.cs b/Kavita.Database/Migrations/20210122165809_ServerSettingsChange.cs similarity index 96% rename from API/Data/Migrations/20210122165809_ServerSettingsChange.cs rename to Kavita.Database/Migrations/20210122165809_ServerSettingsChange.cs index 69df81fa0..2aca691e0 100644 --- a/API/Data/Migrations/20210122165809_ServerSettingsChange.cs +++ b/Kavita.Database/Migrations/20210122165809_ServerSettingsChange.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ServerSettingsChange : Migration { diff --git a/API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs b/Kavita.Database/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs similarity index 99% rename from API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs rename to Kavita.Database/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs index 8bf49d1c5..59bf7173c 100644 --- a/API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs +++ b/Kavita.Database/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210122172455_ServerSettingsPrimaryKey")] diff --git a/API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.cs b/Kavita.Database/Migrations/20210122172455_ServerSettingsPrimaryKey.cs similarity index 98% rename from API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.cs rename to Kavita.Database/Migrations/20210122172455_ServerSettingsPrimaryKey.cs index 795c82683..efbe74035 100644 --- a/API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.cs +++ b/Kavita.Database/Migrations/20210122172455_ServerSettingsPrimaryKey.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ServerSettingsPrimaryKey : Migration { diff --git a/API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs b/Kavita.Database/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs similarity index 99% rename from API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs rename to Kavita.Database/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs index 17cb4b81d..1d248b40b 100644 --- a/API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs +++ b/Kavita.Database/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210128143348_SeriesVolumeChapterChange")] diff --git a/API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.cs b/Kavita.Database/Migrations/20210128143348_SeriesVolumeChapterChange.cs similarity index 99% rename from API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.cs rename to Kavita.Database/Migrations/20210128143348_SeriesVolumeChapterChange.cs index ae6e6b6d1..0c713d109 100644 --- a/API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.cs +++ b/Kavita.Database/Migrations/20210128143348_SeriesVolumeChapterChange.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesVolumeChapterChange : Migration { diff --git a/API/Data/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs b/Kavita.Database/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs similarity index 99% rename from API/Data/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs rename to Kavita.Database/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs index 5d0cfa7b5..3ef67df0a 100644 --- a/API/Data/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs +++ b/Kavita.Database/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210128201832_MangaFileChapterRelationship")] diff --git a/API/Data/Migrations/20210128201832_MangaFileChapterRelationship.cs b/Kavita.Database/Migrations/20210128201832_MangaFileChapterRelationship.cs similarity index 98% rename from API/Data/Migrations/20210128201832_MangaFileChapterRelationship.cs rename to Kavita.Database/Migrations/20210128201832_MangaFileChapterRelationship.cs index a04e77dd2..e120ffc39 100644 --- a/API/Data/Migrations/20210128201832_MangaFileChapterRelationship.cs +++ b/Kavita.Database/Migrations/20210128201832_MangaFileChapterRelationship.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MangaFileChapterRelationship : Migration { diff --git a/API/Data/Migrations/20210203164258_ServerSettingsKey.Designer.cs b/Kavita.Database/Migrations/20210203164258_ServerSettingsKey.Designer.cs similarity index 99% rename from API/Data/Migrations/20210203164258_ServerSettingsKey.Designer.cs rename to Kavita.Database/Migrations/20210203164258_ServerSettingsKey.Designer.cs index 75d0a2244..3c949a520 100644 --- a/API/Data/Migrations/20210203164258_ServerSettingsKey.Designer.cs +++ b/Kavita.Database/Migrations/20210203164258_ServerSettingsKey.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210203164258_ServerSettingsKey")] diff --git a/API/Data/Migrations/20210203164258_ServerSettingsKey.cs b/Kavita.Database/Migrations/20210203164258_ServerSettingsKey.cs similarity index 95% rename from API/Data/Migrations/20210203164258_ServerSettingsKey.cs rename to Kavita.Database/Migrations/20210203164258_ServerSettingsKey.cs index 0a2a64920..13e51c1ec 100644 --- a/API/Data/Migrations/20210203164258_ServerSettingsKey.cs +++ b/Kavita.Database/Migrations/20210203164258_ServerSettingsKey.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ServerSettingsKey : Migration { diff --git a/API/Data/Migrations/20210205220227_UserPreferences.Designer.cs b/Kavita.Database/Migrations/20210205220227_UserPreferences.Designer.cs similarity index 99% rename from API/Data/Migrations/20210205220227_UserPreferences.Designer.cs rename to Kavita.Database/Migrations/20210205220227_UserPreferences.Designer.cs index 1bea7a402..bb47f4270 100644 --- a/API/Data/Migrations/20210205220227_UserPreferences.Designer.cs +++ b/Kavita.Database/Migrations/20210205220227_UserPreferences.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210205220227_UserPreferences")] diff --git a/API/Data/Migrations/20210205220227_UserPreferences.cs b/Kavita.Database/Migrations/20210205220227_UserPreferences.cs similarity index 98% rename from API/Data/Migrations/20210205220227_UserPreferences.cs rename to Kavita.Database/Migrations/20210205220227_UserPreferences.cs index 892eb9767..10f48c39a 100644 --- a/API/Data/Migrations/20210205220227_UserPreferences.cs +++ b/Kavita.Database/Migrations/20210205220227_UserPreferences.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class UserPreferences : Migration { diff --git a/API/Data/Migrations/20210207231256_SeriesNormalizedName.Designer.cs b/Kavita.Database/Migrations/20210207231256_SeriesNormalizedName.Designer.cs similarity index 99% rename from API/Data/Migrations/20210207231256_SeriesNormalizedName.Designer.cs rename to Kavita.Database/Migrations/20210207231256_SeriesNormalizedName.Designer.cs index 04c5c3d3d..86911ad4a 100644 --- a/API/Data/Migrations/20210207231256_SeriesNormalizedName.Designer.cs +++ b/Kavita.Database/Migrations/20210207231256_SeriesNormalizedName.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210207231256_SeriesNormalizedName")] diff --git a/API/Data/Migrations/20210207231256_SeriesNormalizedName.cs b/Kavita.Database/Migrations/20210207231256_SeriesNormalizedName.cs similarity index 94% rename from API/Data/Migrations/20210207231256_SeriesNormalizedName.cs rename to Kavita.Database/Migrations/20210207231256_SeriesNormalizedName.cs index 262583441..30422f129 100644 --- a/API/Data/Migrations/20210207231256_SeriesNormalizedName.cs +++ b/Kavita.Database/Migrations/20210207231256_SeriesNormalizedName.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesNormalizedName : Migration { diff --git a/API/Data/Migrations/20210225150830_AddLocalizedName.Designer.cs b/Kavita.Database/Migrations/20210225150830_AddLocalizedName.Designer.cs similarity index 99% rename from API/Data/Migrations/20210225150830_AddLocalizedName.Designer.cs rename to Kavita.Database/Migrations/20210225150830_AddLocalizedName.Designer.cs index 04a9cc8de..1a5e22d04 100644 --- a/API/Data/Migrations/20210225150830_AddLocalizedName.Designer.cs +++ b/Kavita.Database/Migrations/20210225150830_AddLocalizedName.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210225150830_AddLocalizedName")] diff --git a/API/Data/Migrations/20210225150830_AddLocalizedName.cs b/Kavita.Database/Migrations/20210225150830_AddLocalizedName.cs similarity index 94% rename from API/Data/Migrations/20210225150830_AddLocalizedName.cs rename to Kavita.Database/Migrations/20210225150830_AddLocalizedName.cs index 4c8059dd6..b6de098a7 100644 --- a/API/Data/Migrations/20210225150830_AddLocalizedName.cs +++ b/Kavita.Database/Migrations/20210225150830_AddLocalizedName.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddLocalizedName : Migration { diff --git a/API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs b/Kavita.Database/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs similarity index 99% rename from API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs rename to Kavita.Database/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs index a407ccc28..8f05a7ec6 100644 --- a/API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs +++ b/Kavita.Database/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210315134028_SearchIndexAndProgressDates")] diff --git a/API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.cs b/Kavita.Database/Migrations/20210315134028_SearchIndexAndProgressDates.cs similarity index 97% rename from API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.cs rename to Kavita.Database/Migrations/20210315134028_SearchIndexAndProgressDates.cs index 02dc1db2c..9d0806398 100644 --- a/API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.cs +++ b/Kavita.Database/Migrations/20210315134028_SearchIndexAndProgressDates.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SearchIndexAndProgressDates : Migration { diff --git a/API/Data/Migrations/20210322212724_MangaFileToPages.Designer.cs b/Kavita.Database/Migrations/20210322212724_MangaFileToPages.Designer.cs similarity index 99% rename from API/Data/Migrations/20210322212724_MangaFileToPages.Designer.cs rename to Kavita.Database/Migrations/20210322212724_MangaFileToPages.Designer.cs index f5d2d7ef9..2882f36f9 100644 --- a/API/Data/Migrations/20210322212724_MangaFileToPages.Designer.cs +++ b/Kavita.Database/Migrations/20210322212724_MangaFileToPages.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210322212724_MangaFileToPages")] diff --git a/API/Data/Migrations/20210322212724_MangaFileToPages.cs b/Kavita.Database/Migrations/20210322212724_MangaFileToPages.cs similarity index 94% rename from API/Data/Migrations/20210322212724_MangaFileToPages.cs rename to Kavita.Database/Migrations/20210322212724_MangaFileToPages.cs index 63fecfb72..0430f0c32 100644 --- a/API/Data/Migrations/20210322212724_MangaFileToPages.cs +++ b/Kavita.Database/Migrations/20210322212724_MangaFileToPages.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MangaFileToPages : Migration { diff --git a/API/Data/Migrations/20210323213507_LastModifiedOnMangaFiles.Designer.cs b/Kavita.Database/Migrations/20210323213507_LastModifiedOnMangaFiles.Designer.cs similarity index 99% rename from API/Data/Migrations/20210323213507_LastModifiedOnMangaFiles.Designer.cs rename to Kavita.Database/Migrations/20210323213507_LastModifiedOnMangaFiles.Designer.cs index 1da79f6f7..c1459b34d 100644 --- a/API/Data/Migrations/20210323213507_LastModifiedOnMangaFiles.Designer.cs +++ b/Kavita.Database/Migrations/20210323213507_LastModifiedOnMangaFiles.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210323213507_LastModifiedOnMangaFiles")] diff --git a/API/Data/Migrations/20210323213507_LastModifiedOnMangaFiles.cs b/Kavita.Database/Migrations/20210323213507_LastModifiedOnMangaFiles.cs similarity index 95% rename from API/Data/Migrations/20210323213507_LastModifiedOnMangaFiles.cs rename to Kavita.Database/Migrations/20210323213507_LastModifiedOnMangaFiles.cs index 854498896..7a7ec67d5 100644 --- a/API/Data/Migrations/20210323213507_LastModifiedOnMangaFiles.cs +++ b/Kavita.Database/Migrations/20210323213507_LastModifiedOnMangaFiles.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class LastModifiedOnMangaFiles : Migration { diff --git a/API/Data/Migrations/20210330134414_IsSpecialOnChapters.Designer.cs b/Kavita.Database/Migrations/20210330134414_IsSpecialOnChapters.Designer.cs similarity index 99% rename from API/Data/Migrations/20210330134414_IsSpecialOnChapters.Designer.cs rename to Kavita.Database/Migrations/20210330134414_IsSpecialOnChapters.Designer.cs index 910085fd2..ce2533aad 100644 --- a/API/Data/Migrations/20210330134414_IsSpecialOnChapters.Designer.cs +++ b/Kavita.Database/Migrations/20210330134414_IsSpecialOnChapters.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210330134414_IsSpecialOnChapters")] diff --git a/API/Data/Migrations/20210330134414_IsSpecialOnChapters.cs b/Kavita.Database/Migrations/20210330134414_IsSpecialOnChapters.cs similarity index 94% rename from API/Data/Migrations/20210330134414_IsSpecialOnChapters.cs rename to Kavita.Database/Migrations/20210330134414_IsSpecialOnChapters.cs index 6653a0b77..bc5a5e738 100644 --- a/API/Data/Migrations/20210330134414_IsSpecialOnChapters.cs +++ b/Kavita.Database/Migrations/20210330134414_IsSpecialOnChapters.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class IsSpecialOnChapters : Migration { diff --git a/API/Data/Migrations/20210419222000_BookReaderPreferences.Designer.cs b/Kavita.Database/Migrations/20210419222000_BookReaderPreferences.Designer.cs similarity index 99% rename from API/Data/Migrations/20210419222000_BookReaderPreferences.Designer.cs rename to Kavita.Database/Migrations/20210419222000_BookReaderPreferences.Designer.cs index eb4dd459a..7941f4dba 100644 --- a/API/Data/Migrations/20210419222000_BookReaderPreferences.Designer.cs +++ b/Kavita.Database/Migrations/20210419222000_BookReaderPreferences.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210419222000_BookReaderPreferences")] diff --git a/API/Data/Migrations/20210419222000_BookReaderPreferences.cs b/Kavita.Database/Migrations/20210419222000_BookReaderPreferences.cs similarity index 97% rename from API/Data/Migrations/20210419222000_BookReaderPreferences.cs rename to Kavita.Database/Migrations/20210419222000_BookReaderPreferences.cs index 0dd1089eb..b474815d7 100644 --- a/API/Data/Migrations/20210419222000_BookReaderPreferences.cs +++ b/Kavita.Database/Migrations/20210419222000_BookReaderPreferences.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookReaderPreferences : Migration { diff --git a/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs b/Kavita.Database/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs similarity index 99% rename from API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs rename to Kavita.Database/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs index 95005cf47..462919507 100644 --- a/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs +++ b/Kavita.Database/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210419234652_BookReaderPreferencesFontSize")] diff --git a/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.cs b/Kavita.Database/Migrations/20210419234652_BookReaderPreferencesFontSize.cs similarity index 94% rename from API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.cs rename to Kavita.Database/Migrations/20210419234652_BookReaderPreferencesFontSize.cs index 1745e4f73..e869e46e3 100644 --- a/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.cs +++ b/Kavita.Database/Migrations/20210419234652_BookReaderPreferencesFontSize.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookReaderPreferencesFontSize : Migration { diff --git a/API/Data/Migrations/20210423132900_CustomChapterTitle.Designer.cs b/Kavita.Database/Migrations/20210423132900_CustomChapterTitle.Designer.cs similarity index 99% rename from API/Data/Migrations/20210423132900_CustomChapterTitle.Designer.cs rename to Kavita.Database/Migrations/20210423132900_CustomChapterTitle.Designer.cs index 693480dd3..656cde11f 100644 --- a/API/Data/Migrations/20210423132900_CustomChapterTitle.Designer.cs +++ b/Kavita.Database/Migrations/20210423132900_CustomChapterTitle.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210423132900_CustomChapterTitle")] diff --git a/API/Data/Migrations/20210423132900_CustomChapterTitle.cs b/Kavita.Database/Migrations/20210423132900_CustomChapterTitle.cs similarity index 96% rename from API/Data/Migrations/20210423132900_CustomChapterTitle.cs rename to Kavita.Database/Migrations/20210423132900_CustomChapterTitle.cs index b3958127c..9ccc413c4 100644 --- a/API/Data/Migrations/20210423132900_CustomChapterTitle.cs +++ b/Kavita.Database/Migrations/20210423132900_CustomChapterTitle.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CustomChapterTitle : Migration { diff --git a/API/Data/Migrations/20210504184715_TapToPaginatePref.Designer.cs b/Kavita.Database/Migrations/20210504184715_TapToPaginatePref.Designer.cs similarity index 99% rename from API/Data/Migrations/20210504184715_TapToPaginatePref.Designer.cs rename to Kavita.Database/Migrations/20210504184715_TapToPaginatePref.Designer.cs index 86db800ce..ef255d387 100644 --- a/API/Data/Migrations/20210504184715_TapToPaginatePref.Designer.cs +++ b/Kavita.Database/Migrations/20210504184715_TapToPaginatePref.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210504184715_TapToPaginatePref")] diff --git a/API/Data/Migrations/20210504184715_TapToPaginatePref.cs b/Kavita.Database/Migrations/20210504184715_TapToPaginatePref.cs similarity index 94% rename from API/Data/Migrations/20210504184715_TapToPaginatePref.cs rename to Kavita.Database/Migrations/20210504184715_TapToPaginatePref.cs index c1f86ee4b..76bd386a6 100644 --- a/API/Data/Migrations/20210504184715_TapToPaginatePref.cs +++ b/Kavita.Database/Migrations/20210504184715_TapToPaginatePref.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class TapToPaginatePref : Migration { diff --git a/API/Data/Migrations/20210509014029_SiteDarkModePreference.Designer.cs b/Kavita.Database/Migrations/20210509014029_SiteDarkModePreference.Designer.cs similarity index 99% rename from API/Data/Migrations/20210509014029_SiteDarkModePreference.Designer.cs rename to Kavita.Database/Migrations/20210509014029_SiteDarkModePreference.Designer.cs index a33cd0809..cce558a2c 100644 --- a/API/Data/Migrations/20210509014029_SiteDarkModePreference.Designer.cs +++ b/Kavita.Database/Migrations/20210509014029_SiteDarkModePreference.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210509014029_SiteDarkModePreference")] diff --git a/API/Data/Migrations/20210509014029_SiteDarkModePreference.cs b/Kavita.Database/Migrations/20210509014029_SiteDarkModePreference.cs similarity index 94% rename from API/Data/Migrations/20210509014029_SiteDarkModePreference.cs rename to Kavita.Database/Migrations/20210509014029_SiteDarkModePreference.cs index 863eea564..83c2784f1 100644 --- a/API/Data/Migrations/20210509014029_SiteDarkModePreference.cs +++ b/Kavita.Database/Migrations/20210509014029_SiteDarkModePreference.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SiteDarkModePreference : Migration { diff --git a/API/Data/Migrations/20210519215934_CollectionTag.Designer.cs b/Kavita.Database/Migrations/20210519215934_CollectionTag.Designer.cs similarity index 99% rename from API/Data/Migrations/20210519215934_CollectionTag.Designer.cs rename to Kavita.Database/Migrations/20210519215934_CollectionTag.Designer.cs index 17c4ec353..46bd40f28 100644 --- a/API/Data/Migrations/20210519215934_CollectionTag.Designer.cs +++ b/Kavita.Database/Migrations/20210519215934_CollectionTag.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210519215934_CollectionTag")] diff --git a/API/Data/Migrations/20210519215934_CollectionTag.cs b/Kavita.Database/Migrations/20210519215934_CollectionTag.cs similarity index 98% rename from API/Data/Migrations/20210519215934_CollectionTag.cs rename to Kavita.Database/Migrations/20210519215934_CollectionTag.cs index b95a3bd9b..fdfb836fd 100644 --- a/API/Data/Migrations/20210519215934_CollectionTag.cs +++ b/Kavita.Database/Migrations/20210519215934_CollectionTag.cs @@ -1,8 +1,7 @@ -using API.Entities; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CollectionTag : Migration { diff --git a/API/Data/Migrations/20210528150353_CollectionCoverImage.Designer.cs b/Kavita.Database/Migrations/20210528150353_CollectionCoverImage.Designer.cs similarity index 99% rename from API/Data/Migrations/20210528150353_CollectionCoverImage.Designer.cs rename to Kavita.Database/Migrations/20210528150353_CollectionCoverImage.Designer.cs index b3d4c3d4a..20a22afd9 100644 --- a/API/Data/Migrations/20210528150353_CollectionCoverImage.Designer.cs +++ b/Kavita.Database/Migrations/20210528150353_CollectionCoverImage.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210528150353_CollectionCoverImage")] diff --git a/API/Data/Migrations/20210528150353_CollectionCoverImage.cs b/Kavita.Database/Migrations/20210528150353_CollectionCoverImage.cs similarity index 94% rename from API/Data/Migrations/20210528150353_CollectionCoverImage.cs rename to Kavita.Database/Migrations/20210528150353_CollectionCoverImage.cs index a38f8cf93..8f97e982b 100644 --- a/API/Data/Migrations/20210528150353_CollectionCoverImage.cs +++ b/Kavita.Database/Migrations/20210528150353_CollectionCoverImage.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CollectionCoverImage : Migration { diff --git a/API/Data/Migrations/20210530201541_CollectionSummary.Designer.cs b/Kavita.Database/Migrations/20210530201541_CollectionSummary.Designer.cs similarity index 99% rename from API/Data/Migrations/20210530201541_CollectionSummary.Designer.cs rename to Kavita.Database/Migrations/20210530201541_CollectionSummary.Designer.cs index 9d5507b38..4f03a3e5e 100644 --- a/API/Data/Migrations/20210530201541_CollectionSummary.Designer.cs +++ b/Kavita.Database/Migrations/20210530201541_CollectionSummary.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210530201541_CollectionSummary")] diff --git a/API/Data/Migrations/20210530201541_CollectionSummary.cs b/Kavita.Database/Migrations/20210530201541_CollectionSummary.cs similarity index 94% rename from API/Data/Migrations/20210530201541_CollectionSummary.cs rename to Kavita.Database/Migrations/20210530201541_CollectionSummary.cs index 255ad78f3..321697b59 100644 --- a/API/Data/Migrations/20210530201541_CollectionSummary.cs +++ b/Kavita.Database/Migrations/20210530201541_CollectionSummary.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CollectionSummary : Migration { diff --git a/API/Data/Migrations/20210603133957_BookReadingDirectionPref.Designer.cs b/Kavita.Database/Migrations/20210603133957_BookReadingDirectionPref.Designer.cs similarity index 99% rename from API/Data/Migrations/20210603133957_BookReadingDirectionPref.Designer.cs rename to Kavita.Database/Migrations/20210603133957_BookReadingDirectionPref.Designer.cs index 2ef682c3c..acac8d1a0 100644 --- a/API/Data/Migrations/20210603133957_BookReadingDirectionPref.Designer.cs +++ b/Kavita.Database/Migrations/20210603133957_BookReadingDirectionPref.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210603133957_BookReadingDirectionPref")] diff --git a/API/Data/Migrations/20210603133957_BookReadingDirectionPref.cs b/Kavita.Database/Migrations/20210603133957_BookReadingDirectionPref.cs similarity index 94% rename from API/Data/Migrations/20210603133957_BookReadingDirectionPref.cs rename to Kavita.Database/Migrations/20210603133957_BookReadingDirectionPref.cs index 9f2d9760e..19f527a83 100644 --- a/API/Data/Migrations/20210603133957_BookReadingDirectionPref.cs +++ b/Kavita.Database/Migrations/20210603133957_BookReadingDirectionPref.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookReadingDirectionPref : Migration { diff --git a/API/Data/Migrations/20210603212429_BookScrollIdProgress.Designer.cs b/Kavita.Database/Migrations/20210603212429_BookScrollIdProgress.Designer.cs similarity index 99% rename from API/Data/Migrations/20210603212429_BookScrollIdProgress.Designer.cs rename to Kavita.Database/Migrations/20210603212429_BookScrollIdProgress.Designer.cs index 01a7c07a1..c8bf2fb37 100644 --- a/API/Data/Migrations/20210603212429_BookScrollIdProgress.Designer.cs +++ b/Kavita.Database/Migrations/20210603212429_BookScrollIdProgress.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210603212429_BookScrollIdProgress")] diff --git a/API/Data/Migrations/20210603212429_BookScrollIdProgress.cs b/Kavita.Database/Migrations/20210603212429_BookScrollIdProgress.cs similarity index 94% rename from API/Data/Migrations/20210603212429_BookScrollIdProgress.cs rename to Kavita.Database/Migrations/20210603212429_BookScrollIdProgress.cs index f2be301fe..a841c3890 100644 --- a/API/Data/Migrations/20210603212429_BookScrollIdProgress.cs +++ b/Kavita.Database/Migrations/20210603212429_BookScrollIdProgress.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookScrollIdProgress : Migration { diff --git a/API/Data/Migrations/20210622164318_NewUserPreferences.Designer.cs b/Kavita.Database/Migrations/20210622164318_NewUserPreferences.Designer.cs similarity index 99% rename from API/Data/Migrations/20210622164318_NewUserPreferences.Designer.cs rename to Kavita.Database/Migrations/20210622164318_NewUserPreferences.Designer.cs index 2797f05ab..3eb7f4d62 100644 --- a/API/Data/Migrations/20210622164318_NewUserPreferences.Designer.cs +++ b/Kavita.Database/Migrations/20210622164318_NewUserPreferences.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210622164318_NewUserPreferences")] diff --git a/API/Data/Migrations/20210622164318_NewUserPreferences.cs b/Kavita.Database/Migrations/20210622164318_NewUserPreferences.cs similarity index 96% rename from API/Data/Migrations/20210622164318_NewUserPreferences.cs rename to Kavita.Database/Migrations/20210622164318_NewUserPreferences.cs index bd75d5b2c..f697206c2 100644 --- a/API/Data/Migrations/20210622164318_NewUserPreferences.cs +++ b/Kavita.Database/Migrations/20210622164318_NewUserPreferences.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class NewUserPreferences : Migration { diff --git a/API/Data/Migrations/20210722223304_AddedSeriesFormat.Designer.cs b/Kavita.Database/Migrations/20210722223304_AddedSeriesFormat.Designer.cs similarity index 99% rename from API/Data/Migrations/20210722223304_AddedSeriesFormat.Designer.cs rename to Kavita.Database/Migrations/20210722223304_AddedSeriesFormat.Designer.cs index dff2d3868..df99a1d17 100644 --- a/API/Data/Migrations/20210722223304_AddedSeriesFormat.Designer.cs +++ b/Kavita.Database/Migrations/20210722223304_AddedSeriesFormat.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210722223304_AddedSeriesFormat")] diff --git a/API/Data/Migrations/20210722223304_AddedSeriesFormat.cs b/Kavita.Database/Migrations/20210722223304_AddedSeriesFormat.cs similarity index 97% rename from API/Data/Migrations/20210722223304_AddedSeriesFormat.cs rename to Kavita.Database/Migrations/20210722223304_AddedSeriesFormat.cs index f236b6ec2..1c007b5a6 100644 --- a/API/Data/Migrations/20210722223304_AddedSeriesFormat.cs +++ b/Kavita.Database/Migrations/20210722223304_AddedSeriesFormat.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddedSeriesFormat : Migration { diff --git a/API/Data/Migrations/20210809210326_BookmarkPages.Designer.cs b/Kavita.Database/Migrations/20210809210326_BookmarkPages.Designer.cs similarity index 99% rename from API/Data/Migrations/20210809210326_BookmarkPages.Designer.cs rename to Kavita.Database/Migrations/20210809210326_BookmarkPages.Designer.cs index b339bbd99..eeb3eb7e7 100644 --- a/API/Data/Migrations/20210809210326_BookmarkPages.Designer.cs +++ b/Kavita.Database/Migrations/20210809210326_BookmarkPages.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210809210326_BookmarkPages")] diff --git a/API/Data/Migrations/20210809210326_BookmarkPages.cs b/Kavita.Database/Migrations/20210809210326_BookmarkPages.cs similarity index 97% rename from API/Data/Migrations/20210809210326_BookmarkPages.cs rename to Kavita.Database/Migrations/20210809210326_BookmarkPages.cs index 0ae48eeed..8c2bb29cd 100644 --- a/API/Data/Migrations/20210809210326_BookmarkPages.cs +++ b/Kavita.Database/Migrations/20210809210326_BookmarkPages.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookmarkPages : Migration { diff --git a/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs b/Kavita.Database/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs similarity index 99% rename from API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs rename to Kavita.Database/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs index 991c616fc..64f296a44 100644 --- a/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs +++ b/Kavita.Database/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210813010210_CoverImageLockFieldsPart1")] diff --git a/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.cs b/Kavita.Database/Migrations/20210813010210_CoverImageLockFieldsPart1.cs similarity index 96% rename from API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.cs rename to Kavita.Database/Migrations/20210813010210_CoverImageLockFieldsPart1.cs index 1b04826cd..bd736c5dc 100644 --- a/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.cs +++ b/Kavita.Database/Migrations/20210813010210_CoverImageLockFieldsPart1.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CoverImageLockFieldsPart1 : Migration { diff --git a/API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs b/Kavita.Database/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs similarity index 99% rename from API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs rename to Kavita.Database/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs index a7d6f8afe..e7d1b0adc 100644 --- a/API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs +++ b/Kavita.Database/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210814215831_CoverImageLockedFieldsPart2")] diff --git a/API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.cs b/Kavita.Database/Migrations/20210814215831_CoverImageLockedFieldsPart2.cs similarity index 94% rename from API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.cs rename to Kavita.Database/Migrations/20210814215831_CoverImageLockedFieldsPart2.cs index 1c3ac713a..2d564d49b 100644 --- a/API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.cs +++ b/Kavita.Database/Migrations/20210814215831_CoverImageLockedFieldsPart2.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CoverImageLockedFieldsPart2 : Migration { diff --git a/API/Data/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs b/Kavita.Database/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs similarity index 99% rename from API/Data/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs rename to Kavita.Database/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs index 830e86064..bc54e43c4 100644 --- a/API/Data/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs +++ b/Kavita.Database/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210817152226_ProgressConcurencyCheck")] diff --git a/API/Data/Migrations/20210817152226_ProgressConcurencyCheck.cs b/Kavita.Database/Migrations/20210817152226_ProgressConcurencyCheck.cs similarity index 94% rename from API/Data/Migrations/20210817152226_ProgressConcurencyCheck.cs rename to Kavita.Database/Migrations/20210817152226_ProgressConcurencyCheck.cs index d6ec6aba9..97d366241 100644 --- a/API/Data/Migrations/20210817152226_ProgressConcurencyCheck.cs +++ b/Kavita.Database/Migrations/20210817152226_ProgressConcurencyCheck.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ProgressConcurencyCheck : Migration { diff --git a/API/Data/Migrations/20210826203258_userApiKey.Designer.cs b/Kavita.Database/Migrations/20210826203258_userApiKey.Designer.cs similarity index 99% rename from API/Data/Migrations/20210826203258_userApiKey.Designer.cs rename to Kavita.Database/Migrations/20210826203258_userApiKey.Designer.cs index ece3e3dec..59195994c 100644 --- a/API/Data/Migrations/20210826203258_userApiKey.Designer.cs +++ b/Kavita.Database/Migrations/20210826203258_userApiKey.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210826203258_userApiKey")] diff --git a/API/Data/Migrations/20210826203258_userApiKey.cs b/Kavita.Database/Migrations/20210826203258_userApiKey.cs similarity index 94% rename from API/Data/Migrations/20210826203258_userApiKey.cs rename to Kavita.Database/Migrations/20210826203258_userApiKey.cs index 5f95a253d..283f2dde9 100644 --- a/API/Data/Migrations/20210826203258_userApiKey.cs +++ b/Kavita.Database/Migrations/20210826203258_userApiKey.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class userApiKey : Migration { diff --git a/API/Data/Migrations/20210901150310_ReadingLists.Designer.cs b/Kavita.Database/Migrations/20210901150310_ReadingLists.Designer.cs similarity index 99% rename from API/Data/Migrations/20210901150310_ReadingLists.Designer.cs rename to Kavita.Database/Migrations/20210901150310_ReadingLists.Designer.cs index fef65fdcf..16254d8a1 100644 --- a/API/Data/Migrations/20210901150310_ReadingLists.Designer.cs +++ b/Kavita.Database/Migrations/20210901150310_ReadingLists.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210901150310_ReadingLists")] diff --git a/API/Data/Migrations/20210901150310_ReadingLists.cs b/Kavita.Database/Migrations/20210901150310_ReadingLists.cs similarity index 99% rename from API/Data/Migrations/20210901150310_ReadingLists.cs rename to Kavita.Database/Migrations/20210901150310_ReadingLists.cs index 709d3e17a..4a3b43a7a 100644 --- a/API/Data/Migrations/20210901150310_ReadingLists.cs +++ b/Kavita.Database/Migrations/20210901150310_ReadingLists.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadingLists : Migration { diff --git a/API/Data/Migrations/20210901200442_ReadingListsAdditions.Designer.cs b/Kavita.Database/Migrations/20210901200442_ReadingListsAdditions.Designer.cs similarity index 99% rename from API/Data/Migrations/20210901200442_ReadingListsAdditions.Designer.cs rename to Kavita.Database/Migrations/20210901200442_ReadingListsAdditions.Designer.cs index 8ee5bdec8..b6fec1dee 100644 --- a/API/Data/Migrations/20210901200442_ReadingListsAdditions.Designer.cs +++ b/Kavita.Database/Migrations/20210901200442_ReadingListsAdditions.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210901200442_ReadingListsAdditions")] diff --git a/API/Data/Migrations/20210901200442_ReadingListsAdditions.cs b/Kavita.Database/Migrations/20210901200442_ReadingListsAdditions.cs similarity index 98% rename from API/Data/Migrations/20210901200442_ReadingListsAdditions.cs rename to Kavita.Database/Migrations/20210901200442_ReadingListsAdditions.cs index b44c2ac4d..b9bf473b0 100644 --- a/API/Data/Migrations/20210901200442_ReadingListsAdditions.cs +++ b/Kavita.Database/Migrations/20210901200442_ReadingListsAdditions.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadingListsAdditions : Migration { diff --git a/API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs b/Kavita.Database/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs similarity index 99% rename from API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs rename to Kavita.Database/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs index 566d2c5be..feefa5ea0 100644 --- a/API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs +++ b/Kavita.Database/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210902110705_ReadingListsExtraRealationships")] diff --git a/API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.cs b/Kavita.Database/Migrations/20210902110705_ReadingListsExtraRealationships.cs similarity index 98% rename from API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.cs rename to Kavita.Database/Migrations/20210902110705_ReadingListsExtraRealationships.cs index 9ddb1b5fc..8d5333439 100644 --- a/API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.cs +++ b/Kavita.Database/Migrations/20210902110705_ReadingListsExtraRealationships.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadingListsExtraRealationships : Migration { diff --git a/API/Data/Migrations/20210906140845_ReadingListsChanges.Designer.cs b/Kavita.Database/Migrations/20210906140845_ReadingListsChanges.Designer.cs similarity index 99% rename from API/Data/Migrations/20210906140845_ReadingListsChanges.Designer.cs rename to Kavita.Database/Migrations/20210906140845_ReadingListsChanges.Designer.cs index 836a496e0..f8655884f 100644 --- a/API/Data/Migrations/20210906140845_ReadingListsChanges.Designer.cs +++ b/Kavita.Database/Migrations/20210906140845_ReadingListsChanges.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210906140845_ReadingListsChanges")] diff --git a/API/Data/Migrations/20210906140845_ReadingListsChanges.cs b/Kavita.Database/Migrations/20210906140845_ReadingListsChanges.cs similarity index 97% rename from API/Data/Migrations/20210906140845_ReadingListsChanges.cs rename to Kavita.Database/Migrations/20210906140845_ReadingListsChanges.cs index e4ea07e2e..6a9875afd 100644 --- a/API/Data/Migrations/20210906140845_ReadingListsChanges.cs +++ b/Kavita.Database/Migrations/20210906140845_ReadingListsChanges.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadingListsChanges : Migration { diff --git a/API/Data/Migrations/20210916142418_EntityImageRefactor.Designer.cs b/Kavita.Database/Migrations/20210916142418_EntityImageRefactor.Designer.cs similarity index 99% rename from API/Data/Migrations/20210916142418_EntityImageRefactor.Designer.cs rename to Kavita.Database/Migrations/20210916142418_EntityImageRefactor.Designer.cs index b4c6f62f1..1533122d6 100644 --- a/API/Data/Migrations/20210916142418_EntityImageRefactor.Designer.cs +++ b/Kavita.Database/Migrations/20210916142418_EntityImageRefactor.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210916142418_EntityImageRefactor")] diff --git a/API/Data/Migrations/20210916142418_EntityImageRefactor.cs b/Kavita.Database/Migrations/20210916142418_EntityImageRefactor.cs similarity index 98% rename from API/Data/Migrations/20210916142418_EntityImageRefactor.cs rename to Kavita.Database/Migrations/20210916142418_EntityImageRefactor.cs index deafb134b..93dc4196c 100644 --- a/API/Data/Migrations/20210916142418_EntityImageRefactor.cs +++ b/Kavita.Database/Migrations/20210916142418_EntityImageRefactor.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class EntityImageRefactor : Migration { diff --git a/API/Data/Migrations/20211001113608_LastScannedLibrary.Designer.cs b/Kavita.Database/Migrations/20211001113608_LastScannedLibrary.Designer.cs similarity index 99% rename from API/Data/Migrations/20211001113608_LastScannedLibrary.Designer.cs rename to Kavita.Database/Migrations/20211001113608_LastScannedLibrary.Designer.cs index ad28c5839..2ad96ac03 100644 --- a/API/Data/Migrations/20211001113608_LastScannedLibrary.Designer.cs +++ b/Kavita.Database/Migrations/20211001113608_LastScannedLibrary.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211001113608_LastScannedLibrary")] diff --git a/API/Data/Migrations/20211001113608_LastScannedLibrary.cs b/Kavita.Database/Migrations/20211001113608_LastScannedLibrary.cs similarity index 95% rename from API/Data/Migrations/20211001113608_LastScannedLibrary.cs rename to Kavita.Database/Migrations/20211001113608_LastScannedLibrary.cs index eb1fdc5cb..a0cd62ea3 100644 --- a/API/Data/Migrations/20211001113608_LastScannedLibrary.cs +++ b/Kavita.Database/Migrations/20211001113608_LastScannedLibrary.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class LastScannedLibrary : Migration { diff --git a/API/Data/Migrations/20211127200244_MetadataFoundation.Designer.cs b/Kavita.Database/Migrations/20211127200244_MetadataFoundation.Designer.cs similarity index 99% rename from API/Data/Migrations/20211127200244_MetadataFoundation.Designer.cs rename to Kavita.Database/Migrations/20211127200244_MetadataFoundation.Designer.cs index 32408164b..79ba9f209 100644 --- a/API/Data/Migrations/20211127200244_MetadataFoundation.Designer.cs +++ b/Kavita.Database/Migrations/20211127200244_MetadataFoundation.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211127200244_MetadataFoundation")] diff --git a/API/Data/Migrations/20211127200244_MetadataFoundation.cs b/Kavita.Database/Migrations/20211127200244_MetadataFoundation.cs similarity index 99% rename from API/Data/Migrations/20211127200244_MetadataFoundation.cs rename to Kavita.Database/Migrations/20211127200244_MetadataFoundation.cs index f2ea2c9c1..d7142a62b 100644 --- a/API/Data/Migrations/20211127200244_MetadataFoundation.cs +++ b/Kavita.Database/Migrations/20211127200244_MetadataFoundation.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MetadataFoundation : Migration { diff --git a/API/Data/Migrations/20211129231007_RemoveChapterMetadata.Designer.cs b/Kavita.Database/Migrations/20211129231007_RemoveChapterMetadata.Designer.cs similarity index 99% rename from API/Data/Migrations/20211129231007_RemoveChapterMetadata.Designer.cs rename to Kavita.Database/Migrations/20211129231007_RemoveChapterMetadata.Designer.cs index 27436b91f..5da3a2bfd 100644 --- a/API/Data/Migrations/20211129231007_RemoveChapterMetadata.Designer.cs +++ b/Kavita.Database/Migrations/20211129231007_RemoveChapterMetadata.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211129231007_RemoveChapterMetadata")] diff --git a/API/Data/Migrations/20211129231007_RemoveChapterMetadata.cs b/Kavita.Database/Migrations/20211129231007_RemoveChapterMetadata.cs similarity index 99% rename from API/Data/Migrations/20211129231007_RemoveChapterMetadata.cs rename to Kavita.Database/Migrations/20211129231007_RemoveChapterMetadata.cs index c50578ff9..0691ef064 100644 --- a/API/Data/Migrations/20211129231007_RemoveChapterMetadata.cs +++ b/Kavita.Database/Migrations/20211129231007_RemoveChapterMetadata.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class RemoveChapterMetadata : Migration { diff --git a/API/Data/Migrations/20211130134642_GenreProvider.Designer.cs b/Kavita.Database/Migrations/20211130134642_GenreProvider.Designer.cs similarity index 99% rename from API/Data/Migrations/20211130134642_GenreProvider.Designer.cs rename to Kavita.Database/Migrations/20211130134642_GenreProvider.Designer.cs index 4b90e75ba..ae8d5cda8 100644 --- a/API/Data/Migrations/20211130134642_GenreProvider.Designer.cs +++ b/Kavita.Database/Migrations/20211130134642_GenreProvider.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211130134642_GenreProvider")] diff --git a/API/Data/Migrations/20211130134642_GenreProvider.cs b/Kavita.Database/Migrations/20211130134642_GenreProvider.cs similarity index 98% rename from API/Data/Migrations/20211130134642_GenreProvider.cs rename to Kavita.Database/Migrations/20211130134642_GenreProvider.cs index 260210d54..d7a7ec4cf 100644 --- a/API/Data/Migrations/20211130134642_GenreProvider.cs +++ b/Kavita.Database/Migrations/20211130134642_GenreProvider.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class GenreProvider : Migration { diff --git a/API/Data/Migrations/20211201230003_GenreTitle.Designer.cs b/Kavita.Database/Migrations/20211201230003_GenreTitle.Designer.cs similarity index 99% rename from API/Data/Migrations/20211201230003_GenreTitle.Designer.cs rename to Kavita.Database/Migrations/20211201230003_GenreTitle.Designer.cs index 81f69b5a0..4292249c4 100644 --- a/API/Data/Migrations/20211201230003_GenreTitle.Designer.cs +++ b/Kavita.Database/Migrations/20211201230003_GenreTitle.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211201230003_GenreTitle")] diff --git a/API/Data/Migrations/20211201230003_GenreTitle.cs b/Kavita.Database/Migrations/20211201230003_GenreTitle.cs similarity index 98% rename from API/Data/Migrations/20211201230003_GenreTitle.cs rename to Kavita.Database/Migrations/20211201230003_GenreTitle.cs index ab3e65daf..b5bf17387 100644 --- a/API/Data/Migrations/20211201230003_GenreTitle.cs +++ b/Kavita.Database/Migrations/20211201230003_GenreTitle.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class GenreTitle : Migration { diff --git a/API/Data/Migrations/20211205185207_MetadataAgeRating.Designer.cs b/Kavita.Database/Migrations/20211205185207_MetadataAgeRating.Designer.cs similarity index 99% rename from API/Data/Migrations/20211205185207_MetadataAgeRating.Designer.cs rename to Kavita.Database/Migrations/20211205185207_MetadataAgeRating.Designer.cs index 58704e29d..f8d928a33 100644 --- a/API/Data/Migrations/20211205185207_MetadataAgeRating.Designer.cs +++ b/Kavita.Database/Migrations/20211205185207_MetadataAgeRating.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211205185207_MetadataAgeRating")] diff --git a/API/Data/Migrations/20211205185207_MetadataAgeRating.cs b/Kavita.Database/Migrations/20211205185207_MetadataAgeRating.cs similarity index 94% rename from API/Data/Migrations/20211205185207_MetadataAgeRating.cs rename to Kavita.Database/Migrations/20211205185207_MetadataAgeRating.cs index 8f03753f6..a9baefd12 100644 --- a/API/Data/Migrations/20211205185207_MetadataAgeRating.cs +++ b/Kavita.Database/Migrations/20211205185207_MetadataAgeRating.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MetadataAgeRating : Migration { diff --git a/API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs b/Kavita.Database/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs similarity index 99% rename from API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs rename to Kavita.Database/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs index eade9e871..038c9fd40 100644 --- a/API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs +++ b/Kavita.Database/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211206193225_AgeRatingAndReleaseDate")] diff --git a/API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.cs b/Kavita.Database/Migrations/20211206193225_AgeRatingAndReleaseDate.cs similarity index 97% rename from API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.cs rename to Kavita.Database/Migrations/20211206193225_AgeRatingAndReleaseDate.cs index 76a7f05c6..c05f7a013 100644 --- a/API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.cs +++ b/Kavita.Database/Migrations/20211206193225_AgeRatingAndReleaseDate.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AgeRatingAndReleaseDate : Migration { diff --git a/API/Data/Migrations/20211217013734_BookmarkRefactor.Designer.cs b/Kavita.Database/Migrations/20211217013734_BookmarkRefactor.Designer.cs similarity index 99% rename from API/Data/Migrations/20211217013734_BookmarkRefactor.Designer.cs rename to Kavita.Database/Migrations/20211217013734_BookmarkRefactor.Designer.cs index 5db4111f6..253205078 100644 --- a/API/Data/Migrations/20211217013734_BookmarkRefactor.Designer.cs +++ b/Kavita.Database/Migrations/20211217013734_BookmarkRefactor.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211217013734_BookmarkRefactor")] diff --git a/API/Data/Migrations/20211217013734_BookmarkRefactor.cs b/Kavita.Database/Migrations/20211217013734_BookmarkRefactor.cs similarity index 94% rename from API/Data/Migrations/20211217013734_BookmarkRefactor.cs rename to Kavita.Database/Migrations/20211217013734_BookmarkRefactor.cs index 7ac831e07..e5479e662 100644 --- a/API/Data/Migrations/20211217013734_BookmarkRefactor.cs +++ b/Kavita.Database/Migrations/20211217013734_BookmarkRefactor.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookmarkRefactor : Migration { diff --git a/API/Data/Migrations/20211217180457_filteringChanges.Designer.cs b/Kavita.Database/Migrations/20211217180457_filteringChanges.Designer.cs similarity index 99% rename from API/Data/Migrations/20211217180457_filteringChanges.Designer.cs rename to Kavita.Database/Migrations/20211217180457_filteringChanges.Designer.cs index 39377a6c3..ab6d1fed9 100644 --- a/API/Data/Migrations/20211217180457_filteringChanges.Designer.cs +++ b/Kavita.Database/Migrations/20211217180457_filteringChanges.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211217180457_filteringChanges")] diff --git a/API/Data/Migrations/20211217180457_filteringChanges.cs b/Kavita.Database/Migrations/20211217180457_filteringChanges.cs similarity index 99% rename from API/Data/Migrations/20211217180457_filteringChanges.cs rename to Kavita.Database/Migrations/20211217180457_filteringChanges.cs index 28c4d00b3..4638ed986 100644 --- a/API/Data/Migrations/20211217180457_filteringChanges.cs +++ b/Kavita.Database/Migrations/20211217180457_filteringChanges.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class filteringChanges : Migration { diff --git a/API/Data/Migrations/20211227180752_FullscreenPref.Designer.cs b/Kavita.Database/Migrations/20211227180752_FullscreenPref.Designer.cs similarity index 99% rename from API/Data/Migrations/20211227180752_FullscreenPref.Designer.cs rename to Kavita.Database/Migrations/20211227180752_FullscreenPref.Designer.cs index b649b12b6..4673be8fc 100644 --- a/API/Data/Migrations/20211227180752_FullscreenPref.Designer.cs +++ b/Kavita.Database/Migrations/20211227180752_FullscreenPref.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211227180752_FullscreenPref")] diff --git a/API/Data/Migrations/20211227180752_FullscreenPref.cs b/Kavita.Database/Migrations/20211227180752_FullscreenPref.cs similarity index 94% rename from API/Data/Migrations/20211227180752_FullscreenPref.cs rename to Kavita.Database/Migrations/20211227180752_FullscreenPref.cs index ab6cbc8a8..6a53dd414 100644 --- a/API/Data/Migrations/20211227180752_FullscreenPref.cs +++ b/Kavita.Database/Migrations/20211227180752_FullscreenPref.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class FullscreenPref : Migration { diff --git a/API/Data/Migrations/20220107232822_ChapterMetadataOptimization.Designer.cs b/Kavita.Database/Migrations/20220107232822_ChapterMetadataOptimization.Designer.cs similarity index 99% rename from API/Data/Migrations/20220107232822_ChapterMetadataOptimization.Designer.cs rename to Kavita.Database/Migrations/20220107232822_ChapterMetadataOptimization.Designer.cs index 9df425dbd..2beed06ab 100644 --- a/API/Data/Migrations/20220107232822_ChapterMetadataOptimization.Designer.cs +++ b/Kavita.Database/Migrations/20220107232822_ChapterMetadataOptimization.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220107232822_ChapterMetadataOptimization")] diff --git a/API/Data/Migrations/20220107232822_ChapterMetadataOptimization.cs b/Kavita.Database/Migrations/20220107232822_ChapterMetadataOptimization.cs similarity index 98% rename from API/Data/Migrations/20220107232822_ChapterMetadataOptimization.cs rename to Kavita.Database/Migrations/20220107232822_ChapterMetadataOptimization.cs index 28e874f03..4aaaf387b 100644 --- a/API/Data/Migrations/20220107232822_ChapterMetadataOptimization.cs +++ b/Kavita.Database/Migrations/20220107232822_ChapterMetadataOptimization.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ChapterMetadataOptimization : Migration { diff --git a/API/Data/Migrations/20220108200822_CountMetadata.Designer.cs b/Kavita.Database/Migrations/20220108200822_CountMetadata.Designer.cs similarity index 99% rename from API/Data/Migrations/20220108200822_CountMetadata.Designer.cs rename to Kavita.Database/Migrations/20220108200822_CountMetadata.Designer.cs index 1866b6e58..89da25f6b 100644 --- a/API/Data/Migrations/20220108200822_CountMetadata.Designer.cs +++ b/Kavita.Database/Migrations/20220108200822_CountMetadata.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220108200822_CountMetadata")] diff --git a/API/Data/Migrations/20220108200822_CountMetadata.cs b/Kavita.Database/Migrations/20220108200822_CountMetadata.cs similarity index 96% rename from API/Data/Migrations/20220108200822_CountMetadata.cs rename to Kavita.Database/Migrations/20220108200822_CountMetadata.cs index 98a7f7e11..26ade1010 100644 --- a/API/Data/Migrations/20220108200822_CountMetadata.cs +++ b/Kavita.Database/Migrations/20220108200822_CountMetadata.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CountMetadata : Migration { diff --git a/API/Data/Migrations/20220108202027_PublicationStatus.Designer.cs b/Kavita.Database/Migrations/20220108202027_PublicationStatus.Designer.cs similarity index 99% rename from API/Data/Migrations/20220108202027_PublicationStatus.Designer.cs rename to Kavita.Database/Migrations/20220108202027_PublicationStatus.Designer.cs index 8479775bf..75b370181 100644 --- a/API/Data/Migrations/20220108202027_PublicationStatus.Designer.cs +++ b/Kavita.Database/Migrations/20220108202027_PublicationStatus.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220108202027_PublicationStatus")] diff --git a/API/Data/Migrations/20220108202027_PublicationStatus.cs b/Kavita.Database/Migrations/20220108202027_PublicationStatus.cs similarity index 96% rename from API/Data/Migrations/20220108202027_PublicationStatus.cs rename to Kavita.Database/Migrations/20220108202027_PublicationStatus.cs index a8d676ed0..f3b663546 100644 --- a/API/Data/Migrations/20220108202027_PublicationStatus.cs +++ b/Kavita.Database/Migrations/20220108202027_PublicationStatus.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class PublicationStatus : Migration { diff --git a/API/Data/Migrations/20220215163317_SiteTheme.Designer.cs b/Kavita.Database/Migrations/20220215163317_SiteTheme.Designer.cs similarity index 99% rename from API/Data/Migrations/20220215163317_SiteTheme.Designer.cs rename to Kavita.Database/Migrations/20220215163317_SiteTheme.Designer.cs index 43b538c9a..ebe3668b3 100644 --- a/API/Data/Migrations/20220215163317_SiteTheme.Designer.cs +++ b/Kavita.Database/Migrations/20220215163317_SiteTheme.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220215163317_SiteTheme")] diff --git a/API/Data/Migrations/20220215163317_SiteTheme.cs b/Kavita.Database/Migrations/20220215163317_SiteTheme.cs similarity index 98% rename from API/Data/Migrations/20220215163317_SiteTheme.cs rename to Kavita.Database/Migrations/20220215163317_SiteTheme.cs index e2f519f8b..1ee747ae7 100644 --- a/API/Data/Migrations/20220215163317_SiteTheme.cs +++ b/Kavita.Database/Migrations/20220215163317_SiteTheme.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SiteTheme : Migration { diff --git a/API/Data/Migrations/20220303205301_SeriesLockedFields.Designer.cs b/Kavita.Database/Migrations/20220303205301_SeriesLockedFields.Designer.cs similarity index 99% rename from API/Data/Migrations/20220303205301_SeriesLockedFields.Designer.cs rename to Kavita.Database/Migrations/20220303205301_SeriesLockedFields.Designer.cs index 00fc7a10f..c3e969abc 100644 --- a/API/Data/Migrations/20220303205301_SeriesLockedFields.Designer.cs +++ b/Kavita.Database/Migrations/20220303205301_SeriesLockedFields.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220303205301_SeriesLockedFields")] diff --git a/API/Data/Migrations/20220303205301_SeriesLockedFields.cs b/Kavita.Database/Migrations/20220303205301_SeriesLockedFields.cs similarity index 99% rename from API/Data/Migrations/20220303205301_SeriesLockedFields.cs rename to Kavita.Database/Migrations/20220303205301_SeriesLockedFields.cs index e3903db9e..d8cf5174e 100644 --- a/API/Data/Migrations/20220303205301_SeriesLockedFields.cs +++ b/Kavita.Database/Migrations/20220303205301_SeriesLockedFields.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesLockedFields : Migration { diff --git a/API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs b/Kavita.Database/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs similarity index 99% rename from API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs rename to Kavita.Database/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs index a21ca1e92..ed165d128 100644 --- a/API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs +++ b/Kavita.Database/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220306155456_MangaReaderBackgroundAndLayoutMode")] diff --git a/API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.cs b/Kavita.Database/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.cs similarity index 93% rename from API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.cs rename to Kavita.Database/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.cs index 078e51684..9929e88a7 100644 --- a/API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.cs +++ b/Kavita.Database/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.cs @@ -1,9 +1,9 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MangaReaderBackgroundAndLayoutMode : Migration { diff --git a/API/Data/Migrations/20220307153053_ScreenHints.Designer.cs b/Kavita.Database/Migrations/20220307153053_ScreenHints.Designer.cs similarity index 99% rename from API/Data/Migrations/20220307153053_ScreenHints.Designer.cs rename to Kavita.Database/Migrations/20220307153053_ScreenHints.Designer.cs index f54b0ab0b..975ab51d4 100644 --- a/API/Data/Migrations/20220307153053_ScreenHints.Designer.cs +++ b/Kavita.Database/Migrations/20220307153053_ScreenHints.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220307153053_ScreenHints")] diff --git a/API/Data/Migrations/20220307153053_ScreenHints.cs b/Kavita.Database/Migrations/20220307153053_ScreenHints.cs similarity index 94% rename from API/Data/Migrations/20220307153053_ScreenHints.cs rename to Kavita.Database/Migrations/20220307153053_ScreenHints.cs index 6c7b67ade..a15a82d23 100644 --- a/API/Data/Migrations/20220307153053_ScreenHints.cs +++ b/Kavita.Database/Migrations/20220307153053_ScreenHints.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ScreenHints : Migration { diff --git a/API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs b/Kavita.Database/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs similarity index 99% rename from API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs rename to Kavita.Database/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs index 27d16bfde..9851710a6 100644 --- a/API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs +++ b/Kavita.Database/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220410230540_SeriesLastChapterAddedAndReadingListNormalization")] diff --git a/API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.cs b/Kavita.Database/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.cs similarity index 97% rename from API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.cs rename to Kavita.Database/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.cs index 445895472..b090a9d56 100644 --- a/API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.cs +++ b/Kavita.Database/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesLastChapterAddedAndReadingListNormalization : Migration { diff --git a/API/Data/Migrations/20220416211340_RemoveCustomIndex.Designer.cs b/Kavita.Database/Migrations/20220416211340_RemoveCustomIndex.Designer.cs similarity index 99% rename from API/Data/Migrations/20220416211340_RemoveCustomIndex.Designer.cs rename to Kavita.Database/Migrations/20220416211340_RemoveCustomIndex.Designer.cs index dd2c6ce88..0bc554388 100644 --- a/API/Data/Migrations/20220416211340_RemoveCustomIndex.Designer.cs +++ b/Kavita.Database/Migrations/20220416211340_RemoveCustomIndex.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220416211340_RemoveCustomIndex")] diff --git a/API/Data/Migrations/20220416211340_RemoveCustomIndex.cs b/Kavita.Database/Migrations/20220416211340_RemoveCustomIndex.cs similarity index 95% rename from API/Data/Migrations/20220416211340_RemoveCustomIndex.cs rename to Kavita.Database/Migrations/20220416211340_RemoveCustomIndex.cs index eb60f2349..d5510636e 100644 --- a/API/Data/Migrations/20220416211340_RemoveCustomIndex.cs +++ b/Kavita.Database/Migrations/20220416211340_RemoveCustomIndex.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class RemoveCustomIndex : Migration { diff --git a/API/Data/Migrations/20220421214448_SeriesRelations.Designer.cs b/Kavita.Database/Migrations/20220421214448_SeriesRelations.Designer.cs similarity index 99% rename from API/Data/Migrations/20220421214448_SeriesRelations.Designer.cs rename to Kavita.Database/Migrations/20220421214448_SeriesRelations.Designer.cs index 11937eb15..cf928393f 100644 --- a/API/Data/Migrations/20220421214448_SeriesRelations.Designer.cs +++ b/Kavita.Database/Migrations/20220421214448_SeriesRelations.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220421214448_SeriesRelations")] diff --git a/API/Data/Migrations/20220421214448_SeriesRelations.cs b/Kavita.Database/Migrations/20220421214448_SeriesRelations.cs similarity index 98% rename from API/Data/Migrations/20220421214448_SeriesRelations.cs rename to Kavita.Database/Migrations/20220421214448_SeriesRelations.cs index 1f6d5d7ab..172eeb79a 100644 --- a/API/Data/Migrations/20220421214448_SeriesRelations.cs +++ b/Kavita.Database/Migrations/20220421214448_SeriesRelations.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesRelations : Migration { diff --git a/API/Data/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs b/Kavita.Database/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs similarity index 99% rename from API/Data/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs rename to Kavita.Database/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs index 321cf7056..797771658 100644 --- a/API/Data/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs +++ b/Kavita.Database/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220425125505_ChangeCountToTotalCount")] diff --git a/API/Data/Migrations/20220425125505_ChangeCountToTotalCount.cs b/Kavita.Database/Migrations/20220425125505_ChangeCountToTotalCount.cs similarity index 97% rename from API/Data/Migrations/20220425125505_ChangeCountToTotalCount.cs rename to Kavita.Database/Migrations/20220425125505_ChangeCountToTotalCount.cs index 469430bc7..e132ba583 100644 --- a/API/Data/Migrations/20220425125505_ChangeCountToTotalCount.cs +++ b/Kavita.Database/Migrations/20220425125505_ChangeCountToTotalCount.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ChangeCountToTotalCount : Migration { diff --git a/API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs b/Kavita.Database/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs similarity index 99% rename from API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs rename to Kavita.Database/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs index 0580b7497..73e2adc2e 100644 --- a/API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs +++ b/Kavita.Database/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220425131122_AddMaxCountToSeriesMetadata")] diff --git a/API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.cs b/Kavita.Database/Migrations/20220425131122_AddMaxCountToSeriesMetadata.cs similarity index 94% rename from API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.cs rename to Kavita.Database/Migrations/20220425131122_AddMaxCountToSeriesMetadata.cs index 550dac20b..7c94b2077 100644 --- a/API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.cs +++ b/Kavita.Database/Migrations/20220425131122_AddMaxCountToSeriesMetadata.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddMaxCountToSeriesMetadata : Migration { diff --git a/API/Data/Migrations/20220508162841_BookReaderUpdate.Designer.cs b/Kavita.Database/Migrations/20220508162841_BookReaderUpdate.Designer.cs similarity index 99% rename from API/Data/Migrations/20220508162841_BookReaderUpdate.Designer.cs rename to Kavita.Database/Migrations/20220508162841_BookReaderUpdate.Designer.cs index b8e7c6082..44fe9f673 100644 --- a/API/Data/Migrations/20220508162841_BookReaderUpdate.Designer.cs +++ b/Kavita.Database/Migrations/20220508162841_BookReaderUpdate.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220508162841_BookReaderUpdate")] diff --git a/API/Data/Migrations/20220508162841_BookReaderUpdate.cs b/Kavita.Database/Migrations/20220508162841_BookReaderUpdate.cs similarity index 97% rename from API/Data/Migrations/20220508162841_BookReaderUpdate.cs rename to Kavita.Database/Migrations/20220508162841_BookReaderUpdate.cs index 6df40e5fd..6ffb2757b 100644 --- a/API/Data/Migrations/20220508162841_BookReaderUpdate.cs +++ b/Kavita.Database/Migrations/20220508162841_BookReaderUpdate.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookReaderUpdate : Migration { diff --git a/API/Data/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs b/Kavita.Database/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs similarity index 99% rename from API/Data/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs rename to Kavita.Database/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs index 26c9a1397..67929729d 100644 --- a/API/Data/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs +++ b/Kavita.Database/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220513234708_BookReaderImmersiveMode")] diff --git a/API/Data/Migrations/20220513234708_BookReaderImmersiveMode.cs b/Kavita.Database/Migrations/20220513234708_BookReaderImmersiveMode.cs similarity index 95% rename from API/Data/Migrations/20220513234708_BookReaderImmersiveMode.cs rename to Kavita.Database/Migrations/20220513234708_BookReaderImmersiveMode.cs index f194a3b87..cabd694eb 100644 --- a/API/Data/Migrations/20220513234708_BookReaderImmersiveMode.cs +++ b/Kavita.Database/Migrations/20220513234708_BookReaderImmersiveMode.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookReaderImmersiveMode : Migration { diff --git a/API/Data/Migrations/20220524172543_WordCount.Designer.cs b/Kavita.Database/Migrations/20220524172543_WordCount.Designer.cs similarity index 99% rename from API/Data/Migrations/20220524172543_WordCount.Designer.cs rename to Kavita.Database/Migrations/20220524172543_WordCount.Designer.cs index 04f2b5f38..6d51b6eaf 100644 --- a/API/Data/Migrations/20220524172543_WordCount.Designer.cs +++ b/Kavita.Database/Migrations/20220524172543_WordCount.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220524172543_WordCount")] diff --git a/API/Data/Migrations/20220524172543_WordCount.cs b/Kavita.Database/Migrations/20220524172543_WordCount.cs similarity index 96% rename from API/Data/Migrations/20220524172543_WordCount.cs rename to Kavita.Database/Migrations/20220524172543_WordCount.cs index 2828985b6..fbfcad99b 100644 --- a/API/Data/Migrations/20220524172543_WordCount.cs +++ b/Kavita.Database/Migrations/20220524172543_WordCount.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class WordCount : Migration { diff --git a/API/Data/Migrations/20220610153822_TimeEstimateInDB.Designer.cs b/Kavita.Database/Migrations/20220610153822_TimeEstimateInDB.Designer.cs similarity index 99% rename from API/Data/Migrations/20220610153822_TimeEstimateInDB.Designer.cs rename to Kavita.Database/Migrations/20220610153822_TimeEstimateInDB.Designer.cs index dc5cfc8f2..cbb8d21e8 100644 --- a/API/Data/Migrations/20220610153822_TimeEstimateInDB.Designer.cs +++ b/Kavita.Database/Migrations/20220610153822_TimeEstimateInDB.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220610153822_TimeEstimateInDB")] diff --git a/API/Data/Migrations/20220610153822_TimeEstimateInDB.cs b/Kavita.Database/Migrations/20220610153822_TimeEstimateInDB.cs similarity index 99% rename from API/Data/Migrations/20220610153822_TimeEstimateInDB.cs rename to Kavita.Database/Migrations/20220610153822_TimeEstimateInDB.cs index 9986cc909..7d408a956 100644 --- a/API/Data/Migrations/20220610153822_TimeEstimateInDB.cs +++ b/Kavita.Database/Migrations/20220610153822_TimeEstimateInDB.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class TimeEstimateInDB : Migration { diff --git a/API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs b/Kavita.Database/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs similarity index 99% rename from API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs rename to Kavita.Database/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs index 73e3a675a..28a66f168 100644 --- a/API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs +++ b/Kavita.Database/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220613131125_RenamedBookReaderLayoutMode")] diff --git a/API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.cs b/Kavita.Database/Migrations/20220613131125_RenamedBookReaderLayoutMode.cs similarity index 94% rename from API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.cs rename to Kavita.Database/Migrations/20220613131125_RenamedBookReaderLayoutMode.cs index c7a5c5c13..f4a88a92b 100644 --- a/API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.cs +++ b/Kavita.Database/Migrations/20220613131125_RenamedBookReaderLayoutMode.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class RenamedBookReaderLayoutMode : Migration { diff --git a/API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs b/Kavita.Database/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs similarity index 99% rename from API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs rename to Kavita.Database/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs index 44545d8a6..165a4740e 100644 --- a/API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs +++ b/Kavita.Database/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220613131302_GlobalPageLayoutModeUserSetting")] diff --git a/API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.cs b/Kavita.Database/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.cs similarity index 95% rename from API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.cs rename to Kavita.Database/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.cs index 397f9a734..d8441b8ee 100644 --- a/API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.cs +++ b/Kavita.Database/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class GlobalPageLayoutModeUserSetting : Migration { diff --git a/API/Data/Migrations/20220615190640_LastFileAnalysis.Designer.cs b/Kavita.Database/Migrations/20220615190640_LastFileAnalysis.Designer.cs similarity index 99% rename from API/Data/Migrations/20220615190640_LastFileAnalysis.Designer.cs rename to Kavita.Database/Migrations/20220615190640_LastFileAnalysis.Designer.cs index 4c5a53f7f..c3c402c5f 100644 --- a/API/Data/Migrations/20220615190640_LastFileAnalysis.Designer.cs +++ b/Kavita.Database/Migrations/20220615190640_LastFileAnalysis.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220615190640_LastFileAnalysis")] diff --git a/API/Data/Migrations/20220615190640_LastFileAnalysis.cs b/Kavita.Database/Migrations/20220615190640_LastFileAnalysis.cs similarity index 95% rename from API/Data/Migrations/20220615190640_LastFileAnalysis.cs rename to Kavita.Database/Migrations/20220615190640_LastFileAnalysis.cs index b1fac2ae4..8df889d66 100644 --- a/API/Data/Migrations/20220615190640_LastFileAnalysis.cs +++ b/Kavita.Database/Migrations/20220615190640_LastFileAnalysis.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class LastFileAnalysis : Migration { diff --git a/API/Data/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs b/Kavita.Database/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs similarity index 99% rename from API/Data/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs rename to Kavita.Database/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs index 4aa051023..f1903c8eb 100644 --- a/API/Data/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs +++ b/Kavita.Database/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220625215526_BlurUnreadSummaries")] diff --git a/API/Data/Migrations/20220625215526_BlurUnreadSummaries.cs b/Kavita.Database/Migrations/20220625215526_BlurUnreadSummaries.cs similarity index 94% rename from API/Data/Migrations/20220625215526_BlurUnreadSummaries.cs rename to Kavita.Database/Migrations/20220625215526_BlurUnreadSummaries.cs index 1da6e8d3e..463e50dba 100644 --- a/API/Data/Migrations/20220625215526_BlurUnreadSummaries.cs +++ b/Kavita.Database/Migrations/20220625215526_BlurUnreadSummaries.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BlurUnreadSummaries : Migration { diff --git a/API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs b/Kavita.Database/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs similarity index 99% rename from API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs rename to Kavita.Database/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs index a2eb08e68..a83b64f20 100644 --- a/API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs +++ b/Kavita.Database/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220712161611_PromptForDownloadSizeUserOption")] diff --git a/API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.cs b/Kavita.Database/Migrations/20220712161611_PromptForDownloadSizeUserOption.cs similarity index 95% rename from API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.cs rename to Kavita.Database/Migrations/20220712161611_PromptForDownloadSizeUserOption.cs index 9bd994ed7..5eb9da3d5 100644 --- a/API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.cs +++ b/Kavita.Database/Migrations/20220712161611_PromptForDownloadSizeUserOption.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class PromptForDownloadSizeUserOption : Migration { diff --git a/API/Data/Migrations/20220717145254_UserConfirmationLink.Designer.cs b/Kavita.Database/Migrations/20220717145254_UserConfirmationLink.Designer.cs similarity index 99% rename from API/Data/Migrations/20220717145254_UserConfirmationLink.Designer.cs rename to Kavita.Database/Migrations/20220717145254_UserConfirmationLink.Designer.cs index 14c2500a8..564df3cb4 100644 --- a/API/Data/Migrations/20220717145254_UserConfirmationLink.Designer.cs +++ b/Kavita.Database/Migrations/20220717145254_UserConfirmationLink.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220717145254_UserConfirmationLink")] diff --git a/API/Data/Migrations/20220717145254_UserConfirmationLink.cs b/Kavita.Database/Migrations/20220717145254_UserConfirmationLink.cs similarity index 94% rename from API/Data/Migrations/20220717145254_UserConfirmationLink.cs rename to Kavita.Database/Migrations/20220717145254_UserConfirmationLink.cs index 2ceba5b04..c48e4bfeb 100644 --- a/API/Data/Migrations/20220717145254_UserConfirmationLink.cs +++ b/Kavita.Database/Migrations/20220717145254_UserConfirmationLink.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class UserConfirmationLink : Migration { diff --git a/API/Data/Migrations/20220728193758_WantToReadList.Designer.cs b/Kavita.Database/Migrations/20220728193758_WantToReadList.Designer.cs similarity index 99% rename from API/Data/Migrations/20220728193758_WantToReadList.Designer.cs rename to Kavita.Database/Migrations/20220728193758_WantToReadList.Designer.cs index 989841071..68e592e52 100644 --- a/API/Data/Migrations/20220728193758_WantToReadList.Designer.cs +++ b/Kavita.Database/Migrations/20220728193758_WantToReadList.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220728193758_WantToReadList")] diff --git a/API/Data/Migrations/20220728193758_WantToReadList.cs b/Kavita.Database/Migrations/20220728193758_WantToReadList.cs similarity index 97% rename from API/Data/Migrations/20220728193758_WantToReadList.cs rename to Kavita.Database/Migrations/20220728193758_WantToReadList.cs index 6a3688380..b68a62995 100644 --- a/API/Data/Migrations/20220728193758_WantToReadList.cs +++ b/Kavita.Database/Migrations/20220728193758_WantToReadList.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class WantToReadList : Migration { diff --git a/API/Data/Migrations/20220802222910_BookmarkHasDate.Designer.cs b/Kavita.Database/Migrations/20220802222910_BookmarkHasDate.Designer.cs similarity index 99% rename from API/Data/Migrations/20220802222910_BookmarkHasDate.Designer.cs rename to Kavita.Database/Migrations/20220802222910_BookmarkHasDate.Designer.cs index 7ca5b6beb..141b4d1bb 100644 --- a/API/Data/Migrations/20220802222910_BookmarkHasDate.Designer.cs +++ b/Kavita.Database/Migrations/20220802222910_BookmarkHasDate.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220802222910_BookmarkHasDate")] diff --git a/API/Data/Migrations/20220802222910_BookmarkHasDate.cs b/Kavita.Database/Migrations/20220802222910_BookmarkHasDate.cs similarity index 96% rename from API/Data/Migrations/20220802222910_BookmarkHasDate.cs rename to Kavita.Database/Migrations/20220802222910_BookmarkHasDate.cs index eee40b647..2bb18387e 100644 --- a/API/Data/Migrations/20220802222910_BookmarkHasDate.cs +++ b/Kavita.Database/Migrations/20220802222910_BookmarkHasDate.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookmarkHasDate : Migration { diff --git a/API/Data/Migrations/20220814134725_MangaFileCreatedDate.Designer.cs b/Kavita.Database/Migrations/20220814134725_MangaFileCreatedDate.Designer.cs similarity index 99% rename from API/Data/Migrations/20220814134725_MangaFileCreatedDate.Designer.cs rename to Kavita.Database/Migrations/20220814134725_MangaFileCreatedDate.Designer.cs index 747bfecea..72255b076 100644 --- a/API/Data/Migrations/20220814134725_MangaFileCreatedDate.Designer.cs +++ b/Kavita.Database/Migrations/20220814134725_MangaFileCreatedDate.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220814134725_MangaFileCreatedDate")] diff --git a/API/Data/Migrations/20220814134725_MangaFileCreatedDate.cs b/Kavita.Database/Migrations/20220814134725_MangaFileCreatedDate.cs similarity index 95% rename from API/Data/Migrations/20220814134725_MangaFileCreatedDate.cs rename to Kavita.Database/Migrations/20220814134725_MangaFileCreatedDate.cs index 09aa49746..90d46cc2d 100644 --- a/API/Data/Migrations/20220814134725_MangaFileCreatedDate.cs +++ b/Kavita.Database/Migrations/20220814134725_MangaFileCreatedDate.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MangaFileCreatedDate : Migration { diff --git a/API/Data/Migrations/20220817173731_SeriesFolder.Designer.cs b/Kavita.Database/Migrations/20220817173731_SeriesFolder.Designer.cs similarity index 99% rename from API/Data/Migrations/20220817173731_SeriesFolder.Designer.cs rename to Kavita.Database/Migrations/20220817173731_SeriesFolder.Designer.cs index 96fed7004..08341edb2 100644 --- a/API/Data/Migrations/20220817173731_SeriesFolder.Designer.cs +++ b/Kavita.Database/Migrations/20220817173731_SeriesFolder.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220817173731_SeriesFolder")] diff --git a/API/Data/Migrations/20220817173731_SeriesFolder.cs b/Kavita.Database/Migrations/20220817173731_SeriesFolder.cs similarity index 96% rename from API/Data/Migrations/20220817173731_SeriesFolder.cs rename to Kavita.Database/Migrations/20220817173731_SeriesFolder.cs index 33373c0c4..f1d58f4a7 100644 --- a/API/Data/Migrations/20220817173731_SeriesFolder.cs +++ b/Kavita.Database/Migrations/20220817173731_SeriesFolder.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesFolder : Migration { diff --git a/API/Data/Migrations/20220819223212_NormalizedLocalizedName.Designer.cs b/Kavita.Database/Migrations/20220819223212_NormalizedLocalizedName.Designer.cs similarity index 99% rename from API/Data/Migrations/20220819223212_NormalizedLocalizedName.Designer.cs rename to Kavita.Database/Migrations/20220819223212_NormalizedLocalizedName.Designer.cs index 41bf29e94..7ab149613 100644 --- a/API/Data/Migrations/20220819223212_NormalizedLocalizedName.Designer.cs +++ b/Kavita.Database/Migrations/20220819223212_NormalizedLocalizedName.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220819223212_NormalizedLocalizedName")] diff --git a/API/Data/Migrations/20220819223212_NormalizedLocalizedName.cs b/Kavita.Database/Migrations/20220819223212_NormalizedLocalizedName.cs similarity index 94% rename from API/Data/Migrations/20220819223212_NormalizedLocalizedName.cs rename to Kavita.Database/Migrations/20220819223212_NormalizedLocalizedName.cs index 600a3a6b2..12c52b93e 100644 --- a/API/Data/Migrations/20220819223212_NormalizedLocalizedName.cs +++ b/Kavita.Database/Migrations/20220819223212_NormalizedLocalizedName.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class NormalizedLocalizedName : Migration { diff --git a/API/Data/Migrations/20220921023455_DeviceSupport.Designer.cs b/Kavita.Database/Migrations/20220921023455_DeviceSupport.Designer.cs similarity index 99% rename from API/Data/Migrations/20220921023455_DeviceSupport.Designer.cs rename to Kavita.Database/Migrations/20220921023455_DeviceSupport.Designer.cs index dbf4a0af6..95ccb38b6 100644 --- a/API/Data/Migrations/20220921023455_DeviceSupport.Designer.cs +++ b/Kavita.Database/Migrations/20220921023455_DeviceSupport.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220921023455_DeviceSupport")] diff --git a/API/Data/Migrations/20220921023455_DeviceSupport.cs b/Kavita.Database/Migrations/20220921023455_DeviceSupport.cs similarity index 98% rename from API/Data/Migrations/20220921023455_DeviceSupport.cs rename to Kavita.Database/Migrations/20220921023455_DeviceSupport.cs index 7723daa41..ae8c3d220 100644 --- a/API/Data/Migrations/20220921023455_DeviceSupport.cs +++ b/Kavita.Database/Migrations/20220921023455_DeviceSupport.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class DeviceSupport : Migration { diff --git a/API/Data/Migrations/20220926145902_AddNoTransitions.Designer.cs b/Kavita.Database/Migrations/20220926145902_AddNoTransitions.Designer.cs similarity index 99% rename from API/Data/Migrations/20220926145902_AddNoTransitions.Designer.cs rename to Kavita.Database/Migrations/20220926145902_AddNoTransitions.Designer.cs index af7f8bd07..d811a7b62 100644 --- a/API/Data/Migrations/20220926145902_AddNoTransitions.Designer.cs +++ b/Kavita.Database/Migrations/20220926145902_AddNoTransitions.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220926145902_AddNoTransitions")] diff --git a/API/Data/Migrations/20220926145902_AddNoTransitions.cs b/Kavita.Database/Migrations/20220926145902_AddNoTransitions.cs similarity index 94% rename from API/Data/Migrations/20220926145902_AddNoTransitions.cs rename to Kavita.Database/Migrations/20220926145902_AddNoTransitions.cs index fcef3979a..a14bf84c9 100644 --- a/API/Data/Migrations/20220926145902_AddNoTransitions.cs +++ b/Kavita.Database/Migrations/20220926145902_AddNoTransitions.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddNoTransitions : Migration { diff --git a/API/Data/Migrations/20221006013956_ReleaseYearOnSeriesEdit.Designer.cs b/Kavita.Database/Migrations/20221006013956_ReleaseYearOnSeriesEdit.Designer.cs similarity index 99% rename from API/Data/Migrations/20221006013956_ReleaseYearOnSeriesEdit.Designer.cs rename to Kavita.Database/Migrations/20221006013956_ReleaseYearOnSeriesEdit.Designer.cs index fcc054561..554c699f8 100644 --- a/API/Data/Migrations/20221006013956_ReleaseYearOnSeriesEdit.Designer.cs +++ b/Kavita.Database/Migrations/20221006013956_ReleaseYearOnSeriesEdit.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221006013956_ReleaseYearOnSeriesEdit")] diff --git a/API/Data/Migrations/20221006013956_ReleaseYearOnSeriesEdit.cs b/Kavita.Database/Migrations/20221006013956_ReleaseYearOnSeriesEdit.cs similarity index 94% rename from API/Data/Migrations/20221006013956_ReleaseYearOnSeriesEdit.cs rename to Kavita.Database/Migrations/20221006013956_ReleaseYearOnSeriesEdit.cs index e96557e4e..85b89b11e 100644 --- a/API/Data/Migrations/20221006013956_ReleaseYearOnSeriesEdit.cs +++ b/Kavita.Database/Migrations/20221006013956_ReleaseYearOnSeriesEdit.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReleaseYearOnSeriesEdit : Migration { diff --git a/API/Data/Migrations/20221009172653_ReadingListAgeRating.Designer.cs b/Kavita.Database/Migrations/20221009172653_ReadingListAgeRating.Designer.cs similarity index 99% rename from API/Data/Migrations/20221009172653_ReadingListAgeRating.Designer.cs rename to Kavita.Database/Migrations/20221009172653_ReadingListAgeRating.Designer.cs index f93e7a58d..f297b18a0 100644 --- a/API/Data/Migrations/20221009172653_ReadingListAgeRating.Designer.cs +++ b/Kavita.Database/Migrations/20221009172653_ReadingListAgeRating.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221009172653_ReadingListAgeRating")] diff --git a/API/Data/Migrations/20221009172653_ReadingListAgeRating.cs b/Kavita.Database/Migrations/20221009172653_ReadingListAgeRating.cs similarity index 94% rename from API/Data/Migrations/20221009172653_ReadingListAgeRating.cs rename to Kavita.Database/Migrations/20221009172653_ReadingListAgeRating.cs index dfc69a9cf..9894ae82b 100644 --- a/API/Data/Migrations/20221009172653_ReadingListAgeRating.cs +++ b/Kavita.Database/Migrations/20221009172653_ReadingListAgeRating.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadingListAgeRating : Migration { diff --git a/API/Data/Migrations/20221009211237_UserAgeRating.Designer.cs b/Kavita.Database/Migrations/20221009211237_UserAgeRating.Designer.cs similarity index 99% rename from API/Data/Migrations/20221009211237_UserAgeRating.Designer.cs rename to Kavita.Database/Migrations/20221009211237_UserAgeRating.Designer.cs index 1a9e9fade..77e74d838 100644 --- a/API/Data/Migrations/20221009211237_UserAgeRating.Designer.cs +++ b/Kavita.Database/Migrations/20221009211237_UserAgeRating.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221009211237_UserAgeRating")] diff --git a/API/Data/Migrations/20221009211237_UserAgeRating.cs b/Kavita.Database/Migrations/20221009211237_UserAgeRating.cs similarity index 89% rename from API/Data/Migrations/20221009211237_UserAgeRating.cs rename to Kavita.Database/Migrations/20221009211237_UserAgeRating.cs index a619255ef..12436af4a 100644 --- a/API/Data/Migrations/20221009211237_UserAgeRating.cs +++ b/Kavita.Database/Migrations/20221009211237_UserAgeRating.cs @@ -1,9 +1,9 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class UserAgeRating : Migration { diff --git a/API/Data/Migrations/20221017131711_IncludeUnknowns.Designer.cs b/Kavita.Database/Migrations/20221017131711_IncludeUnknowns.Designer.cs similarity index 99% rename from API/Data/Migrations/20221017131711_IncludeUnknowns.Designer.cs rename to Kavita.Database/Migrations/20221017131711_IncludeUnknowns.Designer.cs index 9ad6b3542..f728792bc 100644 --- a/API/Data/Migrations/20221017131711_IncludeUnknowns.Designer.cs +++ b/Kavita.Database/Migrations/20221017131711_IncludeUnknowns.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221017131711_IncludeUnknowns")] diff --git a/API/Data/Migrations/20221017131711_IncludeUnknowns.cs b/Kavita.Database/Migrations/20221017131711_IncludeUnknowns.cs similarity index 94% rename from API/Data/Migrations/20221017131711_IncludeUnknowns.cs rename to Kavita.Database/Migrations/20221017131711_IncludeUnknowns.cs index 34c0dfd9e..1ea104222 100644 --- a/API/Data/Migrations/20221017131711_IncludeUnknowns.cs +++ b/Kavita.Database/Migrations/20221017131711_IncludeUnknowns.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class IncludeUnknowns : Migration { diff --git a/API/Data/Migrations/20221115021908_SeriesRelationChange.Designer.cs b/Kavita.Database/Migrations/20221115021908_SeriesRelationChange.Designer.cs similarity index 99% rename from API/Data/Migrations/20221115021908_SeriesRelationChange.Designer.cs rename to Kavita.Database/Migrations/20221115021908_SeriesRelationChange.Designer.cs index d9a964ad0..b07980071 100644 --- a/API/Data/Migrations/20221115021908_SeriesRelationChange.Designer.cs +++ b/Kavita.Database/Migrations/20221115021908_SeriesRelationChange.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221115021908_SeriesRelationChange")] diff --git a/API/Data/Migrations/20221115021908_SeriesRelationChange.cs b/Kavita.Database/Migrations/20221115021908_SeriesRelationChange.cs similarity index 98% rename from API/Data/Migrations/20221115021908_SeriesRelationChange.cs rename to Kavita.Database/Migrations/20221115021908_SeriesRelationChange.cs index 83c3fdc60..e73e1b4da 100644 --- a/API/Data/Migrations/20221115021908_SeriesRelationChange.cs +++ b/Kavita.Database/Migrations/20221115021908_SeriesRelationChange.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesRelationChange : Migration { diff --git a/API/Data/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs b/Kavita.Database/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs similarity index 99% rename from API/Data/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs rename to Kavita.Database/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs index e79dddcbc..b83fea2cf 100644 --- a/API/Data/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs +++ b/Kavita.Database/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221118131123_ExtendedLibrarySettings")] diff --git a/API/Data/Migrations/20221118131123_ExtendedLibrarySettings.cs b/Kavita.Database/Migrations/20221118131123_ExtendedLibrarySettings.cs similarity index 97% rename from API/Data/Migrations/20221118131123_ExtendedLibrarySettings.cs rename to Kavita.Database/Migrations/20221118131123_ExtendedLibrarySettings.cs index 1c05b6b5b..0b291ed62 100644 --- a/API/Data/Migrations/20221118131123_ExtendedLibrarySettings.cs +++ b/Kavita.Database/Migrations/20221118131123_ExtendedLibrarySettings.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ExtendedLibrarySettings : Migration { diff --git a/API/Data/Migrations/20221126133824_FileLengthAndExtension.Designer.cs b/Kavita.Database/Migrations/20221126133824_FileLengthAndExtension.Designer.cs similarity index 99% rename from API/Data/Migrations/20221126133824_FileLengthAndExtension.Designer.cs rename to Kavita.Database/Migrations/20221126133824_FileLengthAndExtension.Designer.cs index 17cfe499d..6762bc0e9 100644 --- a/API/Data/Migrations/20221126133824_FileLengthAndExtension.Designer.cs +++ b/Kavita.Database/Migrations/20221126133824_FileLengthAndExtension.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221126133824_FileLengthAndExtension")] diff --git a/API/Data/Migrations/20221126133824_FileLengthAndExtension.cs b/Kavita.Database/Migrations/20221126133824_FileLengthAndExtension.cs similarity index 96% rename from API/Data/Migrations/20221126133824_FileLengthAndExtension.cs rename to Kavita.Database/Migrations/20221126133824_FileLengthAndExtension.cs index d07deaf89..92c45bd00 100644 --- a/API/Data/Migrations/20221126133824_FileLengthAndExtension.cs +++ b/Kavita.Database/Migrations/20221126133824_FileLengthAndExtension.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class FileLengthAndExtension : Migration { diff --git a/API/Data/Migrations/20221128230726_UserProgressLibraryId.Designer.cs b/Kavita.Database/Migrations/20221128230726_UserProgressLibraryId.Designer.cs similarity index 99% rename from API/Data/Migrations/20221128230726_UserProgressLibraryId.Designer.cs rename to Kavita.Database/Migrations/20221128230726_UserProgressLibraryId.Designer.cs index 067f7d486..1957b4c83 100644 --- a/API/Data/Migrations/20221128230726_UserProgressLibraryId.Designer.cs +++ b/Kavita.Database/Migrations/20221128230726_UserProgressLibraryId.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221128230726_UserProgressLibraryId")] diff --git a/API/Data/Migrations/20221128230726_UserProgressLibraryId.cs b/Kavita.Database/Migrations/20221128230726_UserProgressLibraryId.cs similarity index 94% rename from API/Data/Migrations/20221128230726_UserProgressLibraryId.cs rename to Kavita.Database/Migrations/20221128230726_UserProgressLibraryId.cs index 383507825..adb5daad6 100644 --- a/API/Data/Migrations/20221128230726_UserProgressLibraryId.cs +++ b/Kavita.Database/Migrations/20221128230726_UserProgressLibraryId.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class UserProgressLibraryId : Migration { diff --git a/API/Data/Migrations/20221212215914_EmulateBookPref.Designer.cs b/Kavita.Database/Migrations/20221212215914_EmulateBookPref.Designer.cs similarity index 99% rename from API/Data/Migrations/20221212215914_EmulateBookPref.Designer.cs rename to Kavita.Database/Migrations/20221212215914_EmulateBookPref.Designer.cs index 431307ba2..fab4c0d35 100644 --- a/API/Data/Migrations/20221212215914_EmulateBookPref.Designer.cs +++ b/Kavita.Database/Migrations/20221212215914_EmulateBookPref.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221212215914_EmulateBookPref")] diff --git a/API/Data/Migrations/20221212215914_EmulateBookPref.cs b/Kavita.Database/Migrations/20221212215914_EmulateBookPref.cs similarity index 94% rename from API/Data/Migrations/20221212215914_EmulateBookPref.cs rename to Kavita.Database/Migrations/20221212215914_EmulateBookPref.cs index d2883ba0c..baeea39d5 100644 --- a/API/Data/Migrations/20221212215914_EmulateBookPref.cs +++ b/Kavita.Database/Migrations/20221212215914_EmulateBookPref.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class EmulateBookPref : Migration { diff --git a/API/Data/Migrations/20230111014852_YearlyStats.Designer.cs b/Kavita.Database/Migrations/20230111014852_YearlyStats.Designer.cs similarity index 99% rename from API/Data/Migrations/20230111014852_YearlyStats.Designer.cs rename to Kavita.Database/Migrations/20230111014852_YearlyStats.Designer.cs index 2a34ad07b..a4a0d597f 100644 --- a/API/Data/Migrations/20230111014852_YearlyStats.Designer.cs +++ b/Kavita.Database/Migrations/20230111014852_YearlyStats.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230111014852_YearlyStats")] diff --git a/API/Data/Migrations/20230111014852_YearlyStats.cs b/Kavita.Database/Migrations/20230111014852_YearlyStats.cs similarity index 97% rename from API/Data/Migrations/20230111014852_YearlyStats.cs rename to Kavita.Database/Migrations/20230111014852_YearlyStats.cs index c2ec76e3b..58ef17de6 100644 --- a/API/Data/Migrations/20230111014852_YearlyStats.cs +++ b/Kavita.Database/Migrations/20230111014852_YearlyStats.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class YearlyStats : Migration { diff --git a/API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs b/Kavita.Database/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs similarity index 99% rename from API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs rename to Kavita.Database/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs index ea948ab31..5ebcf503d 100644 --- a/API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs +++ b/Kavita.Database/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230129210741_SwipeToPaginatePref")] diff --git a/API/Data/Migrations/20230129210741_SwipeToPaginatePref.cs b/Kavita.Database/Migrations/20230129210741_SwipeToPaginatePref.cs similarity index 94% rename from API/Data/Migrations/20230129210741_SwipeToPaginatePref.cs rename to Kavita.Database/Migrations/20230129210741_SwipeToPaginatePref.cs index 0f99c4c26..e8b7e7d90 100644 --- a/API/Data/Migrations/20230129210741_SwipeToPaginatePref.cs +++ b/Kavita.Database/Migrations/20230129210741_SwipeToPaginatePref.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SwipeToPaginatePref : Migration { diff --git a/API/Data/Migrations/20230130210252_AutoCollections.Designer.cs b/Kavita.Database/Migrations/20230130210252_AutoCollections.Designer.cs similarity index 99% rename from API/Data/Migrations/20230130210252_AutoCollections.Designer.cs rename to Kavita.Database/Migrations/20230130210252_AutoCollections.Designer.cs index 6406e7335..5bc4dc44e 100644 --- a/API/Data/Migrations/20230130210252_AutoCollections.Designer.cs +++ b/Kavita.Database/Migrations/20230130210252_AutoCollections.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230130210252_AutoCollections")] diff --git a/API/Data/Migrations/20230130210252_AutoCollections.cs b/Kavita.Database/Migrations/20230130210252_AutoCollections.cs similarity index 96% rename from API/Data/Migrations/20230130210252_AutoCollections.cs rename to Kavita.Database/Migrations/20230130210252_AutoCollections.cs index 86d2dd3c1..707647539 100644 --- a/API/Data/Migrations/20230130210252_AutoCollections.cs +++ b/Kavita.Database/Migrations/20230130210252_AutoCollections.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AutoCollections : Migration { diff --git a/API/Data/Migrations/20230202182602_ReadingListFields.Designer.cs b/Kavita.Database/Migrations/20230202182602_ReadingListFields.Designer.cs similarity index 99% rename from API/Data/Migrations/20230202182602_ReadingListFields.Designer.cs rename to Kavita.Database/Migrations/20230202182602_ReadingListFields.Designer.cs index 9ccab0a26..e492754b1 100644 --- a/API/Data/Migrations/20230202182602_ReadingListFields.Designer.cs +++ b/Kavita.Database/Migrations/20230202182602_ReadingListFields.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230202182602_ReadingListFields")] diff --git a/API/Data/Migrations/20230202182602_ReadingListFields.cs b/Kavita.Database/Migrations/20230202182602_ReadingListFields.cs similarity index 98% rename from API/Data/Migrations/20230202182602_ReadingListFields.cs rename to Kavita.Database/Migrations/20230202182602_ReadingListFields.cs index b8cc32bd2..26e4efbab 100644 --- a/API/Data/Migrations/20230202182602_ReadingListFields.cs +++ b/Kavita.Database/Migrations/20230202182602_ReadingListFields.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadingListFields : Migration { diff --git a/API/Data/Migrations/20230203112022_RemoveExternalFromTagAndGenre.Designer.cs b/Kavita.Database/Migrations/20230203112022_RemoveExternalFromTagAndGenre.Designer.cs similarity index 99% rename from API/Data/Migrations/20230203112022_RemoveExternalFromTagAndGenre.Designer.cs rename to Kavita.Database/Migrations/20230203112022_RemoveExternalFromTagAndGenre.Designer.cs index 008e9690f..cf041be6b 100644 --- a/API/Data/Migrations/20230203112022_RemoveExternalFromTagAndGenre.Designer.cs +++ b/Kavita.Database/Migrations/20230203112022_RemoveExternalFromTagAndGenre.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230203112022_RemoveExternalFromTagAndGenre")] diff --git a/API/Data/Migrations/20230203112022_RemoveExternalFromTagAndGenre.cs b/Kavita.Database/Migrations/20230203112022_RemoveExternalFromTagAndGenre.cs similarity index 98% rename from API/Data/Migrations/20230203112022_RemoveExternalFromTagAndGenre.cs rename to Kavita.Database/Migrations/20230203112022_RemoveExternalFromTagAndGenre.cs index 44216e4db..96f2a8a1f 100644 --- a/API/Data/Migrations/20230203112022_RemoveExternalFromTagAndGenre.cs +++ b/Kavita.Database/Migrations/20230203112022_RemoveExternalFromTagAndGenre.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class RemoveExternalFromTagAndGenre : Migration { diff --git a/API/Data/Migrations/20230210153842_UtcTimes.Designer.cs b/Kavita.Database/Migrations/20230210153842_UtcTimes.Designer.cs similarity index 99% rename from API/Data/Migrations/20230210153842_UtcTimes.Designer.cs rename to Kavita.Database/Migrations/20230210153842_UtcTimes.Designer.cs index ff9394649..7f2a71b8d 100644 --- a/API/Data/Migrations/20230210153842_UtcTimes.Designer.cs +++ b/Kavita.Database/Migrations/20230210153842_UtcTimes.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230210153842_UtcTimes")] diff --git a/API/Data/Migrations/20230210153842_UtcTimes.cs b/Kavita.Database/Migrations/20230210153842_UtcTimes.cs similarity index 99% rename from API/Data/Migrations/20230210153842_UtcTimes.cs rename to Kavita.Database/Migrations/20230210153842_UtcTimes.cs index 9354cded7..6eaf4906c 100644 --- a/API/Data/Migrations/20230210153842_UtcTimes.cs +++ b/Kavita.Database/Migrations/20230210153842_UtcTimes.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class UtcTimes : Migration { diff --git a/API/Data/Migrations/20230220203128_CollapseSeriesRelationships.Designer.cs b/Kavita.Database/Migrations/20230220203128_CollapseSeriesRelationships.Designer.cs similarity index 99% rename from API/Data/Migrations/20230220203128_CollapseSeriesRelationships.Designer.cs rename to Kavita.Database/Migrations/20230220203128_CollapseSeriesRelationships.Designer.cs index 521ac509f..797767a7b 100644 --- a/API/Data/Migrations/20230220203128_CollapseSeriesRelationships.Designer.cs +++ b/Kavita.Database/Migrations/20230220203128_CollapseSeriesRelationships.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230220203128_CollapseSeriesRelationships")] diff --git a/API/Data/Migrations/20230220203128_CollapseSeriesRelationships.cs b/Kavita.Database/Migrations/20230220203128_CollapseSeriesRelationships.cs similarity index 94% rename from API/Data/Migrations/20230220203128_CollapseSeriesRelationships.cs rename to Kavita.Database/Migrations/20230220203128_CollapseSeriesRelationships.cs index 2e06924cd..ee190814f 100644 --- a/API/Data/Migrations/20230220203128_CollapseSeriesRelationships.cs +++ b/Kavita.Database/Migrations/20230220203128_CollapseSeriesRelationships.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CollapseSeriesRelationships : Migration { diff --git a/API/Data/Migrations/20230304202540_BookWritingStylePref.Designer.cs b/Kavita.Database/Migrations/20230304202540_BookWritingStylePref.Designer.cs similarity index 99% rename from API/Data/Migrations/20230304202540_BookWritingStylePref.Designer.cs rename to Kavita.Database/Migrations/20230304202540_BookWritingStylePref.Designer.cs index 37cc255ae..6d2353347 100644 --- a/API/Data/Migrations/20230304202540_BookWritingStylePref.Designer.cs +++ b/Kavita.Database/Migrations/20230304202540_BookWritingStylePref.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230304202540_BookWritingStylePref")] diff --git a/API/Data/Migrations/20230304202540_BookWritingStylePref.cs b/Kavita.Database/Migrations/20230304202540_BookWritingStylePref.cs similarity index 94% rename from API/Data/Migrations/20230304202540_BookWritingStylePref.cs rename to Kavita.Database/Migrations/20230304202540_BookWritingStylePref.cs index fd6703060..601bce1cb 100644 --- a/API/Data/Migrations/20230304202540_BookWritingStylePref.cs +++ b/Kavita.Database/Migrations/20230304202540_BookWritingStylePref.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookWritingStylePref : Migration { diff --git a/API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs b/Kavita.Database/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs similarity index 99% rename from API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs rename to Kavita.Database/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs index 2edee6323..e7083fee9 100644 --- a/API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs +++ b/Kavita.Database/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230310142630_MoveCollapseSeriesToUserPref")] diff --git a/API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs b/Kavita.Database/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs similarity index 95% rename from API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs rename to Kavita.Database/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs index db5920d0a..a704587d5 100644 --- a/API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs +++ b/Kavita.Database/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class MoveCollapseSeriesToUserPref : Migration diff --git a/API/Data/Migrations/20230313125914_ReadingListDateRange.Designer.cs b/Kavita.Database/Migrations/20230313125914_ReadingListDateRange.Designer.cs similarity index 99% rename from API/Data/Migrations/20230313125914_ReadingListDateRange.Designer.cs rename to Kavita.Database/Migrations/20230313125914_ReadingListDateRange.Designer.cs index 3500a3080..98a916137 100644 --- a/API/Data/Migrations/20230313125914_ReadingListDateRange.Designer.cs +++ b/Kavita.Database/Migrations/20230313125914_ReadingListDateRange.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230313125914_ReadingListDateRange")] diff --git a/API/Data/Migrations/20230313125914_ReadingListDateRange.cs b/Kavita.Database/Migrations/20230313125914_ReadingListDateRange.cs similarity index 98% rename from API/Data/Migrations/20230313125914_ReadingListDateRange.cs rename to Kavita.Database/Migrations/20230313125914_ReadingListDateRange.cs index e4de75aa2..6ecd2b68c 100644 --- a/API/Data/Migrations/20230313125914_ReadingListDateRange.cs +++ b/Kavita.Database/Migrations/20230313125914_ReadingListDateRange.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ReadingListDateRange : Migration diff --git a/API/Data/Migrations/20230316123908_SecurityEvent.Designer.cs b/Kavita.Database/Migrations/20230316123908_SecurityEvent.Designer.cs similarity index 99% rename from API/Data/Migrations/20230316123908_SecurityEvent.Designer.cs rename to Kavita.Database/Migrations/20230316123908_SecurityEvent.Designer.cs index e0c1b3bfb..539ada05b 100644 --- a/API/Data/Migrations/20230316123908_SecurityEvent.Designer.cs +++ b/Kavita.Database/Migrations/20230316123908_SecurityEvent.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230316123908_SecurityEvent")] diff --git a/API/Data/Migrations/20230316123908_SecurityEvent.cs b/Kavita.Database/Migrations/20230316123908_SecurityEvent.cs similarity index 97% rename from API/Data/Migrations/20230316123908_SecurityEvent.cs rename to Kavita.Database/Migrations/20230316123908_SecurityEvent.cs index ec4eab520..477121fbe 100644 --- a/API/Data/Migrations/20230316123908_SecurityEvent.cs +++ b/Kavita.Database/Migrations/20230316123908_SecurityEvent.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SecurityEvent : Migration diff --git a/API/Data/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs b/Kavita.Database/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs similarity index 99% rename from API/Data/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs rename to Kavita.Database/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs index f6da45449..1e051b6e8 100644 --- a/API/Data/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs +++ b/Kavita.Database/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230316233133_RemoveSecurityEvent")] diff --git a/API/Data/Migrations/20230316233133_RemoveSecurityEvent.cs b/Kavita.Database/Migrations/20230316233133_RemoveSecurityEvent.cs similarity index 97% rename from API/Data/Migrations/20230316233133_RemoveSecurityEvent.cs rename to Kavita.Database/Migrations/20230316233133_RemoveSecurityEvent.cs index d0d4c5c73..23e4078a7 100644 --- a/API/Data/Migrations/20230316233133_RemoveSecurityEvent.cs +++ b/Kavita.Database/Migrations/20230316233133_RemoveSecurityEvent.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class RemoveSecurityEvent : Migration diff --git a/API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs b/Kavita.Database/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs similarity index 99% rename from API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs rename to Kavita.Database/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs index 3ef88948b..cd6bc19ea 100644 --- a/API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs +++ b/Kavita.Database/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230415123449_ManageReadingListOnLibrary")] diff --git a/API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.cs b/Kavita.Database/Migrations/20230415123449_ManageReadingListOnLibrary.cs similarity index 95% rename from API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.cs rename to Kavita.Database/Migrations/20230415123449_ManageReadingListOnLibrary.cs index 3c57d3de3..40294f07b 100644 --- a/API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.cs +++ b/Kavita.Database/Migrations/20230415123449_ManageReadingListOnLibrary.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ManageReadingListOnLibrary : Migration diff --git a/API/Data/Migrations/20230505124430_MediaError.Designer.cs b/Kavita.Database/Migrations/20230505124430_MediaError.Designer.cs similarity index 99% rename from API/Data/Migrations/20230505124430_MediaError.Designer.cs rename to Kavita.Database/Migrations/20230505124430_MediaError.Designer.cs index f3e770fa1..f9faaccff 100644 --- a/API/Data/Migrations/20230505124430_MediaError.Designer.cs +++ b/Kavita.Database/Migrations/20230505124430_MediaError.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230505124430_MediaError")] diff --git a/API/Data/Migrations/20230505124430_MediaError.cs b/Kavita.Database/Migrations/20230505124430_MediaError.cs similarity index 97% rename from API/Data/Migrations/20230505124430_MediaError.cs rename to Kavita.Database/Migrations/20230505124430_MediaError.cs index 9bf69d3a2..7b5b04fcc 100644 --- a/API/Data/Migrations/20230505124430_MediaError.cs +++ b/Kavita.Database/Migrations/20230505124430_MediaError.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class MediaError : Migration diff --git a/API/Data/Migrations/20230511165427_WebLinksForChapter.Designer.cs b/Kavita.Database/Migrations/20230511165427_WebLinksForChapter.Designer.cs similarity index 99% rename from API/Data/Migrations/20230511165427_WebLinksForChapter.Designer.cs rename to Kavita.Database/Migrations/20230511165427_WebLinksForChapter.Designer.cs index 0dfd240c1..d64c5a8c2 100644 --- a/API/Data/Migrations/20230511165427_WebLinksForChapter.Designer.cs +++ b/Kavita.Database/Migrations/20230511165427_WebLinksForChapter.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230511165427_WebLinksForChapter")] diff --git a/API/Data/Migrations/20230511165427_WebLinksForChapter.cs b/Kavita.Database/Migrations/20230511165427_WebLinksForChapter.cs similarity index 95% rename from API/Data/Migrations/20230511165427_WebLinksForChapter.cs rename to Kavita.Database/Migrations/20230511165427_WebLinksForChapter.cs index 837117072..7438bb342 100644 --- a/API/Data/Migrations/20230511165427_WebLinksForChapter.cs +++ b/Kavita.Database/Migrations/20230511165427_WebLinksForChapter.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class WebLinksForChapter : Migration diff --git a/API/Data/Migrations/20230511183339_WebLinksForSeries.Designer.cs b/Kavita.Database/Migrations/20230511183339_WebLinksForSeries.Designer.cs similarity index 99% rename from API/Data/Migrations/20230511183339_WebLinksForSeries.Designer.cs rename to Kavita.Database/Migrations/20230511183339_WebLinksForSeries.Designer.cs index 5c6250d34..0d29e738a 100644 --- a/API/Data/Migrations/20230511183339_WebLinksForSeries.Designer.cs +++ b/Kavita.Database/Migrations/20230511183339_WebLinksForSeries.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230511183339_WebLinksForSeries")] diff --git a/API/Data/Migrations/20230511183339_WebLinksForSeries.cs b/Kavita.Database/Migrations/20230511183339_WebLinksForSeries.cs similarity index 95% rename from API/Data/Migrations/20230511183339_WebLinksForSeries.cs rename to Kavita.Database/Migrations/20230511183339_WebLinksForSeries.cs index 01117d2ad..b979adb11 100644 --- a/API/Data/Migrations/20230511183339_WebLinksForSeries.cs +++ b/Kavita.Database/Migrations/20230511183339_WebLinksForSeries.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class WebLinksForSeries : Migration diff --git a/API/Data/Migrations/20230512004545_ChapterISBN.Designer.cs b/Kavita.Database/Migrations/20230512004545_ChapterISBN.Designer.cs similarity index 99% rename from API/Data/Migrations/20230512004545_ChapterISBN.Designer.cs rename to Kavita.Database/Migrations/20230512004545_ChapterISBN.Designer.cs index 8354a7c30..3f2652709 100644 --- a/API/Data/Migrations/20230512004545_ChapterISBN.Designer.cs +++ b/Kavita.Database/Migrations/20230512004545_ChapterISBN.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230512004545_ChapterISBN")] diff --git a/API/Data/Migrations/20230512004545_ChapterISBN.cs b/Kavita.Database/Migrations/20230512004545_ChapterISBN.cs similarity index 95% rename from API/Data/Migrations/20230512004545_ChapterISBN.cs rename to Kavita.Database/Migrations/20230512004545_ChapterISBN.cs index b5d9ea84f..bcd26e1d0 100644 --- a/API/Data/Migrations/20230512004545_ChapterISBN.cs +++ b/Kavita.Database/Migrations/20230512004545_ChapterISBN.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChapterISBN : Migration diff --git a/API/Data/Migrations/20230527215722_LicenseAndScrobble.Designer.cs b/Kavita.Database/Migrations/20230527215722_LicenseAndScrobble.Designer.cs similarity index 99% rename from API/Data/Migrations/20230527215722_LicenseAndScrobble.Designer.cs rename to Kavita.Database/Migrations/20230527215722_LicenseAndScrobble.Designer.cs index d1260ed6c..0efb91b0d 100644 --- a/API/Data/Migrations/20230527215722_LicenseAndScrobble.Designer.cs +++ b/Kavita.Database/Migrations/20230527215722_LicenseAndScrobble.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230527215722_LicenseAndScrobble")] diff --git a/API/Data/Migrations/20230527215722_LicenseAndScrobble.cs b/Kavita.Database/Migrations/20230527215722_LicenseAndScrobble.cs similarity index 99% rename from API/Data/Migrations/20230527215722_LicenseAndScrobble.cs rename to Kavita.Database/Migrations/20230527215722_LicenseAndScrobble.cs index e54f0ade9..fb57b6bee 100644 --- a/API/Data/Migrations/20230527215722_LicenseAndScrobble.cs +++ b/Kavita.Database/Migrations/20230527215722_LicenseAndScrobble.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class LicenseAndScrobble : Migration diff --git a/API/Data/Migrations/20230601172306_ScrobbleErrors.Designer.cs b/Kavita.Database/Migrations/20230601172306_ScrobbleErrors.Designer.cs similarity index 99% rename from API/Data/Migrations/20230601172306_ScrobbleErrors.Designer.cs rename to Kavita.Database/Migrations/20230601172306_ScrobbleErrors.Designer.cs index ddf9f8c6b..75881b18b 100644 --- a/API/Data/Migrations/20230601172306_ScrobbleErrors.Designer.cs +++ b/Kavita.Database/Migrations/20230601172306_ScrobbleErrors.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230601172306_ScrobbleErrors")] diff --git a/API/Data/Migrations/20230601172306_ScrobbleErrors.cs b/Kavita.Database/Migrations/20230601172306_ScrobbleErrors.cs similarity index 98% rename from API/Data/Migrations/20230601172306_ScrobbleErrors.cs rename to Kavita.Database/Migrations/20230601172306_ScrobbleErrors.cs index 22aeae714..94d628ed3 100644 --- a/API/Data/Migrations/20230601172306_ScrobbleErrors.cs +++ b/Kavita.Database/Migrations/20230601172306_ScrobbleErrors.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ScrobbleErrors : Migration diff --git a/API/Data/Migrations/20230612154313_ScrobbleEventProcessed.Designer.cs b/Kavita.Database/Migrations/20230612154313_ScrobbleEventProcessed.Designer.cs similarity index 99% rename from API/Data/Migrations/20230612154313_ScrobbleEventProcessed.Designer.cs rename to Kavita.Database/Migrations/20230612154313_ScrobbleEventProcessed.Designer.cs index ad8d11d07..9defe8546 100644 --- a/API/Data/Migrations/20230612154313_ScrobbleEventProcessed.Designer.cs +++ b/Kavita.Database/Migrations/20230612154313_ScrobbleEventProcessed.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230612154313_ScrobbleEventProcessed")] diff --git a/API/Data/Migrations/20230612154313_ScrobbleEventProcessed.cs b/Kavita.Database/Migrations/20230612154313_ScrobbleEventProcessed.cs similarity index 99% rename from API/Data/Migrations/20230612154313_ScrobbleEventProcessed.cs rename to Kavita.Database/Migrations/20230612154313_ScrobbleEventProcessed.cs index adfa1e1ce..71d499f00 100644 --- a/API/Data/Migrations/20230612154313_ScrobbleEventProcessed.cs +++ b/Kavita.Database/Migrations/20230612154313_ScrobbleEventProcessed.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ScrobbleEventProcessed : Migration diff --git a/API/Data/Migrations/20230615133219_ReviewTaglineAndOptInShares.Designer.cs b/Kavita.Database/Migrations/20230615133219_ReviewTaglineAndOptInShares.Designer.cs similarity index 99% rename from API/Data/Migrations/20230615133219_ReviewTaglineAndOptInShares.Designer.cs rename to Kavita.Database/Migrations/20230615133219_ReviewTaglineAndOptInShares.Designer.cs index 3da312853..eefd873f1 100644 --- a/API/Data/Migrations/20230615133219_ReviewTaglineAndOptInShares.Designer.cs +++ b/Kavita.Database/Migrations/20230615133219_ReviewTaglineAndOptInShares.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230615133219_ReviewTaglineAndOptInShares")] diff --git a/API/Data/Migrations/20230615133219_ReviewTaglineAndOptInShares.cs b/Kavita.Database/Migrations/20230615133219_ReviewTaglineAndOptInShares.cs similarity index 96% rename from API/Data/Migrations/20230615133219_ReviewTaglineAndOptInShares.cs rename to Kavita.Database/Migrations/20230615133219_ReviewTaglineAndOptInShares.cs index 7a44cce97..7b8e6d749 100644 --- a/API/Data/Migrations/20230615133219_ReviewTaglineAndOptInShares.cs +++ b/Kavita.Database/Migrations/20230615133219_ReviewTaglineAndOptInShares.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ReviewTaglineAndOptInShares : Migration diff --git a/API/Data/Migrations/20230618150728_ScrobbleHolds.Designer.cs b/Kavita.Database/Migrations/20230618150728_ScrobbleHolds.Designer.cs similarity index 99% rename from API/Data/Migrations/20230618150728_ScrobbleHolds.Designer.cs rename to Kavita.Database/Migrations/20230618150728_ScrobbleHolds.Designer.cs index 7773f41e0..5f6020e0c 100644 --- a/API/Data/Migrations/20230618150728_ScrobbleHolds.Designer.cs +++ b/Kavita.Database/Migrations/20230618150728_ScrobbleHolds.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230618150728_ScrobbleHolds")] diff --git a/API/Data/Migrations/20230618150728_ScrobbleHolds.cs b/Kavita.Database/Migrations/20230618150728_ScrobbleHolds.cs similarity index 98% rename from API/Data/Migrations/20230618150728_ScrobbleHolds.cs rename to Kavita.Database/Migrations/20230618150728_ScrobbleHolds.cs index 9023376d3..ea8cfa697 100644 --- a/API/Data/Migrations/20230618150728_ScrobbleHolds.cs +++ b/Kavita.Database/Migrations/20230618150728_ScrobbleHolds.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ScrobbleHolds : Migration diff --git a/API/Data/Migrations/20230621211421_RemoveUserLicense.Designer.cs b/Kavita.Database/Migrations/20230621211421_RemoveUserLicense.Designer.cs similarity index 99% rename from API/Data/Migrations/20230621211421_RemoveUserLicense.Designer.cs rename to Kavita.Database/Migrations/20230621211421_RemoveUserLicense.Designer.cs index 5ca2fb3b0..0af796fbc 100644 --- a/API/Data/Migrations/20230621211421_RemoveUserLicense.Designer.cs +++ b/Kavita.Database/Migrations/20230621211421_RemoveUserLicense.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230621211421_RemoveUserLicense")] diff --git a/API/Data/Migrations/20230621211421_RemoveUserLicense.cs b/Kavita.Database/Migrations/20230621211421_RemoveUserLicense.cs similarity index 96% rename from API/Data/Migrations/20230621211421_RemoveUserLicense.cs rename to Kavita.Database/Migrations/20230621211421_RemoveUserLicense.cs index 0c2d19a96..ad2ddb0e4 100644 --- a/API/Data/Migrations/20230621211421_RemoveUserLicense.cs +++ b/Kavita.Database/Migrations/20230621211421_RemoveUserLicense.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class RemoveUserLicense : Migration diff --git a/API/Data/Migrations/20230623192231_ScrobbleReview.Designer.cs b/Kavita.Database/Migrations/20230623192231_ScrobbleReview.Designer.cs similarity index 99% rename from API/Data/Migrations/20230623192231_ScrobbleReview.Designer.cs rename to Kavita.Database/Migrations/20230623192231_ScrobbleReview.Designer.cs index 2dc5d7c09..56cfd7049 100644 --- a/API/Data/Migrations/20230623192231_ScrobbleReview.Designer.cs +++ b/Kavita.Database/Migrations/20230623192231_ScrobbleReview.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230623192231_ScrobbleReview")] diff --git a/API/Data/Migrations/20230623192231_ScrobbleReview.cs b/Kavita.Database/Migrations/20230623192231_ScrobbleReview.cs similarity index 97% rename from API/Data/Migrations/20230623192231_ScrobbleReview.cs rename to Kavita.Database/Migrations/20230623192231_ScrobbleReview.cs index a35e658c2..8e1a98eda 100644 --- a/API/Data/Migrations/20230623192231_ScrobbleReview.cs +++ b/Kavita.Database/Migrations/20230623192231_ScrobbleReview.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ScrobbleReview : Migration diff --git a/API/Data/Migrations/20230715125951_OnDeckRemoval.Designer.cs b/Kavita.Database/Migrations/20230715125951_OnDeckRemoval.Designer.cs similarity index 99% rename from API/Data/Migrations/20230715125951_OnDeckRemoval.Designer.cs rename to Kavita.Database/Migrations/20230715125951_OnDeckRemoval.Designer.cs index 90035e9f0..a469e1ff2 100644 --- a/API/Data/Migrations/20230715125951_OnDeckRemoval.Designer.cs +++ b/Kavita.Database/Migrations/20230715125951_OnDeckRemoval.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230715125951_OnDeckRemoval")] diff --git a/API/Data/Migrations/20230715125951_OnDeckRemoval.cs b/Kavita.Database/Migrations/20230715125951_OnDeckRemoval.cs similarity index 98% rename from API/Data/Migrations/20230715125951_OnDeckRemoval.cs rename to Kavita.Database/Migrations/20230715125951_OnDeckRemoval.cs index 3cc27196f..e417080f4 100644 --- a/API/Data/Migrations/20230715125951_OnDeckRemoval.cs +++ b/Kavita.Database/Migrations/20230715125951_OnDeckRemoval.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class OnDeckRemoval : Migration diff --git a/API/Data/Migrations/20230719173458_PersonalToC.Designer.cs b/Kavita.Database/Migrations/20230719173458_PersonalToC.Designer.cs similarity index 99% rename from API/Data/Migrations/20230719173458_PersonalToC.Designer.cs rename to Kavita.Database/Migrations/20230719173458_PersonalToC.Designer.cs index 50e9ffd61..787a2351e 100644 --- a/API/Data/Migrations/20230719173458_PersonalToC.Designer.cs +++ b/Kavita.Database/Migrations/20230719173458_PersonalToC.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230719173458_PersonalToC")] diff --git a/API/Data/Migrations/20230719173458_PersonalToC.cs b/Kavita.Database/Migrations/20230719173458_PersonalToC.cs similarity index 98% rename from API/Data/Migrations/20230719173458_PersonalToC.cs rename to Kavita.Database/Migrations/20230719173458_PersonalToC.cs index c3eb9e025..ef80e73ca 100644 --- a/API/Data/Migrations/20230719173458_PersonalToC.cs +++ b/Kavita.Database/Migrations/20230719173458_PersonalToC.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PersonalToC : Migration diff --git a/API/Data/Migrations/20230725133536_ChangeRatingScale.Designer.cs b/Kavita.Database/Migrations/20230725133536_ChangeRatingScale.Designer.cs similarity index 99% rename from API/Data/Migrations/20230725133536_ChangeRatingScale.Designer.cs rename to Kavita.Database/Migrations/20230725133536_ChangeRatingScale.Designer.cs index 8b5edb0ff..3dd1472ca 100644 --- a/API/Data/Migrations/20230725133536_ChangeRatingScale.Designer.cs +++ b/Kavita.Database/Migrations/20230725133536_ChangeRatingScale.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230725133536_ChangeRatingScale")] diff --git a/API/Data/Migrations/20230725133536_ChangeRatingScale.cs b/Kavita.Database/Migrations/20230725133536_ChangeRatingScale.cs similarity index 97% rename from API/Data/Migrations/20230725133536_ChangeRatingScale.cs rename to Kavita.Database/Migrations/20230725133536_ChangeRatingScale.cs index 4f97e008b..0e3bb00f5 100644 --- a/API/Data/Migrations/20230725133536_ChangeRatingScale.cs +++ b/Kavita.Database/Migrations/20230725133536_ChangeRatingScale.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChangeRatingScale : Migration diff --git a/API/Data/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs b/Kavita.Database/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs similarity index 99% rename from API/Data/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs rename to Kavita.Database/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs index fb1afbeb9..a1520e27c 100644 --- a/API/Data/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs +++ b/Kavita.Database/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230727175518_AddLocaleOnPrefs")] diff --git a/API/Data/Migrations/20230727175518_AddLocaleOnPrefs.cs b/Kavita.Database/Migrations/20230727175518_AddLocaleOnPrefs.cs similarity index 95% rename from API/Data/Migrations/20230727175518_AddLocaleOnPrefs.cs rename to Kavita.Database/Migrations/20230727175518_AddLocaleOnPrefs.cs index 6b8d01bfe..4aa0ed181 100644 --- a/API/Data/Migrations/20230727175518_AddLocaleOnPrefs.cs +++ b/Kavita.Database/Migrations/20230727175518_AddLocaleOnPrefs.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AddLocaleOnPrefs : Migration diff --git a/API/Data/Migrations/20230904184205_SmartFilters.Designer.cs b/Kavita.Database/Migrations/20230904184205_SmartFilters.Designer.cs similarity index 99% rename from API/Data/Migrations/20230904184205_SmartFilters.Designer.cs rename to Kavita.Database/Migrations/20230904184205_SmartFilters.Designer.cs index 2379ec2ad..dc3368593 100644 --- a/API/Data/Migrations/20230904184205_SmartFilters.Designer.cs +++ b/Kavita.Database/Migrations/20230904184205_SmartFilters.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230904184205_SmartFilters")] diff --git a/API/Data/Migrations/20230904184205_SmartFilters.cs b/Kavita.Database/Migrations/20230904184205_SmartFilters.cs similarity index 97% rename from API/Data/Migrations/20230904184205_SmartFilters.cs rename to Kavita.Database/Migrations/20230904184205_SmartFilters.cs index c902b907b..16c69bcc2 100644 --- a/API/Data/Migrations/20230904184205_SmartFilters.cs +++ b/Kavita.Database/Migrations/20230904184205_SmartFilters.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SmartFilters : Migration diff --git a/API/Data/Migrations/20230908190713_DashboardStream.Designer.cs b/Kavita.Database/Migrations/20230908190713_DashboardStream.Designer.cs similarity index 99% rename from API/Data/Migrations/20230908190713_DashboardStream.Designer.cs rename to Kavita.Database/Migrations/20230908190713_DashboardStream.Designer.cs index 8e436f836..7c3bf5eb0 100644 --- a/API/Data/Migrations/20230908190713_DashboardStream.Designer.cs +++ b/Kavita.Database/Migrations/20230908190713_DashboardStream.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230908190713_DashboardStream")] diff --git a/API/Data/Migrations/20230908190713_DashboardStream.cs b/Kavita.Database/Migrations/20230908190713_DashboardStream.cs similarity index 98% rename from API/Data/Migrations/20230908190713_DashboardStream.cs rename to Kavita.Database/Migrations/20230908190713_DashboardStream.cs index 10826c176..202e4ce5c 100644 --- a/API/Data/Migrations/20230908190713_DashboardStream.cs +++ b/Kavita.Database/Migrations/20230908190713_DashboardStream.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class DashboardStream : Migration diff --git a/API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs b/Kavita.Database/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs similarity index 99% rename from API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs rename to Kavita.Database/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs index 708bcb46e..5a2d349be 100644 --- a/API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs +++ b/Kavita.Database/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20231013194957_SideNavStreamAndExternalSource")] diff --git a/API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.cs b/Kavita.Database/Migrations/20231013194957_SideNavStreamAndExternalSource.cs similarity index 99% rename from API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.cs rename to Kavita.Database/Migrations/20231013194957_SideNavStreamAndExternalSource.cs index b8dd6111e..23bc08437 100644 --- a/API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.cs +++ b/Kavita.Database/Migrations/20231013194957_SideNavStreamAndExternalSource.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SideNavStreamAndExternalSource : Migration diff --git a/API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs b/Kavita.Database/Migrations/20231113215006_LibraryFileTypes.Designer.cs similarity index 99% rename from API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs rename to Kavita.Database/Migrations/20231113215006_LibraryFileTypes.Designer.cs index ec955717c..4c70e9dc6 100644 --- a/API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs +++ b/Kavita.Database/Migrations/20231113215006_LibraryFileTypes.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20231113215006_LibraryFileTypes")] diff --git a/API/Data/Migrations/20231113215006_LibraryFileTypes.cs b/Kavita.Database/Migrations/20231113215006_LibraryFileTypes.cs similarity index 97% rename from API/Data/Migrations/20231113215006_LibraryFileTypes.cs rename to Kavita.Database/Migrations/20231113215006_LibraryFileTypes.cs index 7fed106e7..35f721e67 100644 --- a/API/Data/Migrations/20231113215006_LibraryFileTypes.cs +++ b/Kavita.Database/Migrations/20231113215006_LibraryFileTypes.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class LibraryFileTypes : Migration diff --git a/API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs b/Kavita.Database/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs similarity index 99% rename from API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs rename to Kavita.Database/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs index b53aa8138..086860f75 100644 --- a/API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs +++ b/Kavita.Database/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20231117234829_LibraryExcludePatterns")] diff --git a/API/Data/Migrations/20231117234829_LibraryExcludePatterns.cs b/Kavita.Database/Migrations/20231117234829_LibraryExcludePatterns.cs similarity index 97% rename from API/Data/Migrations/20231117234829_LibraryExcludePatterns.cs rename to Kavita.Database/Migrations/20231117234829_LibraryExcludePatterns.cs index d1dd084f7..4c2bf0d0e 100644 --- a/API/Data/Migrations/20231117234829_LibraryExcludePatterns.cs +++ b/Kavita.Database/Migrations/20231117234829_LibraryExcludePatterns.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class LibraryExcludePatterns : Migration diff --git a/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs b/Kavita.Database/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs similarity index 99% rename from API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs rename to Kavita.Database/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs index e7fdad65e..6c33c0af7 100644 --- a/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs +++ b/Kavita.Database/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240121223643_ExternalSeriesMetadata")] diff --git a/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.cs b/Kavita.Database/Migrations/20240121223643_ExternalSeriesMetadata.cs similarity index 99% rename from API/Data/Migrations/20240121223643_ExternalSeriesMetadata.cs rename to Kavita.Database/Migrations/20240121223643_ExternalSeriesMetadata.cs index 718332b9f..34af2125b 100644 --- a/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.cs +++ b/Kavita.Database/Migrations/20240121223643_ExternalSeriesMetadata.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ExternalSeriesMetadata : Migration diff --git a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs b/Kavita.Database/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs similarity index 99% rename from API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs rename to Kavita.Database/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs index 730b40ec0..60a47bd35 100644 --- a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs +++ b/Kavita.Database/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240128153433_VolumeMinMaxNumbers")] diff --git a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs b/Kavita.Database/Migrations/20240128153433_VolumeMinMaxNumbers.cs similarity index 96% rename from API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs rename to Kavita.Database/Migrations/20240128153433_VolumeMinMaxNumbers.cs index 491fd057f..ceea75856 100644 --- a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs +++ b/Kavita.Database/Migrations/20240128153433_VolumeMinMaxNumbers.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class VolumeMinMaxNumbers : Migration diff --git a/API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs b/Kavita.Database/Migrations/20240130190617_WantToReadFix.Designer.cs similarity index 99% rename from API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs rename to Kavita.Database/Migrations/20240130190617_WantToReadFix.Designer.cs index a4203171c..e0845bb4f 100644 --- a/API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs +++ b/Kavita.Database/Migrations/20240130190617_WantToReadFix.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240130190617_WantToReadFix")] diff --git a/API/Data/Migrations/20240130190617_WantToReadFix.cs b/Kavita.Database/Migrations/20240130190617_WantToReadFix.cs similarity index 99% rename from API/Data/Migrations/20240130190617_WantToReadFix.cs rename to Kavita.Database/Migrations/20240130190617_WantToReadFix.cs index 386160db3..ba0c14c13 100644 --- a/API/Data/Migrations/20240130190617_WantToReadFix.cs +++ b/Kavita.Database/Migrations/20240130190617_WantToReadFix.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class WantToReadFix : Migration diff --git a/API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs b/Kavita.Database/Migrations/20240204141206_BlackListSeries.Designer.cs similarity index 99% rename from API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs rename to Kavita.Database/Migrations/20240204141206_BlackListSeries.Designer.cs index c399f13cc..757286f05 100644 --- a/API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs +++ b/Kavita.Database/Migrations/20240204141206_BlackListSeries.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240204141206_BlackListSeries")] diff --git a/API/Data/Migrations/20240204141206_BlackListSeries.cs b/Kavita.Database/Migrations/20240204141206_BlackListSeries.cs similarity index 98% rename from API/Data/Migrations/20240204141206_BlackListSeries.cs rename to Kavita.Database/Migrations/20240204141206_BlackListSeries.cs index 9e051e5a7..3b3d959aa 100644 --- a/API/Data/Migrations/20240204141206_BlackListSeries.cs +++ b/Kavita.Database/Migrations/20240204141206_BlackListSeries.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class BlackListSeries : Migration diff --git a/API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs b/Kavita.Database/Migrations/20240205184724_ScrobbleEventError.Designer.cs similarity index 99% rename from API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs rename to Kavita.Database/Migrations/20240205184724_ScrobbleEventError.Designer.cs index df5692eb4..e22acda0c 100644 --- a/API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs +++ b/Kavita.Database/Migrations/20240205184724_ScrobbleEventError.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240205184724_ScrobbleEventError")] diff --git a/API/Data/Migrations/20240205184724_ScrobbleEventError.cs b/Kavita.Database/Migrations/20240205184724_ScrobbleEventError.cs similarity index 97% rename from API/Data/Migrations/20240205184724_ScrobbleEventError.cs rename to Kavita.Database/Migrations/20240205184724_ScrobbleEventError.cs index 5c8071b18..c36bad7d5 100644 --- a/API/Data/Migrations/20240205184724_ScrobbleEventError.cs +++ b/Kavita.Database/Migrations/20240205184724_ScrobbleEventError.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ScrobbleEventError : Migration diff --git a/API/Data/Migrations/20240209224347_DBTweaks.Designer.cs b/Kavita.Database/Migrations/20240209224347_DBTweaks.Designer.cs similarity index 99% rename from API/Data/Migrations/20240209224347_DBTweaks.Designer.cs rename to Kavita.Database/Migrations/20240209224347_DBTweaks.Designer.cs index 0afb2e5cb..5a63295f4 100644 --- a/API/Data/Migrations/20240209224347_DBTweaks.Designer.cs +++ b/Kavita.Database/Migrations/20240209224347_DBTweaks.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240209224347_DBTweaks")] diff --git a/API/Data/Migrations/20240209224347_DBTweaks.cs b/Kavita.Database/Migrations/20240209224347_DBTweaks.cs similarity index 95% rename from API/Data/Migrations/20240209224347_DBTweaks.cs rename to Kavita.Database/Migrations/20240209224347_DBTweaks.cs index 797905930..7adaac1d6 100644 --- a/API/Data/Migrations/20240209224347_DBTweaks.cs +++ b/Kavita.Database/Migrations/20240209224347_DBTweaks.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class DBTweaks : Migration diff --git a/API/Data/Migrations/20240214232436_ChapterNumber.Designer.cs b/Kavita.Database/Migrations/20240214232436_ChapterNumber.Designer.cs similarity index 99% rename from API/Data/Migrations/20240214232436_ChapterNumber.Designer.cs rename to Kavita.Database/Migrations/20240214232436_ChapterNumber.Designer.cs index d770ccbbd..3956e0be0 100644 --- a/API/Data/Migrations/20240214232436_ChapterNumber.Designer.cs +++ b/Kavita.Database/Migrations/20240214232436_ChapterNumber.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240214232436_ChapterNumber")] diff --git a/API/Data/Migrations/20240214232436_ChapterNumber.cs b/Kavita.Database/Migrations/20240214232436_ChapterNumber.cs similarity index 96% rename from API/Data/Migrations/20240214232436_ChapterNumber.cs rename to Kavita.Database/Migrations/20240214232436_ChapterNumber.cs index c1e277d58..d6062fedc 100644 --- a/API/Data/Migrations/20240214232436_ChapterNumber.cs +++ b/Kavita.Database/Migrations/20240214232436_ChapterNumber.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChapterNumber : Migration diff --git a/API/Data/Migrations/20240216000223_MangaFileNameTemp.Designer.cs b/Kavita.Database/Migrations/20240216000223_MangaFileNameTemp.Designer.cs similarity index 99% rename from API/Data/Migrations/20240216000223_MangaFileNameTemp.Designer.cs rename to Kavita.Database/Migrations/20240216000223_MangaFileNameTemp.Designer.cs index 7709d9afa..c89e0225c 100644 --- a/API/Data/Migrations/20240216000223_MangaFileNameTemp.Designer.cs +++ b/Kavita.Database/Migrations/20240216000223_MangaFileNameTemp.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240216000223_MangaFileNameTemp")] diff --git a/API/Data/Migrations/20240216000223_MangaFileNameTemp.cs b/Kavita.Database/Migrations/20240216000223_MangaFileNameTemp.cs similarity index 94% rename from API/Data/Migrations/20240216000223_MangaFileNameTemp.cs rename to Kavita.Database/Migrations/20240216000223_MangaFileNameTemp.cs index 8a14c912c..925550971 100644 --- a/API/Data/Migrations/20240216000223_MangaFileNameTemp.cs +++ b/Kavita.Database/Migrations/20240216000223_MangaFileNameTemp.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class MangaFileNameTemp : Migration diff --git a/API/Data/Migrations/20240222125420_ChapterIssueSort.Designer.cs b/Kavita.Database/Migrations/20240222125420_ChapterIssueSort.Designer.cs similarity index 99% rename from API/Data/Migrations/20240222125420_ChapterIssueSort.Designer.cs rename to Kavita.Database/Migrations/20240222125420_ChapterIssueSort.Designer.cs index 68c1a12e5..3f600cc76 100644 --- a/API/Data/Migrations/20240222125420_ChapterIssueSort.Designer.cs +++ b/Kavita.Database/Migrations/20240222125420_ChapterIssueSort.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240222125420_ChapterIssueSort")] diff --git a/API/Data/Migrations/20240222125420_ChapterIssueSort.cs b/Kavita.Database/Migrations/20240222125420_ChapterIssueSort.cs similarity index 95% rename from API/Data/Migrations/20240222125420_ChapterIssueSort.cs rename to Kavita.Database/Migrations/20240222125420_ChapterIssueSort.cs index 0689a8e88..23e1aca2d 100644 --- a/API/Data/Migrations/20240222125420_ChapterIssueSort.cs +++ b/Kavita.Database/Migrations/20240222125420_ChapterIssueSort.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChapterIssueSort : Migration diff --git a/API/Data/Migrations/20240225235816_VolumeLookupName.Designer.cs b/Kavita.Database/Migrations/20240225235816_VolumeLookupName.Designer.cs similarity index 99% rename from API/Data/Migrations/20240225235816_VolumeLookupName.Designer.cs rename to Kavita.Database/Migrations/20240225235816_VolumeLookupName.Designer.cs index c7f646f73..cbb63d0e7 100644 --- a/API/Data/Migrations/20240225235816_VolumeLookupName.Designer.cs +++ b/Kavita.Database/Migrations/20240225235816_VolumeLookupName.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240225235816_VolumeLookupName")] diff --git a/API/Data/Migrations/20240225235816_VolumeLookupName.cs b/Kavita.Database/Migrations/20240225235816_VolumeLookupName.cs similarity index 94% rename from API/Data/Migrations/20240225235816_VolumeLookupName.cs rename to Kavita.Database/Migrations/20240225235816_VolumeLookupName.cs index 3d42e9645..85bae38be 100644 --- a/API/Data/Migrations/20240225235816_VolumeLookupName.cs +++ b/Kavita.Database/Migrations/20240225235816_VolumeLookupName.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class VolumeLookupName : Migration diff --git a/API/Data/Migrations/20240309140117_SeriesImprints.Designer.cs b/Kavita.Database/Migrations/20240309140117_SeriesImprints.Designer.cs similarity index 99% rename from API/Data/Migrations/20240309140117_SeriesImprints.Designer.cs rename to Kavita.Database/Migrations/20240309140117_SeriesImprints.Designer.cs index d99650e86..e4f08d6aa 100644 --- a/API/Data/Migrations/20240309140117_SeriesImprints.Designer.cs +++ b/Kavita.Database/Migrations/20240309140117_SeriesImprints.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240309140117_SeriesImprints")] diff --git a/API/Data/Migrations/20240309140117_SeriesImprints.cs b/Kavita.Database/Migrations/20240309140117_SeriesImprints.cs similarity index 95% rename from API/Data/Migrations/20240309140117_SeriesImprints.cs rename to Kavita.Database/Migrations/20240309140117_SeriesImprints.cs index a48ac7c48..c81f7b331 100644 --- a/API/Data/Migrations/20240309140117_SeriesImprints.cs +++ b/Kavita.Database/Migrations/20240309140117_SeriesImprints.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SeriesImprints : Migration diff --git a/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs b/Kavita.Database/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs similarity index 99% rename from API/Data/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs rename to Kavita.Database/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs index 707d6ea0a..f852fdedb 100644 --- a/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs +++ b/Kavita.Database/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240313112552_SeriesLowestFolderPath")] diff --git a/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.cs b/Kavita.Database/Migrations/20240313112552_SeriesLowestFolderPath.cs similarity index 95% rename from API/Data/Migrations/20240313112552_SeriesLowestFolderPath.cs rename to Kavita.Database/Migrations/20240313112552_SeriesLowestFolderPath.cs index e138bd8f1..ead8ac337 100644 --- a/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.cs +++ b/Kavita.Database/Migrations/20240313112552_SeriesLowestFolderPath.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SeriesLowestFolderPath : Migration diff --git a/API/Data/Migrations/20240314194402_TeamsAndLocations.Designer.cs b/Kavita.Database/Migrations/20240314194402_TeamsAndLocations.Designer.cs similarity index 99% rename from API/Data/Migrations/20240314194402_TeamsAndLocations.Designer.cs rename to Kavita.Database/Migrations/20240314194402_TeamsAndLocations.Designer.cs index 21616f684..da8dc68f5 100644 --- a/API/Data/Migrations/20240314194402_TeamsAndLocations.Designer.cs +++ b/Kavita.Database/Migrations/20240314194402_TeamsAndLocations.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240314194402_TeamsAndLocations")] diff --git a/API/Data/Migrations/20240314194402_TeamsAndLocations.cs b/Kavita.Database/Migrations/20240314194402_TeamsAndLocations.cs similarity index 96% rename from API/Data/Migrations/20240314194402_TeamsAndLocations.cs rename to Kavita.Database/Migrations/20240314194402_TeamsAndLocations.cs index dca377c99..7f96dd655 100644 --- a/API/Data/Migrations/20240314194402_TeamsAndLocations.cs +++ b/Kavita.Database/Migrations/20240314194402_TeamsAndLocations.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class TeamsAndLocations : Migration diff --git a/API/Data/Migrations/20240321173812_UserMalToken.Designer.cs b/Kavita.Database/Migrations/20240321173812_UserMalToken.Designer.cs similarity index 99% rename from API/Data/Migrations/20240321173812_UserMalToken.Designer.cs rename to Kavita.Database/Migrations/20240321173812_UserMalToken.Designer.cs index ee182676d..aac2b7c5d 100644 --- a/API/Data/Migrations/20240321173812_UserMalToken.Designer.cs +++ b/Kavita.Database/Migrations/20240321173812_UserMalToken.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240321173812_UserMalToken")] diff --git a/API/Data/Migrations/20240321173812_UserMalToken.cs b/Kavita.Database/Migrations/20240321173812_UserMalToken.cs similarity index 96% rename from API/Data/Migrations/20240321173812_UserMalToken.cs rename to Kavita.Database/Migrations/20240321173812_UserMalToken.cs index f1b1d3caa..37810eae8 100644 --- a/API/Data/Migrations/20240321173812_UserMalToken.cs +++ b/Kavita.Database/Migrations/20240321173812_UserMalToken.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class UserMalToken : Migration diff --git a/API/Data/Migrations/20240328130057_PdfSettings.Designer.cs b/Kavita.Database/Migrations/20240328130057_PdfSettings.Designer.cs similarity index 99% rename from API/Data/Migrations/20240328130057_PdfSettings.Designer.cs rename to Kavita.Database/Migrations/20240328130057_PdfSettings.Designer.cs index cba2d534f..a11582887 100644 --- a/API/Data/Migrations/20240328130057_PdfSettings.Designer.cs +++ b/Kavita.Database/Migrations/20240328130057_PdfSettings.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240328130057_PdfSettings")] diff --git a/API/Data/Migrations/20240328130057_PdfSettings.cs b/Kavita.Database/Migrations/20240328130057_PdfSettings.cs similarity index 97% rename from API/Data/Migrations/20240328130057_PdfSettings.cs rename to Kavita.Database/Migrations/20240328130057_PdfSettings.cs index 699875968..a0a33bbfb 100644 --- a/API/Data/Migrations/20240328130057_PdfSettings.cs +++ b/Kavita.Database/Migrations/20240328130057_PdfSettings.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PdfSettings : Migration diff --git a/API/Data/Migrations/20240331172900_UserBasedCollections.Designer.cs b/Kavita.Database/Migrations/20240331172900_UserBasedCollections.Designer.cs similarity index 99% rename from API/Data/Migrations/20240331172900_UserBasedCollections.Designer.cs rename to Kavita.Database/Migrations/20240331172900_UserBasedCollections.Designer.cs index 5527a0fbb..4a17378af 100644 --- a/API/Data/Migrations/20240331172900_UserBasedCollections.Designer.cs +++ b/Kavita.Database/Migrations/20240331172900_UserBasedCollections.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240331172900_UserBasedCollections")] diff --git a/API/Data/Migrations/20240331172900_UserBasedCollections.cs b/Kavita.Database/Migrations/20240331172900_UserBasedCollections.cs similarity index 99% rename from API/Data/Migrations/20240331172900_UserBasedCollections.cs rename to Kavita.Database/Migrations/20240331172900_UserBasedCollections.cs index c5a376bd8..163c351bd 100644 --- a/API/Data/Migrations/20240331172900_UserBasedCollections.cs +++ b/Kavita.Database/Migrations/20240331172900_UserBasedCollections.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class UserBasedCollections : Migration diff --git a/API/Data/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs b/Kavita.Database/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs similarity index 99% rename from API/Data/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs rename to Kavita.Database/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs index 3cd3291b2..a65466722 100644 --- a/API/Data/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs +++ b/Kavita.Database/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240418163829_ChapterSortOrderLock")] diff --git a/API/Data/Migrations/20240418163829_ChapterSortOrderLock.cs b/Kavita.Database/Migrations/20240418163829_ChapterSortOrderLock.cs similarity index 96% rename from API/Data/Migrations/20240418163829_ChapterSortOrderLock.cs rename to Kavita.Database/Migrations/20240418163829_ChapterSortOrderLock.cs index 197085b0c..69579c2f0 100644 --- a/API/Data/Migrations/20240418163829_ChapterSortOrderLock.cs +++ b/Kavita.Database/Migrations/20240418163829_ChapterSortOrderLock.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChapterSortOrderLock : Migration diff --git a/API/Data/Migrations/20240503120147_SmartCollectionFields.Designer.cs b/Kavita.Database/Migrations/20240503120147_SmartCollectionFields.Designer.cs similarity index 99% rename from API/Data/Migrations/20240503120147_SmartCollectionFields.Designer.cs rename to Kavita.Database/Migrations/20240503120147_SmartCollectionFields.Designer.cs index 1dff0c0e5..3bd473401 100644 --- a/API/Data/Migrations/20240503120147_SmartCollectionFields.Designer.cs +++ b/Kavita.Database/Migrations/20240503120147_SmartCollectionFields.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240503120147_SmartCollectionFields")] diff --git a/API/Data/Migrations/20240503120147_SmartCollectionFields.cs b/Kavita.Database/Migrations/20240503120147_SmartCollectionFields.cs similarity index 96% rename from API/Data/Migrations/20240503120147_SmartCollectionFields.cs rename to Kavita.Database/Migrations/20240503120147_SmartCollectionFields.cs index f0b6ed693..c4d2f7228 100644 --- a/API/Data/Migrations/20240503120147_SmartCollectionFields.cs +++ b/Kavita.Database/Migrations/20240503120147_SmartCollectionFields.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SmartCollectionFields : Migration diff --git a/API/Data/Migrations/20240510134030_SiteThemeFields.Designer.cs b/Kavita.Database/Migrations/20240510134030_SiteThemeFields.Designer.cs similarity index 99% rename from API/Data/Migrations/20240510134030_SiteThemeFields.Designer.cs rename to Kavita.Database/Migrations/20240510134030_SiteThemeFields.Designer.cs index c88a1628f..1f6e57714 100644 --- a/API/Data/Migrations/20240510134030_SiteThemeFields.Designer.cs +++ b/Kavita.Database/Migrations/20240510134030_SiteThemeFields.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240510134030_SiteThemeFields")] diff --git a/API/Data/Migrations/20240510134030_SiteThemeFields.cs b/Kavita.Database/Migrations/20240510134030_SiteThemeFields.cs similarity index 98% rename from API/Data/Migrations/20240510134030_SiteThemeFields.cs rename to Kavita.Database/Migrations/20240510134030_SiteThemeFields.cs index 36171fa0a..8f91d66ea 100644 --- a/API/Data/Migrations/20240510134030_SiteThemeFields.cs +++ b/Kavita.Database/Migrations/20240510134030_SiteThemeFields.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SiteThemeFields : Migration diff --git a/API/Data/Migrations/20240704144224_PersonFields.Designer.cs b/Kavita.Database/Migrations/20240704144224_PersonFields.Designer.cs similarity index 99% rename from API/Data/Migrations/20240704144224_PersonFields.Designer.cs rename to Kavita.Database/Migrations/20240704144224_PersonFields.Designer.cs index ddc41d811..ce6ddc6da 100644 --- a/API/Data/Migrations/20240704144224_PersonFields.Designer.cs +++ b/Kavita.Database/Migrations/20240704144224_PersonFields.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240704144224_PersonFields")] diff --git a/API/Data/Migrations/20240704144224_PersonFields.cs b/Kavita.Database/Migrations/20240704144224_PersonFields.cs similarity index 98% rename from API/Data/Migrations/20240704144224_PersonFields.cs rename to Kavita.Database/Migrations/20240704144224_PersonFields.cs index 2d30696ce..5c93e753b 100644 --- a/API/Data/Migrations/20240704144224_PersonFields.cs +++ b/Kavita.Database/Migrations/20240704144224_PersonFields.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PersonFields : Migration diff --git a/API/Data/Migrations/20240808100353_CoverPrimaryColors.Designer.cs b/Kavita.Database/Migrations/20240808100353_CoverPrimaryColors.Designer.cs similarity index 99% rename from API/Data/Migrations/20240808100353_CoverPrimaryColors.Designer.cs rename to Kavita.Database/Migrations/20240808100353_CoverPrimaryColors.Designer.cs index d105ece92..27d8a89ea 100644 --- a/API/Data/Migrations/20240808100353_CoverPrimaryColors.Designer.cs +++ b/Kavita.Database/Migrations/20240808100353_CoverPrimaryColors.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240808100353_CoverPrimaryColors")] diff --git a/API/Data/Migrations/20240808100353_CoverPrimaryColors.cs b/Kavita.Database/Migrations/20240808100353_CoverPrimaryColors.cs similarity index 99% rename from API/Data/Migrations/20240808100353_CoverPrimaryColors.cs rename to Kavita.Database/Migrations/20240808100353_CoverPrimaryColors.cs index c69c906b0..d4f29ff81 100644 --- a/API/Data/Migrations/20240808100353_CoverPrimaryColors.cs +++ b/Kavita.Database/Migrations/20240808100353_CoverPrimaryColors.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class CoverPrimaryColors : Migration diff --git a/API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs b/Kavita.Database/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs similarity index 99% rename from API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs rename to Kavita.Database/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs index 07723e833..6cb6a9202 100644 --- a/API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs +++ b/Kavita.Database/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240811154857_ChapterMetadataLocks")] diff --git a/API/Data/Migrations/20240811154857_ChapterMetadataLocks.cs b/Kavita.Database/Migrations/20240811154857_ChapterMetadataLocks.cs similarity index 99% rename from API/Data/Migrations/20240811154857_ChapterMetadataLocks.cs rename to Kavita.Database/Migrations/20240811154857_ChapterMetadataLocks.cs index b0b58b3b3..219cce059 100644 --- a/API/Data/Migrations/20240811154857_ChapterMetadataLocks.cs +++ b/Kavita.Database/Migrations/20240811154857_ChapterMetadataLocks.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChapterMetadataLocks : Migration diff --git a/API/Data/Migrations/20240813194728_VolumeCoverLocked.Designer.cs b/Kavita.Database/Migrations/20240813194728_VolumeCoverLocked.Designer.cs similarity index 99% rename from API/Data/Migrations/20240813194728_VolumeCoverLocked.Designer.cs rename to Kavita.Database/Migrations/20240813194728_VolumeCoverLocked.Designer.cs index 1471c1de7..9567f8ad6 100644 --- a/API/Data/Migrations/20240813194728_VolumeCoverLocked.Designer.cs +++ b/Kavita.Database/Migrations/20240813194728_VolumeCoverLocked.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240813194728_VolumeCoverLocked")] diff --git a/API/Data/Migrations/20240813194728_VolumeCoverLocked.cs b/Kavita.Database/Migrations/20240813194728_VolumeCoverLocked.cs similarity index 95% rename from API/Data/Migrations/20240813194728_VolumeCoverLocked.cs rename to Kavita.Database/Migrations/20240813194728_VolumeCoverLocked.cs index c9127ae6a..2bcef093b 100644 --- a/API/Data/Migrations/20240813194728_VolumeCoverLocked.cs +++ b/Kavita.Database/Migrations/20240813194728_VolumeCoverLocked.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class VolumeCoverLocked : Migration diff --git a/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs b/Kavita.Database/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs similarity index 99% rename from API/Data/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs rename to Kavita.Database/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs index f9b858de5..1d34279bb 100644 --- a/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs +++ b/Kavita.Database/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240917180034_AvgReadingTimeFloat")] diff --git a/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.cs b/Kavita.Database/Migrations/20240917180034_AvgReadingTimeFloat.cs similarity index 98% rename from API/Data/Migrations/20240917180034_AvgReadingTimeFloat.cs rename to Kavita.Database/Migrations/20240917180034_AvgReadingTimeFloat.cs index 70e9238ec..ae8ce5b48 100644 --- a/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.cs +++ b/Kavita.Database/Migrations/20240917180034_AvgReadingTimeFloat.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AvgReadingTimeFloat : Migration diff --git a/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs b/Kavita.Database/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs similarity index 99% rename from API/Data/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs rename to Kavita.Database/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs index 3865e6007..cd4aeaeac 100644 --- a/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs +++ b/Kavita.Database/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20241011143144_PeopleOverhaulPart1")] diff --git a/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.cs b/Kavita.Database/Migrations/20241011143144_PeopleOverhaulPart1.cs similarity index 99% rename from API/Data/Migrations/20241011143144_PeopleOverhaulPart1.cs rename to Kavita.Database/Migrations/20241011143144_PeopleOverhaulPart1.cs index 1bf0cf6c4..d83f25f06 100644 --- a/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.cs +++ b/Kavita.Database/Migrations/20241011143144_PeopleOverhaulPart1.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PeopleOverhaulPart1 : Migration diff --git a/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs b/Kavita.Database/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs similarity index 99% rename from API/Data/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs rename to Kavita.Database/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs index bbbf0f989..6205bf3f2 100644 --- a/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs +++ b/Kavita.Database/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20241011152321_PeopleOverhaulPart2")] diff --git a/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.cs b/Kavita.Database/Migrations/20241011152321_PeopleOverhaulPart2.cs similarity index 97% rename from API/Data/Migrations/20241011152321_PeopleOverhaulPart2.cs rename to Kavita.Database/Migrations/20241011152321_PeopleOverhaulPart2.cs index 4fd8e4b8d..caf1fc873 100644 --- a/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.cs +++ b/Kavita.Database/Migrations/20241011152321_PeopleOverhaulPart2.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PeopleOverhaulPart2 : Migration diff --git a/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs b/Kavita.Database/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs similarity index 99% rename from API/Data/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs rename to Kavita.Database/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs index 6f76df92c..a8062c1d1 100644 --- a/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs +++ b/Kavita.Database/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20241011172428_PeopleOverhaulPart3")] diff --git a/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.cs b/Kavita.Database/Migrations/20241011172428_PeopleOverhaulPart3.cs similarity index 98% rename from API/Data/Migrations/20241011172428_PeopleOverhaulPart3.cs rename to Kavita.Database/Migrations/20241011172428_PeopleOverhaulPart3.cs index 13aa9e050..8f1b55171 100644 --- a/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.cs +++ b/Kavita.Database/Migrations/20241011172428_PeopleOverhaulPart3.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PeopleOverhaulPart3 : Migration diff --git a/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs b/Kavita.Database/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs similarity index 99% rename from API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs rename to Kavita.Database/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs index a5158ebc1..7bda1a134 100644 --- a/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs +++ b/Kavita.Database/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250105180131_SeriesDontMatchAndBlacklist")] diff --git a/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs b/Kavita.Database/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs similarity index 96% rename from API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs rename to Kavita.Database/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs index ab80f0621..9e1a7c6ec 100644 --- a/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs +++ b/Kavita.Database/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SeriesDontMatchAndBlacklist : Migration diff --git a/API/Data/Migrations/20250109173537_EmailHistory.Designer.cs b/Kavita.Database/Migrations/20250109173537_EmailHistory.Designer.cs similarity index 99% rename from API/Data/Migrations/20250109173537_EmailHistory.Designer.cs rename to Kavita.Database/Migrations/20250109173537_EmailHistory.Designer.cs index ff3212562..69cfdba3a 100644 --- a/API/Data/Migrations/20250109173537_EmailHistory.Designer.cs +++ b/Kavita.Database/Migrations/20250109173537_EmailHistory.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250109173537_EmailHistory")] diff --git a/API/Data/Migrations/20250109173537_EmailHistory.cs b/Kavita.Database/Migrations/20250109173537_EmailHistory.cs similarity index 98% rename from API/Data/Migrations/20250109173537_EmailHistory.cs rename to Kavita.Database/Migrations/20250109173537_EmailHistory.cs index b31bf20c3..57b4a7125 100644 --- a/API/Data/Migrations/20250109173537_EmailHistory.cs +++ b/Kavita.Database/Migrations/20250109173537_EmailHistory.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class EmailHistory : Migration diff --git a/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs b/Kavita.Database/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs similarity index 99% rename from API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs rename to Kavita.Database/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs index 835510a1e..b2ec168a6 100644 --- a/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs +++ b/Kavita.Database/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250202163454_KavitaPlusUserAndMetadataSettings")] diff --git a/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs b/Kavita.Database/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs similarity index 99% rename from API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs rename to Kavita.Database/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs index b23d7896b..657da1dbf 100644 --- a/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs +++ b/Kavita.Database/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class KavitaPlusUserAndMetadataSettings : Migration diff --git a/API/Data/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs b/Kavita.Database/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs similarity index 99% rename from API/Data/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs rename to Kavita.Database/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs index 9aaa63101..6a7b99290 100644 --- a/API/Data/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs +++ b/Kavita.Database/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250208200843_MoreMetadtaSettings")] diff --git a/API/Data/Migrations/20250208200843_MoreMetadtaSettings.cs b/Kavita.Database/Migrations/20250208200843_MoreMetadtaSettings.cs similarity index 98% rename from API/Data/Migrations/20250208200843_MoreMetadtaSettings.cs rename to Kavita.Database/Migrations/20250208200843_MoreMetadtaSettings.cs index 70e42cd11..6aecb7d71 100644 --- a/API/Data/Migrations/20250208200843_MoreMetadtaSettings.cs +++ b/Kavita.Database/Migrations/20250208200843_MoreMetadtaSettings.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class MoreMetadtaSettings : Migration diff --git a/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs b/Kavita.Database/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs similarity index 99% rename from API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs rename to Kavita.Database/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs index be3d5e3f9..fcf3d7e0c 100644 --- a/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs +++ b/Kavita.Database/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250328125012_AutomaticWebtoonReaderMode")] diff --git a/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs b/Kavita.Database/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs similarity index 95% rename from API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs rename to Kavita.Database/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs index 38b772811..508c6f0f8 100644 --- a/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs +++ b/Kavita.Database/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AutomaticWebtoonReaderMode : Migration diff --git a/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs b/Kavita.Database/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs similarity index 99% rename from API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs rename to Kavita.Database/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs index 53e450b3b..2163133b5 100644 --- a/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs +++ b/Kavita.Database/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250408222330_ScrobbleGenerationDbCapture")] diff --git a/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs b/Kavita.Database/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs similarity index 97% rename from API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs rename to Kavita.Database/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs index 7431a7338..cfb63e61e 100644 --- a/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs +++ b/Kavita.Database/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ScrobbleGenerationDbCapture : Migration diff --git a/API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs b/Kavita.Database/Migrations/20250415194829_KavitaPlusCBR.Designer.cs similarity index 99% rename from API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs rename to Kavita.Database/Migrations/20250415194829_KavitaPlusCBR.Designer.cs index fd287c085..c2ba1a5e6 100644 --- a/API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs +++ b/Kavita.Database/Migrations/20250415194829_KavitaPlusCBR.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250415194829_KavitaPlusCBR")] diff --git a/API/Data/Migrations/20250415194829_KavitaPlusCBR.cs b/Kavita.Database/Migrations/20250415194829_KavitaPlusCBR.cs similarity index 98% rename from API/Data/Migrations/20250415194829_KavitaPlusCBR.cs rename to Kavita.Database/Migrations/20250415194829_KavitaPlusCBR.cs index 188969476..0a52c1cba 100644 --- a/API/Data/Migrations/20250415194829_KavitaPlusCBR.cs +++ b/Kavita.Database/Migrations/20250415194829_KavitaPlusCBR.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class KavitaPlusCBR : Migration diff --git a/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs b/Kavita.Database/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs similarity index 99% rename from API/Data/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs rename to Kavita.Database/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs index 52e2c4a86..75e0aa81f 100644 --- a/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs +++ b/Kavita.Database/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250429150140_ChapterRatingAndReviews")] diff --git a/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.cs b/Kavita.Database/Migrations/20250429150140_ChapterRatingAndReviews.cs similarity index 99% rename from API/Data/Migrations/20250429150140_ChapterRatingAndReviews.cs rename to Kavita.Database/Migrations/20250429150140_ChapterRatingAndReviews.cs index 5ab51aaba..6ed3f7e97 100644 --- a/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.cs +++ b/Kavita.Database/Migrations/20250429150140_ChapterRatingAndReviews.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChapterRatingAndReviews : Migration diff --git a/API/Data/Migrations/20250507221026_PersonAliases.Designer.cs b/Kavita.Database/Migrations/20250507221026_PersonAliases.Designer.cs similarity index 99% rename from API/Data/Migrations/20250507221026_PersonAliases.Designer.cs rename to Kavita.Database/Migrations/20250507221026_PersonAliases.Designer.cs index 5d76571e1..b2cc5e41f 100644 --- a/API/Data/Migrations/20250507221026_PersonAliases.Designer.cs +++ b/Kavita.Database/Migrations/20250507221026_PersonAliases.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250507221026_PersonAliases")] diff --git a/API/Data/Migrations/20250507221026_PersonAliases.cs b/Kavita.Database/Migrations/20250507221026_PersonAliases.cs similarity index 97% rename from API/Data/Migrations/20250507221026_PersonAliases.cs rename to Kavita.Database/Migrations/20250507221026_PersonAliases.cs index cb046a131..33c212b97 100644 --- a/API/Data/Migrations/20250507221026_PersonAliases.cs +++ b/Kavita.Database/Migrations/20250507221026_PersonAliases.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PersonAliases : Migration diff --git a/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs b/Kavita.Database/Migrations/20250519151126_KoreaderHash.Designer.cs similarity index 99% rename from API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs rename to Kavita.Database/Migrations/20250519151126_KoreaderHash.Designer.cs index 79f6f9504..0409544a8 100644 --- a/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs +++ b/Kavita.Database/Migrations/20250519151126_KoreaderHash.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250519151126_KoreaderHash")] diff --git a/API/Data/Migrations/20250519151126_KoreaderHash.cs b/Kavita.Database/Migrations/20250519151126_KoreaderHash.cs similarity index 94% rename from API/Data/Migrations/20250519151126_KoreaderHash.cs rename to Kavita.Database/Migrations/20250519151126_KoreaderHash.cs index 006070b72..1d3c5ee18 100644 --- a/API/Data/Migrations/20250519151126_KoreaderHash.cs +++ b/Kavita.Database/Migrations/20250519151126_KoreaderHash.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class KoreaderHash : Migration diff --git a/API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs b/Kavita.Database/Migrations/20250601200056_ReadingProfiles.Designer.cs similarity index 99% rename from API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs rename to Kavita.Database/Migrations/20250601200056_ReadingProfiles.Designer.cs index 762eae142..9f8689fd3 100644 --- a/API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs +++ b/Kavita.Database/Migrations/20250601200056_ReadingProfiles.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250601200056_ReadingProfiles")] diff --git a/API/Data/Migrations/20250601200056_ReadingProfiles.cs b/Kavita.Database/Migrations/20250601200056_ReadingProfiles.cs similarity index 99% rename from API/Data/Migrations/20250601200056_ReadingProfiles.cs rename to Kavita.Database/Migrations/20250601200056_ReadingProfiles.cs index 66b9e53e5..334bec60e 100644 --- a/API/Data/Migrations/20250601200056_ReadingProfiles.cs +++ b/Kavita.Database/Migrations/20250601200056_ReadingProfiles.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ReadingProfiles : Migration diff --git a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs b/Kavita.Database/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs similarity index 99% rename from API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs rename to Kavita.Database/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs index 0e9f00b4e..3b99719ad 100644 --- a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs +++ b/Kavita.Database/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint")] diff --git a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs b/Kavita.Database/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs similarity index 95% rename from API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs rename to Kavita.Database/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs index 11a554bdf..80840d33f 100644 --- a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs +++ b/Kavita.Database/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AppUserReadingProfileDisableWidthOverrideBreakPoint : Migration diff --git a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs b/Kavita.Database/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs similarity index 99% rename from API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs rename to Kavita.Database/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs index c15f9f77b..3afdd6073 100644 --- a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs +++ b/Kavita.Database/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250620215058_EnableMetadataLibrary")] diff --git a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs b/Kavita.Database/Migrations/20250620215058_EnableMetadataLibrary.cs similarity index 95% rename from API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs rename to Kavita.Database/Migrations/20250620215058_EnableMetadataLibrary.cs index f9e38c01d..b51119aea 100644 --- a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs +++ b/Kavita.Database/Migrations/20250620215058_EnableMetadataLibrary.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class EnableMetadataLibrary : Migration diff --git a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs b/Kavita.Database/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs similarity index 99% rename from API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs rename to Kavita.Database/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs index b72239924..bbb86cfd0 100644 --- a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs +++ b/Kavita.Database/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250626162548_TrackKavitaPlusMetadata")] diff --git a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.cs b/Kavita.Database/Migrations/20250626162548_TrackKavitaPlusMetadata.cs similarity index 96% rename from API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.cs rename to Kavita.Database/Migrations/20250626162548_TrackKavitaPlusMetadata.cs index ac253e0a8..80f3faf29 100644 --- a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.cs +++ b/Kavita.Database/Migrations/20250626162548_TrackKavitaPlusMetadata.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class TrackKavitaPlusMetadata : Migration diff --git a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs b/Kavita.Database/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs similarity index 99% rename from API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs rename to Kavita.Database/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs index 165663f3d..3712f6e60 100644 --- a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs +++ b/Kavita.Database/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250629153840_LibraryRemoveSortPrefix")] diff --git a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs b/Kavita.Database/Migrations/20250629153840_LibraryRemoveSortPrefix.cs similarity index 95% rename from API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs rename to Kavita.Database/Migrations/20250629153840_LibraryRemoveSortPrefix.cs index 4800cf3fa..4d2b4ccfa 100644 --- a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs +++ b/Kavita.Database/Migrations/20250629153840_LibraryRemoveSortPrefix.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class LibraryRemoveSortPrefix : Migration diff --git a/API/Data/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.Designer.cs b/Kavita.Database/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.Designer.cs similarity index 99% rename from API/Data/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.Designer.cs rename to Kavita.Database/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.Designer.cs index fe8bfb231..5d0d16fe7 100644 --- a/API/Data/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.Designer.cs +++ b/Kavita.Database/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250727185204_AddEnableExtendedMetadataProcessing")] diff --git a/API/Data/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.cs b/Kavita.Database/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.cs similarity index 95% rename from API/Data/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.cs rename to Kavita.Database/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.cs index 6a35bcbdd..1292d1aea 100644 --- a/API/Data/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.cs +++ b/Kavita.Database/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AddEnableExtendedMetadataProcessing : Migration diff --git a/API/Data/Migrations/20250802103258_OpenIDConnect.Designer.cs b/Kavita.Database/Migrations/20250802103258_OpenIDConnect.Designer.cs similarity index 99% rename from API/Data/Migrations/20250802103258_OpenIDConnect.Designer.cs rename to Kavita.Database/Migrations/20250802103258_OpenIDConnect.Designer.cs index e49b83a9b..0575aff14 100644 --- a/API/Data/Migrations/20250802103258_OpenIDConnect.Designer.cs +++ b/Kavita.Database/Migrations/20250802103258_OpenIDConnect.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250802103258_OpenIDConnect")] diff --git a/API/Data/Migrations/20250802103258_OpenIDConnect.cs b/Kavita.Database/Migrations/20250802103258_OpenIDConnect.cs similarity index 96% rename from API/Data/Migrations/20250802103258_OpenIDConnect.cs rename to Kavita.Database/Migrations/20250802103258_OpenIDConnect.cs index 0bad34851..37ceb58ce 100644 --- a/API/Data/Migrations/20250802103258_OpenIDConnect.cs +++ b/Kavita.Database/Migrations/20250802103258_OpenIDConnect.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class OpenIDConnect : Migration diff --git a/API/Data/Migrations/20250820150458_BookAnnotations.Designer.cs b/Kavita.Database/Migrations/20250820150458_BookAnnotations.Designer.cs similarity index 99% rename from API/Data/Migrations/20250820150458_BookAnnotations.Designer.cs rename to Kavita.Database/Migrations/20250820150458_BookAnnotations.Designer.cs index a8822d149..9a8e5ae43 100644 --- a/API/Data/Migrations/20250820150458_BookAnnotations.Designer.cs +++ b/Kavita.Database/Migrations/20250820150458_BookAnnotations.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250820150458_BookAnnotations")] diff --git a/API/Data/Migrations/20250820150458_BookAnnotations.cs b/Kavita.Database/Migrations/20250820150458_BookAnnotations.cs similarity index 99% rename from API/Data/Migrations/20250820150458_BookAnnotations.cs rename to Kavita.Database/Migrations/20250820150458_BookAnnotations.cs index ac0e88f8e..315690627 100644 --- a/API/Data/Migrations/20250820150458_BookAnnotations.cs +++ b/Kavita.Database/Migrations/20250820150458_BookAnnotations.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class BookAnnotations : Migration diff --git a/API/Data/Migrations/20250919114119_ColorScapeSetting.Designer.cs b/Kavita.Database/Migrations/20250919114119_ColorScapeSetting.Designer.cs similarity index 99% rename from API/Data/Migrations/20250919114119_ColorScapeSetting.Designer.cs rename to Kavita.Database/Migrations/20250919114119_ColorScapeSetting.Designer.cs index 67c146f48..2e0e04e7c 100644 --- a/API/Data/Migrations/20250919114119_ColorScapeSetting.Designer.cs +++ b/Kavita.Database/Migrations/20250919114119_ColorScapeSetting.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250919114119_ColorScapeSetting")] diff --git a/API/Data/Migrations/20250919114119_ColorScapeSetting.cs b/Kavita.Database/Migrations/20250919114119_ColorScapeSetting.cs similarity index 95% rename from API/Data/Migrations/20250919114119_ColorScapeSetting.cs rename to Kavita.Database/Migrations/20250919114119_ColorScapeSetting.cs index 445ada7a3..4e12d320d 100644 --- a/API/Data/Migrations/20250919114119_ColorScapeSetting.cs +++ b/Kavita.Database/Migrations/20250919114119_ColorScapeSetting.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ColorScapeSetting : Migration diff --git a/API/Data/Migrations/20250920212509_CustomEpubFonts.Designer.cs b/Kavita.Database/Migrations/20250920212509_CustomEpubFonts.Designer.cs similarity index 99% rename from API/Data/Migrations/20250920212509_CustomEpubFonts.Designer.cs rename to Kavita.Database/Migrations/20250920212509_CustomEpubFonts.Designer.cs index 5ce969a9a..e83b94038 100644 --- a/API/Data/Migrations/20250920212509_CustomEpubFonts.Designer.cs +++ b/Kavita.Database/Migrations/20250920212509_CustomEpubFonts.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250920212509_CustomEpubFonts")] diff --git a/API/Data/Migrations/20250920212509_CustomEpubFonts.cs b/Kavita.Database/Migrations/20250920212509_CustomEpubFonts.cs similarity index 97% rename from API/Data/Migrations/20250920212509_CustomEpubFonts.cs rename to Kavita.Database/Migrations/20250920212509_CustomEpubFonts.cs index 1a8505b52..ddd5f87bd 100644 --- a/API/Data/Migrations/20250920212509_CustomEpubFonts.cs +++ b/Kavita.Database/Migrations/20250920212509_CustomEpubFonts.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class CustomEpubFonts : Migration diff --git a/API/Data/Migrations/20250921211542_EpubPageCalcMethod.Designer.cs b/Kavita.Database/Migrations/20250921211542_EpubPageCalcMethod.Designer.cs similarity index 99% rename from API/Data/Migrations/20250921211542_EpubPageCalcMethod.Designer.cs rename to Kavita.Database/Migrations/20250921211542_EpubPageCalcMethod.Designer.cs index 0c1db0f09..2b9b24e58 100644 --- a/API/Data/Migrations/20250921211542_EpubPageCalcMethod.Designer.cs +++ b/Kavita.Database/Migrations/20250921211542_EpubPageCalcMethod.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250921211542_EpubPageCalcMethod")] diff --git a/API/Data/Migrations/20250921211542_EpubPageCalcMethod.cs b/Kavita.Database/Migrations/20250921211542_EpubPageCalcMethod.cs similarity index 95% rename from API/Data/Migrations/20250921211542_EpubPageCalcMethod.cs rename to Kavita.Database/Migrations/20250921211542_EpubPageCalcMethod.cs index d6a8b02c0..54bc0fb74 100644 --- a/API/Data/Migrations/20250921211542_EpubPageCalcMethod.cs +++ b/Kavita.Database/Migrations/20250921211542_EpubPageCalcMethod.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class EpubPageCalcMethod : Migration diff --git a/API/Data/Migrations/20250924142016_AddAnnotationsHtmlContent.Designer.cs b/Kavita.Database/Migrations/20250924142016_AddAnnotationsHtmlContent.Designer.cs similarity index 99% rename from API/Data/Migrations/20250924142016_AddAnnotationsHtmlContent.Designer.cs rename to Kavita.Database/Migrations/20250924142016_AddAnnotationsHtmlContent.Designer.cs index d239e558d..1a3839910 100644 --- a/API/Data/Migrations/20250924142016_AddAnnotationsHtmlContent.Designer.cs +++ b/Kavita.Database/Migrations/20250924142016_AddAnnotationsHtmlContent.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250924142016_AddAnnotationsHtmlContent")] diff --git a/API/Data/Migrations/20250924142016_AddAnnotationsHtmlContent.cs b/Kavita.Database/Migrations/20250924142016_AddAnnotationsHtmlContent.cs similarity index 98% rename from API/Data/Migrations/20250924142016_AddAnnotationsHtmlContent.cs rename to Kavita.Database/Migrations/20250924142016_AddAnnotationsHtmlContent.cs index 620e37d03..8effff21e 100644 --- a/API/Data/Migrations/20250924142016_AddAnnotationsHtmlContent.cs +++ b/Kavita.Database/Migrations/20250924142016_AddAnnotationsHtmlContent.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AddAnnotationsHtmlContent : Migration diff --git a/API/Data/Migrations/20250928181727_RemoveEpubPageCalc.Designer.cs b/Kavita.Database/Migrations/20250928181727_RemoveEpubPageCalc.Designer.cs similarity index 99% rename from API/Data/Migrations/20250928181727_RemoveEpubPageCalc.Designer.cs rename to Kavita.Database/Migrations/20250928181727_RemoveEpubPageCalc.Designer.cs index 437890075..dac2eb8ba 100644 --- a/API/Data/Migrations/20250928181727_RemoveEpubPageCalc.Designer.cs +++ b/Kavita.Database/Migrations/20250928181727_RemoveEpubPageCalc.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250928181727_RemoveEpubPageCalc")] diff --git a/API/Data/Migrations/20250928181727_RemoveEpubPageCalc.cs b/Kavita.Database/Migrations/20250928181727_RemoveEpubPageCalc.cs similarity index 95% rename from API/Data/Migrations/20250928181727_RemoveEpubPageCalc.cs rename to Kavita.Database/Migrations/20250928181727_RemoveEpubPageCalc.cs index 8074f1fc5..30e65ffb1 100644 --- a/API/Data/Migrations/20250928181727_RemoveEpubPageCalc.cs +++ b/Kavita.Database/Migrations/20250928181727_RemoveEpubPageCalc.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class RemoveEpubPageCalc : Migration diff --git a/API/Data/Migrations/20251003110154_SocialAnnotations.Designer.cs b/Kavita.Database/Migrations/20251003110154_SocialAnnotations.Designer.cs similarity index 99% rename from API/Data/Migrations/20251003110154_SocialAnnotations.Designer.cs rename to Kavita.Database/Migrations/20251003110154_SocialAnnotations.Designer.cs index 687585e5c..cb0fd84ea 100644 --- a/API/Data/Migrations/20251003110154_SocialAnnotations.Designer.cs +++ b/Kavita.Database/Migrations/20251003110154_SocialAnnotations.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251003110154_SocialAnnotations")] diff --git a/API/Data/Migrations/20251003110154_SocialAnnotations.cs b/Kavita.Database/Migrations/20251003110154_SocialAnnotations.cs similarity index 98% rename from API/Data/Migrations/20251003110154_SocialAnnotations.cs rename to Kavita.Database/Migrations/20251003110154_SocialAnnotations.cs index ecd522211..f14ed4937 100644 --- a/API/Data/Migrations/20251003110154_SocialAnnotations.cs +++ b/Kavita.Database/Migrations/20251003110154_SocialAnnotations.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SocialAnnotations : Migration diff --git a/API/Data/Migrations/20251009150922_DataSaverUserSetting.Designer.cs b/Kavita.Database/Migrations/20251009150922_DataSaverUserSetting.Designer.cs similarity index 99% rename from API/Data/Migrations/20251009150922_DataSaverUserSetting.Designer.cs rename to Kavita.Database/Migrations/20251009150922_DataSaverUserSetting.Designer.cs index 690f5152d..d6508c583 100644 --- a/API/Data/Migrations/20251009150922_DataSaverUserSetting.Designer.cs +++ b/Kavita.Database/Migrations/20251009150922_DataSaverUserSetting.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251009150922_DataSaverUserSetting")] diff --git a/API/Data/Migrations/20251009150922_DataSaverUserSetting.cs b/Kavita.Database/Migrations/20251009150922_DataSaverUserSetting.cs similarity index 95% rename from API/Data/Migrations/20251009150922_DataSaverUserSetting.cs rename to Kavita.Database/Migrations/20251009150922_DataSaverUserSetting.cs index 0a744dfc7..0c5484637 100644 --- a/API/Data/Migrations/20251009150922_DataSaverUserSetting.cs +++ b/Kavita.Database/Migrations/20251009150922_DataSaverUserSetting.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class DataSaverUserSetting : Migration diff --git a/API/Data/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.Designer.cs b/Kavita.Database/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.Designer.cs similarity index 99% rename from API/Data/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.Designer.cs rename to Kavita.Database/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.Designer.cs index 81b85a6f7..53dbdf0bb 100644 --- a/API/Data/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.Designer.cs +++ b/Kavita.Database/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251023205956_SeriesInheritWebLinksFromFirstChapter")] diff --git a/API/Data/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.cs b/Kavita.Database/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.cs similarity index 95% rename from API/Data/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.cs rename to Kavita.Database/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.cs index 0cfae8900..e57d1aff5 100644 --- a/API/Data/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.cs +++ b/Kavita.Database/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SeriesInheritWebLinksFromFirstChapter : Migration diff --git a/API/Data/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.Designer.cs b/Kavita.Database/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.Designer.cs similarity index 99% rename from API/Data/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.Designer.cs rename to Kavita.Database/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.Designer.cs index 7fab60ea3..eff6f70f5 100644 --- a/API/Data/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.Designer.cs +++ b/Kavita.Database/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251026234845_LibraryDefaultLanguageCustomKeyBinds")] diff --git a/API/Data/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.cs b/Kavita.Database/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.cs similarity index 96% rename from API/Data/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.cs rename to Kavita.Database/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.cs index e1ad21e18..7d323ae37 100644 --- a/API/Data/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.cs +++ b/Kavita.Database/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class LibraryDefaultLanguageCustomKeyBinds : Migration diff --git a/API/Data/Migrations/20251101152738_OpdsSettings.Designer.cs b/Kavita.Database/Migrations/20251101152738_OpdsSettings.Designer.cs similarity index 99% rename from API/Data/Migrations/20251101152738_OpdsSettings.Designer.cs rename to Kavita.Database/Migrations/20251101152738_OpdsSettings.Designer.cs index 2ec446aba..7bc381e3d 100644 --- a/API/Data/Migrations/20251101152738_OpdsSettings.Designer.cs +++ b/Kavita.Database/Migrations/20251101152738_OpdsSettings.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251101152738_OpdsSettings")] diff --git a/API/Data/Migrations/20251101152738_OpdsSettings.cs b/Kavita.Database/Migrations/20251101152738_OpdsSettings.cs similarity index 95% rename from API/Data/Migrations/20251101152738_OpdsSettings.cs rename to Kavita.Database/Migrations/20251101152738_OpdsSettings.cs index b40b1991d..45220bee9 100644 --- a/API/Data/Migrations/20251101152738_OpdsSettings.cs +++ b/Kavita.Database/Migrations/20251101152738_OpdsSettings.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class OpdsSettings : Migration diff --git a/API/Data/Migrations/20251207204514_StatsRevampPartOne.Designer.cs b/Kavita.Database/Migrations/20251207204514_StatsRevampPartOne.Designer.cs similarity index 99% rename from API/Data/Migrations/20251207204514_StatsRevampPartOne.Designer.cs rename to Kavita.Database/Migrations/20251207204514_StatsRevampPartOne.Designer.cs index ac9d1ff08..17a221003 100644 --- a/API/Data/Migrations/20251207204514_StatsRevampPartOne.Designer.cs +++ b/Kavita.Database/Migrations/20251207204514_StatsRevampPartOne.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251207204514_StatsRevampPartOne")] diff --git a/API/Data/Migrations/20251207204514_StatsRevampPartOne.cs b/Kavita.Database/Migrations/20251207204514_StatsRevampPartOne.cs similarity index 99% rename from API/Data/Migrations/20251207204514_StatsRevampPartOne.cs rename to Kavita.Database/Migrations/20251207204514_StatsRevampPartOne.cs index c95f34e71..287901513 100644 --- a/API/Data/Migrations/20251207204514_StatsRevampPartOne.cs +++ b/Kavita.Database/Migrations/20251207204514_StatsRevampPartOne.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class StatsRevampPartOne : Migration diff --git a/API/Data/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.Designer.cs b/Kavita.Database/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.Designer.cs similarity index 99% rename from API/Data/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.Designer.cs rename to Kavita.Database/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.Designer.cs index 8f4cfdfdf..810dd9ced 100644 --- a/API/Data/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.Designer.cs +++ b/Kavita.Database/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251210145923_BookmarkRelationshipAndSearchIndex")] diff --git a/API/Data/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.cs b/Kavita.Database/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.cs similarity index 99% rename from API/Data/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.cs rename to Kavita.Database/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.cs index 93cf79fbf..72f5dcac0 100644 --- a/API/Data/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.cs +++ b/Kavita.Database/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class BookmarkRelationshipAndSearchIndex : Migration diff --git a/API/Data/Migrations/20251218200802_ReadingSessionFormatAndIndecies.Designer.cs b/Kavita.Database/Migrations/20251218200802_ReadingSessionFormatAndIndecies.Designer.cs similarity index 99% rename from API/Data/Migrations/20251218200802_ReadingSessionFormatAndIndecies.Designer.cs rename to Kavita.Database/Migrations/20251218200802_ReadingSessionFormatAndIndecies.Designer.cs index 5f5a9e133..f174db443 100644 --- a/API/Data/Migrations/20251218200802_ReadingSessionFormatAndIndecies.Designer.cs +++ b/Kavita.Database/Migrations/20251218200802_ReadingSessionFormatAndIndecies.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251218200802_ReadingSessionFormatAndIndecies")] diff --git a/API/Data/Migrations/20251218200802_ReadingSessionFormatAndIndecies.cs b/Kavita.Database/Migrations/20251218200802_ReadingSessionFormatAndIndecies.cs similarity index 98% rename from API/Data/Migrations/20251218200802_ReadingSessionFormatAndIndecies.cs rename to Kavita.Database/Migrations/20251218200802_ReadingSessionFormatAndIndecies.cs index a9509e1ce..84815ce42 100644 --- a/API/Data/Migrations/20251218200802_ReadingSessionFormatAndIndecies.cs +++ b/Kavita.Database/Migrations/20251218200802_ReadingSessionFormatAndIndecies.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ReadingSessionFormatAndIndecies : Migration diff --git a/API/Data/Migrations/20251224133055_AddDataProtectionKeys.Designer.cs b/Kavita.Database/Migrations/20251224133055_AddDataProtectionKeys.Designer.cs similarity index 99% rename from API/Data/Migrations/20251224133055_AddDataProtectionKeys.Designer.cs rename to Kavita.Database/Migrations/20251224133055_AddDataProtectionKeys.Designer.cs index f04ab9609..a765111ba 100644 --- a/API/Data/Migrations/20251224133055_AddDataProtectionKeys.Designer.cs +++ b/Kavita.Database/Migrations/20251224133055_AddDataProtectionKeys.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251224133055_AddDataProtectionKeys")] diff --git a/API/Data/Migrations/20251224133055_AddDataProtectionKeys.cs b/Kavita.Database/Migrations/20251224133055_AddDataProtectionKeys.cs similarity index 96% rename from API/Data/Migrations/20251224133055_AddDataProtectionKeys.cs rename to Kavita.Database/Migrations/20251224133055_AddDataProtectionKeys.cs index 19956c5f9..8948df86f 100644 --- a/API/Data/Migrations/20251224133055_AddDataProtectionKeys.cs +++ b/Kavita.Database/Migrations/20251224133055_AddDataProtectionKeys.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AddDataProtectionKeys : Migration diff --git a/API/Data/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.Designer.cs b/Kavita.Database/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.Designer.cs similarity index 99% rename from API/Data/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.Designer.cs rename to Kavita.Database/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.Designer.cs index a3a42e2f1..a521b41cf 100644 --- a/API/Data/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.Designer.cs +++ b/Kavita.Database/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251229144718_AddDeviceIdsToReadingProfiles")] diff --git a/API/Data/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.cs b/Kavita.Database/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.cs similarity index 98% rename from API/Data/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.cs rename to Kavita.Database/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.cs index 80df7309e..fbc94b5f3 100644 --- a/API/Data/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.cs +++ b/Kavita.Database/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AddDeviceIdsToReadingProfiles : Migration diff --git a/API/Data/Migrations/20260109144351_ReadingSessionIndex.Designer.cs b/Kavita.Database/Migrations/20260109144351_ReadingSessionIndex.Designer.cs similarity index 99% rename from API/Data/Migrations/20260109144351_ReadingSessionIndex.Designer.cs rename to Kavita.Database/Migrations/20260109144351_ReadingSessionIndex.Designer.cs index d38a7c19d..1994f26f6 100644 --- a/API/Data/Migrations/20260109144351_ReadingSessionIndex.Designer.cs +++ b/Kavita.Database/Migrations/20260109144351_ReadingSessionIndex.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20260109144351_ReadingSessionIndex")] diff --git a/API/Data/Migrations/20260109144351_ReadingSessionIndex.cs b/Kavita.Database/Migrations/20260109144351_ReadingSessionIndex.cs similarity index 97% rename from API/Data/Migrations/20260109144351_ReadingSessionIndex.cs rename to Kavita.Database/Migrations/20260109144351_ReadingSessionIndex.cs index e7dcbf04f..50280a630 100644 --- a/API/Data/Migrations/20260109144351_ReadingSessionIndex.cs +++ b/Kavita.Database/Migrations/20260109144351_ReadingSessionIndex.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ReadingSessionIndex : Migration diff --git a/API/Data/Migrations/20260110164419_AppUserAuthKeyUtcMissing.Designer.cs b/Kavita.Database/Migrations/20260110164419_AppUserAuthKeyUtcMissing.Designer.cs similarity index 99% rename from API/Data/Migrations/20260110164419_AppUserAuthKeyUtcMissing.Designer.cs rename to Kavita.Database/Migrations/20260110164419_AppUserAuthKeyUtcMissing.Designer.cs index 302d41386..df0223a7b 100644 --- a/API/Data/Migrations/20260110164419_AppUserAuthKeyUtcMissing.Designer.cs +++ b/Kavita.Database/Migrations/20260110164419_AppUserAuthKeyUtcMissing.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20260110164419_AppUserAuthKeyUtcMissing")] diff --git a/API/Data/Migrations/20260110164419_AppUserAuthKeyUtcMissing.cs b/Kavita.Database/Migrations/20260110164419_AppUserAuthKeyUtcMissing.cs similarity index 95% rename from API/Data/Migrations/20260110164419_AppUserAuthKeyUtcMissing.cs rename to Kavita.Database/Migrations/20260110164419_AppUserAuthKeyUtcMissing.cs index f3fdb69cd..09ff54ca0 100644 --- a/API/Data/Migrations/20260110164419_AppUserAuthKeyUtcMissing.cs +++ b/Kavita.Database/Migrations/20260110164419_AppUserAuthKeyUtcMissing.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AppUserAuthKeyUtcMissing : Migration diff --git a/API/Data/Migrations/20260112165908_ReadingHistoryChanges.Designer.cs b/Kavita.Database/Migrations/20260112165908_ReadingHistoryChanges.Designer.cs similarity index 99% rename from API/Data/Migrations/20260112165908_ReadingHistoryChanges.Designer.cs rename to Kavita.Database/Migrations/20260112165908_ReadingHistoryChanges.Designer.cs index 988d3823b..e98766293 100644 --- a/API/Data/Migrations/20260112165908_ReadingHistoryChanges.Designer.cs +++ b/Kavita.Database/Migrations/20260112165908_ReadingHistoryChanges.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20260112165908_ReadingHistoryChanges")] diff --git a/API/Data/Migrations/20260112165908_ReadingHistoryChanges.cs b/Kavita.Database/Migrations/20260112165908_ReadingHistoryChanges.cs similarity index 98% rename from API/Data/Migrations/20260112165908_ReadingHistoryChanges.cs rename to Kavita.Database/Migrations/20260112165908_ReadingHistoryChanges.cs index e22cb1a61..53214c013 100644 --- a/API/Data/Migrations/20260112165908_ReadingHistoryChanges.cs +++ b/Kavita.Database/Migrations/20260112165908_ReadingHistoryChanges.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ReadingHistoryChanges : Migration diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/Kavita.Database/Migrations/DataContextModelSnapshot.cs similarity index 99% rename from API/Data/Migrations/DataContextModelSnapshot.cs rename to Kavita.Database/Migrations/DataContextModelSnapshot.cs index 6c8d15a19..f8049cfb0 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/Kavita.Database/Migrations/DataContextModelSnapshot.cs @@ -1,16 +1,14 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] partial class DataContextModelSnapshot : ModelSnapshot diff --git a/API/Data/Repositories/AnnotationRepository.cs b/Kavita.Database/Repositories/AnnotationRepository.cs similarity index 78% rename from API/Data/Repositories/AnnotationRepository.cs rename to Kavita.Database/Repositories/AnnotationRepository.cs index a2b69a39f..7a947e78f 100644 --- a/API/Data/Repositories/AnnotationRepository.cs +++ b/Kavita.Database/Repositories/AnnotationRepository.cs @@ -1,39 +1,25 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.DTOs.Filtering.v2; -using API.DTOs.Metadata.Browse.Requests; -using API.DTOs.Annotations; -using API.DTOs.Reader; -using API.Entities; -using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; -using API.Helpers.Converters; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Helpers; +using Kavita.Database.Converters; +using Kavita.Database.Extensions; +using Kavita.Database.Extensions.Filters; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Annotations; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -public interface IAnnotationRepository -{ - void Attach(AppUserAnnotation annotation); - void Update(AppUserAnnotation annotation); - void Remove(AppUserAnnotation annotation); - void Remove(IEnumerable annotations); - Task GetAnnotationDto(int id); - Task GetAnnotation(int id); - Task> GetAllAnnotations(); - Task> GetAnnotations(int userId, IList ids); - Task> GetFullAnnotationsByUserIdAsync(int userId); - Task> GetFullAnnotations(int userId, IList annotationIds); - Task> GetAnnotationDtos(int userId, BrowseAnnotationFilterDto filter, UserParams userParams); - Task> GetSeriesWithAnnotations(int userId); -} public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnotationRepository { @@ -57,43 +43,44 @@ public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnota context.AppUserAnnotation.RemoveRange(annotations); } - public async Task GetAnnotationDto(int id) + public async Task GetAnnotationDto(int id, CancellationToken ct = default) { return await context.AppUserAnnotation .ProjectTo(mapper.ConfigurationProvider) - .FirstOrDefaultAsync(a => a.Id == id); + .FirstOrDefaultAsync(a => a.Id == id, ct); } - public async Task GetAnnotation(int id) + public async Task GetAnnotation(int id, CancellationToken ct = default) { return await context.AppUserAnnotation - .FirstOrDefaultAsync(a => a.Id == id); + .FirstOrDefaultAsync(a => a.Id == id, ct); } - public async Task> GetAllAnnotations() + public async Task> GetAllAnnotations(CancellationToken ct = default) { - return await context.AppUserAnnotation.ToListAsync(); + return await context.AppUserAnnotation.ToListAsync(ct); } - public async Task> GetAnnotations(int userId, IList ids) + public async Task> GetAnnotations(int userId, IList ids, CancellationToken ct = default) { - var userPreferences = await context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); return await context.AppUserAnnotation .Where(a => ids.Contains(a.Id)) .RestrictBySocialPreferences(userId, userPreferences) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAnnotationDtos(int userId, BrowseAnnotationFilterDto filter, UserParams userParams) + public async Task> GetAnnotationDtos(int userId, BrowseAnnotationFilterDto filter, + UserParams userParams, CancellationToken ct = default) { var query = await CreatedFilteredAnnotationQueryable(userId, filter); - return await PagedList.CreateAsync(query, userParams); + return await PagedList.CreateAsync(query, userParams, ct); } - public async Task> GetSeriesWithAnnotations(int userId) + public async Task> GetSeriesWithAnnotations(int userId, CancellationToken ct = default) { - var userPreferences = await context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); var libraryIds = context.AppUser.GetLibraryIdsForUser(userId); var userRating = await context.AppUser.GetUserAgeRestriction(userId); @@ -101,13 +88,13 @@ public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnota var seriesIdsWithAnnotations = await context.AppUserAnnotation .RestrictBySocialPreferences(userId, userPreferences) .Select(a => a.SeriesId) - .ToListAsync(); + .ToListAsync(ct); return await context.Series .Where(s => libraryIds.Contains(s.LibraryId) && seriesIdsWithAnnotations.Contains(s.Id)) .RestrictAgainstAgeRestriction(userRating) .ProjectTo(mapper.ConfigurationProvider) - .ToListAsync(); + .ToListAsync(ct); } @@ -180,9 +167,10 @@ public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnota }; } - public async Task> GetFullAnnotations(int userId, IList annotationIds) + public async Task> GetFullAnnotations(int userId, IList annotationIds, + CancellationToken ct = default) { - var userPreferences = await context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); return await context.AppUserAnnotation .AsNoTracking() @@ -190,22 +178,23 @@ public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnota .RestrictBySocialPreferences(userId, userPreferences) .ProjectTo(mapper.ConfigurationProvider) .OrderFullAnnotation() - .ToListAsync(); + .ToListAsync(ct); } /// /// This does not track! /// /// + /// /// - public async Task> GetFullAnnotationsByUserIdAsync(int userId) + public async Task> GetFullAnnotationsByUserIdAsync(int userId, CancellationToken ct = default) { - var userPreferences = await context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); return await context.AppUserAnnotation .RestrictBySocialPreferences(userId, userPreferences) .ProjectTo(mapper.ConfigurationProvider) .OrderFullAnnotation() - .ToListAsync(); + .ToListAsync(ct); } } diff --git a/API/Data/Repositories/AppUserExternalSourceRepository.cs b/Kavita.Database/Repositories/AppUserExternalSourceRepository.cs similarity index 50% rename from API/Data/Repositories/AppUserExternalSourceRepository.cs rename to Kavita.Database/Repositories/AppUserExternalSourceRepository.cs index 60f335599..9e3f4327f 100644 --- a/API/Data/Repositories/AppUserExternalSourceRepository.cs +++ b/Kavita.Database/Repositories/AppUserExternalSourceRepository.cs @@ -1,77 +1,65 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.SideNav; -using API.Entities; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; using Kavita.Common.Helpers; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; +namespace Kavita.Database.Repositories; -public interface IAppUserExternalSourceRepository + +public class AppUserExternalSourceRepository(DataContext context, IMapper mapper) : IAppUserExternalSourceRepository { - void Update(AppUserExternalSource source); - void Delete(AppUserExternalSource source); - Task GetById(int externalSourceId); - Task> GetExternalSources(int userId); - Task ExternalSourceExists(int userId, string name, string host, string apiKey); -} - -public class AppUserExternalSourceRepository : IAppUserExternalSourceRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public AppUserExternalSourceRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } public void Update(AppUserExternalSource source) { - _context.Entry(source).State = EntityState.Modified; + context.AppUserExternalSource.Update(source); } public void Delete(AppUserExternalSource source) { - _context.AppUserExternalSource.Remove(source); + context.AppUserExternalSource.Remove(source); } - public async Task GetById(int externalSourceId) + public async Task GetById(int externalSourceId, CancellationToken ct = default) { - return await _context.AppUserExternalSource + return await context.AppUserExternalSource .Where(s => s.Id == externalSourceId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetExternalSources(int userId) + public async Task> GetExternalSources(int userId, CancellationToken ct = default) { - return await _context.AppUserExternalSource.Where(s => s.AppUserId == userId) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + return await context.AppUserExternalSource.Where(s => s.AppUserId == userId) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } /// /// Checks if all the properties match exactly. This will allow a user to setup 2 External Sources with different Users /// /// - /// /// + /// /// + /// /// - public async Task ExternalSourceExists(int userId, string name, string host, string apiKey) + public async Task ExternalSourceExists(int userId, string name, string host, string apiKey, + CancellationToken ct = default) { host = host.Trim(); if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(name) || string.IsNullOrEmpty(apiKey)) return false; var hostWithEndingSlash = UrlHelper.EnsureEndsWithSlash(host)!; - return await _context.AppUserExternalSource + return await context.AppUserExternalSource .Where(s => s.AppUserId == userId ) .Where(s => s.Host.ToUpper().Equals(hostWithEndingSlash.ToUpper()) && s.Name.ToUpper().Equals(name.ToUpper()) && s.ApiKey.Equals(apiKey)) - .AnyAsync(); + .AnyAsync(ct); } } diff --git a/API/Data/Repositories/AppUserProgressRepository.cs b/Kavita.Database/Repositories/AppUserProgressRepository.cs similarity index 55% rename from API/Data/Repositories/AppUserProgressRepository.cs rename to Kavita.Database/Repositories/AppUserProgressRepository.cs index 24b6cc0e9..e2d6f9549 100644 --- a/API/Data/Repositories/AppUserProgressRepository.cs +++ b/Kavita.Database/Repositories/AppUserProgressRepository.cs @@ -2,116 +2,87 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.Progress; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Extensions.QueryExtensions; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Constants; +using Kavita.Database.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -public interface IAppUserProgressRepository +public class AppUserProgressRepository(DataContext context, IMapper mapper) : IAppUserProgressRepository { - void Update(AppUserProgress userProgress); - void Remove(AppUserProgress userProgress); - Task CleanupAbandonedChapters(); - Task UserHasProgress(LibraryType libraryType, int userId); - Task GetUserProgressAsync(int chapterId, int userId); - Task HasAnyProgressOnSeriesAsync(int seriesId, int userId); - Task> GetUserProgressForSeriesAsync(int seriesId, int userId); - Task> GetAllProgress(); - Task GetLatestProgress(); - Task GetUserProgressDtoAsync(int chapterId, int userId); - Task AnyUserProgressForSeriesAsync(int seriesId, int userId); - Task GetHighestFullyReadChapterForSeries(int seriesId, int userId); - Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId); - Task GetLatestProgressForSeries(int seriesId, int userId); - Task GetLatestProgressForVolume(int volumeId, int userId); - Task GetLatestProgressForChapter(int chapterId, int userId); - Task GetFirstProgressForSeries(int seriesId, int userId); - Task GetFirstProgressForUser(int userId); - Task UpdateAllProgressThatAreMoreThanChapterPages(); - Task> GetUserProgressForChapter(int chapterId, int userId = 0); -} - -public class AppUserProgressRepository : IAppUserProgressRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public AppUserProgressRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Update(AppUserProgress userProgress) { - _context.Entry(userProgress).State = EntityState.Modified; + context.Entry(userProgress).State = EntityState.Modified; } public void Remove(AppUserProgress userProgress) { - _context.Remove(userProgress); + context.Remove(userProgress); } /// /// This will remove any entries that have chapterIds that no longer exists. This will execute the save as well. /// - public async Task CleanupAbandonedChapters() + /// + public async Task CleanupAbandonedChapters(CancellationToken ct = default) { - var chapterIds = _context.Chapter.Select(c => c.Id); + var chapterIds = context.Chapter.Select(c => c.Id); - var rowsToRemove = await _context.AppUserProgresses + var rowsToRemove = await context.AppUserProgresses .Where(progress => !chapterIds.Contains(progress.ChapterId)) - .ToListAsync(); + .ToListAsync(ct); - var rowsToRemoveBookmarks = await _context.AppUserBookmark + var rowsToRemoveBookmarks = await context.AppUserBookmark .Where(progress => !chapterIds.Contains(progress.ChapterId)) - .ToListAsync(); + .ToListAsync(ct); - var rowsToRemoveReadingLists = await _context.ReadingListItem + var rowsToRemoveReadingLists = await context.ReadingListItem .Where(item => !chapterIds.Contains(item.ChapterId)) - .ToListAsync(); + .ToListAsync(ct); - _context.RemoveRange(rowsToRemove); - _context.RemoveRange(rowsToRemoveBookmarks); - _context.RemoveRange(rowsToRemoveReadingLists); - return await _context.SaveChangesAsync() > 0 ? rowsToRemove.Count : 0; + context.RemoveRange(rowsToRemove); + context.RemoveRange(rowsToRemoveBookmarks); + context.RemoveRange(rowsToRemoveReadingLists); + return await context.SaveChangesAsync(ct) > 0 ? rowsToRemove.Count : 0; } /// - /// Checks if user has any progress against a library of passed type + /// Checks if a user has any progress against a library of a passed type /// /// /// + /// /// - public async Task UserHasProgress(LibraryType libraryType, int userId) + public async Task UserHasProgress(LibraryType libraryType, int userId, CancellationToken ct = default) { - var seriesIds = await _context.AppUserProgresses + var seriesIds = await context.AppUserProgresses .Where(aup => aup.PagesRead > 0 && aup.AppUserId == userId) .AsNoTracking() .Select(aup => aup.SeriesId) - .ToListAsync(); + .ToListAsync(ct); if (seriesIds.Count == 0) return false; - return await _context.Series + return await context.Series .Include(s => s.Library) .Where(s => seriesIds.Contains(s.Id) && s.Library.Type == libraryType) .AsNoTracking() - .AnyAsync(); + .AnyAsync(ct); } - public async Task HasAnyProgressOnSeriesAsync(int seriesId, int userId) + public async Task HasAnyProgressOnSeriesAsync(int seriesId, int userId, CancellationToken ct = default) { - return await _context.AppUserProgresses - .AnyAsync(aup => aup.PagesRead > 0 && aup.AppUserId == userId && aup.SeriesId == seriesId); + return await context.AppUserProgresses + .AnyAsync(aup => aup.PagesRead > 0 && aup.AppUserId == userId && aup.SeriesId == seriesId, ct); } /// @@ -119,118 +90,121 @@ public class AppUserProgressRepository : IAppUserProgressRepository /// /// /// + /// /// - public async Task> GetUserProgressForSeriesAsync(int seriesId, int userId) + public async Task> GetUserProgressForSeriesAsync(int seriesId, int userId, + CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => p.SeriesId == seriesId && p.AppUserId == userId && p.PagesRead > 0) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllProgress() + public async Task> GetAllProgress(CancellationToken ct = default) { - return await _context.AppUserProgresses.ToListAsync(); + return await context.AppUserProgresses.ToListAsync(ct); } /// /// Returns the latest progress in UTC /// + /// /// - public async Task GetLatestProgress() + public async Task GetLatestProgress(CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Select(d => d.LastModifiedUtc) .OrderByDescending(d => d) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetUserProgressDtoAsync(int chapterId, int userId) + public async Task GetUserProgressDtoAsync(int chapterId, int userId, CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => p.AppUserId == userId && p.ChapterId == chapterId) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); } - public async Task AnyUserProgressForSeriesAsync(int seriesId, int userId) + public async Task AnyUserProgressForSeriesAsync(int seriesId, int userId, CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => p.SeriesId == seriesId && p.AppUserId == userId && p.PagesRead > 0) - .AnyAsync(); + .AnyAsync(ct); } - public async Task GetHighestFullyReadChapterForSeries(int seriesId, int userId) + public async Task GetHighestFullyReadChapterForSeries(int seriesId, int userId, CancellationToken ct = default) { - var list = await _context.AppUserProgresses - .Join(_context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id, + var list = await context.AppUserProgresses + .Join(context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id, (appUserProgresses, chapter) => new {appUserProgresses, chapter}) .Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId && p.appUserProgresses.PagesRead >= p.chapter.Pages) - .Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber) + .Where(p => p.chapter.MaxNumber != ParserConstants.SpecialVolumeNumber) .Select(p => p.chapter.MaxNumber) - .ToListAsync(); + .ToListAsync(ct); return list.Count == 0 ? 0 : (int) list.DefaultIfEmpty().Max(d => d); } - public async Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId) + public async Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId, CancellationToken ct = default) { - var list = await _context.AppUserProgresses - .Join(_context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id, + var list = await context.AppUserProgresses + .Join(context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id, (appUserProgresses, chapter) => new {appUserProgresses, chapter}) .Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId && p.appUserProgresses.PagesRead >= p.chapter.Pages) - .Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber) + .Where(p => p.chapter.MaxNumber != ParserConstants.SpecialVolumeNumber) .Select(p => p.chapter.Volume.MaxNumber) - .ToListAsync(); + .ToListAsync(ct); return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max(); } - public async Task GetLatestProgressForSeries(int seriesId, int userId) + public async Task GetLatestProgressForSeries(int seriesId, int userId, CancellationToken ct = default) { - var list = await _context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId) + var list = await context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId) .Select(p => p.LastModifiedUtc) - .ToListAsync(); + .ToListAsync(ct); return list.Count == 0 ? null : list.DefaultIfEmpty().Max(); } - public async Task GetLatestProgressForVolume(int volumeId, int userId) + public async Task GetLatestProgressForVolume(int volumeId, int userId, CancellationToken ct = default) { - var list = await _context.AppUserProgresses.Where(p => p.AppUserId == userId && p.VolumeId == volumeId) + var list = await context.AppUserProgresses.Where(p => p.AppUserId == userId && p.VolumeId == volumeId) .Select(p => p.LastModifiedUtc) - .ToListAsync(); + .ToListAsync(ct); return list.Count == 0 ? null : list.DefaultIfEmpty().Max(); } - public async Task GetLatestProgressForChapter(int chapterId, int userId) + public async Task GetLatestProgressForChapter(int chapterId, int userId, CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => p.AppUserId == userId && p.ChapterId == chapterId) .Select(p => p.LastModifiedUtc) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetFirstProgressForSeries(int seriesId, int userId) + public async Task GetFirstProgressForSeries(int seriesId, int userId, CancellationToken ct = default) { - var list = await _context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId) + var list = await context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId) .Select(p => p.LastModifiedUtc) - .ToListAsync(); + .ToListAsync(ct); return list.Count == 0 ? null : list.DefaultIfEmpty().Min(); } - public async Task GetFirstProgressForUser(int userId) + public async Task GetFirstProgressForUser(int userId, CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => p.AppUserId == userId) .OrderBy(p => p.CreatedUtc) .Select(p => p.CreatedUtc) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task UpdateAllProgressThatAreMoreThanChapterPages() + public async Task UpdateAllProgressThatAreMoreThanChapterPages(CancellationToken ct = default) { - var updates = _context.AppUserProgresses - .Join(_context.Chapter, + var updates = context.AppUserProgresses + .Join(context.Chapter, progress => progress.ChapterId, chapter => chapter.Id, (progress, chapter) => new @@ -255,7 +229,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository // Execute the batch SQL var batchSql = sqlBuilder.ToString(); - await _context.Database.ExecuteSqlRawAsync(batchSql); + await context.Database.ExecuteSqlRawAsync(batchSql, ct); } /// @@ -263,10 +237,12 @@ public class AppUserProgressRepository : IAppUserProgressRepository /// /// /// If 0, will pull all records + /// /// - public async Task> GetUserProgressForChapter(int chapterId, int userId = 0) + public async Task> GetUserProgressForChapter(int chapterId, int userId = 0, + CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .WhereIf(userId > 0, p => p.AppUserId == userId) .Where(p => p.ChapterId == chapterId) .Include(p => p.AppUser) @@ -282,14 +258,13 @@ public class AppUserProgressRepository : IAppUserProgressRepository LastModifiedUtc = p.LastModifiedUtc, UserName = p.AppUser.UserName }) - .ToListAsync(); + .ToListAsync(ct); } -#nullable enable - public async Task GetUserProgressAsync(int chapterId, int userId) + public async Task GetUserProgressAsync(int chapterId, int userId, CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => p.ChapterId == chapterId && p.AppUserId == userId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } } diff --git a/API/Data/Repositories/AppUserReadingProfileRepository.cs b/Kavita.Database/Repositories/AppUserReadingProfileRepository.cs similarity index 56% rename from API/Data/Repositories/AppUserReadingProfileRepository.cs rename to Kavita.Database/Repositories/AppUserReadingProfileRepository.cs index 541f17561..47ccfffec 100644 --- a/API/Data/Repositories/AppUserReadingProfileRepository.cs +++ b/Kavita.Database/Repositories/AppUserReadingProfileRepository.cs @@ -1,81 +1,25 @@ -#nullable enable using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Extensions.QueryExtensions; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; +namespace Kavita.Database.Repositories; -public interface IAppUserReadingProfileRepository -{ - - /// - /// Returns the reading profile to use for the given series - /// - /// - /// - /// - /// - /// - /// - Task GetProfileForSeries(int userId, int libraryId, int seriesId, int? activeDeviceId = null, bool skipImplicit = false); - - /// - /// Get all profiles assigned to a library - /// - /// - /// - /// - Task> GetProfilesForLibrary(int userId, int libraryId); - /// - /// Return the profile if it belongs the user - /// - /// - /// - /// - Task GetUserProfile(int userId, int profileId); - - /// - /// Returns all reading profiles for the user - /// - /// - /// - /// - Task> GetProfilesForUser(int userId, bool skipImplicit = false); - - /// - /// Returns all reading profiles for the user - /// - /// - /// - /// - Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false); - /// - /// Is there a user reading profile with this name (normalized) - /// - /// - /// - /// - Task IsProfileNameInUse(int userId, string name); - - void Add(AppUserReadingProfile readingProfile); - void Update(AppUserReadingProfile readingProfile); - void Remove(AppUserReadingProfile readingProfile); - void RemoveRange(IEnumerable readingProfiles); -} - public class AppUserReadingProfileRepository(DataContext context, IMapper mapper): IAppUserReadingProfileRepository { - public Task GetProfileForSeries(int userId, int libraryId, int seriesId, int? activeDeviceId = null, bool skipImplicit = false) + public Task GetProfileForSeries(int userId, int libraryId, int seriesId, + int? activeDeviceId = null, bool skipImplicit = false, CancellationToken ct = default) { return context.AppUserReadingProfiles .Where(rp => rp.AppUserId == userId) @@ -88,52 +32,57 @@ public class AppUserReadingProfileRepository(DataContext context, IMapper mapper .ThenByDescending(rp => rp.LibraryIds.Contains(libraryId) && (rp.DeviceIds.Count == 0 || (activeDeviceId != null && rp.DeviceIds.Contains(activeDeviceId.Value)))) .ThenByDescending(rp => rp.LibraryIds.Contains(libraryId)) .ThenByDescending(rp => rp.Kind == ReadingProfileKind.Default) - .FirstAsync(); + .FirstAsync(ct); } - public Task> GetProfilesForLibrary(int userId, int libraryId) + public Task> GetProfilesForLibrary(int userId, int libraryId, + CancellationToken ct = default) { return context.AppUserReadingProfiles .Where(rp => rp.AppUserId == userId && rp.LibraryIds.Contains(libraryId)) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetUserProfile(int userId, int profileId) + public async Task GetUserProfile(int userId, int profileId, CancellationToken ct = default) { return await context.AppUserReadingProfiles .Where(rp => rp.AppUserId == userId && rp.Id == profileId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetProfilesForUser(int userId, bool skipImplicit = false) + public async Task> GetProfilesForUser(int userId, bool skipImplicit = false, + CancellationToken ct = default) { return await context.AppUserReadingProfiles .Where(rp => rp.AppUserId == userId) .WhereIf(skipImplicit, rp => rp.Kind != ReadingProfileKind.Implicit) - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns all Reading Profiles for the User /// /// + /// + /// /// - public async Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false) + public async Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false, + CancellationToken ct = default) { return await context.AppUserReadingProfiles .Where(rp => rp.AppUserId == userId) .WhereIf(skipImplicit, rp => rp.Kind != ReadingProfileKind.Implicit) .ProjectTo(mapper.ConfigurationProvider) - .ToListAsync(); + .ToListAsync(ct); } - public async Task IsProfileNameInUse(int userId, string name) + public async Task IsProfileNameInUse(int userId, string name, CancellationToken ct = default) { var normalizedName = name.ToNormalized(); return await context.AppUserReadingProfiles .Where(rp => rp.NormalizedName == normalizedName && rp.AppUserId == userId) - .AnyAsync(); + .AnyAsync(ct); } public void Add(AppUserReadingProfile readingProfile) diff --git a/Kavita.Database/Repositories/AppUserSmartFilterRepository.cs b/Kavita.Database/Repositories/AppUserSmartFilterRepository.cs new file mode 100644 index 000000000..8dbb904e7 --- /dev/null +++ b/Kavita.Database/Repositories/AppUserSmartFilterRepository.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.Entities.User; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class AppUserSmartFilterRepository(DataContext context, IMapper mapper) : IAppUserSmartFilterRepository +{ + public void Update(AppUserSmartFilter filter) + { + context.Entry(filter).State = EntityState.Modified; + } + + public void Attach(AppUserSmartFilter filter) + { + context.AppUserSmartFilter.Attach(filter); + } + + public void Delete(AppUserSmartFilter filter) + { + context.AppUserSmartFilter.Remove(filter); + } + + public async Task> GetAllDtosByUserId(int userId, CancellationToken ct = default) + { + return await context.AppUserSmartFilter + .Where(f => f.AppUserId == userId) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } + + public Task> GetPagedDtosByUserIdAsync(int userId, UserParams userParams, + CancellationToken ct = default) + { + var filters = context.AppUserSmartFilter + .Where(f => f.AppUserId == userId) + .ProjectTo(mapper.ConfigurationProvider); + + return PagedList.CreateAsync(filters, userParams, ct); + } + + public async Task GetById(int smartFilterId, CancellationToken ct = default) + { + return await context.AppUserSmartFilter + .FirstOrDefaultAsync(d => d.Id == smartFilterId, ct); + } +} diff --git a/API/Data/Repositories/ChapterRepository.cs b/Kavita.Database/Repositories/ChapterRepository.cs similarity index 55% rename from API/Data/Repositories/ChapterRepository.cs rename to Kavita.Database/Repositories/ChapterRepository.cs index 568463f26..ddec4c795 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/Kavita.Database/Repositories/ChapterRepository.cs @@ -1,115 +1,61 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.DTOs.Metadata; -using API.DTOs.Reader; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Database.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum ChapterIncludes + +public class ChapterRepository(DataContext context, IMapper mapper) : IChapterRepository { - None = 1, - Volumes = 2, - Files = 4, - People = 8, - Genres = 16, - Tags = 32, - ExternalReviews = 1 << 6, - ExternalRatings = 1 << 7 -} - -public interface IChapterRepository -{ - void Update(Chapter chapter); - void Remove(Chapter chapter); - void Remove(IList chapters); - Task> GetChaptersByIdsAsync(IList chapterIds, ChapterIncludes includes = ChapterIncludes.None); - Task GetChapterInfoDtoAsync(int chapterId); - Task GetChapterTotalPagesAsync(int chapterId); - Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); - Task GetChapterDtoAsync(int chapterId, int userId); - Task> GetChapterDtoByIdsAsync(IEnumerable chapterIds, int userId); - Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); - Task> GetFilesForChapterAsync(int chapterId); - Task GetFilesizeForChapterAsync(int chapterId); - Task> GetFilesizeForChaptersAsync(IList chapterIds); - Task> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None); - Task> GetChapterDtosAsync(int volumeId, int userId); - Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds); - Task GetChapterCoverImageAsync(int chapterId); - Task> GetAllCoverImagesAsync(); - Task> GetAllChaptersWithCoversInDifferentEncoding(EncodeFormat format); - Task> GetCoverImagesForLockedChaptersAsync(); - IQueryable GetChaptersForSeries(int seriesId); - Task> GetAllChaptersForSeries(int seriesId); - Task GetAverageUserRating(int chapterId, int userId); - Task> GetExternalChapterReviewDtos(int chapterId); - Task> GetExternalChapterReview(int chapterId); - Task> GetExternalChapterRatingDtos(int chapterId); - Task> GetExternalChapterRatings(int chapterId); - Task GetCurrentlyReadingChapterAsync(int seriesId, int userId); - Task GetFirstChapterForSeriesAsync(int seriesId, int userId); - Task GetFirstChapterForVolumeAsync(int volumeId, int userId); - Task> GetChapterDtosAsync(IEnumerable chapterIds, int userId); -} -public class ChapterRepository : IChapterRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public ChapterRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Update(Chapter chapter) { - _context.Entry(chapter).State = EntityState.Modified; + context.Entry(chapter).State = EntityState.Modified; } public void Remove(Chapter chapter) { - _context.Chapter.Remove(chapter); + context.Chapter.Remove(chapter); } public void Remove(IList chapters) { - _context.Chapter.RemoveRange(chapters); + context.Chapter.RemoveRange(chapters); } - public async Task> GetChaptersByIdsAsync(IList chapterIds, ChapterIncludes includes = ChapterIncludes.None) + public async Task> GetChaptersByIdsAsync(IList chapterIds, + ChapterIncludes includes = ChapterIncludes.None, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => chapterIds.Contains(c.Id)) .Includes(includes) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } /// /// Populates a partial IChapterInfoDto /// /// - public async Task GetChapterInfoDtoAsync(int chapterId) + public async Task GetChapterInfoDtoAsync(int chapterId, CancellationToken ct = default) { - var chapterInfo = await _context.Chapter + var chapterInfo = await context.Chapter .Where(c => c.Id == chapterId) - .Join(_context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new + .Join(context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new { ChapterNumber = chapter.MinNumber, VolumeNumber = volume.Name, @@ -119,7 +65,7 @@ public class ChapterRepository : IChapterRepository volume.SeriesId, chapter.Pages, }) - .Join(_context.Series, data => data.SeriesId, series => series.Id, (data, series) => new + .Join(context.Series, data => data.SeriesId, series => series.Id, (data, series) => new { data.ChapterNumber, data.VolumeNumber, @@ -149,49 +95,51 @@ public class ChapterRepository : IChapterRepository }) .AsNoTracking() .AsSplitQuery() - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); return chapterInfo; } - public Task GetChapterTotalPagesAsync(int chapterId) + public Task GetChapterTotalPagesAsync(int chapterId, CancellationToken ct = default) { - return _context.Chapter + return context.Chapter .Where(c => c.Id == chapterId) .Select(c => c.Pages) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetChapterDtoAsync(int chapterId, int userId) + public async Task GetChapterDtoAsync(int chapterId, int userId, CancellationToken ct = default) { - var chapter = await _context.Chapter + var chapter = await context.Chapter .Includes(ChapterIncludes.Files | ChapterIncludes.People) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() - .FirstOrDefaultAsync(c => c.Id == chapterId); + .FirstOrDefaultAsync(c => c.Id == chapterId, ct); return chapter; } - public async Task> GetChapterDtoByIdsAsync(IEnumerable chapterIds, int userId) + public async Task> GetChapterDtoByIdsAsync(IEnumerable chapterIds, int userId, + CancellationToken ct = default) { - var chapters = await _context.Chapter + var chapters = await context.Chapter .Where(c => chapterIds.Contains(c.Id)) .Includes(ChapterIncludes.Files | ChapterIncludes.People) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() - .ToListAsync() ; + .ToListAsync(ct) ; return chapters; } - public async Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) + public async Task GetChapterMetadataDtoAsync(int chapterId, + ChapterIncludes includes = ChapterIncludes.Files, CancellationToken ct = default) { - var chapter = await _context.Chapter + var chapter = await context.Chapter .Includes(includes) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsNoTracking() .AsSplitQuery() - .SingleOrDefaultAsync(c => c.Id == chapterId); + .SingleOrDefaultAsync(c => c.Id == chapterId, ct); return chapter; } @@ -200,209 +148,226 @@ public class ChapterRepository : IChapterRepository /// Returns non-tracked files for a given chapterId /// /// + /// /// - public async Task> GetFilesForChapterAsync(int chapterId) + public async Task> GetFilesForChapterAsync(int chapterId, CancellationToken ct = default) { - return await _context.MangaFile + return await context.MangaFile .Where(c => chapterId == c.ChapterId) .AsNoTracking() - .ToListAsync(); - } - - public async Task GetFilesizeForChapterAsync(int chapterId) - { - return await _context.MangaFile - .Where(c => c.ChapterId == chapterId) - .SumAsync(c => c.Bytes); - } - - public async Task> GetFilesizeForChaptersAsync(IList chapterIds) - { - return await chapterIds.BatchToDictionaryAsync(50, batch => - _context.MangaFile - .Where(f => batch.Contains(f.ChapterId)) - .ToDictionaryAsync(f => f.ChapterId, f => f.Bytes)); + .ToListAsync(ct); } /// - /// Returns a Chapter for an Id. Includes linked s. + /// Returns a Chapter for an id. Includes linked s. /// /// /// + /// /// - public async Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) + public async Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files, + CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Includes(includes) .OrderBy(c => c.SortOrder) - .FirstOrDefaultAsync(c => c.Id == chapterId); + .FirstOrDefaultAsync(c => c.Id == chapterId, ct); } /// /// Returns Chapters for a volume id. /// /// + /// + /// /// - public async Task> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None) + public async Task> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None, + CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.VolumeId == volumeId) .Includes(includes) .OrderBy(c => c.SortOrder) - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns Chapters for a volume id with Progress /// /// + /// + /// /// - public async Task> GetChapterDtosAsync(int volumeId, int userId) + public async Task> GetChapterDtosAsync(int volumeId, int userId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.VolumeId == volumeId) .Includes(ChapterIncludes.Files | ChapterIncludes.People) .OrderBy(c => c.SortOrder) - .ProjectToWithProgress(_mapper, userId) - .ToListAsync(); + .ProjectToWithProgress(mapper, userId) + .ToListAsync(ct); } + /// /// Returns the cover image for a chapter id. /// /// + /// /// - public async Task GetChapterCoverImageAsync(int chapterId) + public async Task GetChapterCoverImageAsync(int chapterId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.Id == chapterId) .Select(c => c.CoverImage) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task> GetAllCoverImagesAsync() + public async Task> GetAllCoverImagesAsync(CancellationToken ct = default) { - return (await _context.Chapter + return (await context.Chapter .Select(c => c.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync())!; + .ToListAsync(ct))!; } - public async Task> GetAllChaptersWithCoversInDifferentEncoding(EncodeFormat format) + public async Task> GetAllChaptersWithCoversInDifferentEncoding(EncodeFormat format, + CancellationToken ct = default) { var extension = format.GetExtension(); - return await _context.Chapter + return await context.Chapter .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns cover images for locked chapters /// /// - public async Task> GetCoverImagesForLockedChaptersAsync() + public async Task> GetCoverImagesForLockedChaptersAsync(CancellationToken ct = default) { - return (await _context.Chapter + return (await context.Chapter .Where(c => c.CoverImageLocked) .Select(c => c.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync())!; + .ToListAsync(ct))!; } /// /// Returns non-tracked files for a set of /// /// List of chapter Ids + /// /// - public async Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds) + public async Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds, + CancellationToken ct = default) { - return await _context.MangaFile + return await context.MangaFile .Where(c => chapterIds.Contains(c.ChapterId)) .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); + } + + public async Task GetFilesizeForChapterAsync(int chapterId, CancellationToken ct = default) + { + return await context.MangaFile + .Where(c => c.ChapterId == chapterId) + .SumAsync(c => c.Bytes, cancellationToken: ct); + } + + public async Task> GetFilesizeForChaptersAsync(IList chapterIds, CancellationToken ct = default) + { + return await chapterIds.BatchToDictionaryAsync(50, batch => + context.MangaFile + .Where(f => batch.Contains(f.ChapterId)) + .ToDictionaryAsync(f => f.ChapterId, f => f.Bytes, cancellationToken: ct)); } /// /// Includes Volumes /// /// + /// /// - public IQueryable GetChaptersForSeries(int seriesId) + public IQueryable GetChaptersForSeries(int seriesId, CancellationToken ct = default) { - return _context.Chapter + return context.Chapter .Where(c => c.Volume.SeriesId == seriesId) .OrderBy(c => c.SortOrder) .Include(c => c.Volume); } - public async Task> GetAllChaptersForSeries(int seriesId) + public async Task> GetAllChaptersForSeries(int seriesId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.Volume.SeriesId == seriesId) .OrderBy(c => c.SortOrder) .Include(c => c.Volume) .Include(c => c.People) .ThenInclude(cp => cp.Person) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetAverageUserRating(int chapterId, int userId) + public async Task GetAverageUserRating(int chapterId, int userId, CancellationToken ct = default) { - // If there is 0 or 1 rating and that rating is you, return 0 back - var countOfRatingsThatAreUser = await _context.AppUserChapterRating + // If there is a 0 or 1 rating and that rating is you, return 0 back + var countOfRatingsThatAreUser = await context.AppUserChapterRating .Where(r => r.ChapterId == chapterId && r.HasBeenRated) - .CountAsync(u => u.AppUserId == userId); + .CountAsync(u => u.AppUserId == userId, ct); + if (countOfRatingsThatAreUser == 1) { return 0; } - var avg = (await _context.AppUserChapterRating + + var avg = await context.AppUserChapterRating .Where(r => r.ChapterId == chapterId && r.HasBeenRated) - .AverageAsync(r => (int?) r.Rating)); + .AverageAsync(r => (int?) r.Rating, ct); + return avg.HasValue ? (int) (avg.Value * 20) : 0; } - public async Task> GetExternalChapterReviewDtos(int chapterId) + public async Task> GetExternalChapterReviewDtos(int chapterId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.Id == chapterId) .SelectMany(c => c.ExternalReviews) // Don't use ProjectTo, it fails to map int to float (??) - .Select(r => _mapper.Map(r)) - .ToListAsync(); + .Select(r => mapper.Map(r)) + .ToListAsync(ct); } - public async Task> GetExternalChapterReview(int chapterId) + public async Task> GetExternalChapterReview(int chapterId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.Id == chapterId) .SelectMany(c => c.ExternalReviews) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetExternalChapterRatingDtos(int chapterId) + public async Task> GetExternalChapterRatingDtos(int chapterId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.Id == chapterId) .SelectMany(c => c.ExternalRatings) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetExternalChapterRatings(int chapterId) + public async Task> GetExternalChapterRatings(int chapterId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.Id == chapterId) .SelectMany(c => c.ExternalRatings) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetCurrentlyReadingChapterAsync(int seriesId, int userId) + public async Task GetCurrentlyReadingChapterAsync(int seriesId, int userId, CancellationToken ct = default) { - var chapterWithProgress = await _context.AppUserProgresses + var chapterWithProgress = await context.AppUserProgresses .Where(p => p.AppUserId == userId) .Join( - _context.Chapter + context.Chapter .Include(c => c.Volume) .Include(c => c.Files), p => p.ChapterId, @@ -410,56 +375,65 @@ public class ChapterRepository : IChapterRepository (p, c) => new { Chapter = c, p.PagesRead } ) .Where(x => x.Chapter.Volume.SeriesId == seriesId) - .Where(x => x.Chapter.Volume.Number != Parser.LooseLeafVolumeNumber) + .Where(x => x.Chapter.Volume.Number != ParserConstants.LooseLeafVolumeNumber) .Where(x => x.PagesRead > 0 && x.PagesRead < x.Chapter.Pages) .OrderBy(x => x.Chapter.Volume.Number) .ThenBy(x => x.Chapter.SortOrder) .AsNoTracking() - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (chapterWithProgress == null) return null; // Map chapter to DTO - var dto = _mapper.Map(chapterWithProgress.Chapter); + var dto = mapper.Map(chapterWithProgress.Chapter); dto.PagesRead = chapterWithProgress.PagesRead; return dto; } - public async Task GetFirstChapterForSeriesAsync(int seriesId, int userId) + public async Task GetFirstChapterForSeriesAsync(int seriesId, int userId, CancellationToken ct = default) { // Get the chapter entity with proper ordering - return await _context.Chapter + return await context.Chapter .Include(c => c.Volume) .Include(c => c.Files) .Where(c => c.Volume.SeriesId == seriesId) .ApplyDefaultChapterOrdering() .AsNoTracking() - .ProjectToWithProgress(_mapper, userId) - .FirstOrDefaultAsync(); + .ProjectToWithProgress(mapper, userId) + .FirstOrDefaultAsync(ct); } - public async Task GetFirstChapterForVolumeAsync(int volumeId, int userId) + public async Task GetFirstChapterForVolumeAsync(int volumeId, int userId, CancellationToken ct = default) { // Get the chapter entity with proper ordering - return await _context.Chapter + return await context.Chapter .Include(c => c.Volume) .Include(c => c.Files) .Where(c => c.Volume.Id == volumeId) .ApplyDefaultChapterOrdering() .AsNoTracking() - .ProjectToWithProgress(_mapper, userId) - .FirstOrDefaultAsync(); + .ProjectToWithProgress(mapper, userId) + .FirstOrDefaultAsync(ct); } - public async Task> GetChapterDtosAsync(IEnumerable chapterIds, int userId) + public async Task> GetChapterDtosAsync(IEnumerable chapterIds, int userId, + CancellationToken ct = default) { var chapterIdList = chapterIds.ToList(); if (chapterIdList.Count == 0) return []; - return await _context.Chapter + return await context.Chapter .Where(c => chapterIdList.Contains(c.Id)) - .ProjectToWithProgress(_mapper, userId) - .ToListAsync(); + .ProjectToWithProgress(mapper, userId) + .ToListAsync(ct); + } + + public async Task GetSeriesIdForChapter(int chapterId, CancellationToken ct = default) + { + return await context.Chapter + .Where(chp => chp.Id == chapterId) + .Select(chp => chp.Volume.SeriesId) + .FirstOrDefaultAsync(ct); } } diff --git a/Kavita.Database/Repositories/ClientDeviceRepository.cs b/Kavita.Database/Repositories/ClientDeviceRepository.cs new file mode 100644 index 000000000..3d85e84a1 --- /dev/null +++ b/Kavita.Database/Repositories/ClientDeviceRepository.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.User; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class ClientDeviceRepository(DataContext context, IMapper mapper): IClientDeviceRepository +{ + public Task GetClientDeviceById(int id, int userId, CancellationToken cancellationToken = default) + { + return context.ClientDevice.FirstOrDefaultAsync(c => c.Id == id && c.AppUserId == userId, cancellationToken); + } + + public async Task GetClientDeviceByClientFingerprint(int userId, string uiFingerprint, CancellationToken cancellationToken) + { + return await context.ClientDevice + .Include(d => d.History.OrderByDescending(h => h.CapturedAtUtc).Take(1)) + .FirstOrDefaultAsync(d => + d.AppUserId == userId && + d.UiFingerprint == uiFingerprint && + d.IsActive, cancellationToken: cancellationToken); + } + + public async Task> GetUserDevicesAsync(int userId, bool includeInactive = false, CancellationToken cancellationToken = default) + { + return await context.ClientDevice + .Where(d => d.AppUserId == userId) + .WhereIf(!includeInactive, d => d.IsActive) + .OrderByDescending(d => d.LastSeenUtc) + .ToListAsync(cancellationToken: cancellationToken); + } + + public async Task> GetUserDeviceDtosAsync(int userId, bool includeInactive = false, CancellationToken cancellationToken = default) + { + return await context.ClientDevice + .Where(d => d.AppUserId == userId) + .WhereIf(!includeInactive, d => d.IsActive) + .OrderByDescending(d => d.LastSeenUtc) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(cancellationToken: cancellationToken); + } + + public async Task> GetAllUserDeviceDtos(bool includeInactive = false, + CancellationToken cancellationToken = default) + { + return await context.ClientDevice + .WhereIf(!includeInactive, d => d.IsActive) + .OrderByDescending(d => d.LastSeenUtc) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(cancellationToken: cancellationToken); + } +} diff --git a/Kavita.Database/Repositories/CollectionTagRepository.cs b/Kavita.Database/Repositories/CollectionTagRepository.cs new file mode 100644 index 000000000..20158b23e --- /dev/null +++ b/Kavita.Database/Repositories/CollectionTagRepository.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + + + +public class CollectionTagRepository(DataContext context, IMapper mapper) : ICollectionTagRepository +{ + public void Remove(AppUserCollection tag) + { + context.AppUserCollection.Remove(tag); + } + + public void Update(AppUserCollection tag) + { + context.Entry(tag).State = EntityState.Modified; + } + + /// + /// Removes any collection tags without any series + /// + /// + public async Task RemoveCollectionsWithoutSeries(CancellationToken ct = default) + { + var tagsToDelete = await context.AppUserCollection + .Include(c => c.Items) + .Where(c => c.Items.Count == 0) + .AsSplitQuery() + .ToListAsync(ct); + + context.RemoveRange(tagsToDelete); + + return await context.SaveChangesAsync(ct); + } + + public async Task> GetAllCollectionsAsync( + CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default) + { + return await context.AppUserCollection + .OrderBy(c => c.NormalizedTitle) + .Includes(includes) + .ToListAsync(ct); + } + + public async Task> GetCollectionDtosAsync(int userId, + bool includePromoted = false, CancellationToken ct = default) + { + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.AppUserCollection + .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) + .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) + .OrderBy(uc => uc.Title) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } + + public async Task GetCollectionDtoAsync(int collectionId, int userId, CancellationToken ct = default) + { + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.AppUserCollection + .Where(uc => (uc.AppUserId == userId || uc.Promoted) && uc.Id == collectionId) + .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) + .OrderBy(uc => uc.Title) + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); + } + + public async Task> GetCollectionDtosPagedAsync(int userId, UserParams userParams, + bool includePromoted = false, CancellationToken ct = default) + { + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var collections = context.AppUserCollection + .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) + .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) + .OrderBy(uc => uc.Title) + .ProjectTo(mapper.ConfigurationProvider); + + return await PagedList.CreateAsync(collections, userParams, ct); + } + + public async Task> GetCollectionDtosBySeriesAsync(int userId, int seriesId, + bool includePromoted = false, CancellationToken ct = default) + { + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.AppUserCollection + .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) + .Where(uc => uc.Items.Any(s => s.Id == seriesId)) + .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) + .OrderBy(uc => uc.Title) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } + + public async Task GetCoverImageAsync(int collectionTagId, CancellationToken ct = default) + { + return await context.AppUserCollection + .Where(c => c.Id == collectionTagId) + .Select(c => c.CoverImage) + .SingleOrDefaultAsync(ct); + } + + public async Task> GetAllCoverImagesAsync(CancellationToken ct = default) + { + return await context.AppUserCollection + .Select(t => t.CoverImage) + .Where(t => !string.IsNullOrEmpty(t)) + .ToListAsync(ct); + } + + /// + /// If any tag exists for that given user's collections + /// + /// + /// + /// + /// + public async Task CollectionExists(string title, int userId, CancellationToken ct = default) + { + var normalized = title.ToNormalized(); + return await context.AppUserCollection + .Where(uc => uc.AppUserId == userId) + .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized), ct); + } + + public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, + CancellationToken ct = default) + { + var extension = encodeFormat.GetExtension(); + return await context.AppUserCollection + .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) + .ToListAsync(ct); + } + + public async Task> GetRandomCoverImagesAsync(int collectionId, CancellationToken ct = default) + { + var random = new Random(); + var data = await context.AppUserCollection + .Where(t => t.Id == collectionId) + .SelectMany(uc => uc.Items.Select(series => series.CoverImage)) + .Where(t => !string.IsNullOrEmpty(t)) + .ToListAsync(ct); + + return data + .OrderBy(_ => random.Next()) + .Take(4) + .ToList(); + } + + public async Task> GetCollectionsForUserAsync(int userId, + CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default) + { + return await context.AppUserCollection + .Where(c => c.AppUserId == userId) + .Includes(includes) + .ToListAsync(ct); + } + + public async Task UpdateCollectionAgeRating(AppUserCollection tag, CancellationToken ct = default) + { + var maxAgeRating = await context.AppUserCollection + .Where(t => t.Id == tag.Id) + .SelectMany(uc => uc.Items.Select(s => s.Metadata)) + .Select(sm => sm.AgeRating) + .ToListAsync(ct); + + + tag.AgeRating = maxAgeRating.Count != 0 ? maxAgeRating.Max() : AgeRating.Unknown; + await context.SaveChangesAsync(ct); + } + + public async Task> GetCollectionsByIds(IEnumerable tags, + CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default) + { + return await context.AppUserCollection + .Where(c => tags.Contains(c.Id)) + .Includes(includes) + .AsSplitQuery() + .ToListAsync(ct); + } + + public async Task> GetAllCollectionsForSyncing(DateTime expirationTime, + CancellationToken ct = default) + { + return await context.AppUserCollection + .Where(c => c.Source == ScrobbleProvider.Mal) + .Where(c => c.LastSyncUtc <= expirationTime) + .Include(c => c.Items) + .AsSplitQuery() + .ToListAsync(ct); + } + + public async Task GetCollectionAsync(int tagId, + CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default) + { + return await context.AppUserCollection + .Where(c => c.Id == tagId) + .Includes(includes) + .AsSplitQuery() + .SingleOrDefaultAsync(ct); + } + + private async Task GetUserAgeRestriction(int userId, CancellationToken ct = default) + { + return await context.AppUser + .AsNoTracking() + .Where(u => u.Id == userId) + .Select(u => + new AgeRestriction(){ + AgeRating = u.AgeRestriction, + IncludeUnknowns = u.AgeRestrictionIncludeUnknowns + }) + .SingleAsync(ct); + } + + public async Task> SearchTagDtosAsync(string searchQuery, int userId, CancellationToken ct = default) + { + var userRating = await GetUserAgeRestriction(userId, ct); + return await context.AppUserCollection + .Search(searchQuery, userId, userRating) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } +} diff --git a/API/Data/Repositories/CoverDbRepository.cs b/Kavita.Database/Repositories/CoverDbRepository.cs similarity index 95% rename from API/Data/Repositories/CoverDbRepository.cs rename to Kavita.Database/Repositories/CoverDbRepository.cs index 5d7b4b726..8dd7049c2 100644 --- a/API/Data/Repositories/CoverDbRepository.cs +++ b/Kavita.Database/Repositories/CoverDbRepository.cs @@ -2,12 +2,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using API.DTOs.CoverDb; -using API.Entities.Person; +using Kavita.Models.DTOs.CoverDb; +using Kavita.Models.Entities.Person; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -namespace API.Data.Repositories; +namespace Kavita.Database.Repositories; #nullable enable /// diff --git a/Kavita.Database/Repositories/DeviceRepository.cs b/Kavita.Database/Repositories/DeviceRepository.cs new file mode 100644 index 000000000..688be844e --- /dev/null +++ b/Kavita.Database/Repositories/DeviceRepository.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class DeviceRepository(DataContext context, IMapper mapper) : IDeviceRepository +{ + public void Update(Device device) + { + context.Entry(device).State = EntityState.Modified; + } + + public async Task> GetDevicesForUserAsync(int userId, CancellationToken ct = default) + { + return await context.Device + .Where(d => d.AppUserId == userId) + .OrderBy(d => d.LastUsed) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } + + public async Task GetDeviceById(int deviceId, CancellationToken ct = default) + { + return await context.Device + .Where(d => d.Id == deviceId) + .SingleOrDefaultAsync(ct); + } +} diff --git a/Kavita.Database/Repositories/EmailHistoryRepository.cs b/Kavita.Database/Repositories/EmailHistoryRepository.cs new file mode 100644 index 000000000..e912107d0 --- /dev/null +++ b/Kavita.Database/Repositories/EmailHistoryRepository.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Email; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class EmailHistoryRepository(DataContext context, IMapper mapper) : IEmailHistoryRepository +{ + public async Task> GetEmailDtos(UserParams userParams, CancellationToken ct = default) + { + return await context.EmailHistory + .OrderByDescending(h => h.SendDate) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } +} diff --git a/Kavita.Database/Repositories/EpubFontRepository.cs b/Kavita.Database/Repositories/EpubFontRepository.cs new file mode 100644 index 000000000..59120112e --- /dev/null +++ b/Kavita.Database/Repositories/EpubFontRepository.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Common.Extensions; +using Kavita.Models; +using Kavita.Models.DTOs.Font; +using Kavita.Models.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class EpubFontRepository(DataContext context, IMapper mapper) : IEpubFontRepository +{ + public void Add(EpubFont font) + { + context.Add(font); + } + + public void Remove(EpubFont font) + { + context.Remove(font); + } + + public void Update(EpubFont font) + { + context.Entry(font).State = EntityState.Modified; + } + + public async Task> GetFontDtosAsync(CancellationToken ct = default) + { + return await context.EpubFont + .OrderBy(s => s.Name == Defaults.DefaultFont ? -1 : 0) + .ThenBy(s => s) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } + + public async Task GetFontDtoAsync(int fontId, CancellationToken ct = default) + { + return await context.EpubFont + .Where(f => f.Id == fontId) + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); + } + + public async Task GetFontDtoByNameAsync(string name, CancellationToken ct = default) + { + return await context.EpubFont + .Where(f => f.NormalizedName.Equals(name.ToNormalized())) + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); + } + + public async Task> GetFontsAsync(CancellationToken ct = default) + { + return await context.EpubFont.ToListAsync(ct); + } + + public async Task GetFontAsync(int fontId, CancellationToken ct = default) + { + return await context.EpubFont + .Where(f => f.Id == fontId) + .FirstOrDefaultAsync(ct); + } + + public async Task IsFontInUseAsync(int fontId, CancellationToken ct = default) + { + return await context.AppUserReadingProfiles + .Join(context.EpubFont, + preference => preference.BookReaderFontFamily, + font => font.Name, + (preference, font) => new { preference, font }) + .AnyAsync(joined => joined.font.Id == fontId, ct); + } + +} diff --git a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs b/Kavita.Database/Repositories/ExternalSeriesMetadataRepository.cs similarity index 59% rename from API/Data/Repositories/ExternalSeriesMetadataRepository.cs rename to Kavita.Database/Repositories/ExternalSeriesMetadataRepository.cs index 1e7302b05..fc31955b1 100644 --- a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs +++ b/Kavita.Database/Repositories/ExternalSeriesMetadataRepository.cs @@ -1,127 +1,101 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.DTOs.KavitaPlus.Manage; -using API.DTOs.Recommendation; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions.QueryExtensions; -using API.Helpers; -using API.Services.Plus; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.KavitaPlus.Manage; +using Kavita.Models.DTOs.Recommendation; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -public interface IExternalSeriesMetadataRepository +public class ExternalSeriesMetadataRepository(DataContext context, IMapper mapper) : IExternalSeriesMetadataRepository { - void Attach(ExternalSeriesMetadata metadata); - void Attach(ExternalRating rating); - void Attach(ExternalReview review); - void Remove(IEnumerable? reviews); - void Remove(IEnumerable? ratings); - void Remove(IEnumerable? recommendations); - void Remove(ExternalSeriesMetadata metadata); - Task GetExternalSeriesMetadata(int seriesId); - Task NeedsDataRefresh(int seriesId); - Task GetSeriesDetailPlusDto(int seriesId); - Task LinkRecommendationsToSeries(Series series); - Task IsBlacklistedSeries(int seriesId); - Task> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false); - Task> GetAllSeries(ManageMatchFilterDto filter, UserParams userParams); -} - -public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public ExternalSeriesMetadataRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Attach(ExternalSeriesMetadata metadata) { - _context.ExternalSeriesMetadata.Attach(metadata); + context.ExternalSeriesMetadata.Attach(metadata); } public void Attach(ExternalRating rating) { - _context.ExternalRating.Attach(rating); + context.ExternalRating.Attach(rating); } public void Attach(ExternalReview review) { - _context.ExternalReview.Attach(review); + context.ExternalReview.Attach(review); } public void Remove(IEnumerable? reviews) { if (reviews == null) return; - _context.ExternalReview.RemoveRange(reviews); + context.ExternalReview.RemoveRange(reviews); } public void Remove(IEnumerable? ratings) { if (ratings == null) return; - _context.ExternalRating.RemoveRange(ratings); + context.ExternalRating.RemoveRange(ratings); } public void Remove(IEnumerable? recommendations) { if (recommendations == null) return; - _context.ExternalRecommendation.RemoveRange(recommendations); + context.ExternalRecommendation.RemoveRange(recommendations); } public void Remove(ExternalSeriesMetadata? metadata) { if (metadata == null) return; - _context.ExternalSeriesMetadata.Remove(metadata); + context.ExternalSeriesMetadata.Remove(metadata); } /// /// Returns the ExternalSeriesMetadata entity for the given Series including all linked tables /// /// + /// /// - public Task GetExternalSeriesMetadata(int seriesId) + public Task GetExternalSeriesMetadata(int seriesId, CancellationToken ct = default) { - return _context.ExternalSeriesMetadata + return context.ExternalSeriesMetadata .Where(s => s.SeriesId == seriesId) .Include(s => s.ExternalReviews) .Include(s => s.ExternalRatings.OrderBy(r => r.AverageScore)) .Include(s => s.ExternalRecommendations.OrderBy(r => r.Id)) .AsSplitQuery() - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task NeedsDataRefresh(int seriesId) + public async Task NeedsDataRefresh(int seriesId, CancellationToken ct = default) { - // TODO: Add unit test - return await _context.ExternalSeriesMetadata + return await context.ExternalSeriesMetadata .Where(s => s.SeriesId == seriesId) .Select(s => s.ValidUntilUtc) .Where(date => date < DateTime.UtcNow) - .AnyAsync(); + .AnyAsync(ct); } - public async Task GetSeriesDetailPlusDto(int seriesId) + public async Task GetSeriesDetailPlusDto(int seriesId, CancellationToken ct = default) { - // TODO: Add unit test - var seriesDetailDto = await _context.ExternalSeriesMetadata + var seriesDetailDto = await context.ExternalSeriesMetadata .Where(m => m.SeriesId == seriesId) .Include(m => m.ExternalRatings) .Include(m => m.ExternalReviews) .Include(m => m.ExternalRecommendations) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (seriesDetailDto == null) { @@ -130,7 +104,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor var externalSeriesRecommendations = seriesDetailDto.ExternalRecommendations .Where(r => r.SeriesId == null) - .Select(r => _mapper.Map(r)) + .Select(mapper.Map) .ToList(); var ownedIds = seriesDetailDto.ExternalRecommendations @@ -138,11 +112,11 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor .Select(r => r.SeriesId) .ToList(); - var ownedSeriesRecommendations = await _context.Series + var ownedSeriesRecommendations = await context.Series .Where(s => ownedIds.Contains(s.Id)) .OrderBy(s => s.SortName.ToLower()) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); IEnumerable reviews = []; if (seriesDetailDto.ExternalReviews != null && seriesDetailDto.ExternalReviews.Any()) @@ -150,7 +124,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor reviews = seriesDetailDto.ExternalReviews .Select(r => { - var ret = _mapper.Map(r); + var ret = mapper.Map(r); ret.IsExternal = true; return ret; }) @@ -161,7 +135,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Count != 0) { ratings = seriesDetailDto.ExternalRatings - .Select(r => _mapper.Map(r)); + .Select(mapper.Map); } @@ -183,36 +157,38 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor /// Searches Recommendations without a SeriesId on record and attempts to link based on Series Name/Localized Name /// /// + /// /// - public async Task LinkRecommendationsToSeries(Series series) + public async Task LinkRecommendationsToSeries(Series series, CancellationToken ct = default) { - var recMatches = await _context.ExternalRecommendation + var recMatches = await context.ExternalRecommendation .Where(r => r.SeriesId == null || r.SeriesId == 0) .Where(r => EF.Functions.Like(r.Name, series.Name) || EF.Functions.Like(r.Name, series.LocalizedName)) - .ToListAsync(); + .ToListAsync(ct); foreach (var rec in recMatches) { rec.SeriesId = series.Id; } - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(ct); } - public Task IsBlacklistedSeries(int seriesId) + public Task IsBlacklistedSeries(int seriesId, CancellationToken ct = default) { - return _context.Series + return context.Series .Where(s => s.Id == seriesId) .Select(s => s.IsBlacklisted) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false) + public async Task> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false, + CancellationToken ct = default) { - return await _context.Series - .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) + return await context.Series + .Where(s => !IExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) .Where(s => s.Library.AllowMetadataMatching) .WhereIf(includeStaleData, s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow) .Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.AniListId == 0) @@ -221,21 +197,22 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor .ThenBy(s => s.NormalizedName) .Select(s => s.Id) .Take(limit) - .ToListAsync(); + .ToListAsync(ct); } - public Task> GetAllSeries(ManageMatchFilterDto filter, UserParams userParams) + public Task> GetAllSeries(ManageMatchFilterDto filter, UserParams userParams, + CancellationToken ct = default) { - var source = _context.Series + var source = context.Series .Include(s => s.Library) .Include(s => s.ExternalSeriesMetadata) - .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) + .Where(s => !IExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) .Where(s => s.Library.AllowMetadataMatching) .WhereIf(filter.LibraryType >= 0, s => s.Library.Type == (LibraryType) filter.LibraryType) .FilterMatchState(filter.MatchStateOption) .OrderBy(s => s.NormalizedName) - .ProjectTo(_mapper.ConfigurationProvider); + .ProjectTo(mapper.ConfigurationProvider); - return PagedList.CreateAsync(source, userParams); + return PagedList.CreateAsync(source, userParams, ct); } } diff --git a/API/Data/Repositories/GenreRepository.cs b/Kavita.Database/Repositories/GenreRepository.cs similarity index 55% rename from API/Data/Repositories/GenreRepository.cs rename to Kavita.Database/Repositories/GenreRepository.cs index 7f705e8ae..5fecc8c75 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/Kavita.Database/Repositories/GenreRepository.cs @@ -1,115 +1,92 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.Metadata; -using API.DTOs.Metadata.Browse; -using API.Entities; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Helpers; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.Entities; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -public interface IGenreRepository +public class GenreRepository(DataContext context, IMapper mapper) : IGenreRepository { - void Attach(Genre genre); - void Remove(Genre genre); - Task FindByNameAsync(string genreName); - Task> GetAllGenresAsync(); - Task> GetAllGenresByNamesAsync(IEnumerable normalizedNames); - Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false); - Task> GetAllGenreDtosForLibrariesAsync(int userId, IList? libraryIds = null, QueryContext context = QueryContext.None); - Task GetCountAsync(); - Task GetRandomGenre(); - Task GetGenreById(int id); - Task> GetAllGenresNotInListAsync(ICollection genreNames); - Task> GetBrowseableGenre(int userId, UserParams userParams); -} - -public class GenreRepository : IGenreRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public GenreRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Attach(Genre genre) { - _context.Genre.Attach(genre); + context.Genre.Attach(genre); } public void Remove(Genre genre) { - _context.Genre.Remove(genre); + context.Genre.Remove(genre); } - public async Task FindByNameAsync(string genreName) + public async Task FindByNameAsync(string genreName, CancellationToken ct = default) { var normalizedName = genreName.ToNormalized(); - return await _context.Genre - .FirstOrDefaultAsync(g => g.NormalizedTitle != null && g.NormalizedTitle.Equals(normalizedName)); + return await context.Genre + .FirstOrDefaultAsync(g => g.NormalizedTitle != null && g.NormalizedTitle.Equals(normalizedName), cancellationToken: ct); } - public async Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false) + public async Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false, CancellationToken ct = default) { - var genresWithNoConnections = await _context.Genre + var genresWithNoConnections = await context.Genre .Include(p => p.SeriesMetadatas) .Include(p => p.Chapters) .Where(p => p.SeriesMetadatas.Count == 0 && p.Chapters.Count == 0) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(cancellationToken: ct); - _context.Genre.RemoveRange(genresWithNoConnections); + context.Genre.RemoveRange(genresWithNoConnections); - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(ct); } - public async Task GetCountAsync() + public async Task GetCountAsync(CancellationToken ct = default) { - return await _context.Genre.CountAsync(); + return await context.Genre.CountAsync(cancellationToken: ct); } - public async Task GetRandomGenre() + public async Task GetRandomGenre(CancellationToken ct = default) { - var genreCount = await GetCountAsync(); + var genreCount = await GetCountAsync(ct); if (genreCount == 0) return null; var randomIndex = new Random().Next(0, genreCount); - return await _context.Genre + return await context.Genre .Skip(randomIndex) .Take(1) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(cancellationToken: ct); } - public async Task GetGenreById(int id) + public async Task GetGenreById(int id, CancellationToken ct = default) { - return await _context.Genre + return await context.Genre .Where(g => g.Id == id) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(cancellationToken: ct); } - public async Task> GetAllGenresAsync() + public async Task> GetAllGenresAsync(CancellationToken ct = default) { - return await _context.Genre.ToListAsync(); + return await context.Genre.ToListAsync(ct); } - public async Task> GetAllGenresByNamesAsync(IEnumerable normalizedNames) + public async Task> GetAllGenresByNamesAsync(IEnumerable normalizedNames, + CancellationToken ct = default) { - return await _context.Genre + return await context.Genre .Where(g => normalizedNames.Contains(g.NormalizedTitle)) - .ToListAsync(); + .ToListAsync(ct); } /// @@ -118,26 +95,29 @@ public class GenreRepository : IGenreRepository /// /// /// + /// + /// /// - public async Task> GetAllGenreDtosForLibrariesAsync(int userId, IList? libraryIds = null, QueryContext context = QueryContext.None) + public async Task> GetAllGenreDtosForLibrariesAsync(int userId, IList? libraryIds = null, + QueryContext context1 = QueryContext.None, CancellationToken ct = default) { - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = await _context.Library.GetUserLibraries(userId, context).ToListAsync(); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = await context.Library.GetUserLibraries(userId, context1).ToListAsync(ct); if (libraryIds is {Count: > 0}) { userLibs = userLibs.Where(libraryIds.Contains).ToList(); } - return await _context.Series + return await context.Series .Where(s => userLibs.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) .SelectMany(s => s.Metadata.Genres) .AsSplitQuery() .Distinct() .OrderBy(p => p.NormalizedTitle) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } /// @@ -145,22 +125,24 @@ public class GenreRepository : IGenreRepository /// Normalizes genres for lookup, but returns non-normalized names for creation. /// /// The list of genre names (non-normalized). + /// /// A list of genre names that do not exist in the system. - public async Task> GetAllGenresNotInListAsync(ICollection genreNames) + public async Task> GetAllGenresNotInListAsync(ICollection genreNames, + CancellationToken ct = default) { // Group the genres by their normalized names, keeping track of the original names var normalizedToOriginalMap = genreNames .Distinct() - .GroupBy(Parser.Normalize) + .GroupBy(g => g.ToNormalized()) .ToDictionary(group => group.Key, group => group.First()); // Take the first original name for each normalized name var normalizedGenreNames = normalizedToOriginalMap.Keys.ToList(); // Query the database for existing genres using the normalized names - var existingGenres = await _context.Genre + var existingGenres = await context.Genre .Where(g => normalizedGenreNames.Contains(g.NormalizedTitle)) // Assuming you have a normalized field .Select(g => g.NormalizedTitle) - .ToListAsync(); + .ToListAsync(ct); // Find the normalized genres that do not exist in the database var missingGenres = normalizedGenreNames.Except(existingGenres).ToList(); @@ -169,16 +151,19 @@ public class GenreRepository : IGenreRepository return missingGenres.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList(); } - public async Task> GetBrowseableGenre(int userId, UserParams userParams) + public async Task> GetBrowseableGenre(int userId, UserParams userParams, + CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); - var allLibrariesCount = await _context.Library.CountAsync(); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + var allLibrariesCount = await context.Library.CountAsync(ct); + var userLibs = await context.Library.GetUserLibraries(userId).ToListAsync(ct); - var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync(); + var seriesIds = await context.Series + .Where(s => userLibs.Contains(s.LibraryId)) + .Select(s => s.Id).ToListAsync(ct); - var query = _context.Genre + var query = context.Genre .RestrictAgainstAgeRestriction(ageRating) .WhereIf(allLibrariesCount != userLibs.Count, genre => genre.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) || @@ -200,6 +185,6 @@ public class GenreRepository : IGenreRepository }) .OrderBy(g => g.Title); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } } diff --git a/API/Data/Repositories/LibraryRepository.cs b/Kavita.Database/Repositories/LibraryRepository.cs similarity index 53% rename from API/Data/Repositories/LibraryRepository.cs rename to Kavita.Database/Repositories/LibraryRepository.cs index 9c32b169c..b001b1aa8 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/Kavita.Database/Repositories/LibraryRepository.cs @@ -2,179 +2,136 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.DTOs.Filtering; -using API.DTOs.JumpBar; -using API.DTOs.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; using Kavita.Common.Extensions; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.JumpBar; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum LibraryIncludes + + +public class LibraryRepository(DataContext context, IMapper mapper) : ILibraryRepository { - None = 1, - Series = 2, - AppUser = 4, - Folders = 8, - FileTypes = 16, - ExcludePatterns = 32 -} - -public interface ILibraryRepository -{ - void Add(Library library); - void Update(Library library); - void Delete(Library? library); - Task> GetLibraryDtosAsync(); - Task GetLibraryDtoByIdAsync(int libraryId); - Task GetLiteLibraryDtoByIdAsync(int libraryId); - Task LibraryExists(string libraryName); - Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None); - Task> GetLibraryDtosForUsernameAsync(string userName); - Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, bool track = true); - Task> GetLibrariesForUserIdAsync(int userId); - Task> GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None); - Task GetLibraryTypeAsync(int libraryId); - Task GetLibraryTypeBySeriesIdAsync(int seriesId); - Task> GetLibraryForIdsAsync(IEnumerable libraryIds, LibraryIncludes includes = LibraryIncludes.None); - Task GetTotalFiles(); - IEnumerable GetJumpBarAsync(int libraryId); - Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds); - Task> GetAllLanguagesForLibrariesAsync(List? libraryIds); - IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds); - Task DoAnySeriesFoldersMatch(IEnumerable folders); - Task GetLibraryCoverImageAsync(int libraryId); - Task> GetAllCoverImagesAsync(); - Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); - Task GetAllowsScrobblingBySeriesId(int seriesId); - - Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds); -} - -public class LibraryRepository : ILibraryRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public LibraryRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Add(Library library) { - _context.Library.Add(library); + context.Library.Add(library); } public void Update(Library library) { - _context.Entry(library).State = EntityState.Modified; + context.Entry(library).State = EntityState.Modified; } public void Delete(Library? library) { if (library == null) return; - _context.Library.Remove(library); + context.Library.Remove(library); } - public async Task> GetLibraryDtosForUsernameAsync(string userName) + public async Task> GetLibraryDtosForUsernameAsync(string userName, CancellationToken ct = default) { - return await _context.Library + return await context.Library .Include(l => l.AppUsers) .Include(l => l.LibraryFileTypes) .Include(l => l.LibraryExcludePatterns) .Where(library => library.AppUsers.Any(x => x.UserName!.Equals(userName))) .OrderBy(l => l.Name) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns all libraries including their AppUsers + extra includes /// /// + /// + /// /// - public async Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, bool track = true) + public async Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, + bool track = true, CancellationToken ct = default) { - var query = _context.Library + var query = context.Library .Include(l => l.AppUsers) .Includes(includes) .AsSplitQuery(); - if (track) return await query.ToListAsync(); + if (track) return await query.ToListAsync(ct); - return await query.AsNoTracking().ToListAsync(); + return await query.AsNoTracking().ToListAsync(ct); } /// /// This does not track /// /// + /// /// - public async Task> GetLibrariesForUserIdAsync(int userId) + public async Task> GetLibrariesForUserIdAsync(int userId, CancellationToken ct = default) { - return await _context.Library + return await context.Library .Include(l => l.AppUsers) .Where(l => l.AppUsers.Select(ap => ap.Id).Contains(userId)) .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None) + public async Task> GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None, + CancellationToken ct = default) { - return await _context.Library + return await context.Library .IsRestricted(queryContext) .Where(l => l.AppUsers.Select(ap => ap.Id).Contains(userId)) .Select(l => l.Id) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetLibraryTypeAsync(int libraryId) + public async Task GetLibraryTypeAsync(int libraryId, CancellationToken ct = default) { - return await _context.Library + return await context.Library .Where(l => l.Id == libraryId) .AsNoTracking() .Select(l => l.Type) - .FirstAsync(); + .FirstAsync(ct); } - public async Task GetLibraryTypeBySeriesIdAsync(int seriesId) + public async Task GetLibraryTypeBySeriesIdAsync(int seriesId, CancellationToken ct = default) { - return await _context.Series + return await context.Series .Where(s => s.Id == seriesId) .Select(s => s.Library.Type) - .FirstAsync(); + .FirstAsync(ct); } - public async Task> GetLibraryForIdsAsync(IEnumerable libraryIds, LibraryIncludes includes = LibraryIncludes.None) + public async Task> GetLibraryForIdsAsync(IEnumerable libraryIds, + LibraryIncludes includes = LibraryIncludes.None, CancellationToken ct = default) { - return await _context.Library + return await context.Library .Where(x => libraryIds.Contains(x.Id)) .Includes(includes) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetTotalFiles() + public async Task GetTotalFiles(CancellationToken ct = default) { - return await _context.MangaFile.CountAsync(); + return await context.MangaFile.CountAsync(ct); } - public IEnumerable GetJumpBarAsync(int libraryId) + public IEnumerable GetJumpBarAsync(int libraryId, CancellationToken ct = default) { - var seriesSortCharacters = _context.Series.Where(s => s.LibraryId == libraryId) + var seriesSortCharacters = context.Series.Where(s => s.LibraryId == libraryId) .Select(s => s.SortName!.ToUpper()) .OrderBy(s => s) .AsEnumerable() @@ -203,69 +160,61 @@ public class LibraryRepository : ILibraryRepository /// /// Returns all Libraries with their Folders /// + /// /// - public async Task> GetLibraryDtosAsync() + public async Task> GetLibraryDtosAsync(CancellationToken ct = default) { - return await _context.Library + return await context.Library .Include(f => f.Folders) .Include(l => l.LibraryFileTypes) .OrderBy(l => l.Name) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetLibraryDtoByIdAsync(int libraryId) + public async Task GetLibraryDtoByIdAsync(int libraryId, CancellationToken ct = default) { - return await _context.Library + return await context.Library .Include(f => f.Folders) .Include(l => l.LibraryFileTypes) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() - .FirstOrDefaultAsync(l => l.Id == libraryId); + .FirstOrDefaultAsync(l => l.Id == libraryId, ct); } - public async Task GetLiteLibraryDtoByIdAsync(int libraryId) + public async Task GetLiteLibraryDtoByIdAsync(int libraryId, CancellationToken ct = default) { - return await _context.Library - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(l => l.Id == libraryId); + return await context.Library + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(l => l.Id == libraryId, ct); } - public async Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None) + public async Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None, + CancellationToken ct = default) { - var query = _context.Library + var query = context.Library .Where(x => x.Id == libraryId) .Includes(includes); - return await query.SingleOrDefaultAsync(); + return await query.SingleOrDefaultAsync(ct); } - public async Task LibraryExists(string libraryName) + public async Task LibraryExists(string libraryName, CancellationToken ct = default) { - return await _context.Library + return await context.Library .AsNoTracking() - .AnyAsync(x => x.Name != null && x.Name.Equals(libraryName)); - } - - public async Task> GetLibrariesForUserAsync(AppUser user) - { - return await _context.Library - .Where(library => library.AppUsers.Contains(user)) - .Include(l => l.Folders) - .AsNoTracking() - .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .AnyAsync(x => x.Name != null && x.Name.Equals(libraryName), ct); } - public async Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds) + public async Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds, + CancellationToken ct = default) { - return await _context.Series + return await context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Select(s => s.Metadata.AgeRating) .Distinct() @@ -274,22 +223,23 @@ public class LibraryRepository : ILibraryRepository Value = s, Title = s.ToDescription() }) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllLanguagesForLibrariesAsync(List? libraryIds) + public async Task> GetAllLanguagesForLibrariesAsync(List? libraryIds, + CancellationToken ct = default) { - var ret = await _context.Series + var ret = await context.Series .WhereIf(libraryIds is {Count: > 0} , s => libraryIds!.Contains(s.LibraryId)) .Select(s => s.Metadata.Language) .AsSplitQuery() .AsNoTracking() .Distinct() - .ToListAsync(); + .ToListAsync(ct); return ret .Where(s => !string.IsNullOrEmpty(s)) - .DistinctBy(Parser.Normalize) + .DistinctBy(l => l.ToNormalized()) .Select(GetCulture) .Where(s => s != null) .OrderBy(s => s.Title) @@ -318,9 +268,10 @@ public class LibraryRepository : ILibraryRepository }; } - public IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds) + public IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds, + CancellationToken ct = default) { - return _context.Series + return context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .AsSplitQuery() .Select(s => s.Metadata.PublicationStatus) @@ -338,54 +289,57 @@ public class LibraryRepository : ILibraryRepository /// Checks if any series folders match the folders passed in /// /// + /// /// - public async Task DoAnySeriesFoldersMatch(IEnumerable folders) + public async Task DoAnySeriesFoldersMatch(IEnumerable folders, CancellationToken ct = default) { - var normalized = folders.Select(Parser.NormalizePath); - return await _context.Series.AnyAsync(s => normalized.Contains(s.FolderPath)); + var normalized = folders.Select(f => f.NormalizePath()); + return await context.Series.AnyAsync(s => normalized.Contains(s.FolderPath), ct); } - public Task GetLibraryCoverImageAsync(int libraryId) + public Task GetLibraryCoverImageAsync(int libraryId, CancellationToken ct = default) { - return _context.Library + return context.Library .Where(l => l.Id == libraryId) .Select(l => l.CoverImage) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task> GetAllCoverImagesAsync() + public async Task> GetAllCoverImagesAsync(CancellationToken ct = default) { - return (await _context.ReadingList + return (await context.ReadingList .Select(t => t.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync())!; + .ToListAsync(ct))!; } - public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) + public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, + CancellationToken ct = default) { var extension = encodeFormat.GetExtension(); - return await _context.Library + return await context.Library .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetAllowsScrobblingBySeriesId(int seriesId) + public async Task GetAllowsScrobblingBySeriesId(int seriesId, CancellationToken ct = default) { - return await _context.Series.Where(s => s.Id == seriesId) + return await context.Series.Where(s => s.Id == seriesId) .Select(s => s.Library.AllowScrobbling) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds) + public async Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds, + CancellationToken ct = default) { - return await _context.Series + return await context.Series .Where(series => seriesIds.Contains(series.Id)) .Select(series => new { series.Id, series.Library.Type }) - .ToDictionaryAsync(entity => entity.Id, entity => entity.Type); + .ToDictionaryAsync(entity => entity.Id, entity => entity.Type, ct); } } diff --git a/Kavita.Database/Repositories/MangaFileRepository.cs b/Kavita.Database/Repositories/MangaFileRepository.cs new file mode 100644 index 000000000..bd414df5d --- /dev/null +++ b/Kavita.Database/Repositories/MangaFileRepository.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Repositories; +using Kavita.Models.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class MangaFileRepository(DataContext context) : IMangaFileRepository +{ + public void Update(MangaFile file) + { + context.Entry(file).State = EntityState.Modified; + } + + public async Task> GetAllWithMissingExtension(CancellationToken ct = default) + { + return await context.MangaFile + .Where(f => string.IsNullOrEmpty(f.Extension)) + .ToListAsync(ct); + } + + public async Task GetByKoreaderHash(string hash, CancellationToken ct = default) + { + if (string.IsNullOrEmpty(hash)) return null; + + return await context.MangaFile + .FirstOrDefaultAsync(f => f.KoreaderHash != null && + f.KoreaderHash.Equals(hash.ToUpper()), ct); + } +} diff --git a/Kavita.Database/Repositories/MediaErrorRepository.cs b/Kavita.Database/Repositories/MediaErrorRepository.cs new file mode 100644 index 000000000..12db86b1f --- /dev/null +++ b/Kavita.Database/Repositories/MediaErrorRepository.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.MediaErrors; +using Kavita.Models.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class MediaErrorRepository(DataContext context, IMapper mapper) : IMediaErrorRepository +{ + public void Attach(MediaError? error) + { + if (error == null) return; + context.MediaError.Attach(error); + } + + public void Remove(MediaError? error) + { + if (error == null) return; + context.MediaError.Remove(error); + } + + public void Remove(IList errors) + { + context.MediaError.RemoveRange(errors); + } + + public Task Find(string filename, CancellationToken ct = default) + { + return context.MediaError + .Where(e => e.FilePath == filename) + .FirstOrDefaultAsync(ct); + } + + public async Task> GetAllErrorDtosAsync(CancellationToken ct = default) + { + return await context.MediaError + .OrderByDescending(m => m.Created) + .ProjectTo(mapper.ConfigurationProvider) + .AsNoTracking() + .ToListAsync(ct); + } + + public Task ExistsAsync(MediaError error, CancellationToken ct = default) + { + return context.MediaError.AnyAsync(m => m.FilePath.Equals(error.FilePath) + && m.Comment.Equals(error.Comment) + && m.Details.Equals(error.Details), ct + ); + } + + public async Task DeleteAll(CancellationToken ct = default) + { + await context.MediaError.ExecuteDeleteAsync(ct); + } + + public Task> GetAllErrorsAsync(IList comments, CancellationToken ct = default) + { + return context.MediaError + .Where(m => comments.Contains(m.Comment)) + .ToListAsync(ct); + } +} diff --git a/API/Data/Repositories/PersonRepository.cs b/Kavita.Database/Repositories/PersonRepository.cs similarity index 52% rename from API/Data/Repositories/PersonRepository.cs rename to Kavita.Database/Repositories/PersonRepository.cs index 3ad2f5dee..f0aac5da5 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/Kavita.Database/Repositories/PersonRepository.cs @@ -1,152 +1,88 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data.Misc; -using API.DTOs; -using API.DTOs.Filtering.v2; -using API.DTOs.Metadata.Browse; -using API.DTOs.Metadata.Browse.Requests; -using API.DTOs.Person; -using API.Entities.Enums; -using API.Entities.Person; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; -using API.Helpers.Converters; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Converters; +using Kavita.Database.Extensions; +using Kavita.Database.Extensions.Filters; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.DTOs.Person; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum PersonIncludes +public class PersonRepository(DataContext context, IMapper mapper) : IPersonRepository { - None = 1 << 0, - Aliases = 1 << 1, - ChapterPeople = 1 << 2, - SeriesPeople = 1 << 3, - - All = Aliases | ChapterPeople | SeriesPeople, -} - -public interface IPersonRepository -{ - void Attach(Person person); - void Attach(IEnumerable person); - void Remove(Person person); - void Remove(ChapterPeople person); - void Remove(SeriesMetadataPeople person); - void Update(Person person); - - Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases); - Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None); - Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None); - Task RemoveAllPeopleNoLongerAssociated(); - Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, PersonIncludes includes = PersonIncludes.None); - - Task GetCoverImageAsync(int personId); - Task> GetAllCoverImagesAsync(); - Task GetCoverImageByNameAsync(string name); - Task> GetRolesForPersonByName(int personId, int userId); - Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams); - Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None); - Task GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases); - /// - /// Returns a person matched on normalized name or alias - /// - /// - /// - /// - Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases); - Task IsNameUnique(string name); - - Task> GetSeriesKnownFor(int personId, int userId); - Task> GetChaptersForPersonByRole(int personId, int userId, PersonRole role); - /// - /// Returns all people with a matching name, or alias - /// - /// - /// - /// - Task> GetPeopleByNames(List normalizedNames, PersonIncludes includes = PersonIncludes.Aliases); - Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases); - - Task> SearchPeople(string searchQuery, PersonIncludes includes = PersonIncludes.Aliases); - - Task AnyAliasExist(string alias); -} - -public class PersonRepository : IPersonRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public PersonRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Attach(Person person) { - _context.Person.Attach(person); + context.Person.Attach(person); } public void Attach(IEnumerable person) { - _context.Person.AttachRange(person); + context.Person.AttachRange(person); } public void Remove(Person person) { - _context.Person.Remove(person); + context.Person.Remove(person); } public void Remove(ChapterPeople person) { - _context.ChapterPeople.Remove(person); + context.ChapterPeople.Remove(person); } public void Remove(SeriesMetadataPeople person) { - _context.SeriesMetadataPeople.Remove(person); + context.SeriesMetadataPeople.Remove(person); } public void Update(Person person) { - _context.Person.Update(person); + context.Person.Update(person); } - public async Task RemoveAllPeopleNoLongerAssociated() + public async Task RemoveAllPeopleNoLongerAssociated(CancellationToken ct = default) { - var peopleWithNoConnections = await _context.Person + var peopleWithNoConnections = await context.Person .Include(p => p.SeriesMetadataPeople) .Include(p => p.ChapterPeople) .Where(p => p.SeriesMetadataPeople.Count == 0 && p.ChapterPeople.Count == 0) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); - _context.Person.RemoveRange(peopleWithNoConnections); + context.Person.RemoveRange(peopleWithNoConnections); - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(ct); } - public async Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, PersonIncludes includes = PersonIncludes.Aliases) + public async Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, + PersonIncludes includes = PersonIncludes.None, CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = await context.Library.GetUserLibraries(userId).ToListAsync(ct); if (libraryIds is {Count: > 0}) { userLibs = userLibs.Where(libraryIds.Contains).ToList(); } - return await _context.Series + return await context.Series .Where(s => userLibs.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(ageRating) .SelectMany(s => s.Metadata.People.Select(p => p.Person)) @@ -155,81 +91,83 @@ public class PersonRepository : IPersonRepository .OrderBy(p => p.Name) .AsNoTracking() .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task GetCoverImageAsync(int personId) + public async Task GetCoverImageAsync(int personId, CancellationToken ct = default) { - return await _context.Person + return await context.Person .Where(c => c.Id == personId) .Select(c => c.CoverImage) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task> GetAllCoverImagesAsync() + public async Task> GetAllCoverImagesAsync(CancellationToken ct = default) { - return await _context.Person + return await context.Person .Select(p => p.CoverImage) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetCoverImageByNameAsync(string name) + public async Task GetCoverImageByNameAsync(string name, CancellationToken ct = default) { var normalized = name.ToNormalized(); - return await _context.Person + return await context.Person .Where(c => c.NormalizedName == normalized) .Select(c => c.CoverImage) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task> GetRolesForPersonByName(int personId, int userId) + public async Task> GetRolesForPersonByName(int personId, int userId, + CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = context.Library.GetUserLibraries(userId); // Query roles from ChapterPeople - var chapterRoles = await _context.Person + var chapterRoles = await context.Person .Where(p => p.Id == personId) .SelectMany(p => p.ChapterPeople) .RestrictAgainstAgeRestriction(ageRating) .RestrictByLibrary(userLibs) .Select(cp => cp.Role) .Distinct() - .ToListAsync(); + .ToListAsync(ct); // Query roles from SeriesMetadataPeople - var seriesRoles = await _context.Person + var seriesRoles = await context.Person .Where(p => p.Id == personId) .SelectMany(p => p.SeriesMetadataPeople) .RestrictAgainstAgeRestriction(ageRating) .RestrictByLibrary(userLibs) .Select(smp => smp.Role) .Distinct() - .ToListAsync(); + .ToListAsync(ct); // Combine and return distinct roles return chapterRoles.Union(seriesRoles).Distinct(); } - public async Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams) + public async Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, + UserParams userParams, CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); - var query = await CreateFilteredPersonQueryable(userId, filter, ageRating); + var query = await CreateFilteredPersonQueryable(userId, filter, ageRating, ct); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - private async Task> CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating) + private async Task> CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating, CancellationToken ct = default) { - var allLibrariesCount = await _context.Library.CountAsync(); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + var allLibrariesCount = await context.Library.CountAsync(ct); + var userLibs = await context.Library.GetUserLibraries(userId).ToListAsync(ct); - var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync(); + var seriesIds = await context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync(ct); - var query = _context.Person.AsNoTracking(); + var query = context.Person.AsNoTracking(); // Apply filtering based on statements query = BuildPersonFilterQuery(userId, filter, query); @@ -298,51 +236,54 @@ public class PersonRepository : IPersonRepository return limit <= 0 ? query : query.Take(limit); } - public async Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None) + public async Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None, + CancellationToken ct = default) { - return await _context.Person.Where(p => p.Id == personId) + return await context.Person.Where(p => p.Id == personId) .Includes(includes) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases) + public async Task GetPersonDtoByName(string name, int userId, + PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default) { var normalized = name.ToNormalized(); - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = context.Library.GetUserLibraries(userId); - return await _context.Person + return await context.Person .Where(p => p.NormalizedName == normalized) .Includes(includes) .RestrictAgainstAgeRestriction(ageRating) .RestrictByLibrary(userLibs) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); } - public Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases) + public Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases, + CancellationToken ct = default) { var normalized = name.ToNormalized(); - return _context.Person + return context.Person .Includes(includes) .Where(p => p.NormalizedName == normalized || p.Aliases.Any(pa => pa.NormalizedAlias == normalized)) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task IsNameUnique(string name) + public async Task IsNameUnique(string name, CancellationToken ct = default) { // Should this use Normalized to check? - return !(await _context.Person + return !await context.Person .Includes(PersonIncludes.Aliases) - .AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name))); + .AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name), ct); } - public async Task> GetSeriesKnownFor(int personId, int userId) + public async Task> GetSeriesKnownFor(int personId, int userId, CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = await context.Library.GetUserLibraries(userId).ToListAsync(ct); - return await _context.Person + return await context.Person .Where(p => p.Id == personId) .SelectMany(p => p.SeriesMetadataPeople) .Select(smp => smp.SeriesMetadata) @@ -352,16 +293,17 @@ public class PersonRepository : IPersonRepository .Distinct() .OrderByDescending(s => s.ExternalSeriesMetadata.AverageExternalRating) .Take(20) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectToWithProgress(mapper.ConfigurationProvider, userId) + .ToListAsync(ct); } - public async Task> GetChaptersForPersonByRole(int personId, int userId, PersonRole role) + public async Task> GetChaptersForPersonByRole(int personId, int userId, + PersonRole role, CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = context.Library.GetUserLibraries(userId); - return await _context.ChapterPeople + return await context.ChapterPeople .Where(cp => cp.PersonId == personId && cp.Role == role) .Select(cp => cp.Chapter) .RestrictAgainstAgeRestriction(ageRating) @@ -369,81 +311,87 @@ public class PersonRepository : IPersonRepository .OrderBy(ch => ch.Volume.MinNumber) // Group/Sort volumes as well .ThenBy(ch => ch.SortOrder) .Take(20) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectToWithProgress(mapper.ConfigurationProvider, userId) + .ToListAsync(ct); } - public async Task> GetPeopleByNames(List normalizedNames, PersonIncludes includes = PersonIncludes.Aliases) + public async Task> GetPeopleByNames(List normalizedNames, + PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default) { - return await _context.Person + return await context.Person .Includes(includes) .Where(p => normalizedNames.Contains(p.NormalizedName) || p.Aliases.Any(pa => normalizedNames.Contains(pa.NormalizedAlias))) .OrderBy(p => p.Name) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases) + public async Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases, + CancellationToken ct = default) { - return await _context.Person + return await context.Person .Where(p => p.AniListId == aniListId) .Includes(includes) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> SearchPeople(string searchQuery, PersonIncludes includes = PersonIncludes.Aliases) + public async Task> SearchPeople(string searchQuery, + PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default) { searchQuery = searchQuery.ToNormalized(); - return await _context.Person + return await context.Person .Includes(includes) .Where(p => EF.Functions.Like(p.NormalizedName, $"%{searchQuery}%") || p.Aliases.Any(pa => EF.Functions.Like(pa.NormalizedAlias, $"%{searchQuery}%"))) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task AnyAliasExist(string alias) + public async Task AnyAliasExist(string alias, CancellationToken ct = default) { var normalizedAlias = alias.ToNormalized(); - return await _context.PersonAlias.AnyAsync(pa => pa.NormalizedAlias == normalizedAlias); + return await context.PersonAlias.AnyAsync(pa => pa.NormalizedAlias == normalizedAlias, ct); } - public async Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases) + public async Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases, + CancellationToken ct = default) { - return await _context.Person + return await context.Person .Includes(includes) .OrderBy(p => p.Name) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None) + public async Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None, + CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = context.Library.GetUserLibraries(userId); - return await _context.Person + return await context.Person .Includes(includes) .RestrictAgainstAgeRestriction(ageRating) .RestrictByLibrary(userLibs) .OrderBy(p => p.Name) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None) + public async Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, + PersonIncludes includes = PersonIncludes.None, CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = context.Library.GetUserLibraries(userId); - return await _context.Person + return await context.Person .Where(p => p.SeriesMetadataPeople.Any(smp => smp.Role == role) || p.ChapterPeople.Any(cp => cp.Role == role)) // Filter by role in both series and chapters .Includes(includes) .RestrictAgainstAgeRestriction(ageRating) .RestrictByLibrary(userLibs) .OrderBy(p => p.Name) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } } diff --git a/API/Data/Repositories/ReadingListRepository.cs b/Kavita.Database/Repositories/ReadingListRepository.cs similarity index 59% rename from API/Data/Repositories/ReadingListRepository.cs rename to Kavita.Database/Repositories/ReadingListRepository.cs index a78f3c2ea..9d15a3ef6 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/Kavita.Database/Repositories/ReadingListRepository.cs @@ -1,115 +1,64 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.Person; -using API.DTOs.ReadingLists; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Helpers; -using API.Services; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum ReadingListIncludes +public class ReadingListRepository(DataContext context, IMapper mapper) : IReadingListRepository { - None = 1, - Items = 2, - ItemChapter = 4, -} - -public interface IReadingListRepository -{ - Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true); - Task GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None); - Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId, UserParams? userParams = null); - Task GetReadingListDtoByIdAsync(int readingListId, int userId); - Task GetReadingListDtoByTitleAsync(int userId, string title); - Task> GetReadingListItemsByIdAsync(int readingListId); - Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, - bool includePromoted); - Task> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, - bool includePromoted); - void Remove(ReadingListItem item); - void Add(ReadingList list); - void BulkRemove(IEnumerable items); - void Update(ReadingList list); - Task Count(); - Task GetCoverImageAsync(int readingListId); - Task> GetRandomCoverImagesAsync(int readingListId); - Task> GetAllCoverImagesAsync(); - Task ReadingListExists(string name); - Task ReadingListExistsForUser(string name, int userId); - IEnumerable GetReadingListPeopleAsync(int readingListId, PersonRole role); - Task GetReadingListAllPeopleAsync(int readingListId); - Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); - Task RemoveReadingListsWithoutSeries(); - Task GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items); - Task> GetReadingListsByIds(IList ids, ReadingListIncludes includes = ReadingListIncludes.Items); - Task> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items); - Task GetReadingListInfoAsync(int readingListId); - Task AnyUserReadingProgressAsync(int readingListId, int userId); - Task GetContinueReadingPoint(int readingListId, int userId); - Task GetReadingListItemCountAsync(int readingListId, int userId); -} - -public class ReadingListRepository : IReadingListRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public ReadingListRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Update(ReadingList list) { - _context.Entry(list).State = EntityState.Modified; + context.Entry(list).State = EntityState.Modified; } public void Add(ReadingList list) { - _context.Add(list); + context.Add(list); } - public async Task Count() + public async Task Count(CancellationToken ct = default) { - return await _context.ReadingList.CountAsync(); + return await context.ReadingList.CountAsync(ct); } - public async Task GetCoverImageAsync(int readingListId) + public async Task GetCoverImageAsync(int readingListId, CancellationToken ct = default) { - return await _context.ReadingList + return await context.ReadingList .Where(c => c.Id == readingListId) .Select(c => c.CoverImage) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetAllCoverImagesAsync() + public async Task> GetAllCoverImagesAsync(CancellationToken ct = default) { - return (await _context.ReadingList + return (await context.ReadingList .Select(t => t.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync())!; + .ToListAsync(ct))!; } - public async Task> GetRandomCoverImagesAsync(int readingListId) + public async Task> GetRandomCoverImagesAsync(int readingListId, CancellationToken ct = default) { var random = new Random(); - var data = await _context.ReadingList + var data = await context.ReadingList .Where(r => r.Id == readingListId) .SelectMany(r => r.Items.Select(ri => ri.Chapter.CoverImage)) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync(); + .ToListAsync(ct); return data .OrderBy(_ => random.Next()) @@ -118,46 +67,49 @@ public class ReadingListRepository : IReadingListRepository } - public async Task ReadingListExists(string name) + public async Task ReadingListExists(string name, int? readingListId = null, CancellationToken ct = default) { var normalized = name.ToNormalized(); - return await _context.ReadingList - .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized)); + + return await context.ReadingList + .WhereIf(readingListId != null, x => x.Id != readingListId) + .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized), ct); } - public async Task ReadingListExistsForUser(string name, int userId) + public async Task ReadingListExistsForUser(string name, int userId, CancellationToken ct = default) { var normalized = name.ToNormalized(); - return await _context.ReadingList - .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId); + return await context.ReadingList + .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId, ct); } - public IEnumerable GetReadingListPeopleAsync(int readingListId, PersonRole role) + public IEnumerable GetReadingListPeopleAsync(int readingListId, PersonRole role, + CancellationToken ct = default) { - return _context.ReadingListItem + return context.ReadingListItem .Where(item => item.ReadingListId == readingListId) .SelectMany(item => item.Chapter.People) .Where(p => p.Role == role) .OrderBy(p => p.Person.NormalizedName) .Select(p => p.Person) .Distinct() - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsEnumerable(); } - public async Task GetReadingListAllPeopleAsync(int readingListId) + public async Task GetReadingListAllPeopleAsync(int readingListId, CancellationToken ct = default) { - var allPeople = await _context.ReadingListItem + var allPeople = await context.ReadingListItem .Where(item => item.ReadingListId == readingListId) .SelectMany(item => item.Chapter.People) .OrderBy(p => p.Person.NormalizedName) .Select(p => new { - Role = p.Role, - Person = _mapper.Map(p.Person) + p.Role, + Person = mapper.Map(p.Person) }) .Distinct() - .ToListAsync(); + .ToListAsync(ct); // Create the ReadingListCast object var cast = new ReadingListCast(); @@ -216,62 +168,67 @@ public class ReadingListRepository : IReadingListRepository return cast; } - public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) + public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, + CancellationToken ct = default) { var extension = encodeFormat.GetExtension(); - return await _context.ReadingList + return await context.ReadingList .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) - .ToListAsync(); + .ToListAsync(ct); } - public async Task RemoveReadingListsWithoutSeries() + public async Task RemoveReadingListsWithoutSeries(CancellationToken ct = default) { - var listsToDelete = await _context.ReadingList + var listsToDelete = await context.ReadingList .Include(c => c.Items) .Where(c => c.Items.Count == 0) .AsSplitQuery() - .ToListAsync(); - _context.RemoveRange(listsToDelete); + .ToListAsync(ct); + context.RemoveRange(listsToDelete); - return await _context.SaveChangesAsync(); + return await context.SaveChangesAsync(ct); } - public async Task GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items) + public async Task GetReadingListByTitleAsync(string name, int userId, + ReadingListIncludes includes = ReadingListIncludes.Items, CancellationToken ct = default) { var normalized = name.ToNormalized(); - return await _context.ReadingList + return await context.ReadingList .Includes(includes) - .FirstOrDefaultAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId); + .FirstOrDefaultAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId, ct); } - public async Task> GetReadingListsByIds(IList ids, ReadingListIncludes includes = ReadingListIncludes.Items) + public async Task> GetReadingListsByIds(IList ids, + ReadingListIncludes includes = ReadingListIncludes.Items, CancellationToken ct = default) { - return await _context.ReadingList + return await context.ReadingList .Where(c => ids.Contains(c.Id)) .Includes(includes) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items) + public async Task> GetReadingListsBySeriesId(int seriesId, + ReadingListIncludes includes = ReadingListIncludes.Items, CancellationToken ct = default) { - return await _context.ReadingList + return await context.ReadingList .Where(rl => rl.Items.Any(rli => rli.SeriesId == seriesId)) .Includes(includes) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns a Partial ReadingListInfoDto. The HourEstimate needs to be calculated outside the repo /// /// + /// /// - public async Task GetReadingListInfoAsync(int readingListId) + public async Task GetReadingListInfoAsync(int readingListId, CancellationToken ct = default) { - // Get sum of these across all ReadingListItems: long wordCount, int pageCount, bool isEpub (assume false if any ReadingListeItem.Series.Format is non-epub) - var readingList = await _context.ReadingList + // Get the sum of these across all ReadingListItems: long wordCount, int pageCount, bool isEpub (assume false if any ReadingListItem.Series.Format is non-epub) + var readingList = await context.ReadingList .Where(rl => rl.Id == readingListId) .Include(rl => rl.Items) .ThenInclude(item => item.Series) @@ -285,7 +242,7 @@ public class ReadingListRepository : IReadingListRepository Pages = rl.Items.Sum(item => item.Chapter.Pages), IsAllEpub = rl.Items.All(item => item.Series.Format == MangaFormat.Epub), }) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); return readingList; } @@ -293,104 +250,110 @@ public class ReadingListRepository : IReadingListRepository public void Remove(ReadingListItem item) { - _context.ReadingListItem.Remove(item); + context.ReadingListItem.Remove(item); } public void BulkRemove(IEnumerable items) { - _context.ReadingListItem.RemoveRange(items); + context.ReadingListItem.RemoveRange(items); } - public async Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true) + public async Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, + UserParams userParams, bool sortByLastModified = true, CancellationToken ct = default) { - var user = await _context.AppUser.FirstAsync(u => u.Id == userId); - var query = _context.ReadingList + var user = await context.AppUser.FirstAsync(u => u.Id == userId, ct); + var query = context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) .RestrictAgainstAgeRestriction(user.GetAgeRestriction()); query = sortByLastModified ? query.OrderByDescending(l => l.LastModified) : query.OrderBy(l => l.Title); - var finalQuery = query.ProjectTo(_mapper.ConfigurationProvider) + var finalQuery = query.ProjectTo(mapper.ConfigurationProvider) .AsNoTracking(); - return await PagedList.CreateAsync(finalQuery, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(finalQuery, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, bool includePromoted) + public async Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, + bool includePromoted, CancellationToken ct = default) { - var user = await _context.AppUser.FirstAsync(u => u.Id == userId); - var query = _context.ReadingList + var user = await context.AppUser.FirstAsync(u => u.Id == userId, ct); + var query = context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) .RestrictAgainstAgeRestriction(user.GetAgeRestriction()) .Where(l => l.Items.Any(i => i.SeriesId == seriesId)) .AsSplitQuery() .OrderBy(l => l.Title) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsNoTracking(); - return await query.ToListAsync(); + return await query.ToListAsync(ct); } - public async Task> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, bool includePromoted) + public async Task> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, + bool includePromoted, CancellationToken ct = default) { - var user = await _context.AppUser.FirstAsync(u => u.Id == userId); - var query = _context.ReadingList + var user = await context.AppUser.FirstAsync(u => u.Id == userId, ct); + + var query = context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) .RestrictAgainstAgeRestriction(user.GetAgeRestriction()) .Where(l => l.Items.Any(i => i.ChapterId == chapterId)) .AsSplitQuery() .OrderBy(l => l.Title) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsNoTracking(); - return await query.ToListAsync(); + return await query.ToListAsync(ct); } - public async Task GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None) + public async Task GetReadingListByIdAsync(int readingListId, + ReadingListIncludes includes = ReadingListIncludes.None, CancellationToken ct = default) { - return await _context.ReadingList + return await context.ReadingList .Where(r => r.Id == readingListId) .Includes(includes) .Include(r => r.Items.OrderBy(item => item.Order)) .AsSplitQuery() - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task AnyUserReadingProgressAsync(int readingListId, int userId) + public async Task AnyUserReadingProgressAsync(int readingListId, int userId, CancellationToken ct = default) { // Since the list is already created, we can assume RBS doesn't need to apply - var chapterIdsQuery = _context.ReadingListItem + var chapterIdsQuery = context.ReadingListItem .Where(s => s.ReadingListId == readingListId) .Select(s => s.ChapterId) .AsQueryable(); - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => chapterIdsQuery.Contains(p.ChapterId) && p.AppUserId == userId) .AsNoTracking() - .AnyAsync(); + .AnyAsync(ct); } - public async Task GetContinueReadingPoint(int readingListId, int userId) + public async Task GetContinueReadingPoint(int readingListId, int userId, + CancellationToken ct = default) { - var userLibraries = _context.Library.GetUserLibraries(userId); + var userLibraries = context.Library.GetUserLibraries(userId); - var query = _context.ReadingListItem + var query = context.ReadingListItem .Where(rli => rli.ReadingListId == readingListId) - .Join(_context.Chapter, rli => rli.ChapterId, chapter => chapter.Id, (rli, chapter) => new + .Join(context.Chapter, rli => rli.ChapterId, chapter => chapter.Id, (rli, chapter) => new { ReadingListItem = rli, Chapter = chapter, - FileSize = _context.MangaFile.Where(f => f.ChapterId == chapter.Id).Sum(f => (long?)f.Bytes) ?? 0 + FileSize = context.MangaFile.Where(f => f.ChapterId == chapter.Id).Sum(f => (long?)f.Bytes) ?? 0 }) - .Join(_context.Volume, x => x.ReadingListItem.VolumeId, volume => volume.Id, (x, volume) => new + .Join(context.Volume, x => x.ReadingListItem.VolumeId, volume => volume.Id, (x, volume) => new { x.ReadingListItem, x.Chapter, x.FileSize, Volume = volume }) - .Join(_context.Series, x => x.ReadingListItem.SeriesId, series => series.Id, (x, series) => new + .Join(context.Series, x => x.ReadingListItem.SeriesId, series => series.Id, (x, series) => new { x.ReadingListItem, x.Chapter, @@ -399,7 +362,7 @@ public class ReadingListRepository : IReadingListRepository Series = series }) .Where(x => userLibraries.Contains(x.Series.LibraryId)) - .GroupJoin(_context.AppUserProgresses.Where(p => p.AppUserId == userId), + .GroupJoin(context.AppUserProgresses.Where(p => p.AppUserId == userId), x => x.ReadingListItem.ChapterId, progress => progress.ChapterId, (x, progressGroup) => new @@ -428,20 +391,20 @@ public class ReadingListRepository : IReadingListRepository }) .OrderBy(x => x.ReadingListItem.Order); - // First try to find a partially read item then the first unread item + // First try to find a partially read item, then the first unread item var item = await query .OrderBy(x => x.IsPartiallyRead ? 0 : x.IsUnread ? 1 : 2) .ThenBy(x => x.ReadingListItem.Order) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (item == null) return null; // Map to DTO - var library = await _context.Library + var library = await context.Library .Where(l => l.Id == item.Series.LibraryId) .Select(l => new { l.Name, l.Type }) - .FirstAsync(); + .FirstAsync(ct); var dto = new ReadingListItemDto { @@ -472,21 +435,22 @@ public class ReadingListRepository : IReadingListRepository return dto; } - public Task GetReadingListItemCountAsync(int readingListId, int userId) + public Task GetReadingListItemCountAsync(int readingListId, int userId, CancellationToken ct = default) { - return _context.ReadingListItem.Where(rli => rli.ReadingListId == readingListId).CountAsync(); + return context.ReadingListItem.Where(rli => rli.ReadingListId == readingListId).CountAsync(ct); } - public async Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId, UserParams? userParams = null) + public async Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId, + UserParams? userParams = null, CancellationToken ct = default) { - var userLibraries = _context.Library.GetUserLibraries(userId); + var userLibraries = context.Library.GetUserLibraries(userId); - var query = _context.ReadingListItem + var query = context.ReadingListItem .Where(rli => rli.ReadingListId == readingListId) .Where(rli => userLibraries.Contains(rli.Series.LibraryId)) .OrderBy(rli => rli.Order) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery(); if (userParams != null) @@ -496,34 +460,38 @@ public class ReadingListRepository : IReadingListRepository .Take(userParams.PageSize); } - return await query.ToListAsync(); + return await query.ToListAsync(ct); } - public async Task GetReadingListDtoByIdAsync(int readingListId, int userId) + public async Task GetReadingListDtoByIdAsync(int readingListId, int userId, + CancellationToken ct = default) { - var user = await _context.AppUser.FirstAsync(u => u.Id == userId); - return await _context.ReadingList + var user = await context.AppUser.FirstAsync(u => u.Id == userId, ct); + + return await context.ReadingList .Where(r => r.Id == readingListId && (r.AppUserId == userId || r.Promoted)) .RestrictAgainstAgeRestriction(user.GetAgeRestriction()) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(ct); } - public async Task GetReadingListDtoByTitleAsync(int userId, string title) + public async Task GetReadingListDtoByTitleAsync(int userId, string title, + CancellationToken ct = default) { - return await _context.ReadingList + return await context.ReadingList .Where(r => r.Title.Equals(title) && r.AppUserId == userId) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(ct); } - public async Task> GetReadingListItemsByIdAsync(int readingListId) + public async Task> GetReadingListItemsByIdAsync(int readingListId, + CancellationToken ct = default) { - return await _context.ReadingListItem + return await context.ReadingListItem .Where(r => r.ReadingListId == readingListId) .OrderBy(r => r.Order) - .ToListAsync(); + .ToListAsync(ct); } diff --git a/API/Data/Repositories/ReadingSessionRepository.cs b/Kavita.Database/Repositories/ReadingSessionRepository.cs similarity index 83% rename from API/Data/Repositories/ReadingSessionRepository.cs rename to Kavita.Database/Repositories/ReadingSessionRepository.cs index 26b4da900..5d3ececec 100644 --- a/API/Data/Repositories/ReadingSessionRepository.cs +++ b/Kavita.Database/Repositories/ReadingSessionRepository.cs @@ -1,21 +1,19 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.Progress; using AutoMapper; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.Progress; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; - -public interface IReadingSessionRepository -{ - Task> GetAllReadingSessionAsync(bool isActiveOnly = true); -} +namespace Kavita.Database.Repositories; public class ReadingSessionRepository(DataContext context, IMapper mapper) : IReadingSessionRepository { - public async Task> GetAllReadingSessionAsync(bool isActiveOnly = true) + public async Task> GetAllReadingSessionAsync(bool isActiveOnly = true, + CancellationToken ct = default) { var query = context.AppUserReadingSession .Where(s => !isActiveOnly || s.IsActive); @@ -23,7 +21,7 @@ public class ReadingSessionRepository(DataContext context, IMapper mapper) : IRe var sessions = await query .Include(s => s.ActivityData) .Include(s => s.AppUser) - .ToListAsync(); + .ToListAsync(ct); if (sessions.Count == 0) return []; @@ -37,18 +35,18 @@ public class ReadingSessionRepository(DataContext context, IMapper mapper) : IRe var seriesIds = allActivityData.Select(a => a.SeriesId).Distinct().ToList(); var chapterIds = allActivityData.Select(a => a.ChapterId).Distinct().ToList(); - // Fetch all lookups in parallel - single query per table + // Fetch all lookups in a parallel - single query per table var libraryLookupTask = context.Library .Where(l => libraryIds.Contains(l.Id)) - .ToDictionaryAsync(l => l.Id, l => l.Name); + .ToDictionaryAsync(l => l.Id, l => l.Name, ct); var seriesLookupTask = context.Series .Where(s => seriesIds.Contains(s.Id)) - .ToDictionaryAsync(s => s.Id, s => s.Name); + .ToDictionaryAsync(s => s.Id, s => s.Name, ct); var chapterLookupTask = context.Chapter .Where(c => chapterIds.Contains(c.Id)) - .ToDictionaryAsync(c => c.Id, c => c.TitleName); + .ToDictionaryAsync(c => c.Id, c => c.TitleName, ct); await Task.WhenAll(libraryLookupTask, seriesLookupTask, chapterLookupTask); diff --git a/Kavita.Database/Repositories/ScrobbleEventRepository.cs b/Kavita.Database/Repositories/ScrobbleEventRepository.cs new file mode 100644 index 000000000..90e3a5a26 --- /dev/null +++ b/Kavita.Database/Repositories/ScrobbleEventRepository.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Scrobble; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +/// +/// This handles everything around Scrobbling +/// +public class ScrobbleRepository(DataContext context, IMapper mapper) : IScrobbleRepository +{ + public void Attach(ScrobbleEvent evt) + { + context.ScrobbleEvent.Attach(evt); + } + + public void Attach(ScrobbleError error) + { + context.ScrobbleError.Attach(error); + } + + public void Remove(ScrobbleEvent evt) + { + context.ScrobbleEvent.Remove(evt); + } + + public void Remove(IEnumerable events) + { + context.ScrobbleEvent.RemoveRange(events); + } + + public void Remove(IEnumerable errors) + { + context.ScrobbleError.RemoveRange(errors); + } + + public void Update(ScrobbleEvent evt) + { + context.Entry(evt).State = EntityState.Modified; + } + + public async Task> GetByEvent(ScrobbleEventType type, bool isProcessed = false, + CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Include(s => s.Series) + .ThenInclude(s => s.Library) + .Include(s => s.Series) + .ThenInclude(s => s.Metadata) + .Include(s => s.AppUser) + .ThenInclude(u => u.UserPreferences) + .Where(s => s.ScrobbleEventType == type) + .Where(s => s.IsProcessed == isProcessed) + .AsSplitQuery() + .GroupBy(s => s.SeriesId) + .Select(g => g.OrderByDescending(e => e.ChapterNumber) + .ThenByDescending(e => e.VolumeNumber) + .First()) + .ToListAsync(ct); + } + + /// + /// Returns all processed events processed 7 or more days ago + /// + /// + /// + /// + public async Task> GetProcessedEvents(int daysAgo, CancellationToken ct = default) + { + var date = DateTime.UtcNow.Subtract(TimeSpan.FromDays(daysAgo)); + return await context.ScrobbleEvent + .Where(s => s.IsProcessed) + .Where(s => s.ProcessDateUtc != null && s.ProcessDateUtc < date) + .ToListAsync(ct); + } + + public async Task Exists(int userId, int seriesId, ScrobbleEventType eventType, CancellationToken ct = default) + { + return await context.ScrobbleEvent.AnyAsync(e => + e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType, ct); + } + + public async Task> GetScrobbleErrors(CancellationToken ct = default) + { + return await context.ScrobbleError + .OrderBy(e => e.LastModifiedUtc) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } + + public async Task> GetAllScrobbleErrorsForSeries(int seriesId, CancellationToken ct = default) + { + return await context.ScrobbleError + .Where(e => e.SeriesId == seriesId) + .ToListAsync(ct); + } + + public async Task ClearScrobbleErrors(CancellationToken ct = default) + { + context.ScrobbleError.RemoveRange(context.ScrobbleError); + await context.SaveChangesAsync(ct); + } + + public async Task HasErrorForSeries(int seriesId, CancellationToken ct = default) + { + return await context.ScrobbleError.AnyAsync(n => n.SeriesId == seriesId, ct); + } + + public async Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, + bool isNotProcessed = false, CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Where(e => e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType) + .WhereIf(isNotProcessed, e => !e.IsProcessed) + .OrderBy(e => e.LastModifiedUtc) + .FirstOrDefaultAsync(ct); + } + + public async Task> GetUserEventsForSeries(int userId, int seriesId, + CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Where(e => e.AppUserId == userId && !e.IsProcessed && e.SeriesId == seriesId) + .Include(e => e.Series) + .OrderBy(e => e.LastModifiedUtc) + .AsSplitQuery() + .ToListAsync(ct); + } + + public async Task> GetUserEvents(int userId, IList scrobbleEventIds, + CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Where(e => e.AppUserId == userId && scrobbleEventIds.Contains(e.Id)) + .ToListAsync(ct); + } + + public async Task> GetUserEvents(int userId, ScrobbleEventFilter filter, + UserParams pagination, CancellationToken ct = default) + { + var query = context.ScrobbleEvent + .Where(e => e.AppUserId == userId) + .Include(e => e.Series) + .WhereIf(!string.IsNullOrEmpty(filter.Query), s => + EF.Functions.Like(s.Series.Name, $"%{filter.Query}%") + ) + .WhereIf(!filter.IncludeReviews, e => e.ScrobbleEventType != ScrobbleEventType.Review) + .SortBy(filter.Field, filter.IsDescending) + .AsSplitQuery() + .ProjectTo(mapper.ConfigurationProvider); + + return await PagedList.CreateAsync(query, pagination.PageNumber, pagination.PageSize, ct); + } + + public async Task> GetAllEventsForSeries(int seriesId, CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Where(e => e.SeriesId == seriesId) + .ToListAsync(ct); + } + + public async Task> GetAllEventsWithSeriesIds(IEnumerable seriesIds, + CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Where(e => seriesIds.Contains(e.SeriesId)) + .ToListAsync(ct); + } + + public async Task> GetEvents(CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Include(e => e.AppUser) + .ToListAsync(ct); + } +} diff --git a/Kavita.Database/Repositories/SeriesMetadataRepository.cs b/Kavita.Database/Repositories/SeriesMetadataRepository.cs new file mode 100644 index 000000000..57bc17df6 --- /dev/null +++ b/Kavita.Database/Repositories/SeriesMetadataRepository.cs @@ -0,0 +1,14 @@ +using Kavita.API.Repositories; +using Kavita.Models.Entities.Metadata; + +namespace Kavita.Database.Repositories; + + + +public class SeriesMetadataRepository(DataContext context) : ISeriesMetadataRepository +{ + public void Update(SeriesMetadata seriesMetadata) + { + context.SeriesMetadata.Update(seriesMetadata); + } +} diff --git a/API/Data/Repositories/SeriesRepository.cs b/Kavita.Database/Repositories/SeriesRepository.cs similarity index 70% rename from API/Data/Repositories/SeriesRepository.cs rename to Kavita.Database/Repositories/SeriesRepository.cs index d461fa386..c4642696f 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/Kavita.Database/Repositories/SeriesRepository.cs @@ -3,213 +3,82 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; -using API.Constants; -using API.Data.Misc; -using API.Data.Scanner; -using API.DTOs; -using API.DTOs.Collection; -using API.DTOs.Dashboard; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.Reader; -using API.DTOs.ReadingLists; -using API.DTOs.Scrobbling; -using API.DTOs.Search; -using API.DTOs.SeriesDetail; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; -using API.Helpers.Converters; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.Services.Tasks; -using API.Services.Tasks.Scanner; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Converters; +using Kavita.Database.Extensions; +using Kavita.Database.Extensions.Filters; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.Search; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Models.Misc; +using Kavita.Models.Parser; using Microsoft.EntityFrameworkCore; +using YamlDotNet.Core; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum SeriesIncludes +public class SeriesRepository(DataContext context, IMapper mapper) : ISeriesRepository { - None = 1, - Volumes = 2, - /// - /// This will include all necessary includes - /// - Metadata = 4, - Related = 8, - Library = 16, - Chapters = 32, - ExternalReviews = 64, - ExternalRatings = 128, - ExternalRecommendations = 256, - ExternalMetadata = 512, - - ExternalData = ExternalMetadata | ExternalReviews | ExternalRatings | ExternalRecommendations, -} - -/// -/// For complex queries, Library has certain restrictions where the library should not be included in results. -/// This enum dictates which field to use for the lookup. -/// -public enum QueryContext -{ - None = 1, - Search = 2, - [Obsolete("Use Dashboard")] - Recommended = 3, - Dashboard = 4, -} - -public interface ISeriesRepository -{ - void Add(Series series); - void Attach(SeriesRelation relation); - void Update(Series series); - void Update(SeriesMetadata seriesMetadata); - void Remove(Series series); - void Remove(IEnumerable series); - Task DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format); - /// - /// Adds user information like progress, ratings, etc - /// - /// - /// - /// Pagination info - /// Filtering/Sorting to apply - /// - Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter); - /// - /// Does not add user information like progress, ratings, etc. - /// - /// - /// - /// - /// - /// Includes Files in the Search - /// - Task SearchSeries(int userId, bool isAdmin, IList libraryIds, string searchQuery, bool includeChapterAndFiles = true); - Task> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None); - Task GetSeriesDtoByIdAsync(int seriesId, int userId); - Task GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata); - Task> GetSeriesDtoByIdsAsync(IEnumerable seriesIds, AppUser user); - Task> GetSeriesByIdsAsync(IList seriesIds, bool fullSeries = true); - Task GetChapterIdsForSeriesAsync(IList seriesIds); - Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds); - Task GetSeriesCoverImageAsync(int seriesId); - Task> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto? filter); - Task> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter); - Task> GetRecentlyAddedV2(int userId, UserParams userParams, FilterV2Dto filter); - Task GetSeriesMetadata(int seriesId); - Task> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams); - Task> GetFilesForSeries(int seriesId); - Task GetFilesizeForSeriesAsync(int seriesId); - Task> GetFilesizeForMultipleSeriesAsync(IList seriesIds); - Task> GetSeriesDtoForIdsAsync(IEnumerable seriesIds, int userId); - Task> GetAllCoverImagesAsync(); - Task> GetLockedCoverImagesAsync(); - Task> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams); - Task GetFullSeriesForSeriesIdAsync(int seriesId); - Task GetChunkInfo(int libraryId = 0); - Task> GetRecentlyUpdatedSeries(int userId, UserParams? userParams); - Task GetRelatedSeries(int userId, int seriesId); - Task> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind); - Task> GetQuickReads(int userId, int libraryId, UserParams userParams); - Task> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams); - Task> GetHighlyRated(int userId, int libraryId, UserParams userParams); - Task> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams); - Task> GetRediscover(int userId, int libraryId, UserParams userParams); - Task GetSeriesForMangaFile(int mangaFileId, int userId); - Task GetSeriesForChapter(int chapterId, int userId); - Task> GetWantToReadForUserAsync(int userId, UserParams userParams, FilterDto filter); - Task> GetWantToReadForUserV2Async(int userId, UserParams userParams, FilterV2Dto filter); - Task> GetWantToReadForUserAsync(int userId); - Task IsSeriesInWantToRead(int userId, int seriesId); - Task GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None); - Task GetSeriesThatContainsLowestFolderPath(string path, SeriesIncludes includes = SeriesIncludes.None); - Task> GetAllSeriesByNameAsync(IList normalizedNames, - int userId, SeriesIncludes includes = SeriesIncludes.None); - Task GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true); - Task GetSeriesByAnyName(IList names, IList formats, - int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None); - Task GetSeriesByAnyName(string seriesName, string localizedName, IList formats, int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None); - public Task> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId, - MangaFormat format); - Task> RemoveSeriesNotInList(IList seenSeries, int libraryId); - Task>> GetFolderPathMap(int libraryId); - Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds); - Task> GetSeriesMetadataForIds(IEnumerable seriesIds); - Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true); - Task GetSeriesDtoByNamesAndMetadataIds(IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl); - Task GetAverageUserRating(int seriesId, int userId); - Task RemoveFromOnDeck(int seriesId, int userId); - Task ClearOnDeckRemoval(int seriesId, int userId); - Task> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None); - Task GetPlusSeriesDto(int seriesId); - Task MatchSeries(ExternalSeriesDetailDto externalSeries); -} - -public class SeriesRepository : ISeriesRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - private readonly Regex _yearRegex = new(@"\d{4}", RegexOptions.Compiled, - Services.Tasks.Scanner.Parser.Parser.RegexTimeout); - - public SeriesRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } + private readonly Regex _yearRegex = new(@"\d{4}", RegexOptions.Compiled, TimeSpan.FromMilliseconds(500)); public void Add(Series series) { - _context.Series.Add(series); + context.Series.Add(series); } public void Attach(SeriesRelation relation) { - _context.SeriesRelation.Attach(relation); + context.SeriesRelation.Attach(relation); } public void Attach(ExternalSeriesMetadata metadata) { - _context.ExternalSeriesMetadata.Attach(metadata); + context.ExternalSeriesMetadata.Attach(metadata); } public void Update(Series series) { - _context.Entry(series).State = EntityState.Modified; + context.Entry(series).State = EntityState.Modified; } public void Update(SeriesMetadata seriesMetadata) { - _context.Entry(seriesMetadata).State = EntityState.Modified; + context.Entry(seriesMetadata).State = EntityState.Modified; } public void Remove(Series series) { - _context.Series.Remove(series); + context.Series.Remove(series); } public void Remove(IEnumerable series) { - _context.Series.RemoveRange(series); + context.Series.RemoveRange(series); } /// @@ -218,23 +87,26 @@ public class SeriesRepository : ISeriesRepository /// Name of series /// /// Format of series + /// /// - public async Task DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format) + public async Task DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format, + CancellationToken ct = default) { - return await _context.Series + return await context.Series .AsNoTracking() .Where(s => s.LibraryId == libraryId && s.Name.Equals(name) && s.Format == format) - .AnyAsync(); + .AnyAsync(ct); } - public async Task> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None) + public async Task> GetSeriesForLibraryIdAsync(int libraryId, + SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default) { - return await _context.Series + return await context.Series .Where(s => s.LibraryId == libraryId) .Includes(includes) .OrderBy(s => s.SortName.ToLower()) - .ToListAsync(); + .ToListAsync(ct); } /// @@ -242,11 +114,13 @@ public class SeriesRepository : ISeriesRepository /// /// /// + /// /// - public async Task> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams) + public async Task> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams, + CancellationToken ct = default) { - #nullable disable - var query = _context.Series +#nullable disable + var query = context.Series .Where(s => s.LibraryId == libraryId) .Include(s => s.Metadata) @@ -280,18 +154,19 @@ public class SeriesRepository : ISeriesRepository .OrderBy(s => s.SortName.ToLower()); #nullable enable - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } /// /// This is a heavy call. Returns all entities down to Files and Library and Series Metadata. /// /// + /// /// - public async Task GetFullSeriesForSeriesIdAsync(int seriesId) + public async Task GetFullSeriesForSeriesIdAsync(int seriesId, CancellationToken ct = default) { - #nullable disable - return await _context.Series +#nullable disable + return await context.Series .Where(s => s.Id == seriesId) .Include(s => s.Relations) .Include(s => s.Metadata) @@ -319,8 +194,8 @@ public class SeriesRepository : ISeriesRepository .ThenInclude(v => v.Chapters) .ThenInclude(c => c.Files) .AsSplitQuery() - .SingleOrDefaultAsync(); - #nullable enable + .SingleOrDefaultAsync(ct); +#nullable enable } /// @@ -330,89 +205,92 @@ public class SeriesRepository : ISeriesRepository /// /// /// + /// /// [Obsolete("Use GetSeriesDtoForLibraryIdAsync")] - public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter) + public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, + UserParams userParams, FilterDto filter, CancellationToken ct = default) { - var query = await CreateFilteredSearchQueryable(userId, libraryId, filter, QueryContext.None); + var query = await CreateFilteredSearchQueryable(userId, libraryId, filter, QueryContext.None, ct); var retSeries = query - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() .AsNoTracking(); - return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize, ct); } - private async Task> GetUserLibrariesForFilteredQuery(int libraryId, int userId, QueryContext queryContext) + private async Task> GetUserLibrariesForFilteredQuery(int libraryId, int userId, QueryContext queryContext, CancellationToken ct = default) { if (libraryId == 0) { - return await _context.Library.GetUserLibraries(userId, queryContext).ToListAsync(); + return await context.Library.GetUserLibraries(userId, queryContext).ToListAsync(ct); } return [libraryId]; } - public async Task SearchSeries(int userId, bool isAdmin, IList libraryIds, string searchQuery, bool includeChapterAndFiles = true) + public async Task SearchSeries(int userId, bool isAdmin, IList libraryIds, + string searchQuery, bool includeChapterAndFiles = true, CancellationToken ct = default) { const int maxRecords = 15; var searchQueryNormalized = searchQuery.ToNormalized(); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); var justYear = _yearRegex.Match(searchQuery).Value; var hasYearInQuery = !string.IsNullOrEmpty(justYear); var yearComparison = hasYearInQuery ? int.Parse(justYear) : 0; - var baseSeriesQuery = _context.Series + var baseSeriesQuery = context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating); #region Independent Queries - var librariesTask = _context.Library + var librariesTask = context.Library .Search(searchQuery, userId, libraryIds) .Take(maxRecords) .OrderBy(l => l.Name.ToLower()) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); - var annotationsTask = _context.AppUserAnnotation + var annotationsTask = context.AppUserAnnotation .Where(a => a.AppUserId == userId && (EF.Functions.Like(a.Comment, $"%{searchQueryNormalized}%") || EF.Functions.Like(a.Context, $"%{searchQueryNormalized}%"))) .Take(maxRecords) .OrderBy(l => l.CreatedUtc) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); #endregion var seriesTask = baseSeriesQuery - .Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%") - || (s.OriginalName != null && EF.Functions.Like(s.OriginalName, $"%{searchQuery}%")) - || (s.LocalizedName != null && EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%")) - || EF.Functions.Like(s.NormalizedName, $"%{searchQueryNormalized}%") - || (hasYearInQuery && s.Metadata.ReleaseYear == yearComparison)) - .OrderBy(s => s.SortName!.Length) - .ThenBy(s => s.SortName!.ToLower()) - .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%") + || (s.OriginalName != null && EF.Functions.Like(s.OriginalName, $"%{searchQuery}%")) + || (s.LocalizedName != null && EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%")) + || EF.Functions.Like(s.NormalizedName, $"%{searchQueryNormalized}%") + || (hasYearInQuery && s.Metadata.ReleaseYear == yearComparison)) + .OrderBy(s => s.SortName!.Length) + .ThenBy(s => s.SortName!.ToLower()) + .Take(maxRecords) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); - var readingListsTask = _context.ReadingList + var readingListsTask = context.ReadingList .Search(searchQuery, userId, userRating) .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); - var collectionsTask = _context.AppUserCollection + var collectionsTask = context.AppUserCollection .Search(searchQuery, userId, userRating) .Take(maxRecords) .OrderBy(c => c.NormalizedTitle) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); - var bookmarksTask = _context.AppUserBookmark + var bookmarksTask = context.AppUserBookmark .Where(b => b.AppUserId == userId) .Where(b => libraryIds.Contains(b.Series.LibraryId)) .Where(b => EF.Functions.Like(b.Series.Name, $"%{searchQuery}%") || @@ -431,40 +309,40 @@ public class SeriesRepository : ISeriesRepository VolumeId = g.First().VolumeId }) .Take(maxRecords) - .ToListAsync(); + .ToListAsync(ct); var seriesIdsSubquery = baseSeriesQuery.Select(s => s.Id); - var personsTask = _context.Person - .Where(p => _context.SeriesMetadataPeople + var personsTask = context.Person + .Where(p => context.SeriesMetadataPeople .Any(smp => smp.PersonId == p.Id && seriesIdsSubquery.Contains(smp.SeriesMetadata.SeriesId) && (EF.Functions.Like(p.NormalizedName, $"%{searchQueryNormalized}%") || p.Aliases.Any(a => EF.Functions.Like(a.NormalizedAlias, $"%{searchQueryNormalized}%")) - ))) + ))) .OrderBy(p => p.NormalizedName.Length) .ThenBy(p => p.NormalizedName) .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); - var genresTask = _context.Genre - .Where(g => _context.SeriesMetadata - .Any(sm => seriesIdsSubquery.Contains(sm.SeriesId) && - sm.Genres.Any(sg => sg.Id == g.Id)) && - EF.Functions.Like(g.NormalizedTitle, $"%{searchQueryNormalized}%")) + var genresTask = context.Genre + .Where(g => context.SeriesMetadata + .Any(sm => seriesIdsSubquery.Contains(sm.SeriesId) && + sm.Genres.Any(sg => sg.Id == g.Id)) && + EF.Functions.Like(g.NormalizedTitle, $"%{searchQueryNormalized}%")) .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); - var tagsTask = _context.Tag - .Where(t => _context.SeriesMetadata - .Any(sm => seriesIdsSubquery.Contains(sm.SeriesId) && - sm.Tags.Any(st => st.Id == t.Id)) && - EF.Functions.Like(t.NormalizedTitle, $"%{searchQueryNormalized}%")) + var tagsTask = context.Tag + .Where(t => context.SeriesMetadata + .Any(sm => seriesIdsSubquery.Contains(sm.SeriesId) && + sm.Tags.Any(st => st.Id == t.Id)) && + EF.Functions.Like(t.NormalizedTitle, $"%{searchQueryNormalized}%")) .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); // Run separate DB queries in parallel await Task.WhenAll( @@ -489,7 +367,7 @@ public class SeriesRepository : ISeriesRepository if (includeChapterAndFiles) { // Use EXISTS subquery pattern instead of loading IDs - var chaptersQuery = _context.Chapter + var chaptersQuery = context.Chapter .Where(c => c.Volume.Series.LibraryId > 0 && // Ensure navigation works libraryIds.Contains(c.Volume.Series.LibraryId)) .Where(c => EF.Functions.Like(c.TitleName, $"%{searchQuery}%") @@ -504,19 +382,19 @@ public class SeriesRepository : ISeriesRepository .OrderBy(c => c.TitleName.Length) .ThenBy(c => c.TitleName) .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); if (isAdmin) { - result.Files = await _context.MangaFile + result.Files = await context.MangaFile .Where(f => EF.Functions.Like(f.FilePath, $"%{searchQuery}%")) .Where(f => libraryIds.Contains(f.Chapter.Volume.Series.LibraryId)) .Where(f => baseSeriesQuery.Any(s => s.Id == f.Chapter.Volume.SeriesId)) .OrderBy(f => f.FilePath) .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } } @@ -529,12 +407,12 @@ public class SeriesRepository : ISeriesRepository /// /// /// - public async Task GetSeriesDtoByIdAsync(int seriesId, int userId) + public async Task GetSeriesDtoByIdAsync(int seriesId, int userId, CancellationToken ct = default) { - var series = await _context.Series + var series = await context.Series .Where(x => x.Id == seriesId) - .ProjectToWithProgress(_mapper, userId) - .SingleOrDefaultAsync(); + .ProjectToWithProgress(mapper, userId) + .SingleOrDefaultAsync(ct); return series ?? null; } @@ -544,13 +422,15 @@ public class SeriesRepository : ISeriesRepository /// /// /// + /// /// - public async Task GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata) + public async Task GetSeriesByIdAsync(int seriesId, + SeriesIncludes includes = SeriesIncludes.Metadata | SeriesIncludes.Volumes, CancellationToken ct = default) { - return await _context.Series + return await context.Series .Where(s => s.Id == seriesId) .Includes(includes) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } /// @@ -559,21 +439,21 @@ public class SeriesRepository : ISeriesRepository /// /// Include all the includes or just the Series /// - public async Task> GetSeriesByIdsAsync(IList seriesIds, bool fullSeries = true) + public async Task> GetSeriesByIdsAsync(IList seriesIds, bool fullSeries = true, CancellationToken ct = default) { - var query = _context.Series + var query = context.Series .Where(s => seriesIds.Contains(s.Id)) .AsSplitQuery(); - if (!fullSeries) return await query.ToListAsync(); + if (!fullSeries) return await query.ToListAsync(ct); return await query .Include(s => s.Volumes) - .ThenInclude(v => v.Chapters) - .ThenInclude(c => c.ExternalRatings) + .ThenInclude(v => v.Chapters) + .ThenInclude(c => c.ExternalRatings) .Include(s => s.Volumes) - .ThenInclude(v => v.Chapters) - .ThenInclude(c => c.ExternalReviews) + .ThenInclude(v => v.Chapters) + .ThenInclude(c => c.ExternalReviews) .Include(s => s.Relations) .Include(s => s.Metadata) @@ -585,36 +465,37 @@ public class SeriesRepository : ISeriesRepository .ThenInclude(e => e.ExternalReviews) .Include(s => s.ExternalSeriesMetadata) .ThenInclude(e => e.ExternalRecommendations) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetSeriesDtoByIdsAsync(IEnumerable seriesIds, AppUser user) + public async Task> GetSeriesDtoByIdsAsync(IEnumerable seriesIds, AppUser user, + CancellationToken ct = default) { - var allowedLibraries = await _context.Library + var allowedLibraries = await context.Library .Where(library => library.AppUsers.Any(x => x.Id == user.Id)) .Select(l => l.Id) - .ToListAsync(); + .ToListAsync(ct); var restriction = new AgeRestriction() { AgeRating = user.AgeRestriction, IncludeUnknowns = user.AgeRestrictionIncludeUnknowns }; - return await _context.Series + return await context.Series .Include(s => s.Metadata) .Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(restriction) .AsSplitQuery() - .ProjectToWithProgress(_mapper, user.Id) - .ToListAsync(); + .ProjectToWithProgress(mapper, user.Id) + .ToListAsync(ct); } - public async Task GetChapterIdsForSeriesAsync(IList seriesIds) + public async Task GetChapterIdsForSeriesAsync(IList seriesIds, CancellationToken ct = default) { - var volumes = await _context.Volume + var volumes = await context.Volume .Where(v => seriesIds.Contains(v.SeriesId)) .Include(v => v.Chapters) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); IList chapterIds = new List(); foreach (var v in volumes) @@ -632,14 +513,16 @@ public class SeriesRepository : ISeriesRepository /// This returns a dictionary mapping seriesId -> list of chapters back for each series id passed /// /// + /// /// - public async Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds) + public async Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds, + CancellationToken ct = default) { - var volumes = await _context.Volume + var volumes = await context.Volume .Where(v => seriesIds.Contains(v.SeriesId)) .Include(v => v.Chapters) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); var seriesChapters = new Dictionary>(); foreach (var v in volumes) @@ -658,18 +541,42 @@ public class SeriesRepository : ISeriesRepository return seriesChapters; } - public async Task> GetSeriesMetadataForIds(IEnumerable seriesIds) + public async Task GetFilesizeForSeriesAsync(int seriesId, CancellationToken ct = default) { - return await _context.SeriesMetadata + return await context.Volume + .Where(v => v.SeriesId == seriesId) + .SumAsync(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes)), cancellationToken: ct); + } + + public async Task> GetFilesizeForMultipleSeriesAsync(IList seriesIds, CancellationToken ct = default) + { + return await seriesIds.BatchToDictionaryAsync(50, batch => + context.Volume + .Where(v => batch.Contains(v.SeriesId)) + .GroupBy(v => v.SeriesId) + .Select(g => new + { + SeriesId = g.Key, + TotalBytes = g.SelectMany(v => v.Chapters) + .SelectMany(c => c.Files) + .Sum(f => f.Bytes) + }) + .ToDictionaryAsync(x => x.SeriesId, x => x.TotalBytes, cancellationToken: ct)); + } + + public async Task> GetSeriesMetadataForIds(IEnumerable seriesIds, + CancellationToken ct = default) + { + return await context.SeriesMetadata .Where(metadata => seriesIds.Contains(metadata.SeriesId)) .Include(m => m.Genres.OrderBy(g => g.NormalizedTitle)) .Include(m => m.Tags.OrderBy(g => g.NormalizedTitle)) .Include(m => m.People) .ThenInclude(p => p.Person) .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } /// @@ -678,11 +585,11 @@ public class SeriesRepository : ISeriesRepository /// If customOnly, this will not include any volumes/chapters /// public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, - bool customOnly = true) + bool customOnly = true, CancellationToken ct = default) { var extension = encodeFormat.GetExtension(); - var prefix = ImageService.GetSeriesFormat(0).Replace("0", string.Empty); - var query = _context.Series + var prefix = "series{0}".Replace("0", string.Empty); // default: This actually depends on ImageService#GetSeriesFormat + var query = context.Series .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension) && (!customOnly || c.CoverImage.StartsWith(prefix))) @@ -694,24 +601,25 @@ public class SeriesRepository : ISeriesRepository .ThenInclude(v => v.Chapters); } - return await query.ToListAsync(); + return await query.ToListAsync(ct); } - public async Task> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None) + public async Task> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, + FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None, CancellationToken ct = default) { - var query = await CreateFilteredSearchQueryableV2(userId, filterDto, queryContext); + var query = await CreateFilteredSearchQueryableV2(userId, filterDto, queryContext, ct: ct); - var retSeries = query.ProjectToWithProgress(_mapper, userId); + var retSeries = query.ProjectToWithProgress(mapper, userId); - return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize, ct); } - public async Task GetPlusSeriesDto(int seriesId) + public async Task GetPlusSeriesDto(int seriesId, CancellationToken ct = default) { // I need to check Weblinks when AniListId/MalId is already set in ExternalSeries // Updating stale data should prioritize ExternalSeriesMetadata before Weblinks, to prioritize prior matches - var result = await _context.Series + var result = await context.Series .Where(s => s.Id == seriesId) .Include(s => s.ExternalSeriesMetadata) .Select(series => new PlusSeriesRequestDto() @@ -721,67 +629,70 @@ public class SeriesRepository : ISeriesRepository AltSeriesName = series.LocalizedName, AniListId = series.ExternalSeriesMetadata.AniListId != 0 ? series.ExternalSeriesMetadata.AniListId - : ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.AniListWeblinkWebsite), + : ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingHelper.AniListWeblinkWebsite), MalId = series.ExternalSeriesMetadata.MalId != 0 ? series.ExternalSeriesMetadata.MalId - : ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.MalWeblinkWebsite), + : ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingHelper.MalWeblinkWebsite), CbrId = series.ExternalSeriesMetadata.CbrId, GoogleBooksId = !string.IsNullOrEmpty(series.ExternalSeriesMetadata.GoogleBooksId) ? series.ExternalSeriesMetadata.GoogleBooksId - : ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.GoogleBooksWeblinkWebsite), - MangaDexId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.MangaDexWeblinkWebsite), + : ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingHelper.GoogleBooksWeblinkWebsite), + MangaDexId = ScrobblingHelper.ExtractId(series.Metadata.WebLinks, + ScrobblingHelper.MangaDexWeblinkWebsite), VolumeCount = series.Volumes.Count, ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial), Year = series.Metadata.ReleaseYear }) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); return result; } - public async Task GetSeriesCoverImageAsync(int seriesId) + public async Task GetSeriesCoverImageAsync(int seriesId, CancellationToken ct = default) { - return await _context.Series + return await context.Series .Where(s => s.Id == seriesId) .Select(s => s.CoverImage) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } /// /// Returns a list of Series that were added, ordered by Created desc /// - /// /// Library to restrict to, if 0, will apply to all libraries + /// /// Contains pagination information /// Optional filter on query + /// /// [Obsolete("Use GetRecentlyAddedV2")] - public async Task> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter) + public async Task> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, + FilterDto filter, CancellationToken ct = default) { - var query = await CreateFilteredSearchQueryable(userId, libraryId, filter, QueryContext.Dashboard); + var query = await CreateFilteredSearchQueryable(userId, libraryId, filter, QueryContext.Dashboard, ct); var retSeries = query .OrderByDescending(s => s.Created) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() .AsNoTracking(); - return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetRecentlyAddedV2(int userId, UserParams userParams, FilterV2Dto filter) + public async Task> GetRecentlyAddedV2(int userId, UserParams userParams, FilterV2Dto filter, + CancellationToken ct = default) { - var query = await CreateFilteredSearchQueryableV2(userId, filter, QueryContext.Dashboard); + var query = await CreateFilteredSearchQueryableV2(userId, filter, QueryContext.Dashboard, ct: ct); var retSeries = query .OrderByDescending(s => s.Created) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() .AsNoTracking(); - return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize, ct); } private IList ExtractFilters(int libraryId, int userId, FilterDto filter, ref List userLibraries, @@ -850,7 +761,7 @@ public class SeriesRepository : ISeriesRepository seriesIds = new List(); if (hasProgressFilter) { - seriesIds = _context.Series + seriesIds = context.Series .Include(s => s.Progress) .Select(s => new { @@ -876,37 +787,39 @@ public class SeriesRepository : ISeriesRepository /// Library to restrict to, if 0, will apply to all libraries /// Pagination information /// Optional (default null) filter on query + /// /// - public async Task> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto? filter) + public async Task> GetOnDeck(int userId, int libraryId, UserParams userParams, + FilterDto? filter, CancellationToken ct = default) { - var settings = await _context.ServerSetting + var settings = await context.ServerSetting .Select(x => x) .AsNoTracking() - .ToListAsync(); - var serverSettings = _mapper.Map(settings); + .ToListAsync(ct); + var serverSettings = mapper.Map(settings); var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(serverSettings.OnDeckProgressDays); var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(serverSettings.OnDeckUpdateDays); - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); // Don't allow any series the user has explicitly removed - var onDeckRemovals = _context.AppUserOnDeckRemoval + var onDeckRemovals = context.AppUserOnDeckRemoval .Where(d => d.AppUserId == userId) .Select(d => d.SeriesId) .AsEnumerable(); - var query = _context.Series + var query = context.Series .Where(s => usersSeriesIds.Contains(s.Id)) .Where(s => !onDeckRemovals.Contains(s.Id)) .Select(s => new { Series = s, - PagesRead = _context.AppUserProgresses.Where(p => p.SeriesId == s.Id && p.AppUserId == userId) + PagesRead = context.AppUserProgresses.Where(p => p.SeriesId == s.Id && p.AppUserId == userId) .Sum(s1 => s1.PagesRead), - LatestReadDate = _context.AppUserProgresses + LatestReadDate = context.AppUserProgresses .Where(p => p.SeriesId == s.Id && p.AppUserId == userId) .Max(p => p.LastModified), s.LastChapterAdded, @@ -914,24 +827,24 @@ public class SeriesRepository : ISeriesRepository .Where(s => s.PagesRead > 0 && s.PagesRead < s.Series.Pages) .Where(d => d.LatestReadDate >= cutoffProgressPoint || d.LastChapterAdded >= cutoffLastAddedPoint) - .OrderByDescending(s => s.LatestReadDate) + .OrderByDescending(s => s.LatestReadDate) .ThenByDescending(s => s.LastChapterAdded) .Select(s => s.Series) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() .AsNoTracking(); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - private async Task> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter, QueryContext queryContext) + private async Task> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter, QueryContext queryContext, CancellationToken ct = default) { // NOTE: Why do we even have libraryId when the filter has the actual libraryIds? - var userLibraries = await GetUserLibrariesForFilteredQuery(libraryId, userId, queryContext); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - var onlyParentSeries = await _context.AppUserPreferences.Where(u => u.AppUserId == userId) + var userLibraries = await GetUserLibrariesForFilteredQuery(libraryId, userId, queryContext, ct); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + var onlyParentSeries = await context.AppUserPreferences.Where(u => u.AppUserId == userId) .Select(u => u.CollapseSeriesRelationships) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); var formats = ExtractFilters(libraryId, userId, filter, ref userLibraries, out var allPeopleIds, out var hasPeopleFilter, out var hasGenresFilter, @@ -942,18 +855,18 @@ public class SeriesRepository : ISeriesRepository IList collectionSeries = []; if (hasCollectionTagFilter) { - collectionSeries = await _context.AppUserCollection + collectionSeries = await context.AppUserCollection .Where(uc => uc.Promoted || uc.AppUserId == userId) .Where(uc => filter.CollectionTags.Contains(uc.Id)) .SelectMany(uc => uc.Items) .RestrictAgainstAgeRestriction(userRating) .Select(s => s.Id) .Distinct() - .ToListAsync(); + .ToListAsync(ct); } - var query = _context.Series + var query = context.Series .AsNoTracking() // This new style can handle any filterComparision coming from the user .HasLanguage(hasLanguageFilter, FilterComparison.Contains, filter.Languages) @@ -977,7 +890,7 @@ public class SeriesRepository : ISeriesRepository if (filter.ReadStatus.InProgress) { query = query.HasReadingProgress(hasProgressFilter, FilterComparison.GreaterThan, - 0, userId) + 0, userId) .HasReadingProgress(hasProgressFilter, FilterComparison.LessThan, 100, userId); } else if (filter.ReadStatus.Read) @@ -993,7 +906,7 @@ public class SeriesRepository : ISeriesRepository if (userRating.AgeRating != AgeRating.NotApplicable) { - // this if statement is included in the extension + // this if statement is included in the extension query = query.RestrictAgainstAgeRestriction(userRating); } @@ -1022,16 +935,17 @@ public class SeriesRepository : ISeriesRepository return query.AsSplitQuery(); } - private async Task> CreateFilteredSearchQueryableV2(int userId, FilterV2Dto filter, QueryContext queryContext, IQueryable? query = null) + private async Task> CreateFilteredSearchQueryableV2(int userId, FilterV2Dto filter, + QueryContext queryContext, IQueryable? query = null, CancellationToken ct = default) { - var userLibraries = await GetUserLibrariesForFilteredQuery(0, userId, queryContext); - var allLibraryCount = await _context.Library.CountAsync(); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - var onlyParentSeries = await _context.AppUserPreferences.Where(u => u.AppUserId == userId) + var userLibraries = await GetUserLibrariesForFilteredQuery(0, userId, queryContext, ct); + var allLibraryCount = await context.Library.CountAsync(ct); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + var onlyParentSeries = await context.AppUserPreferences.Where(u => u.AppUserId == userId) .Select(u => u.CollapseSeriesRelationships) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); - query ??= _context.Series + query ??= context.Series .AsNoTracking(); // When the user has no access, just return instantly @@ -1045,7 +959,7 @@ public class SeriesRepository : ISeriesRepository query = ApplyWantToReadFilter(filter, query, userId); - query = await ApplyCollectionFilter(filter, query, userId, userRating); + query = await ApplyCollectionFilter(filter, query, userId, userRating, ct); @@ -1061,12 +975,13 @@ public class SeriesRepository : ISeriesRepository return ApplyLimit(query - .Sort(userId, filter.SortOptions) - .AsSplitQuery() + .Sort(userId, filter.SortOptions) + .AsSplitQuery() , filter.LimitTo); } - private async Task> ApplyCollectionFilter(FilterV2Dto filter, IQueryable query, int userId, AgeRestriction userRating) + private async Task> ApplyCollectionFilter(FilterV2Dto filter, IQueryable query, + int userId, AgeRestriction userRating, CancellationToken ct = default) { var collectionStmt = filter.Statements.FirstOrDefault(stmt => stmt.Field == FilterField.CollectionTags); if (collectionStmt == null) return query; @@ -1078,27 +993,27 @@ public class SeriesRepository : ISeriesRepository return query; } - var collectionSeries = await _context.AppUserCollection + var collectionSeries = await context.AppUserCollection .Where(uc => uc.Promoted || uc.AppUserId == userId) .Where(uc => value.Contains(uc.Id)) .SelectMany(uc => uc.Items) .RestrictAgainstAgeRestriction(userRating) .Select(s => s.Id) .Distinct() - .ToListAsync(); + .ToListAsync(ct); if (collectionStmt.Comparison != FilterComparison.MustContains) return query.HasCollectionTags(true, collectionStmt.Comparison, value, collectionSeries); var collectionSeriesTasks = value.Select(async collectionId => { - return await _context.AppUserCollection + return await context.AppUserCollection .Where(uc => uc.Promoted || uc.AppUserId == userId) .Where(uc => uc.Id == collectionId) .SelectMany(uc => uc.Items) .RestrictAgainstAgeRestriction(userRating) .Select(s => s.Id) - .ToListAsync(); + .ToListAsync(ct); }); var collectionSeriesLists = await Task.WhenAll(collectionSeriesTasks); @@ -1115,7 +1030,7 @@ public class SeriesRepository : ISeriesRepository var wantToReadStmt = filter.Statements.FirstOrDefault(stmt => stmt.Field == FilterField.WantToRead); if (wantToReadStmt == null) return query; - var seriesIds = _context.AppUser.Where(u => u.Id == userId) + var seriesIds = context.AppUser.Where(u => u.Id == userId) .SelectMany(u => u.WantToRead) .Select(s => s.SeriesId); @@ -1278,104 +1193,83 @@ public class SeriesRepository : ISeriesRepository return query.AsSplitQuery(); } - public async Task GetSeriesMetadata(int seriesId) + public async Task GetSeriesMetadata(int seriesId, CancellationToken ct = default) { - return await _context.SeriesMetadata + return await context.SeriesMetadata .Where(metadata => metadata.SeriesId == seriesId) .Include(m => m.Genres.OrderBy(g => g.NormalizedTitle)) .Include(m => m.Tags.OrderBy(g => g.NormalizedTitle)) .Include(m => m.People) .ThenInclude(p => p.Person) .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams) + public async Task> GetSeriesDtoForCollectionAsync(int collectionId, int userId, + UserParams userParams, CancellationToken ct = default) { - var userLibraries = _context.Library.GetUserLibraries(userId); + var userLibraries = context.Library.GetUserLibraries(userId); - var query = _context.AppUserCollection + var query = context.AppUserCollection .Where(s => s.Id == collectionId) .Include(c => c.Items) .SelectMany(c => c.Items.Where(s => userLibraries.Contains(s.LibraryId))) .OrderBy(s => s.LibraryId) .ThenBy(s => s.SortName.ToLower()) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery(); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetFilesForSeries(int seriesId) + public async Task> GetFilesForSeries(int seriesId, CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Where(v => v.SeriesId == seriesId) .Include(v => v.Chapters) .ThenInclude(c => c.Files) .SelectMany(v => v.Chapters.SelectMany(c => c.Files)) .AsSplitQuery() .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetFilesizeForSeriesAsync(int seriesId) + public async Task> GetSeriesDtoForIdsAsync(IEnumerable seriesIds, int userId, + CancellationToken ct = default) { - return await _context.Volume - .Where(v => v.SeriesId == seriesId) - .SumAsync(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))); - } - - public async Task> GetFilesizeForMultipleSeriesAsync(IList seriesIds) - { - return await seriesIds.BatchToDictionaryAsync(50, batch => - _context.Volume - .Where(v => batch.Contains(v.SeriesId)) - .GroupBy(v => v.SeriesId) - .Select(g => new - { - SeriesId = g.Key, - TotalBytes = g.SelectMany(v => v.Chapters) - .SelectMany(c => c.Files) - .Sum(f => f.Bytes) - }) - .ToDictionaryAsync(x => x.SeriesId, x => x.TotalBytes)); - } - - public async Task> GetSeriesDtoForIdsAsync(IEnumerable seriesIds, int userId) - { - var allowedLibraries = _context.Library + var allowedLibraries = context.Library .Include(l => l.AppUsers) .Where(library => library.AppUsers.Any(x => x.Id == userId)) .AsSplitQuery() .Select(l => l.Id); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - return await _context.Series + return await context.Series .RestrictAgainstAgeRestriction(userRating) .Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId)) .OrderBy(s => s.SortName.ToLower()) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsNoTracking() .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllCoverImagesAsync() + public async Task> GetAllCoverImagesAsync(CancellationToken ct = default) { - return (await _context.Series + return (await context.Series .Select(s => s.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync())!; + .ToListAsync(ct))!; } - public async Task> GetLockedCoverImagesAsync() + public async Task> GetLockedCoverImagesAsync(CancellationToken ct = default) { - return (await _context.Series + return (await context.Series .Where(s => s.CoverImageLocked && !string.IsNullOrEmpty(s.CoverImage)) .Select(s => s.CoverImage) - .ToListAsync())!; + .ToListAsync(ct))!; } /// @@ -1383,15 +1277,15 @@ public class SeriesRepository : ISeriesRepository /// /// Defaults to 0, library to restrict count to /// - private async Task GetSeriesCount(int libraryId = 0) + private async Task GetSeriesCount(int libraryId = 0, CancellationToken ct = default) { if (libraryId > 0) { - return await _context.Series + return await context.Series .Where(s => s.LibraryId == libraryId) - .CountAsync(); + .CountAsync(ct); } - return await _context.Series.CountAsync(); + return await context.Series.CountAsync(ct); } /// @@ -1405,11 +1299,11 @@ public class SeriesRepository : ISeriesRepository return new Tuple(totalSeries, 50); } - public async Task GetChunkInfo(int libraryId = 0) + public async Task GetChunkInfo(int libraryId = 0, CancellationToken ct = default) { var (totalSeries, chunkSize) = await GetChunkSize(libraryId); - if (totalSeries == 0) return new Chunk() + if (totalSeries == 0) return new Chunk { TotalChunks = 0, TotalSize = 0, @@ -1418,7 +1312,7 @@ public class SeriesRepository : ISeriesRepository var totalChunks = Math.Max((int) Math.Ceiling((totalSeries * 1.0) / chunkSize), 1); - return new Chunk() + return new Chunk { TotalSize = totalSeries, ChunkSize = chunkSize, @@ -1433,14 +1327,16 @@ public class SeriesRepository : ISeriesRepository /// in memory, we stop after 30 series. /// Used to ensure user has access to libraries /// Page size and offset + /// /// - public async Task> GetRecentlyUpdatedSeries(int userId, UserParams? userParams) + public async Task> GetRecentlyUpdatedSeries(int userId, UserParams? userParams, + CancellationToken ct = default) { userParams ??= UserParams.Default; - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - var items = (await GetRecentlyAddedChaptersQuery(userId)); + var items = await GetRecentlyAddedChaptersQuery(userId, ct); if (userRating.AgeRating != AgeRating.NotApplicable) { items = items.RestrictAgainstAgeRestriction(userRating); @@ -1487,17 +1383,18 @@ public class SeriesRepository : ISeriesRepository return seriesMap.Values.ToList(); } - public async Task> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind) + public async Task> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind, + CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - var usersSeriesIds = _context.Series + var usersSeriesIds = context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) .Select(s => s.Id); - var targetSeries = _context.SeriesRelation + var targetSeries = context.SeriesRelation .Where(sr => sr.SeriesId == seriesId && sr.RelationKind == kind && usersSeriesIds.Contains(sr.TargetSeriesId)) .Include(sr => sr.TargetSeries) @@ -1505,34 +1402,35 @@ public class SeriesRepository : ISeriesRepository .AsNoTracking() .Select(sr => sr.TargetSeriesId); - return await _context.Series + return await context.Series .Where(s => targetSeries.Contains(s.Id)) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams) + public async Task> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams, + CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); // Because this can be called from an API, we need to provide an additional check if the genre has anything the // user with age restrictions can access - var query = _context.Series + var query = context.Series .Where(s => s.Metadata.Genres.Select(g => g.Id).Contains(genreId)) .Where(s => usersSeriesIds.Contains(s.Id)) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() - .ProjectToWithProgress(_mapper, userId); + .ProjectToWithProgress(mapper, userId); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } /// @@ -1541,33 +1439,35 @@ public class SeriesRepository : ISeriesRepository /// /// /// + /// /// - public async Task> GetRediscover(int userId, int libraryId, UserParams userParams) + public async Task> GetRediscover(int userId, int libraryId, UserParams userParams, + CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); - var distinctSeriesIdsWithProgress = _context.AppUserProgresses + var distinctSeriesIdsWithProgress = context.AppUserProgresses .Where(s => usersSeriesIds.Contains(s.SeriesId)) .Select(p => p.SeriesId) .Distinct(); - var query = _context.Series + var query = context.Series .Where(s => distinctSeriesIdsWithProgress.Contains(s.Id) && - _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId) + context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId) .Sum(s1 => s1.PagesRead) >= s.Pages) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider); + .ProjectTo(mapper.ConfigurationProvider); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - public async Task GetSeriesForMangaFile(int mangaFileId, int userId) + public async Task GetSeriesForMangaFile(int mangaFileId, int userId, CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, 0, QueryContext.Search); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, 0, QueryContext.Search); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - return await _context.MangaFile + return await context.MangaFile .Where(m => m.Id == mangaFileId) .AsSplitQuery() .Select(f => f.Chapter) @@ -1575,23 +1475,23 @@ public class SeriesRepository : ISeriesRepository .Select(v => v.Series) .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(ct); } - public async Task GetSeriesForChapter(int chapterId, int userId) + public async Task GetSeriesForChapter(int chapterId, int userId, CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.Chapter + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.Chapter .Where(m => m.Id == chapterId) .AsSplitQuery() .Select(c => c.Volume) .Select(v => v.Series) .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(ct); } /// @@ -1599,19 +1499,22 @@ public class SeriesRepository : ISeriesRepository /// /// This will be normalized in the query and checked against FolderPath and LowestFolderPath /// Additional relationships to include with the base query + /// /// - public async Task GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None) + public async Task GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None, + CancellationToken ct = default) { - var normalized = Services.Tasks.Scanner.Parser.Parser.NormalizePath(folder); + var normalized = folder.NormalizePath(); if (string.IsNullOrEmpty(normalized)) return null; - return await _context.Series + return await context.Series .Where(s => (!string.IsNullOrEmpty(s.FolderPath) && s.FolderPath.Equals(normalized) || (!string.IsNullOrEmpty(s.LowestFolderPath) && s.LowestFolderPath.Equals(normalized)))) .Includes(includes) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task GetSeriesThatContainsLowestFolderPath(string path, SeriesIncludes includes = SeriesIncludes.None) + public async Task GetSeriesThatContainsLowestFolderPath(string path, + SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default) { // Check if the path ends with a file (has a file extension) string directoryPath; @@ -1628,30 +1531,30 @@ public class SeriesRepository : ISeriesRepository } // Normalize the directory path - var normalized = Services.Tasks.Scanner.Parser.Parser.NormalizePath(directoryPath); + var normalized = directoryPath.NormalizePath(); if (string.IsNullOrEmpty(normalized)) return null; normalized = normalized.TrimEnd('/'); - return await _context.Series + return await context.Series .Where(s => !string.IsNullOrEmpty(s.LowestFolderPath) && EF.Functions.Like(normalized, s.LowestFolderPath + "%")) .Includes(includes) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } public async Task> GetAllSeriesByNameAsync(IList normalizedNames, - int userId, SeriesIncludes includes = SeriesIncludes.None) + int userId, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default) { - var libraryIds = _context.Library.GetUserLibraries(userId); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var libraryIds = context.Library.GetUserLibraries(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - return await _context.Series + return await context.Series .Where(s => normalizedNames.Contains(s.NormalizedName) || normalizedNames.Contains(s.NormalizedLocalizedName)) .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) .Includes(includes) - .ToListAsync(); + .ToListAsync(ct); } @@ -1664,13 +1567,14 @@ public class SeriesRepository : ISeriesRepository /// /// /// Defaults to true. This will query against all foreign keys (deep). If false, just the series will come back + /// /// public Task GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, - MangaFormat format, bool withFullIncludes = true) + MangaFormat format, bool withFullIncludes = true, CancellationToken ct = default) { var normalizedSeries = seriesName.ToNormalized(); var normalizedLocalized = localizedName.ToNormalized(); - var query = _context.Series + var query = context.Series .Where(s => s.LibraryId == libraryId) .Where(s => s.Format == format && format != MangaFormat.Unknown) .Where(s => @@ -1684,10 +1588,10 @@ public class SeriesRepository : ISeriesRepository ); if (!withFullIncludes) { - return query.SingleOrDefaultAsync(); + return query.SingleOrDefaultAsync(ct); } - #nullable disable +#nullable disable query = query.Include(s => s.Library) .Include(s => s.Metadata) @@ -1718,23 +1622,23 @@ public class SeriesRepository : ISeriesRepository .ThenInclude(c => c.Files) .AsSplitQuery(); - return query.SingleOrDefaultAsync(); + return query.SingleOrDefaultAsync(ct); - #nullable enable +#nullable enable } public async Task GetSeriesByAnyName(string seriesName, string localizedName, IList formats, - int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None) + int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId); + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId); var normalizedSeries = seriesName.ToNormalized(); var normalizedLocalized = localizedName.ToNormalized(); - var query = _context.Series + var query = context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Where(s => formats.Contains(s.Format)); - if (aniListId.HasValue && aniListId.Value > 0) + if (aniListId is > 0) { // If AniList ID is provided, override name checks query = query.Where(s => s.ExternalSeriesMetadata.AniListId == aniListId.Value); @@ -1753,23 +1657,23 @@ public class SeriesRepository : ISeriesRepository return await query .Includes(includes) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } public async Task GetSeriesByAnyName(IList names, IList formats, - int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None) + int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId); + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId); names = names.Where(s => !string.IsNullOrEmpty(s)).Distinct().ToList(); var normalizedNames = names.Select(s => s.ToNormalized()).ToList(); - var query = _context.Series + var query = context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Where(s => formats.Contains(s.Format)); - if (aniListId.HasValue && aniListId.Value > 0) + if (aniListId is > 0) { // If AniList ID is provided, override name checks query = query.Where(s => s.ExternalSeriesMetadata.AniListId == aniListId.Value || @@ -1788,15 +1692,15 @@ public class SeriesRepository : ISeriesRepository return await query .Includes(includes) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } public async Task> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId, - MangaFormat format) + MangaFormat format, CancellationToken ct = default) { var normalizedSeries = seriesName.ToNormalized(); var normalizedLocalized = localizedName.ToNormalized(); - return await _context.Series + return await context.Series .Where(s => s.LibraryId == libraryId) .Where(s => s.Format == format && format != MangaFormat.Unknown) .Where(s => @@ -1809,7 +1713,7 @@ public class SeriesRepository : ISeriesRepository || (s.OriginalName != null && s.OriginalName.Equals(seriesName)) ) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } @@ -1818,14 +1722,16 @@ public class SeriesRepository : ISeriesRepository /// /// /// - public async Task> RemoveSeriesNotInList(IList seenSeries, int libraryId) + /// + public async Task> RemoveSeriesNotInList(IList seenSeries, int libraryId, + CancellationToken ct = default) { if (!seenSeries.Any()) return Array.Empty(); // Get all series from DB in one go, based on libraryId - var dbSeries = await _context.Series + var dbSeries = await context.Series .Where(s => s.LibraryId == libraryId) - .ToListAsync(); + .ToListAsync(ct); // Get a set of matching series ids for the given parsedSeries var ids = new HashSet(); @@ -1850,161 +1756,165 @@ public class SeriesRepository : ISeriesRepository .ToList(); // Remove series in bulk - _context.Series.RemoveRange(seriesToRemove); + context.Series.RemoveRange(seriesToRemove); return seriesToRemove; } - public async Task> GetHighlyRated(int userId, int libraryId, UserParams userParams) + public async Task> GetHighlyRated(int userId, int libraryId, UserParams userParams, + CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); - var distinctSeriesIdsWithHighRating = _context.AppUserRating + var distinctSeriesIdsWithHighRating = context.AppUserRating .Where(s => usersSeriesIds.Contains(s.SeriesId) && s.Rating > 4) .Select(p => p.SeriesId) .Distinct(); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - var query = _context.Series + var query = context.Series .Where(s => distinctSeriesIdsWithHighRating.Contains(s.Id)) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() - .OrderByDescending(s => _context.AppUserRating.Where(r => r.SeriesId == s.Id).Select(r => r.Rating).Average()) - .ProjectToWithProgress(_mapper, userId); + .OrderByDescending(s => context.AppUserRating.Where(r => r.SeriesId == s.Id).Select(r => r.Rating).Average()) + .ProjectToWithProgress(mapper, userId); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetQuickReads(int userId, int libraryId, UserParams userParams) + public async Task> GetQuickReads(int userId, int libraryId, UserParams userParams, + CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); - var distinctSeriesIdsWithProgress = _context.AppUserProgresses + var distinctSeriesIdsWithProgress = context.AppUserProgresses .Where(s => usersSeriesIds.Contains(s.SeriesId)) .Select(p => p.SeriesId) .Distinct(); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - var query = _context.Series + var query = context.Series .Where(s => ( - (s.Pages / ReaderService.AvgPagesPerMinute / 60 < 10 && s.Format != MangaFormat.Epub) - || (s.WordCount * ReaderService.AvgWordsPerHour < 10 && s.Format == MangaFormat.Epub)) - && !distinctSeriesIdsWithProgress.Contains(s.Id) && - usersSeriesIds.Contains(s.Id)) + (s.Pages / IReaderService.AvgPagesPerMinute / 60 < 10 && s.Format != MangaFormat.Epub) + || (s.WordCount * IReaderService.AvgWordsPerHour < 10 && s.Format == MangaFormat.Epub)) + && !distinctSeriesIdsWithProgress.Contains(s.Id) && + usersSeriesIds.Contains(s.Id)) .Where(s => s.Metadata.PublicationStatus != PublicationStatus.OnGoing) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider); + .ProjectTo(mapper.ConfigurationProvider); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams) + public async Task> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams, + CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); - var distinctSeriesIdsWithProgress = _context.AppUserProgresses + var distinctSeriesIdsWithProgress = context.AppUserProgresses .Where(s => usersSeriesIds.Contains(s.SeriesId)) .Select(p => p.SeriesId) .Distinct(); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - var query = _context.Series + var query = context.Series .Where(s => ( - (s.Pages / ReaderService.AvgPagesPerMinute / 60 < 10 && s.Format != MangaFormat.Epub) - || (s.WordCount * ReaderService.AvgWordsPerHour < 10 && s.Format == MangaFormat.Epub)) + (s.Pages / IReaderService.AvgPagesPerMinute / 60 < 10 && s.Format != MangaFormat.Epub) + || (s.WordCount * IReaderService.AvgWordsPerHour < 10 && s.Format == MangaFormat.Epub)) && !distinctSeriesIdsWithProgress.Contains(s.Id) && usersSeriesIds.Contains(s.Id)) .Where(s => s.Metadata.PublicationStatus == PublicationStatus.OnGoing) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider); + .ProjectTo(mapper.ConfigurationProvider); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - public async Task GetRelatedSeries(int userId, int seriesId) + public async Task GetRelatedSeries(int userId, int seriesId, CancellationToken ct = default) { - var libraryIds = _context.Library.GetUserLibraries(userId); + var libraryIds = context.Library.GetUserLibraries(userId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); return new RelatedSeriesDto() { SourceSeriesId = seriesId, - Adaptations = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Adaptation, userRating), - Characters = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Character, userRating), - Prequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Prequel, userRating), - Sequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Sequel, userRating), - Contains = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Contains, userRating), - SideStories = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SideStory, userRating), - SpinOffs = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SpinOff, userRating), - Others = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Other, userRating), - AlternativeSettings = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeSetting, userRating), - AlternativeVersions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeVersion, userRating), - Doujinshis = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Doujinshi, userRating), - Annuals = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Annual, userRating), - Parent = await _context.SeriesRelation + Adaptations = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Adaptation, userRating, ct), + Characters = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Character, userRating, ct), + Prequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Prequel, userRating, ct), + Sequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Sequel, userRating, ct), + Contains = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Contains, userRating, ct), + SideStories = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SideStory, userRating, ct), + SpinOffs = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SpinOff, userRating, ct), + Others = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Other, userRating, ct), + AlternativeSettings = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeSetting, userRating, ct), + AlternativeVersions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeVersion, userRating, ct), + Doujinshis = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Doujinshi, userRating, ct), + Annuals = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Annual, userRating, ct), + Parent = await context.SeriesRelation .Where(r => r.TargetSeriesId == seriesId - && usersSeriesIds.Contains(r.TargetSeriesId) - && r.RelationKind != RelationKind.Prequel - && r.RelationKind != RelationKind.Sequel - && r.RelationKind != RelationKind.Edition) - .Select(sr => sr.Series) + && usersSeriesIds.Contains(r.TargetSeriesId) + && r.RelationKind != RelationKind.Prequel + && r.RelationKind != RelationKind.Sequel + && r.RelationKind != RelationKind.Edition) + .Select(sr => sr.Series) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(), - Editions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Edition, userRating) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct), + Editions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Edition, userRating, ct) }; } private IQueryable GetSeriesIdsForLibraryIds(IQueryable libraryIds) { - return _context.Series + return context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Select(s => s.Id); } - private async Task> GetRelatedSeriesQuery(int seriesId, IEnumerable usersSeriesIds, RelationKind kind, AgeRestriction userRating) + private async Task> GetRelatedSeriesQuery(int seriesId, IEnumerable usersSeriesIds, + RelationKind kind, AgeRestriction userRating, CancellationToken ct = default) { - return await _context.Series.SelectMany(s => - s.Relations.Where(sr => sr.RelationKind == kind && sr.SeriesId == seriesId && usersSeriesIds.Contains(sr.TargetSeriesId)) - .Select(sr => sr.TargetSeries)) + return await context.Series.SelectMany(s => + s.Relations.Where(sr => sr.RelationKind == kind && sr.SeriesId == seriesId && usersSeriesIds.Contains(sr.TargetSeriesId)) + .Select(sr => sr.TargetSeries)) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - private async Task> GetRecentlyAddedChaptersQuery(int userId) + private async Task> GetRecentlyAddedChaptersQuery(int userId, CancellationToken ct = default) { - var libraryIds = await _context.AppUser + var libraryIds = await context.AppUser .Where(u => u.Id == userId) .SelectMany(u => u.Libraries) .Where(l => l.IncludeInDashboard) .Select(l => l.Id) - .ToListAsync(); + .ToListAsync(ct); var withinLastWeek = DateTime.Now - TimeSpan.FromDays(12); - return _context.Chapter + return context.Chapter .Where(c => c.Created >= withinLastWeek).AsNoTracking() .Include(c => c.Volume) .ThenInclude(v => v.Series) .ThenInclude(s => s.Library) .OrderByDescending(c => c.Created) - .Select(c => new RecentlyAddedSeries() + .Select(c => new RecentlyAddedSeriesDto { LibraryId = c.Volume.Series.LibraryId, LibraryType = c.Volume.Series.Library.Type, @@ -2014,8 +1924,8 @@ public class SeriesRepository : ISeriesRepository VolumeId = c.VolumeId, ChapterId = c.Id, Format = c.Volume.Series.Format, - ChapterNumber = c.MinNumber + string.Empty, // TODO: Refactor this - ChapterRange = c.Range, // TODO: Refactor this + ChapterNumber = c.MinNumber + string.Empty, // default: Refactor this + ChapterRange = c.Range, // default: Refactor this IsSpecial = c.IsSpecial, VolumeNumber = c.Volume.MinNumber, ChapterTitle = c.Title, @@ -2027,10 +1937,11 @@ public class SeriesRepository : ISeriesRepository } [Obsolete("Use GetWantToReadForUserV2Async")] - public async Task> GetWantToReadForUserAsync(int userId, UserParams userParams, FilterDto filter) + public async Task> GetWantToReadForUserAsync(int userId, UserParams userParams, + FilterDto filter, CancellationToken ct = default) { - var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); - var query = _context.AppUser + var libraryIds = await context.Library.GetUserLibraries(userId).ToListAsync(ct); + var query = context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) .Where(s => libraryIds.Contains(s.Series.LibraryId)) @@ -2040,182 +1951,185 @@ public class SeriesRepository : ISeriesRepository var filteredQuery = await CreateFilteredSearchQueryable(userId, 0, filter, query); - return await PagedList.CreateAsync(filteredQuery.ProjectToWithProgress(_mapper, userId), userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(filteredQuery.ProjectToWithProgress(mapper, userId), userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetWantToReadForUserV2Async(int userId, UserParams userParams, FilterV2Dto filter) + public async Task> GetWantToReadForUserV2Async(int userId, UserParams userParams, + FilterV2Dto filter, CancellationToken ct = default) { - var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); - var seriesIds = await _context.AppUser + var libraryIds = await context.Library.GetUserLibraries(userId).ToListAsync(ct); + var seriesIds = await context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) .Where(s => libraryIds.Contains(s.Series.LibraryId)) .Select(w => w.Series.Id) .Distinct() - .ToListAsync(); + .ToListAsync(ct); - var query = await CreateFilteredSearchQueryableV2(userId, filter, QueryContext.None); + var query = await CreateFilteredSearchQueryableV2(userId, filter, QueryContext.None, ct: ct); // Apply the Want to Read filtering query = query.Where(s => seriesIds.Contains(s.Id)); var retSeries = query - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() .AsNoTracking(); - return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetWantToReadForUserAsync(int userId) + public async Task> GetWantToReadForUserAsync(int userId, CancellationToken ct = default) { - var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); - return await _context.AppUser + var libraryIds = await context.Library.GetUserLibraries(userId).ToListAsync(ct); + return await context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) .Where(s => libraryIds.Contains(s.Series.LibraryId)) .Select(w => w.Series) .AsSplitQuery() .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } /// /// Uses multiple names to find a match against a series. If not, returns null. /// /// This does not restrict to the user at all. That is handled at the API level. - public async Task GetSeriesDtoByNamesAndMetadataIds(IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl) + public async Task GetSeriesDtoByNamesAndMetadataIds(IEnumerable names, LibraryType libraryType, + string aniListUrl, string malUrl, CancellationToken ct = default) { - var libraryIds = await _context.Library + var libraryIds = await context.Library .Where(lib => lib.Type == libraryType) .Select(l => l.Id) - .ToListAsync(); + .ToListAsync(ct); var normalizedNames = names.Select(n => n.ToNormalized()).ToList(); SeriesDto? result = null; if (!string.IsNullOrEmpty(aniListUrl) || !string.IsNullOrEmpty(malUrl)) { - // TODO: I can likely work AniList and MalIds from ExternalSeriesMetadata in here - result = await _context.Series + // default: I can likely work AniList and MalIds from ExternalSeriesMetadata in here + result = await context.Series .Where(s => !string.IsNullOrEmpty(s.Metadata.WebLinks)) .Where(s => libraryIds.Contains(s.Library.Id)) .WhereIf(!string.IsNullOrEmpty(aniListUrl), s => s.Metadata.WebLinks.Contains(aniListUrl)) .WhereIf(!string.IsNullOrEmpty(malUrl), s => s.Metadata.WebLinks.Contains(malUrl)) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } if (result != null) return result; - return await _context.Series + return await context.Series .Where(s => normalizedNames.Contains(s.NormalizedName) || normalizedNames.Contains(s.NormalizedLocalizedName)) .Where(s => libraryIds.Contains(s.Library.Id)) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() - .FirstOrDefaultAsync(); // Some users may have improperly configured libraries + .FirstOrDefaultAsync(ct); // Some users may have improperly configured libraries } - public async Task MatchSeries(ExternalSeriesDetailDto externalSeries) + public async Task MatchSeries(ExternalSeriesDetailDto externalSeries, CancellationToken ct = default) { - var libraryIds = await _context.Library + var libraryIds = await context.Library .Where(lib => externalSeries.PlusMediaFormat.ConvertToLibraryTypes().Contains(lib.Type)) .Select(l => l.Id) - .ToListAsync(); + .ToListAsync(ct); var normalizedNames = (externalSeries.Synonyms ?? Enumerable.Empty()) .Prepend(externalSeries.Name) .Select(n => n.ToNormalized()) .ToList(); - var aniListWebLink = - ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, externalSeries.AniListId); - var malWebLink = - ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, externalSeries.MALId); + var aniListWebLink = ScrobblingHelper.CreateUrl(ScrobblingHelper.AniListWeblinkWebsite, externalSeries.AniListId); + var malWebLink = ScrobblingHelper.CreateUrl(ScrobblingHelper.MalWeblinkWebsite, externalSeries.MALId); Series? result = null; if (!string.IsNullOrEmpty(aniListWebLink) || !string.IsNullOrEmpty(malWebLink)) { - result = await _context.Series + result = await context.Series .Where(s => !string.IsNullOrEmpty(s.Metadata.WebLinks)) .Where(s => libraryIds.Contains(s.Library.Id)) .WhereIf(!string.IsNullOrEmpty(aniListWebLink), s => s.Metadata.WebLinks.Contains(aniListWebLink)) .WhereIf(!string.IsNullOrEmpty(malWebLink), s => s.Metadata.WebLinks.Contains(malWebLink)) .Include(s => s.Metadata) .AsSplitQuery() - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } if (result != null) return result; - return await _context.Series + return await context.Series .Where(s => normalizedNames.Contains(s.NormalizedName) || normalizedNames.Contains(s.NormalizedLocalizedName)) .Where(s => libraryIds.Contains(s.Library.Id)) .AsSplitQuery() .Include(s => s.Metadata) - .FirstOrDefaultAsync(); // Some users may have improperly configured libraries + .FirstOrDefaultAsync(ct); // Some users may have improperly configured libraries } /// /// Returns the Average rating for all users within Kavita instance /// /// - public async Task GetAverageUserRating(int seriesId, int userId) + /// + /// + public async Task GetAverageUserRating(int seriesId, int userId, CancellationToken ct = default) { // If there is 0 or 1 rating and that rating is you, return 0 back - var countOfRatingsThatAreUser = await _context.AppUserRating + var countOfRatingsThatAreUser = await context.AppUserRating .Where(r => r.SeriesId == seriesId && r.HasBeenRated) - .CountAsync(u => u.AppUserId == userId); + .CountAsync(u => u.AppUserId == userId, cancellationToken: ct); if (countOfRatingsThatAreUser == 1) { return 0; } - var avg = (await _context.AppUserRating + var avg = (await context.AppUserRating .Where(r => r.SeriesId == seriesId && r.HasBeenRated) - .AverageAsync(r => (int?) r.Rating)); + .AverageAsync(r => (int?) r.Rating, cancellationToken: ct)); return avg.HasValue ? (int) (avg.Value * 20) : 0; } - public async Task RemoveFromOnDeck(int seriesId, int userId) + public async Task RemoveFromOnDeck(int seriesId, int userId, CancellationToken ct = default) { - var existingEntry = await _context.AppUserOnDeckRemoval + var existingEntry = await context.AppUserOnDeckRemoval .Where(u => u.Id == userId && u.SeriesId == seriesId) - .AnyAsync(); + .AnyAsync(ct); if (existingEntry) return; - _context.AppUserOnDeckRemoval.Add(new AppUserOnDeckRemoval() + context.AppUserOnDeckRemoval.Add(new AppUserOnDeckRemoval() { SeriesId = seriesId, AppUserId = userId }); - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(ct); } - public async Task ClearOnDeckRemoval(int seriesId, int userId) + public async Task ClearOnDeckRemoval(int seriesId, int userId, CancellationToken ct = default) { - var existingEntry = await _context.AppUserOnDeckRemoval + var existingEntry = await context.AppUserOnDeckRemoval .Where(u => u.AppUserId == userId && u.SeriesId == seriesId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (existingEntry == null) return; - _context.AppUserOnDeckRemoval.Remove(existingEntry); - await _context.SaveChangesAsync(); + context.AppUserOnDeckRemoval.Remove(existingEntry); + await context.SaveChangesAsync(ct); } - public async Task IsSeriesInWantToRead(int userId, int seriesId) + public async Task IsSeriesInWantToRead(int userId, int seriesId, CancellationToken ct = default) { - var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); - return await _context.AppUser + var libraryIds = await context.Library.GetUserLibraries(userId).ToListAsync(ct); + return await context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead.Where(s => s.SeriesId == seriesId && libraryIds.Contains(s.Series.LibraryId))) .AsSplitQuery() .AsNoTracking() - .AnyAsync(); + .AnyAsync(ct); } - public async Task>> GetFolderPathMap(int libraryId) + public async Task>> GetFolderPathMap(int libraryId, + CancellationToken ct = default) { - var info = await _context.Series + var info = await context.Series .Where(s => s.LibraryId == libraryId) .AsNoTracking() .Where(s => s.FolderPath != null) @@ -2228,7 +2142,7 @@ public class SeriesRepository : ISeriesRepository Format = s.Format, LibraryRoots = s.Library.Folders.Select(f => f.Path) }) - .ToListAsync(); + .ToListAsync(ct); var map = new Dictionary>(); foreach (var series in info) @@ -2268,15 +2182,16 @@ public class SeriesRepository : ISeriesRepository /// Returns the highest Age Rating for a list of Series. Defaults to /// /// + /// /// - public async Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds) + public async Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds, CancellationToken ct = default) { - var ret = await _context.Series + var ret = await context.Series .Where(s => seriesIds.Contains(s.Id)) .Include(s => s.Metadata) .Select(s => s.Metadata.AgeRating) .OrderBy(s => s) - .LastOrDefaultAsync(); + .LastOrDefaultAsync(ct); if (ret == null) return AgeRating.Unknown; return ret; diff --git a/Kavita.Database/Repositories/SettingsRepository.cs b/Kavita.Database/Repositories/SettingsRepository.cs new file mode 100644 index 000000000..e23484bda --- /dev/null +++ b/Kavita.Database/Repositories/SettingsRepository.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + + +public class SettingsRepository(DataContext context, IMapper mapper) : ISettingsRepository +{ + public void Update(ServerSetting settings) + { + context.Entry(settings).State = EntityState.Modified; + } + + public void Update(MetadataSettings settings) + { + context.Entry(settings).State = EntityState.Modified; + } + + public void RemoveRange(List fieldMappings) + { + context.MetadataFieldMapping.RemoveRange(fieldMappings); + } + + public void Remove(ServerSetting setting) + { + context.Remove(setting); + } + + public async Task GetExternalSeriesMetadata(int seriesId, CancellationToken ct = default) + { + return await context.ExternalSeriesMetadata + .Where(s => s.SeriesId == seriesId) + .FirstOrDefaultAsync(ct); + } + + public async Task GetMetadataSettings(CancellationToken ct = default) + { + return await context.MetadataSettings + .Include(m => m.FieldMappings) + .FirstAsync(ct); + } + + public async Task GetMetadataSettingDto(CancellationToken ct = default) + { + return await context.MetadataSettings + .Include(m => m.FieldMappings) + .ProjectTo(mapper.ConfigurationProvider) + .FirstAsync(ct); + } + + public async Task GetSettingsDtoAsync(CancellationToken ct = default) + { + var settings = await context.ServerSetting + .Select(x => x) + .AsNoTracking() + .ToListAsync(ct); + return mapper.Map(settings); + } + + public Task GetSettingAsync(ServerSettingKey key, CancellationToken ct = default) + { + return context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key, ct)!; + } + + public async Task> GetSettingsAsync(CancellationToken ct = default) + { + return await context.ServerSetting.ToListAsync(ct); + } +} diff --git a/Kavita.Database/Repositories/SiteThemeRepository.cs b/Kavita.Database/Repositories/SiteThemeRepository.cs new file mode 100644 index 000000000..987ee5664 --- /dev/null +++ b/Kavita.Database/Repositories/SiteThemeRepository.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class SiteThemeRepository(DataContext context, IMapper mapper) : ISiteThemeRepository +{ + public void Add(SiteTheme theme) + { + context.Add(theme); + } + + public void Remove(SiteTheme theme) + { + context.Remove(theme); + } + + public void Update(SiteTheme siteTheme) + { + context.Entry(siteTheme).State = EntityState.Modified; + } + + public async Task> GetThemeDtos() + { + return await context.SiteTheme + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(); + } + + public async Task GetThemeDtoByName(string themeName) + { + return await context.SiteTheme + .Where(t => t.Name.Equals(themeName)) + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(); + } + + /// + /// Returns default theme, if the default theme is not available, returns the dark theme + /// + /// + public async Task GetDefaultTheme() + { + var result = await context.SiteTheme + .Where(t => t.IsDefault) + .FirstOrDefaultAsync(); + + if (result == null) + { + return await context.SiteTheme + .Where(t => t.NormalizedName == SiteTheme.DefaultTheme.NormalizedName) + .SingleAsync(); + } + + return result; + } + + public async Task> GetThemes() + { + return await context.SiteTheme + .ToListAsync(); + } + + public async Task GetTheme(int themeId) + { + return await context.SiteTheme + .Where(t => t.Id == themeId) + .FirstOrDefaultAsync(); + } + + public async Task IsThemeInUse(int themeId) + { + return await context.AppUserPreferences + .AnyAsync(p => p.Theme.Id == themeId); + } + + public async Task GetThemeDto(int themeId) + { + return await context.SiteTheme + .Where(t => t.Id == themeId) + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(); + } +} diff --git a/API/Data/Repositories/TagRepository.cs b/Kavita.Database/Repositories/TagRepository.cs similarity index 55% rename from API/Data/Repositories/TagRepository.cs rename to Kavita.Database/Repositories/TagRepository.cs index 40d40a675..052b4c8bb 100644 --- a/API/Data/Repositories/TagRepository.cs +++ b/Kavita.Database/Repositories/TagRepository.cs @@ -1,79 +1,59 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.Metadata; -using API.DTOs.Metadata.Browse; -using API.Entities; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Helpers; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.Entities; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -public interface ITagRepository +public class TagRepository(DataContext context, IMapper mapper) : ITagRepository { - void Attach(Tag tag); - void Remove(Tag tag); - Task> GetAllTagsAsync(); - Task> GetAllTagsByNameAsync(IEnumerable normalizedNames); - Task> GetAllTagDtosAsync(int userId); - Task RemoveAllTagNoLongerAssociated(); - Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null); - Task> GetAllTagsNotInListAsync(ICollection tags); - Task> GetBrowseableTag(int userId, UserParams userParams); -} - -public class TagRepository : ITagRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public TagRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Attach(Tag tag) { - _context.Tag.Attach(tag); + context.Tag.Attach(tag); } public void Remove(Tag tag) { - _context.Tag.Remove(tag); + context.Tag.Remove(tag); } - public async Task RemoveAllTagNoLongerAssociated() + public async Task RemoveAllTagNoLongerAssociated(CancellationToken ct = default) { - var tagsWithNoConnections = await _context.Tag + var tagsWithNoConnections = await context.Tag .Include(p => p.SeriesMetadatas) .Include(p => p.Chapters) .Where(p => p.SeriesMetadatas.Count == 0 && p.Chapters.Count == 0) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); - _context.Tag.RemoveRange(tagsWithNoConnections); + context.Tag.RemoveRange(tagsWithNoConnections); - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(ct); } - public async Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null) + public async Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null, + CancellationToken ct = default) { - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = await context.Library.GetUserLibraries(userId).ToListAsync(ct); if (libraryIds is {Count: > 0}) { userLibs = userLibs.Where(libraryIds.Contains).ToList(); } - return await _context.Series + return await context.Series .Where(s => userLibs.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) .SelectMany(s => s.Metadata.Tags) @@ -81,24 +61,24 @@ public class TagRepository : ITagRepository .Distinct() .OrderBy(t => t.NormalizedTitle) .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetAllTagsNotInListAsync(ICollection tags) + public async Task> GetAllTagsNotInListAsync(ICollection tags, CancellationToken ct = default) { // Create a dictionary mapping normalized names to non-normalized names var normalizedToOriginalMap = tags.Distinct() - .GroupBy(Parser.Normalize) + .GroupBy(t => t.ToNormalized()) .ToDictionary(group => group.Key, group => group.First()); var normalizedTagNames = normalizedToOriginalMap.Keys.ToList(); // Query the database for existing genres using the normalized names - var existingTags = await _context.Tag + var existingTags = await context.Tag .Where(g => normalizedTagNames.Contains(g.NormalizedTitle)) // Assuming you have a normalized field .Select(g => g.NormalizedTitle) - .ToListAsync(); + .ToListAsync(ct); // Find the normalized genres that do not exist in the database var missingTags = normalizedTagNames.Except(existingTags).ToList(); @@ -107,16 +87,17 @@ public class TagRepository : ITagRepository return missingTags.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList(); } - public async Task> GetBrowseableTag(int userId, UserParams userParams) + public async Task> GetBrowseableTag(int userId, UserParams userParams, + CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); - var allLibrariesCount = await _context.Library.CountAsync(); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + var allLibrariesCount = await context.Library.CountAsync(ct); + var userLibs = await context.Library.GetUserLibraries(userId).ToListAsync(ct); - var seriesIds = _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id); + var seriesIds = context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id); - var query = _context.Tag + var query = context.Tag .RestrictAgainstAgeRestriction(ageRating) .WhereIf(userLibs.Count != allLibrariesCount, tag => tag.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) || @@ -138,29 +119,29 @@ public class TagRepository : ITagRepository }) .OrderBy(g => g.Title); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetAllTagsAsync() + public async Task> GetAllTagsAsync(CancellationToken ct = default) { - return await _context.Tag.ToListAsync(); + return await context.Tag.ToListAsync(ct); } - public async Task> GetAllTagsByNameAsync(IEnumerable normalizedNames) + public async Task> GetAllTagsByNameAsync(IEnumerable normalizedNames, CancellationToken ct = default) { - return await _context.Tag + return await context.Tag .Where(t => normalizedNames.Contains(t.NormalizedTitle)) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllTagDtosAsync(int userId) + public async Task> GetAllTagDtosAsync(int userId, CancellationToken ct = default) { - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.Tag + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.Tag .AsNoTracking() .RestrictAgainstAgeRestriction(userRating) .OrderBy(t => t.NormalizedTitle) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } } diff --git a/API/Data/Repositories/UserRepository.cs b/Kavita.Database/Repositories/UserRepository.cs similarity index 57% rename from API/Data/Repositories/UserRepository.cs rename to Kavita.Database/Repositories/UserRepository.cs index a78684055..26d96011f 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/Kavita.Database/Repositories/UserRepository.cs @@ -1,234 +1,110 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Constants; -using API.DTOs; -using API.DTOs.Account; -using API.DTOs.Dashboard; -using API.DTOs.Filtering.v2; -using API.DTOs.KavitaPlus.Account; -using API.DTOs.Reader; -using API.DTOs.Scrobbling; -using API.DTOs.SeriesDetail; -using API.DTOs.SideNav; -using API.Entities; -using API.Entities.Enums.UserPreferences; -using API.Entities.User; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.KavitaPlus.Account; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum AppUserIncludes +public class UserRepository(DataContext context, UserManager userManager, IMapper mapper) + : IUserRepository { - None = 1, - Progress = 1 << 1, - Bookmarks = 1 << 2, - ReadingLists = 1 << 3, - Ratings = 1 << 4, - UserPreferences = 1 << 5, - WantToRead = 1 << 6, - ReadingListsWithItems = 1 << 7, - Devices = 1 << 8, - ScrobbleHolds = 1 << 9, - SmartFilters = 1 << 10, - DashboardStreams = 1 << 11, - SideNavStreams = 1 << 12, - ExternalSources = 1 << 13, - Collections = 1 << 14, - ChapterRatings = 1 << 15, - AuthKeys = 1 << 16 -} - -public interface IUserRepository -{ - void Add(AppUserAuthKey key); - void Add(AppUserBookmark bookmark); - void Add(AppUser bookmark); - void Update(AppUser user); - void Update(AppUserPreferences preferences); - void Update(AppUserBookmark bookmark); - void Update(AppUserDashboardStream stream); - void Update(AppUserSideNavStream stream); - void Delete(AppUser? user); - void Delete(AppUserAuthKey? key); - void Delete(AppUserBookmark bookmark); - void Delete(IEnumerable streams); - void Delete(AppUserDashboardStream stream); - void Delete(IEnumerable streams); - void Delete(AppUserSideNavStream stream); - Task> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true); - Task> GetAdminUsersAsync(); - Task IsUserAdminAsync(AppUser? user); - Task> GetRoles(int userId); - Task> GetRolesByAuthKey(string? apiKey); - Task GetUserRatingAsync(int seriesId, int userId); - Task GetUserChapterRatingAsync(int userId, int chapterId); - Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId); - Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId); - Task GetPreferencesAsync(string username); - Task> GetBookmarkDtosForSeries(int userId, int seriesId); - Task> GetBookmarkDtosForVolume(int userId, int volumeId); - Task> GetBookmarkDtosForChapter(int userId, int chapterId); - Task> GetAllBookmarkDtos(int userId, FilterV2Dto filter); - Task> GetAllBookmarksAsync(); - Task GetBookmarkForPage(int page, int chapterId, int imageOffset, int userId); - Task GetBookmarkAsync(int bookmarkId); - Task GetUserDtoByAuthKeyAsync(string authKey); - Task GetUserIdByAuthKeyAsync(string authKey); - Task GetUserDtoById(int userId); - Task GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None); - Task GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None); - Task GetUserByAuthKey(string authKey, AppUserIncludes includeFlags = AppUserIncludes.None); - Task GetUserIdByUsernameAsync(string username); - Task> GetAllBookmarksByIds(IList bookmarkIds); - Task GetUserByEmailAsync(string email, AppUserIncludes includes = AppUserIncludes.None); - Task> GetAllPreferencesByThemeAsync(int themeId); - Task> GetAllPreferencesByFontAsync(string fontName); - Task HasAccessToLibrary(int libraryId, int userId); - Task HasAccessToSeries(int userId, int seriesId); - Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true); - Task GetUserByConfirmationToken(string token); - Task GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None); - Task> GetSeriesWithRatings(int userId); - Task> GetSeriesWithReviews(int userId); - Task HasHoldOnSeries(int userId, int seriesId); - Task> GetHolds(int userId); - Task GetLocale(int userId); - Task> GetDashboardStreams(int userId, bool visibleOnly = false); - Task> GetAllDashboardStreams(); - Task GetDashboardStream(int streamId); - Task> GetDashboardStreamWithFilter(int filterId); - Task> GetSideNavStreams(int userId, bool visibleOnly = false); - Task GetSideNavStream(int streamId); - Task GetSideNavStreamWithUser(int streamId); - Task> GetSideNavStreamWithFilter(int filterId); - Task> GetSideNavStreamsByLibraryId(int libraryId); - Task> GetSideNavStreamWithExternalSource(int externalSourceId); - Task> GetDashboardStreamsByIds(IList streamIds); - Task> GetUserTokenInfo(); - Task GetUserByDeviceEmail(string deviceEmail); - Task> GetAnnotations(int userId, int chapterId); - Task> GetAnnotationsByPage(int userId, int chapterId, int pageNum); - /// - /// Try getting a user by the id provided by OIDC - /// - /// - /// - /// - Task GetByOidcId(string? oidcId, AppUserIncludes includes = AppUserIncludes.None); - Task GetAnnotationDtoById(int userId, int annotationId); - Task> GetAnnotationDtosBySeries(int userId, int seriesId); - Task UpdateUserAsActive(int userId); - Task> GetAllReviewsForUser(int userId, int requestingUserId, string? query = null, float? ratingFilter = null); - Task GetCoverImageAsync(int userId, int requestingUserId); - Task GetPersonCoverImageAsync(int personId); - Task> GetAuthKeysForUserId(int userId); - Task> GetAllAuthKeysDtosWithExpiration(); - Task GetAuthKeyById(int authKeyId); - Task GetAuthKeyExpiration(string authKey, int userId); - Task GetSocialPreferencesForUser(int userId); - Task GetPreferencesForUser(int userId); - Task GetOpdsPreferences(int userId); -} - -public class UserRepository : IUserRepository -{ - private readonly DataContext _context; - private readonly UserManager _userManager; - private readonly IMapper _mapper; - - public UserRepository(DataContext context, UserManager userManager, IMapper mapper) - { - _context = context; - _userManager = userManager; - _mapper = mapper; - } - public void Add(AppUserAuthKey key) { - _context.AppUserAuthKey.Add(key); + context.AppUserAuthKey.Add(key); } public void Add(AppUserBookmark bookmark) { - _context.AppUserBookmark.Add(bookmark); + context.AppUserBookmark.Add(bookmark); } public void Add(AppUser user) { - _context.AppUser.Add(user); + context.AppUser.Add(user); } public void Update(AppUser user) { - _context.Entry(user).State = EntityState.Modified; + context.Entry(user).State = EntityState.Modified; } public void Update(AppUserPreferences preferences) { - _context.Entry(preferences).State = EntityState.Modified; + context.Entry(preferences).State = EntityState.Modified; } public void Update(AppUserBookmark bookmark) { - _context.Entry(bookmark).State = EntityState.Modified; + context.Entry(bookmark).State = EntityState.Modified; } public void Update(AppUserDashboardStream stream) { - _context.Entry(stream).State = EntityState.Modified; + context.Entry(stream).State = EntityState.Modified; } public void Update(AppUserSideNavStream stream) { - _context.Entry(stream).State = EntityState.Modified; + context.Entry(stream).State = EntityState.Modified; } public void Delete(AppUser? user) { if (user == null) return; - _context.AppUser.Remove(user); + context.AppUser.Remove(user); } public void Delete(AppUserAuthKey? key) { if (key == null) return; - _context.AppUserAuthKey.Remove(key); + context.AppUserAuthKey.Remove(key); } public void Delete(AppUserBookmark bookmark) { - _context.AppUserBookmark.Remove(bookmark); + context.AppUserBookmark.Remove(bookmark); } public void Delete(IEnumerable streams) { - _context.AppUserDashboardStream.RemoveRange(streams); + context.AppUserDashboardStream.RemoveRange(streams); } public void Delete(AppUserDashboardStream stream) { - _context.AppUserDashboardStream.Remove(stream); + context.AppUserDashboardStream.Remove(stream); } public void Delete(IEnumerable streams) { - _context.AppUserSideNavStream.RemoveRange(streams); + context.AppUserSideNavStream.RemoveRange(streams); } public void Delete(AppUserSideNavStream stream) { - _context.AppUserSideNavStream.Remove(stream); + context.AppUserSideNavStream.Remove(stream); } @@ -237,13 +113,14 @@ public class UserRepository : IUserRepository /// /// /// Includes() you want. Pass multiple with flag1 | flag2 + /// /// - public async Task GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None) + public async Task GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None, CancellationToken ct = default) { - return await _context.Users + return await context.Users .Where(x => x.UserName == username) .Includes(includeFlags) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } /// @@ -251,44 +128,45 @@ public class UserRepository : IUserRepository /// /// /// Includes() you want. Pass multiple with flag1 | flag2 + /// /// - public async Task GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None) + public async Task GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None, CancellationToken ct = default) { - return await _context.Users + return await context.Users .Where(x => x.Id == userId) .Includes(includeFlags) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetUserByAuthKey(string authKey, AppUserIncludes includeFlags = AppUserIncludes.None) + public async Task GetUserByAuthKey(string authKey, AppUserIncludes includeFlags = AppUserIncludes.None, CancellationToken ct = default) { if (string.IsNullOrEmpty(authKey)) return null; - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(ak => ak.Key == authKey) .HasNotExpired() .Select(ak => ak.AppUser) .Includes(includeFlags) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetAllBookmarksAsync() + public async Task> GetAllBookmarksAsync(CancellationToken ct = default) { - return await _context.AppUserBookmark.ToListAsync(); + return await context.AppUserBookmark.ToListAsync(ct); } - public async Task GetBookmarkForPage(int page, int chapterId, int imageOffset, int userId) + public async Task GetBookmarkForPage(int page, int chapterId, int imageOffset, int userId, CancellationToken ct = default) { - return await _context.AppUserBookmark + return await context.AppUserBookmark .Where(b => b.Page == page && b.ChapterId == chapterId && b.AppUserId == userId && b.ImageOffset == imageOffset) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetBookmarkAsync(int bookmarkId) + public async Task GetBookmarkAsync(int bookmarkId, CancellationToken ct = default) { - return await _context.AppUserBookmark + return await context.AppUserBookmark .Where(b => b.Id == bookmarkId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } @@ -296,13 +174,14 @@ public class UserRepository : IUserRepository /// This fetches the Id for a user. Use whenever you just need an ID. /// /// + /// /// - public async Task GetUserIdByUsernameAsync(string username) + public async Task GetUserIdByUsernameAsync(string username, CancellationToken ct = default) { - return await _context.Users + return await context.Users .Where(x => x.UserName == username) .Select(u => u.Id) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } @@ -310,140 +189,183 @@ public class UserRepository : IUserRepository /// Returns all Bookmarks for a given set of Ids /// /// + /// /// - public async Task> GetAllBookmarksByIds(IList bookmarkIds) + public async Task> GetAllBookmarksByIds(IList bookmarkIds, CancellationToken ct = default) { - return await _context.AppUserBookmark + return await context.AppUserBookmark .Where(b => bookmarkIds.Contains(b.Id)) .OrderBy(b => b.Created) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetUserByEmailAsync(string email, AppUserIncludes includes = AppUserIncludes.None) + public async Task GetUserByEmailAsync(string email, AppUserIncludes includes = AppUserIncludes.None, CancellationToken ct = default) { var lowerEmail = email.ToLower(); - return await _context.AppUser + return await context.AppUser .Includes(includes) - .FirstOrDefaultAsync(u => u.Email != null && u.Email.ToLower().Equals(lowerEmail)); + .FirstOrDefaultAsync(u => u.Email != null && u.Email.ToLower().Equals(lowerEmail), ct); } - public async Task> GetAllPreferencesByThemeAsync(int themeId) + public async Task> GetAllPreferencesByThemeAsync(int themeId, CancellationToken ct = default) { - return await _context.AppUserPreferences + return await context.AppUserPreferences .Include(p => p.Theme) .Where(p => p.Theme.Id == themeId) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllPreferencesByFontAsync(string fontName) + public async Task> GetAllPreferencesByFontAsync(string fontName, CancellationToken ct = default) { - return await _context.AppUserPreferences + return await context.AppUserPreferences .Where(p => p.BookReaderFontFamily == fontName) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task HasAccessToLibrary(int libraryId, int userId) + public async Task HasAccessToLibrary(int libraryId, int userId, CancellationToken ct = default) { - return await _context.Library + return await context.Library .Include(l => l.AppUsers) .AsSplitQuery() - .AnyAsync(library => library.AppUsers.Any(user => user.Id == userId) && library.Id == libraryId); + .AnyAsync(library => library.AppUsers.Any(user => user.Id == userId) && library.Id == libraryId, ct); } /// /// Does the user have library and age restriction access to a given series /// /// - public async Task HasAccessToSeries(int userId, int seriesId) + public async Task HasAccessToSeries(int userId, int seriesId, CancellationToken ct = default) { - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.Series + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.Series .Include(s => s.Library) .Where(s => s.Library.AppUsers.Any(user => user.Id == userId)) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() - .AnyAsync(s => s.Id == seriesId); + .AnyAsync(s => s.Id == seriesId, ct); } - public async Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true) + public async Task HasAccessToVolume(int userId, int volumeId, CancellationToken ct = default) { - var query = _context.AppUser.Includes(includeFlags); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.Volume + .Where(v => v.Id == volumeId) + .Include(v => v.Series) + .ThenInclude(s => s.Library) + .Where(v => v.Series.Library.AppUsers.Any(user => user.Id == userId)) + .Select(v => v.Series) + .RestrictAgainstAgeRestriction(userRating) + .AsSplitQuery() + .AnyAsync(ct); + } + + public async Task HasAccessToChapter(int userId, int chapterId, CancellationToken ct = default) + { + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.Chapter + .Include(c => c.Volume) + .ThenInclude(v => v.Series) + .ThenInclude(s => s.Library) + .Where(c => c.Volume.Series.Library.AppUsers.Any(user => user.Id == userId)) + .RestrictAgainstAgeRestriction(userRating) + .AsSplitQuery() + .AnyAsync(c => c.Id == chapterId, ct); + } + + public async Task HasAccessToPerson(int userId, int personId, CancellationToken ct = default) + { + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.Person + .RestrictAgainstAgeRestriction(userRating) + .AnyAsync(p => p.Id == personId, ct); + } + + public Task HasAccessToReadingList(int userId, int readingListId, CancellationToken ct = default) + { + return context.ReadingList + .Where(rl => rl.AppUserId == userId || rl.Promoted) + .AnyAsync(rl => rl.Id == readingListId, ct); + } + + public async Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true, CancellationToken ct = default) + { + var query = context.AppUser.Includes(includeFlags); if (track) { - return await query.ToListAsync(); + return await query.ToListAsync(ct); } return await query .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetUserByConfirmationToken(string token) + public async Task GetUserByConfirmationToken(string token, CancellationToken ct = default) { - return await _context.AppUser - .SingleOrDefaultAsync(u => u.ConfirmationToken != null && u.ConfirmationToken.Equals(token)); + return await context.AppUser + .SingleOrDefaultAsync(u => u.ConfirmationToken != null && u.ConfirmationToken.Equals(token), ct); } /// /// Returns the first admin account created /// /// - public async Task GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None) + public async Task GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None, CancellationToken ct = default) { - return await _context.AppUser + return await context.AppUser .Includes(includes) .Where(u => u.UserRoles.Any(r => r.Role.Name == PolicyConstants.AdminRole)) .OrderBy(u => u.Created) - .FirstAsync(); + .FirstAsync(ct); } - public async Task> GetSeriesWithRatings(int userId) + public async Task> GetSeriesWithRatings(int userId, CancellationToken ct = default) { - return await _context.AppUserRating + return await context.AppUserRating .Where(u => u.AppUserId == userId && u.Rating > 0) .Include(u => u.Series) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetSeriesWithReviews(int userId) + public async Task> GetSeriesWithReviews(int userId, CancellationToken ct = default) { - return await _context.AppUserRating + return await context.AppUserRating .Where(u => u.AppUserId == userId && !string.IsNullOrEmpty(u.Review)) .Include(u => u.Series) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task HasHoldOnSeries(int userId, int seriesId) + public async Task HasHoldOnSeries(int userId, int seriesId, CancellationToken ct = default) { - return await _context.AppUser + return await context.AppUser .AsSplitQuery() - .AnyAsync(u => u.ScrobbleHolds.Select(s => s.SeriesId).Contains(seriesId) && u.Id == userId); + .AnyAsync(u => u.ScrobbleHolds.Select(s => s.SeriesId).Contains(seriesId) && u.Id == userId, ct); } - public async Task> GetHolds(int userId) + public async Task> GetHolds(int userId, CancellationToken ct = default) { - return await _context.ScrobbleHold + return await context.ScrobbleHold .Where(s => s.AppUserId == userId) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task GetLocale(int userId) + public async Task GetLocale(int userId, CancellationToken ct = default) { - return await _context.AppUserPreferences.Where(p => p.AppUserId == userId) + return await context.AppUserPreferences.Where(p => p.AppUserId == userId) .Select(p => p.Locale) - .SingleAsync(); + .SingleAsync(ct); } - public async Task> GetDashboardStreams(int userId, bool visibleOnly = false) + public async Task> GetDashboardStreams(int userId, bool visibleOnly = false, CancellationToken ct = default) { - return await _context.AppUserDashboardStream + return await context.AppUserDashboardStream .Where(d => d.AppUserId == userId) .WhereIf(visibleOnly, d => d.Visible) .OrderBy(d => d.Order) @@ -459,36 +381,36 @@ public class UserRepository : IUserRepository Order = d.Order, Visible = d.Visible }) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllDashboardStreams() + public async Task> GetAllDashboardStreams(CancellationToken ct = default) { - return await _context.AppUserDashboardStream + return await context.AppUserDashboardStream .OrderBy(d => d.Order) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetDashboardStream(int streamId) + public async Task GetDashboardStream(int streamId, CancellationToken ct = default) { - return await _context.AppUserDashboardStream + return await context.AppUserDashboardStream .Include(d => d.SmartFilter) - .FirstOrDefaultAsync(d => d.Id == streamId); + .FirstOrDefaultAsync(d => d.Id == streamId, ct); } - public async Task> GetDashboardStreamWithFilter(int filterId) + public async Task> GetDashboardStreamWithFilter(int filterId, CancellationToken ct = default) { - return await _context.AppUserDashboardStream + return await context.AppUserDashboardStream .Include(d => d.SmartFilter) .Where(d => d.SmartFilter != null && d.SmartFilter.Id == filterId) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetSideNavStreams(int userId, bool visibleOnly = false) + public async Task> GetSideNavStreams(int userId, bool visibleOnly = false, CancellationToken ct = default) { - var sideNavStreams = await _context.AppUserSideNavStream + var sideNavStreams = await context.AppUserSideNavStream .Where(d => d.AppUserId == userId) .WhereIf(visibleOnly, d => d.Visible) .OrderBy(d => d.Order) @@ -507,16 +429,16 @@ public class UserRepository : IUserRepository Visible = d.Visible }) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); var libraryIds = sideNavStreams.Where(d => d.StreamType == SideNavStreamType.Library) .Select(d => d.LibraryId) .ToList(); - var libraryDtos = await _context.Library + var libraryDtos = await context.Library .Where(l => libraryIds.Contains(l.Id)) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); foreach (var dto in sideNavStreams.Where(dto => dto.StreamType == SideNavStreamType.Library)) { @@ -527,9 +449,9 @@ public class UserRepository : IUserRepository .Select(d => d.ExternalSourceId) .ToList(); - var externalSourceDtos = _context.AppUserExternalSource + var externalSourceDtos = context.AppUserExternalSource .Where(l => externalSourceIds.Contains(l.Id)) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .ToList(); foreach (var dto in sideNavStreams.Where(dto => dto.StreamType == SideNavStreamType.ExternalSource)) @@ -540,53 +462,53 @@ public class UserRepository : IUserRepository return sideNavStreams; } - public async Task GetSideNavStream(int streamId) + public async Task GetSideNavStream(int streamId, CancellationToken ct = default) { - return await _context.AppUserSideNavStream + return await context.AppUserSideNavStream .Include(d => d.SmartFilter) - .FirstOrDefaultAsync(d => d.Id == streamId); + .FirstOrDefaultAsync(d => d.Id == streamId, ct); } - public async Task GetSideNavStreamWithUser(int streamId) + public async Task GetSideNavStreamWithUser(int streamId, CancellationToken ct = default) { - return await _context.AppUserSideNavStream + return await context.AppUserSideNavStream .Include(d => d.SmartFilter) .Include(d => d.AppUser) - .FirstOrDefaultAsync(d => d.Id == streamId); + .FirstOrDefaultAsync(d => d.Id == streamId, ct); } - public async Task> GetSideNavStreamWithFilter(int filterId) + public async Task> GetSideNavStreamWithFilter(int filterId, CancellationToken ct = default) { - return await _context.AppUserSideNavStream + return await context.AppUserSideNavStream .Include(d => d.SmartFilter) .Where(d => d.SmartFilter != null && d.SmartFilter.Id == filterId) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetSideNavStreamsByLibraryId(int libraryId) + public async Task> GetSideNavStreamsByLibraryId(int libraryId, CancellationToken ct = default) { - return await _context.AppUserSideNavStream + return await context.AppUserSideNavStream .Where(d => d.LibraryId == libraryId) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetSideNavStreamWithExternalSource(int externalSourceId) + public async Task> GetSideNavStreamWithExternalSource(int externalSourceId, CancellationToken ct = default) { - return await _context.AppUserSideNavStream + return await context.AppUserSideNavStream .Where(d => d.ExternalSourceId == externalSourceId) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetDashboardStreamsByIds(IList streamIds) + public async Task> GetDashboardStreamsByIds(IList streamIds, CancellationToken ct = default) { - return await _context.AppUserSideNavStream + return await context.AppUserSideNavStream .Where(d => streamIds.Contains(d.Id)) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetUserTokenInfo() + public async Task> GetUserTokenInfo(CancellationToken ct = default) { - var users = await _context.AppUser + var users = await context.AppUser .Select(u => new { u.Id, @@ -594,7 +516,7 @@ public class UserRepository : IUserRepository u.AniListAccessToken, // JWT Token u.MalAccessToken // JWT Token }) - .ToListAsync(); + .ToListAsync(ct); var userTokenInfos = users.Select(user => new UserTokenInfo { @@ -613,12 +535,13 @@ public class UserRepository : IUserRepository /// Returns the first user with a device email matching ///
/// + /// /// - public async Task GetUserByDeviceEmail(string deviceEmail) + public async Task GetUserByDeviceEmail(string deviceEmail, CancellationToken ct = default) { - return await _context.AppUser + return await context.AppUser .Where(u => u.Devices.Any(d => d.EmailAddress == deviceEmail)) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } /// @@ -626,70 +549,71 @@ public class UserRepository : IUserRepository /// /// /// + /// /// - public async Task> GetAnnotations(int userId, int chapterId) + public async Task> GetAnnotations(int userId, int chapterId, CancellationToken ct = default) { - var userPreferences = await _context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); - return await _context.AppUserAnnotation + return await context.AppUserAnnotation .Where(a => a.ChapterId == chapterId) .RestrictBySocialPreferences(userId, userPreferences) .OrderBy(a => a.PageNumber) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetAnnotationsByPage(int userId, int chapterId, int pageNum) + public async Task> GetAnnotationsByPage(int userId, int chapterId, int pageNum, CancellationToken ct = default) { - var userPreferences = await _context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); - return await _context.AppUserAnnotation + return await context.AppUserAnnotation .Where(a => a.ChapterId == chapterId && a.PageNumber == pageNum) .RestrictBySocialPreferences(userId, userPreferences) .OrderBy(a => a.PageNumber) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task GetByOidcId(string? oidcId, AppUserIncludes includes = AppUserIncludes.None) + public async Task GetByOidcId(string? oidcId, AppUserIncludes includes = AppUserIncludes.None, CancellationToken ct = default) { if (string.IsNullOrEmpty(oidcId)) return null; - return await _context.AppUser + return await context.AppUser .Where(u => u.OidcId == oidcId) .Includes(includes) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetAnnotationDtoById(int userId, int annotationId) + public async Task GetAnnotationDtoById(int userId, int annotationId, CancellationToken ct = default) { - var userPreferences = await _context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); - return await _context.AppUserAnnotation + return await context.AppUserAnnotation .Where(a => a.Id == annotationId) .RestrictBySocialPreferences(userId, userPreferences) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); } - public async Task> GetAnnotationDtosBySeries(int userId, int seriesId) + public async Task> GetAnnotationDtosBySeries(int userId, int seriesId, CancellationToken ct = default) { - var userPreferences = await _context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); - return await _context.AppUserAnnotation + return await context.AppUserAnnotation .Where(a => a.SeriesId == seriesId) .RestrictBySocialPreferences(userId, userPreferences) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task UpdateUserAsActive(int userId) + public async Task UpdateUserAsActive(int userId, CancellationToken ct = default) { - await _context.Set() + await context.Set() .Where(u => u.Id == userId) .ExecuteUpdateAsync(setters => setters .SetProperty(u => u.LastActiveUtc, DateTime.UtcNow) - .SetProperty(u => u.LastActive, DateTime.Now)); + .SetProperty(u => u.LastActive, DateTime.Now), ct); } /// @@ -699,14 +623,15 @@ public class UserRepository : IUserRepository /// Viewer UserId /// Search text to match against Series name /// Rating, only applies to series/chapters rated. Will show everything greater or equal to + /// /// - public async Task> GetAllReviewsForUser(int userId, int requestingUserId, string? query = null, float? ratingFilter = null) + public async Task> GetAllReviewsForUser(int userId, int requestingUserId, string? query = null, float? ratingFilter = null, CancellationToken ct = default) { var bypassPreferences = userId == requestingUserId; if (!bypassPreferences) { - var userPreferences = await _context.AppUserPreferences - .FirstOrDefaultAsync(u => u.AppUserId == userId); + var userPreferences = await context.AppUserPreferences + .FirstOrDefaultAsync(u => u.AppUserId == userId, ct); if (userPreferences?.SocialPreferences?.ShareReviews == false) { @@ -714,10 +639,10 @@ public class UserRepository : IUserRepository } } - var userRating = await _context.AppUser.GetUserAgeRestriction(requestingUserId); + var userRating = await context.AppUser.GetUserAgeRestriction(requestingUserId); // Get series-level reviews - var seriesReviews = await _context.AppUserRating + var seriesReviews = await context.AppUserRating .WhereIf(ratingFilter is > 0, r => r.HasBeenRated && r.Rating >= ratingFilter!.Value) .Include(r => r.AppUser) .Include(r => r.Series) @@ -729,11 +654,11 @@ public class UserRepository : IUserRepository .RestrictAgainstAgeRestriction(userRating, requestingUserId) .OrderBy(r => r.SeriesId) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); // Get chapter-level reviews - var chapterReviews = await _context.AppUserChapterRating + var chapterReviews = await context.AppUserChapterRating .Include(r => r.AppUser) .Include(r => r.Series) .Include(r => r.Chapter) @@ -743,148 +668,148 @@ public class UserRepository : IUserRepository .OrderBy(r => r.SeriesId) .ThenBy(r => r.ChapterId) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); // Combine and return both lists return seriesReviews.Concat(chapterReviews).ToList(); } - public async Task> GetAdminUsersAsync() + public async Task> GetAdminUsersAsync(CancellationToken ct = default) { - return (await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole)).OrderBy(u => u.CreatedUtc); + return (await userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole)).OrderBy(u => u.CreatedUtc); } - public async Task IsUserAdminAsync(AppUser? user) + public async Task IsUserAdminAsync(AppUser? user, CancellationToken ct = default) { if (user == null) return false; - return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole); + return await userManager.IsInRoleAsync(user, PolicyConstants.AdminRole); } - public async Task> GetRoles(int userId) + public async Task> GetRoles(int userId, CancellationToken ct = default) { - var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId); + var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId, ct); if (user == null) return ArraySegment.Empty; // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (_userManager == null) + if (userManager == null) { // userManager is null on Unit Tests only - return await _context.UserRoles + return await context.UserRoles .Where(ur => ur.UserId == userId) .Select(ur => ur.Role.Name) - .ToListAsync(); + .ToListAsync(ct); } - return await _userManager.GetRolesAsync(user); + return await userManager.GetRolesAsync(user); } - public async Task> GetRolesByAuthKey(string? apiKey) + public async Task> GetRolesByAuthKey(string? apiKey, CancellationToken ct = default) { if (string.IsNullOrEmpty(apiKey)) return ArraySegment.Empty; - var user = await _context.AppUserAuthKey + var user = await context.AppUserAuthKey .Where(k => k.Key == apiKey) .HasNotExpired() .Select(k => k.AppUser) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (user == null) return ArraySegment.Empty; // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (_userManager == null) + if (userManager == null) { // userManager is null on Unit Tests only - return await _context.UserRoles + return await context.UserRoles .Where(ur => ur.User.AuthKeys.Any(k => k.Key == apiKey && (k.ExpiresAtUtc == null || k.ExpiresAtUtc < DateTime.UtcNow))) .Select(ur => ur.Role.Name) - .ToListAsync(); + .ToListAsync(ct); } - return await _userManager.GetRolesAsync(user); + return await userManager.GetRolesAsync(user); } - public async Task GetUserRatingAsync(int seriesId, int userId) + public async Task GetUserRatingAsync(int seriesId, int userId, CancellationToken ct = default) { - return await _context.AppUserRating + return await context.AppUserRating .Where(r => r.SeriesId == seriesId && r.AppUserId == userId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetUserChapterRatingAsync(int userId, int chapterId) + public async Task GetUserChapterRatingAsync(int userId, int chapterId, CancellationToken ct = default) { - return await _context.AppUserChapterRating + return await context.AppUserChapterRating .Where(r => r.AppUserId == userId && r.ChapterId == chapterId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId) + public async Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId, CancellationToken ct = default) { - var userPreferences = await _context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); - return await _context.AppUserRating + return await context.AppUserRating .Include(r => r.AppUser) .Where(r => r.SeriesId == seriesId) .RestrictBySocialPreferences(userId, userPreferences) .OrderBy(r => r.AppUserId == userId) .ThenBy(r => r.Rating) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId) + public async Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId, CancellationToken ct = default) { - var userPreferences = await _context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); - return await _context.AppUserChapterRating + return await context.AppUserChapterRating .Include(r => r.AppUser) .Where(r => r.ChapterId == chapterId) .RestrictBySocialPreferences(userId, userPreferences) .OrderBy(r => r.AppUserId == userId) .ThenBy(r => r.Rating) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task GetPreferencesAsync(string username) + public async Task GetPreferencesAsync(string username, CancellationToken ct = default) { - return await _context.AppUserPreferences + return await context.AppUserPreferences .Include(p => p.AppUser) .Include(p => p.Theme) .AsSplitQuery() - .SingleOrDefaultAsync(p => p.AppUser.UserName == username); + .SingleOrDefaultAsync(p => p.AppUser.UserName == username, ct); } - public async Task> GetBookmarkDtosForSeries(int userId, int seriesId) + public async Task> GetBookmarkDtosForSeries(int userId, int seriesId, CancellationToken ct = default) { - return await _context.AppUserBookmark + return await context.AppUserBookmark .Where(x => x.AppUserId == userId && x.SeriesId == seriesId) .OrderBy(x => x.Created) .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetBookmarkDtosForVolume(int userId, int volumeId) + public async Task> GetBookmarkDtosForVolume(int userId, int volumeId, CancellationToken ct = default) { - return await _context.AppUserBookmark + return await context.AppUserBookmark .Where(x => x.AppUserId == userId && x.VolumeId == volumeId) .OrderBy(x => x.Created) .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetBookmarkDtosForChapter(int userId, int chapterId) + public async Task> GetBookmarkDtosForChapter(int userId, int chapterId, CancellationToken ct = default) { - return await _context.AppUserBookmark + return await context.AppUserBookmark .Where(x => x.AppUserId == userId && x.ChapterId == chapterId) .OrderBy(x => x.Created) .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } /// @@ -892,16 +817,17 @@ public class UserRepository : IUserRepository /// /// /// Only supports SeriesNameQuery + /// /// - public async Task> GetAllBookmarkDtos(int userId, FilterV2Dto filter) + public async Task> GetAllBookmarkDtos(int userId, FilterV2Dto filter, CancellationToken ct = default) { - var query = _context.AppUserBookmark + var query = context.AppUserBookmark .Where(x => x.AppUserId == userId) .OrderBy(x => x.Created) .AsNoTracking(); - var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id, - (bookmark, series) => new BookmarkSeriesPair() + var filterSeriesQuery = query.Join(context.Series, b => b.SeriesId, s => s.Id, + (bookmark, series) => new BookmarkSeriesPair { Bookmark = bookmark, Series = series @@ -913,8 +839,8 @@ public class UserRepository : IUserRepository return await ApplyLimit(filterSeriesQuery .Sort(filter.SortOptions) .AsSplitQuery(), filter.LimitTo) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } var queryString = filterStatement.Value.ToNormalized(); @@ -968,8 +894,8 @@ public class UserRepository : IUserRepository return await ApplyLimit(filterSeriesQuery .Sort(filter.SortOptions) .AsSplitQuery(), filter.LimitTo) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } private static IQueryable ApplyLimit(IQueryable query, int limit) @@ -977,44 +903,44 @@ public class UserRepository : IUserRepository return limit <= 0 ? query : query.Take(limit); } - public async Task GetUserDtoByAuthKeyAsync(string authKey) + public async Task GetUserDtoByAuthKeyAsync(string authKey, CancellationToken ct = default) { if (string.IsNullOrEmpty(authKey)) return null; - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(k => k.Key == authKey) .HasNotExpired() .Include(k => k.AppUser) .ThenInclude(u => u.UserRoles) .ThenInclude(ur => ur.Role) .Select(k => k.AppUser) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); } - public async Task GetUserIdByAuthKeyAsync(string authKey) + public async Task GetUserIdByAuthKeyAsync(string authKey, CancellationToken ct = default) { if (string.IsNullOrEmpty(authKey)) return 0; - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(k => k.Key == authKey) .HasNotExpired() .Select(k => k.AppUserId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetUserDtoById(int userId) + public async Task GetUserDtoById(int userId, CancellationToken ct = default) { - return await _context.AppUser + return await context.AppUser .Where(u => u.Id == userId) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); } - public async Task> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true) + public async Task> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true, CancellationToken ct = default) { - return await _context.Users + return await context.Users .Where(u => (emailConfirmed && u.EmailConfirmed) || !emailConfirmed) .Include(x => x.Libraries) .Include(r => r.UserRoles) @@ -1048,84 +974,69 @@ public class UserRepository : IUserRepository }) .AsSplitQuery() .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetCoverImageAsync(int userId, int requestingUserId) + public Task GetCoverImageAsync(int userId, CancellationToken ct = default) { - // TODO: .NET JSON Support - // return await _context.AppUser - // .Include(c => c.UserPreferences) - // .Where(c => c.Id == userId && c.UserPreferences.SocialPreferences.ShareProfile) - // .Select(c => c.CoverImage) - // .FirstOrDefaultAsync(); - - var user = await _context.AppUser - .Include(c => c.UserPreferences) - .Where(c => c.Id == userId) - .Select(c => new { c.CoverImage, c.UserPreferences }) - .FirstOrDefaultAsync(); - - if (user?.UserPreferences?.SocialPreferences?.ShareProfile == true || userId == requestingUserId) - { - return user.CoverImage; - } - - return null; + return context.AppUser + .Where(u => u.Id == userId) + .Select(u => u.CoverImage) + .FirstOrDefaultAsync(ct); } - public async Task GetPersonCoverImageAsync(int personId) + public async Task GetPersonCoverImageAsync(int personId, CancellationToken ct = default) { - return await _context.Person + return await context.Person .Where(p => p.Id == personId) .Select(p => p.CoverImage) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetAuthKeysForUserId(int userId) + public async Task> GetAuthKeysForUserId(int userId, CancellationToken ct = default) { - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(k => k.AppUserId == userId) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetAllAuthKeysDtosWithExpiration() + public async Task> GetAllAuthKeysDtosWithExpiration(CancellationToken ct = default) { - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(k => k.ExpiresAtUtc != null) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task GetAuthKeyById(int authKeyId) + public async Task GetAuthKeyById(int authKeyId, CancellationToken ct = default) { - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(k => k.Id == authKeyId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetAuthKeyExpiration(string authKey, int userId) + public async Task GetAuthKeyExpiration(string authKey, int userId, CancellationToken ct = default) { - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(k => k.Key == authKey && k.AppUserId == userId) .Select(k => k.ExpiresAtUtc) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetSocialPreferencesForUser(int userId) + public async Task GetSocialPreferencesForUser(int userId, CancellationToken ct = default) { - return await _context.AppUserPreferences + return await context.AppUserPreferences .Where(p => p.AppUserId == userId) .Select(p => p.SocialPreferences) - .FirstAsync(); + .FirstAsync(ct); } - public async Task GetPreferencesForUser(int userId) + public async Task GetPreferencesForUser(int userId, CancellationToken ct = default) { - return await _context.AppUserPreferences + return await context.AppUserPreferences .Where(p => p.AppUserId == userId) - .FirstAsync(); + .FirstAsync(ct); } /// @@ -1133,12 +1044,12 @@ public class UserRepository : IUserRepository /// /// /// - public async Task GetOpdsPreferences(int userId) + public async Task GetOpdsPreferences(int userId, CancellationToken ct = default) { - return await _context.AppUserPreferences + return await context.AppUserPreferences .Where(p => p.AppUserId == userId) .Select(p => p.OpdsPreferences) .AsNoTracking() - .FirstAsync(); + .FirstAsync(ct); } } diff --git a/Kavita.Database/Repositories/UserTableOfContentRepository.cs b/Kavita.Database/Repositories/UserTableOfContentRepository.cs new file mode 100644 index 000000000..63500eae1 --- /dev/null +++ b/Kavita.Database/Repositories/UserTableOfContentRepository.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.User; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class UserTableOfContentRepository(DataContext context, IMapper mapper) : IUserTableOfContentRepository +{ + public void Attach(AppUserTableOfContent toc) + { + context.AppUserTableOfContent.Attach(toc); + } + + public void Remove(AppUserTableOfContent toc) + { + context.AppUserTableOfContent.Remove(toc); + } + + public async Task IsUnique(int userId, int chapterId, int page, string title) + { + return await context.AppUserTableOfContent.AnyAsync(t => + t.AppUserId == userId && t.PageNumber == page && t.Title == title && t.ChapterId == chapterId); + } + + public async Task> GetPersonalToC(int userId, int chapterId) + { + return await context.AppUserTableOfContent + .Where(t => t.AppUserId == userId && t.ChapterId == chapterId) + .ProjectTo(mapper.ConfigurationProvider) + .OrderBy(t => t.PageNumber) + .ToListAsync(); + } + + public async Task> GetPersonalToCForPage(int userId, int chapterId, int page) + { + return await context.AppUserTableOfContent + .Where(t => t.AppUserId == userId && t.ChapterId == chapterId && t.PageNumber == page) + .ProjectTo(mapper.ConfigurationProvider) + .OrderBy(t => t.PageNumber) + .ToListAsync(); + } + + public async Task Get(int userId,int chapterId, int pageNum, string title) + { + return await context.AppUserTableOfContent + .Where(t => t.AppUserId == userId && t.ChapterId == chapterId && t.PageNumber == pageNum && t.Title == title) + .FirstOrDefaultAsync(); + } +} diff --git a/API/Data/Repositories/VolumeRepository.cs b/Kavita.Database/Repositories/VolumeRepository.cs similarity index 59% rename from API/Data/Repositories/VolumeRepository.cs rename to Kavita.Database/Repositories/VolumeRepository.cs index 85a512c49..9aedf7df0 100644 --- a/API/Data/Repositories/VolumeRepository.cs +++ b/Kavita.Database/Repositories/VolumeRepository.cs @@ -1,155 +1,95 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Extensions.QueryExtensions; using AutoMapper; +using Kavita.API.Repositories; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum VolumeIncludes +public class VolumeRepository(DataContext context, IMapper mapper) : IVolumeRepository { - None = 1, - Chapters = 2, - People = 4, - Tags = 8, - /// - /// This will include Chapters by default - /// - Files = 16 -} - -public interface IVolumeRepository -{ - void Add(Volume volume); - void Update(Volume volume); - void Remove(Volume volume); - void Remove(IList volumes); - Task GetFilesizeForVolumeAsync(int volumeId); - Task> GetFilesizeForVolumesAsync(IList volumeIds); - Task> GetFilesForVolume(int volumeId); - Task GetVolumeCoverImageAsync(int volumeId); - Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds); - Task> GetVolumesDtoAsync(int seriesId, int userId, VolumeIncludes includes = VolumeIncludes.Chapters); - Task GetVolumeByIdAsync(int volumeId, VolumeIncludes includes = VolumeIncludes.Files); - Task GetVolumeDtoAsync(int volumeId, int userId); - Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false); - Task> GetVolumes(int seriesId); - Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None); - Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); - Task> GetCoverImagesForLockedVolumesAsync(); -} -public class VolumeRepository : IVolumeRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public VolumeRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Add(Volume volume) { - _context.Volume.Add(volume); + context.Volume.Add(volume); } public void Update(Volume volume) { - _context.Entry(volume).State = EntityState.Modified; + context.Entry(volume).State = EntityState.Modified; } public void Remove(Volume volume) { - _context.Volume.Remove(volume); + context.Volume.Remove(volume); } public void Remove(IList volumes) { - _context.Volume.RemoveRange(volumes); - } - - public async Task GetFilesizeForVolumeAsync(int volumeId) - { - return await _context.Chapter - .Where(c => volumeId == c.VolumeId) - .Include(c => c.Files) - .SelectMany(c => c.Files) - .SumAsync(f => f.Bytes); - } - - public async Task> GetFilesizeForVolumesAsync(IList volumeIds) - { - return await volumeIds.BatchToDictionaryAsync(50, batch => - _context.Chapter - .Where(c => batch.Contains(c.VolumeId)) - .GroupBy(c => c.VolumeId) - .Select(g => new - { - VolumeId = g.Key, - TotalBytes = g.SelectMany(c => c.Files).Sum(f => f.Bytes) - }) - .ToDictionaryAsync(x => x.VolumeId, x => x.TotalBytes)); + context.Volume.RemoveRange(volumes); } /// /// Returns a list of non-tracked files for a given volume. /// /// + /// /// - public async Task> GetFilesForVolume(int volumeId) + public async Task> GetFilesForVolume(int volumeId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => volumeId == c.VolumeId) .Include(c => c.Files) .SelectMany(c => c.Files) .AsSplitQuery() .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns the cover image file for the given volume /// /// + /// /// - public async Task GetVolumeCoverImageAsync(int volumeId) + public async Task GetVolumeCoverImageAsync(int volumeId, CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Where(v => v.Id == volumeId) .Select(v => v.CoverImage) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } /// /// Returns all chapter Ids belonging to a list of Volume Ids /// /// + /// /// - public async Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds) + public async Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => volumeIds.Contains(c.VolumeId)) .Select(c => c.Id) - .ToListAsync(); + .ToListAsync(ct); } /// - /// Returns all volumes that contain a seriesId in passed array. + /// Returns all volumes that contain a seriesId in a passed array. /// /// /// Include chapter entities + /// /// - public async Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false) + public async Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false, + CancellationToken ct = default) { - var query = _context.Volume + var query = context.Volume .Where(v => seriesIds.Contains(v.SeriesId)); if (includeChapters) @@ -158,7 +98,7 @@ public class VolumeRepository : IVolumeRepository .Includes(VolumeIncludes.Chapters) .AsSplitQuery(); } - var volumes = await query.ToListAsync(); + var volumes = await query.ToListAsync(ct); foreach (var volume in volumes) { @@ -173,53 +113,59 @@ public class VolumeRepository : IVolumeRepository /// /// /// + /// /// - public async Task GetVolumeDtoAsync(int volumeId, int userId) + public async Task GetVolumeDtoAsync(int volumeId, int userId, CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Where(vol => vol.Id == volumeId) .Includes(VolumeIncludes.Chapters | VolumeIncludes.Files) .AsSplitQuery() .OrderBy(v => v.MinNumber) - .ProjectToWithProgress(_mapper, userId) - .FirstOrDefaultAsync(vol => vol.Id == volumeId); + .ProjectToWithProgress(mapper, userId) + .FirstOrDefaultAsync(vol => vol.Id == volumeId, ct); } /// /// Returns the full Volumes including Chapters and Files for a given series /// /// + /// /// - public async Task> GetVolumes(int seriesId) + public async Task> GetVolumes(int seriesId, CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Where(vol => vol.SeriesId == seriesId) .Includes(VolumeIncludes.Chapters | VolumeIncludes.Files) .AsSplitQuery() .OrderBy(vol => vol.MinNumber) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None) + public async Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None, + CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Where(vol => volumeIds.Contains(vol.Id)) .Includes(includes) .AsSplitQuery() .OrderBy(vol => vol.MinNumber) - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns a single volume with Chapter and Files /// /// + /// + /// /// - public async Task GetVolumeByIdAsync(int volumeId, VolumeIncludes includes = VolumeIncludes.Files) + public async Task GetVolumeByIdAsync(int volumeId, VolumeIncludes includes = VolumeIncludes.Files, + CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Includes(includes) .AsSplitQuery() - .SingleOrDefaultAsync(vol => vol.Id == volumeId); + .SingleOrDefaultAsync(vol => vol.Id == volumeId, ct); } @@ -228,39 +174,67 @@ public class VolumeRepository : IVolumeRepository ///
/// /// + /// + /// /// - public async Task> GetVolumesDtoAsync(int seriesId, int userId, VolumeIncludes includes = VolumeIncludes.Chapters) + public async Task> GetVolumesDtoAsync(int seriesId, int userId, + VolumeIncludes includes = VolumeIncludes.Chapters, CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Where(vol => vol.SeriesId == seriesId) .Includes(includes) .OrderBy(volume => volume.MinNumber) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) + public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, + CancellationToken ct = default) { var extension = encodeFormat.GetExtension(); - return await _context.Volume + return await context.Volume .Includes(VolumeIncludes.Chapters) .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns cover images for locked chapters /// + /// /// - public async Task> GetCoverImagesForLockedVolumesAsync() + public async Task> GetCoverImagesForLockedVolumesAsync(CancellationToken ct = default) { - return (await _context.Volume + return (await context.Volume .Where(c => c.CoverImageLocked) .Select(c => c.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync())!; + .ToListAsync(ct))!; + } + + public async Task GetFilesizeForVolumeAsync(int volumeId, CancellationToken ct = default) + { + return await context.Chapter + .Where(c => volumeId == c.VolumeId) + .Include(c => c.Files) + .SelectMany(c => c.Files) + .SumAsync(f => f.Bytes, cancellationToken: ct); + } + + public async Task> GetFilesizeForVolumesAsync(IList volumeIds, CancellationToken ct = default) + { + return await volumeIds.BatchToDictionaryAsync(50, batch => + context.Chapter + .Where(c => batch.Contains(c.VolumeId)) + .GroupBy(c => c.VolumeId) + .Select(g => new + { + VolumeId = g.Key, + TotalBytes = g.SelectMany(c => c.Files).Sum(f => f.Bytes) + }) + .ToDictionaryAsync(x => x.VolumeId, x => x.TotalBytes, cancellationToken: ct)); } } diff --git a/API/Data/Seed.cs b/Kavita.Database/Seed.cs similarity index 57% rename from API/Data/Seed.cs rename to Kavita.Database/Seed.cs index a7afc07af..e2895c6cb 100644 --- a/API/Data/Seed.cs +++ b/Kavita.Database/Seed.cs @@ -1,257 +1,30 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text.Json; using System.Threading.Tasks; -using API.Constants; -using API.Data.Repositories; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.Font; -using API.Entities.Enums.Theme; -using API.Entities.Enums.User; -using API.Entities.MetadataMatching; -using API.Entities.User; -using API.Extensions; -using API.Helpers; -using API.Services; -using API.Services.Tasks; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Models; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -namespace API.Data; +namespace Kavita.Database; public static class Seed { - /// - /// Generated on Startup. Seed.SeedSettings must run before - /// - public static ImmutableArray DefaultSettings; - - public static readonly ImmutableArray DefaultHighlightSlots = - [ - new() - { - Id = 1, - SlotNumber = 0, - Color = new RgbaColor { R = 0, G = 255, B = 255, A = 0.4f } - }, - new() - { - Id = 2, - SlotNumber = 1, - Color = new RgbaColor { R = 0, G = 255, B = 0, A = 0.4f } - }, - new() - { - Id = 3, - SlotNumber = 2, - Color = new RgbaColor { R = 255, G = 255, B = 0, A = 0.4f } - }, - new() - { - Id = 4, - SlotNumber = 3, - Color = new RgbaColor { R = 255, G = 165, B = 0, A = 0.4f } - }, - new() - { - Id = 5, - SlotNumber = 4, - Color = new RgbaColor { R = 255, G = 0, B = 255, A = 0.4f } - } - ]; - - public static readonly ImmutableArray DefaultFonts = - [ - new () - { - Name = FontService.DefaultFont, - NormalizedName = Parser.Normalize(FontService.DefaultFont), - Provider = FontProvider.System, - FileName = string.Empty, - }, - new () - { - Name = "Merriweather", - NormalizedName = Parser.Normalize("Merriweather"), - Provider = FontProvider.System, - FileName = "Merriweather-Regular.woff2", - }, - new () - { - Name = "EB Garamond", - NormalizedName = Parser.Normalize("EB Garamond"), - Provider = FontProvider.System, - FileName = "EBGaramond-VariableFont_wght.woff2", - }, - new () - { - Name = "Fira Sans", - NormalizedName = Parser.Normalize("Fira Sans"), - Provider = FontProvider.System, - FileName = "FiraSans-Regular.woff2", - }, - new () - { - Name = "Lato", - NormalizedName = Parser.Normalize("Lato"), - Provider = FontProvider.System, - FileName = "Lato-Regular.woff2", - }, - new () - { - Name = "Libre Baskerville", - NormalizedName = Parser.Normalize("Libre Baskerville"), - Provider = FontProvider.System, - FileName = "LibreBaskerville-Regular.woff2", - }, - new () - { - Name = "Nanum Gothic", - NormalizedName = Parser.Normalize("Nanum Gothic"), - Provider = FontProvider.System, - FileName = "NanumGothic-Regular.woff2", - }, - new () - { - Name = "Open Dyslexic", - NormalizedName = Parser.Normalize("Open Dyslexic"), - Provider = FontProvider.System, - FileName = "OpenDyslexic-Regular.woff2", - }, - new () - { - Name = "RocknRoll One", - NormalizedName = Parser.Normalize("RocknRoll One"), - Provider = FontProvider.System, - FileName = "RocknRollOne-Regular.woff2", - }, - new () - { - Name = "Fast Font Serif", - NormalizedName = Parser.Normalize("Fast Font Serif"), - Provider = FontProvider.System, - FileName = "Fast_Serif.woff2", - }, - new () - { - Name = "Fast Font Sans", - NormalizedName = Parser.Normalize("Fast Font Sans"), - Provider = FontProvider.System, - FileName = "Fast_Sans.woff2", - } - ]; - - public static readonly ImmutableArray DefaultThemes = [ - ..new List - { - new() - { - Name = "Dark", - NormalizedName = "Dark".ToNormalized(), - Provider = ThemeProvider.System, - FileName = "dark.scss", - IsDefault = true, - Description = "Default theme shipped with Kavita" - } - }.ToArray() - ]; - - public static readonly ImmutableArray DefaultStreams = [ - ..new List - { - new() - { - Name = "on-deck", - StreamType = DashboardStreamType.OnDeck, - Order = 0, - IsProvided = true, - Visible = true - }, - new() - { - Name = "recently-updated", - StreamType = DashboardStreamType.RecentlyUpdated, - Order = 1, - IsProvided = true, - Visible = true - }, - new() - { - Name = "newly-added", - StreamType = DashboardStreamType.NewlyAdded, - Order = 2, - IsProvided = true, - Visible = true - }, - new() - { - Name = "more-in-genre", - StreamType = DashboardStreamType.MoreInGenre, - Order = 3, - IsProvided = true, - Visible = false - }, - }.ToArray() - ]; - - public static readonly ImmutableArray DefaultSideNavStreams = - [ - new() - { - Name = "want-to-read", - StreamType = SideNavStreamType.WantToRead, - Order = 1, - IsProvided = true, - Visible = true - }, new() - { - Name = "collections", - StreamType = SideNavStreamType.Collections, - Order = 2, - IsProvided = true, - Visible = true - }, new() - { - Name = "reading-lists", - StreamType = SideNavStreamType.ReadingLists, - Order = 3, - IsProvided = true, - Visible = true - }, new() - { - Name = "bookmarks", - StreamType = SideNavStreamType.Bookmarks, - Order = 4, - IsProvided = true, - Visible = true - }, new() - { - Name = "all-series", - StreamType = SideNavStreamType.AllSeries, - Order = 5, - IsProvided = true, - Visible = true - }, - new() - { - Name = "browse-authors", - StreamType = SideNavStreamType.BrowsePeople, - Order = 6, - IsProvided = true, - Visible = true - } - ]; - public static async Task SeedRoles(RoleManager roleManager) { @@ -273,11 +46,11 @@ public static class Seed } } - public static async Task SeedThemes(DataContext context) + public static async Task SeedThemes(IDataContext context) { await context.Database.EnsureCreatedAsync(); - foreach (var theme in DefaultThemes) + foreach (var theme in Defaults.DefaultThemes) { var existing = await context.SiteTheme.FirstOrDefaultAsync(s => s.Name.Equals(theme.Name)); if (existing == null) @@ -289,11 +62,11 @@ public static class Seed await context.SaveChangesAsync(); } - public static async Task SeedFonts(DataContext context) + public static async Task SeedFonts(IDataContext context) { await context.Database.EnsureCreatedAsync(); - foreach (var font in DefaultFonts) + foreach (var font in Defaults.DefaultFonts) { var existing = await context.EpubFont.FirstOrDefaultAsync(f => f.Name.Equals(font.Name)); if (existing == null) @@ -312,7 +85,7 @@ public static class Seed { if (user.DashboardStreams.Count != 0) continue; user.DashboardStreams ??= []; - foreach (var defaultStream in DefaultStreams) + foreach (var defaultStream in Defaults.DefaultStreams) { var newStream = new AppUserDashboardStream { @@ -336,7 +109,7 @@ public static class Seed foreach (var user in allUsers) { user.SideNavStreams ??= []; - foreach (var defaultStream in DefaultSideNavStreams) + foreach (var defaultStream in Defaults.DefaultSideNavStreams) { if (user.SideNavStreams.Any(s => s.Name == defaultStream.Name && s.StreamType == defaultStream.StreamType)) continue; var newStream = new AppUserSideNavStream() @@ -362,16 +135,16 @@ public static class Seed { if (user.UserPreferences.BookReaderHighlightSlots.Any()) break; - user.UserPreferences.BookReaderHighlightSlots = DefaultHighlightSlots.ToList(); + user.UserPreferences.BookReaderHighlightSlots = Defaults.DefaultHighlightSlots.ToList(); unitOfWork.UserRepository.Update(user); } await unitOfWork.CommitAsync(); } - public static async Task SeedSettings(DataContext context, IDirectoryService directoryService) + public static async Task SeedSettings(IDataContext context, IDirectoryService directoryService) { await context.Database.EnsureCreatedAsync(); - DefaultSettings = [ + Defaults.DefaultSettings = [ ..new List() { new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory}, @@ -381,7 +154,7 @@ public static class Seed new() {Key = ServerSettingKey.LoggingLevel, Value = "Debug"}, new() { - Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory) + Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(directoryService.BackupDirectory) }, new() { @@ -426,7 +199,7 @@ public static class Seed }.ToArray() ]; - foreach (var defaultSetting in DefaultSettings) + foreach (var defaultSetting in Defaults.DefaultSettings) { var existing = await context.ServerSetting.FirstOrDefaultAsync(s => s.Key == defaultSetting.Key); if (existing == null) @@ -445,7 +218,7 @@ public static class Seed (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.CacheDirectory)).Value = directoryService.CacheDirectory + string.Empty; (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.BackupDirectory)).Value = - DirectoryService.BackupDirectory + string.Empty; + directoryService.BackupDirectory + string.Empty; (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.CacheSize)).Value = Configuration.CacheSize + string.Empty; @@ -455,7 +228,7 @@ public static class Seed await context.SaveChangesAsync(); } - public static async Task SetOidcSettingsFromDisk(DataContext context) + public static async Task SetOidcSettingsFromDisk(IDataContext context) { var oidcSettingEntry = await context.ServerSetting .FirstOrDefaultAsync(setting => setting.Key == ServerSettingKey.OidcConfiguration); @@ -472,7 +245,7 @@ public static class Seed oidcSettingEntry.Value = JsonSerializer.Serialize(storedOidcSettings); } - public static async Task SeedMetadataSettings(DataContext context) + public static async Task SeedMetadataSettings(IDataContext context) { await context.Database.EnsureCreatedAsync(); @@ -506,27 +279,4 @@ public static class Seed await context.SaveChangesAsync(); } - - public static List CreateDefaultAuthKeys() - { - return - [ - new AppUserAuthKey() - { - Name = AuthKeyHelper.OpdsKeyName, - Key = AuthKeyHelper.GenerateKey(32), - CreatedAtUtc = DateTime.UtcNow, - ExpiresAtUtc = null, - Provider = AuthKeyProvider.System, - }, - new AppUserAuthKey() - { - Name = AuthKeyHelper.ImageOnlyKeyName, - Key = AuthKeyHelper.GenerateKey(32), - CreatedAtUtc = DateTime.UtcNow, - ExpiresAtUtc = null, - Provider = AuthKeyProvider.System, - } - ]; - } } diff --git a/API/Data/UnitOfWork.cs b/Kavita.Database/UnitOfWork.cs similarity index 72% rename from API/Data/UnitOfWork.cs rename to Kavita.Database/UnitOfWork.cs index 7f30a0a73..fee29fd91 100644 --- a/API/Data/UnitOfWork.cs +++ b/Kavita.Database/UnitOfWork.cs @@ -1,47 +1,15 @@ using System; +using System.Threading; using System.Threading.Tasks; -using API.Data.Repositories; -using API.Entities; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.Database.Repositories; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.Identity; -namespace API.Data; +namespace Kavita.Database; -public interface IUnitOfWork -{ - DataContext DataContext { get; } - ISeriesRepository SeriesRepository { get; } - IUserRepository UserRepository { get; } - ILibraryRepository LibraryRepository { get; } - IVolumeRepository VolumeRepository { get; } - ISettingsRepository SettingsRepository { get; } - IAppUserProgressRepository AppUserProgressRepository { get; } - ICollectionTagRepository CollectionTagRepository { get; } - IChapterRepository ChapterRepository { get; } - IReadingListRepository ReadingListRepository { get; } - ISeriesMetadataRepository SeriesMetadataRepository { get; } - IPersonRepository PersonRepository { get; } - IGenreRepository GenreRepository { get; } - ITagRepository TagRepository { get; } - ISiteThemeRepository SiteThemeRepository { get; } - IMangaFileRepository MangaFileRepository { get; } - IDeviceRepository DeviceRepository { get; } - IMediaErrorRepository MediaErrorRepository { get; } - IScrobbleRepository ScrobbleRepository { get; } - IUserTableOfContentRepository UserTableOfContentRepository { get; } - IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; } - IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } - IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } - IEmailHistoryRepository EmailHistoryRepository { get; } - IAppUserReadingProfileRepository AppUserReadingProfileRepository { get; } - IAnnotationRepository AnnotationRepository { get; } - IEpubFontRepository EpubFontRepository { get; } - IReadingSessionRepository ReadingSessionRepository { get; } - bool Commit(); - Task CommitAsync(); - bool HasChanges(); - Task RollbackAsync(); -} public class UnitOfWork : IUnitOfWork { @@ -82,12 +50,13 @@ public class UnitOfWork : IUnitOfWork AnnotationRepository = new AnnotationRepository(_context, _mapper); EpubFontRepository = new EpubFontRepository(_context, _mapper); ReadingSessionRepository = new ReadingSessionRepository(_context, _mapper); + ClientDeviceRepository = new ClientDeviceRepository(_context, _mapper); } /// /// This is here for Scanner only. Don't use otherwise. /// - public DataContext DataContext => _context; + public IDataContext DataContext => _context; public ISeriesRepository SeriesRepository { get; } public IUserRepository UserRepository { get; } public ILibraryRepository LibraryRepository { get; } @@ -115,6 +84,7 @@ public class UnitOfWork : IUnitOfWork public IAnnotationRepository AnnotationRepository { get; } public IEpubFontRepository EpubFontRepository { get; } public IReadingSessionRepository ReadingSessionRepository { get; } + public IClientDeviceRepository ClientDeviceRepository { get; } /// /// Commits changes to the DB. Completes the open transaction. @@ -124,13 +94,15 @@ public class UnitOfWork : IUnitOfWork { return _context.SaveChanges() > 0; } + /// /// Commits changes to the DB. Completes the open transaction. /// + /// /// - public async Task CommitAsync() + public async Task CommitAsync(CancellationToken ct = default) { - return await _context.SaveChangesAsync() > 0; + return await _context.SaveChangesAsync(ct) > 0; } /// @@ -155,12 +127,13 @@ public class UnitOfWork : IUnitOfWork /// /// Rollback transaction /// + /// /// - public async Task RollbackAsync() + public async Task RollbackAsync(CancellationToken ct = default) { try { - await _context.Database.RollbackTransactionAsync(); + await _context.Database.RollbackTransactionAsync(ct); } catch (Exception) { diff --git a/API.Tests/Extensions/EncodeFormatExtensionsTests.cs b/Kavita.Models.Tests/Extensions/EncodeFormatExtensionsTests.cs similarity index 67% rename from API.Tests/Extensions/EncodeFormatExtensionsTests.cs rename to Kavita.Models.Tests/Extensions/EncodeFormatExtensionsTests.cs index a71b2e754..109de77ab 100644 --- a/API.Tests/Extensions/EncodeFormatExtensionsTests.cs +++ b/Kavita.Models.Tests/Extensions/EncodeFormatExtensionsTests.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.Entities.Enums; -using API.Extensions; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; -namespace API.Tests.Extensions; +namespace Kavita.Models.Tests.Extensions; public class EncodeFormatExtensionsTests { @@ -21,7 +17,7 @@ public class EncodeFormatExtensionsTests }; // Act & Assert - foreach (var format in Enum.GetValues(typeof(EncodeFormat)).Cast()) + foreach (var format in Enum.GetValues()) { var extension = format.GetExtension(); Assert.Equal(expectedExtensions[format], extension); diff --git a/API.Tests/Extensions/EnumExtensionTests.cs b/Kavita.Models.Tests/Extensions/EnumExtensionTests.cs similarity index 78% rename from API.Tests/Extensions/EnumExtensionTests.cs rename to Kavita.Models.Tests/Extensions/EnumExtensionTests.cs index 0e8b03f09..94dcc37d1 100644 --- a/API.Tests/Extensions/EnumExtensionTests.cs +++ b/Kavita.Models.Tests/Extensions/EnumExtensionTests.cs @@ -1,10 +1,7 @@ -#nullable enable -using System; -using API.Entities.Enums; -using API.Extensions; -using Xunit; +using Kavita.Common.Extensions; +using Kavita.Models.Entities.Enums; -namespace API.Tests.Extensions; +namespace Kavita.Models.Tests.Extensions; public class EnumExtensionTests { diff --git a/Kavita.Models.Tests/Kavita.Models.Tests.csproj b/Kavita.Models.Tests/Kavita.Models.Tests.csproj new file mode 100644 index 000000000..5658ef26a --- /dev/null +++ b/Kavita.Models.Tests/Kavita.Models.Tests.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + \ No newline at end of file diff --git a/API/Data/AutoMapper/AutoMapperChapterProfile.cs b/Kavita.Models/AutoMapper/AutoMapperChapterProfile.cs similarity index 83% rename from API/Data/AutoMapper/AutoMapperChapterProfile.cs rename to Kavita.Models/AutoMapper/AutoMapperChapterProfile.cs index 639235f62..70e8f6f2e 100644 --- a/API/Data/AutoMapper/AutoMapperChapterProfile.cs +++ b/Kavita.Models/AutoMapper/AutoMapperChapterProfile.cs @@ -1,11 +1,11 @@ using System; using System.Linq; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; using AutoMapper; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.Data.AutoMapper; +namespace Kavita.Models.AutoMapper; /// /// Maps Chapter entities to ChapterDto with user progress attached at the DB level via JOIN. @@ -14,10 +14,26 @@ public class AutoMapperChapterProfile : Profile { public AutoMapperChapterProfile() { - int userId = 0; // Placeholder, will be replaced at runtime + int userId = 0; // Placeholder will be replaced at runtime CreateMap() - // Progress fields (previously in AddChapterModifiers) + .MapChapterBase(userId); + + CreateMap() + .MapChapterBase(userId) + .ForMember(dest => dest.SeriesId, opt => opt.MapFrom(src => src.Volume.SeriesId)) + .ForMember(dest => dest.VolumeTitle, opt => opt.MapFrom(src => src.Volume.Name)) + .ForMember(dest => dest.LibraryId, opt => opt.MapFrom(src => src.Volume.Series.LibraryId)) + .ForMember(dest => dest.LibraryType, opt => opt.MapFrom(src => src.Volume.Series.Library.Type)); + } +} + +internal static class AutoMapperChapterProfileBaseExtensions +{ + public static IMappingExpression MapChapterBase(this IMappingExpression mapping, int userId) + where TDest: ChapterDto + { + return mapping .ForMember(dest => dest.PagesRead, opt => opt.MapFrom(src => src.UserProgress diff --git a/API/Data/AutoMapper/AutoMapperProfiles.cs b/Kavita.Models/AutoMapper/AutoMapperProfiles.cs similarity index 90% rename from API/Data/AutoMapper/AutoMapperProfiles.cs rename to Kavita.Models/AutoMapper/AutoMapperProfiles.cs index 97966e257..881430c67 100644 --- a/API/Data/AutoMapper/AutoMapperProfiles.cs +++ b/Kavita.Models/AutoMapper/AutoMapperProfiles.cs @@ -1,49 +1,43 @@ using System; using System.Collections.Generic; using System.Linq; -using API.DTOs; -using API.DTOs.Account; -using API.DTOs.Annotations; -using API.DTOs.Collection; -using API.DTOs.Dashboard; -using API.DTOs.Device.EmailDevice; -using API.DTOs.Email; -using API.DTOs.Font; -using API.DTOs.KavitaPlus.Manage; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.MediaErrors; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.Progress; -using API.DTOs.Reader; -using API.DTOs.ReadingLists; -using API.DTOs.Recommendation; -using API.DTOs.Scrobbling; -using API.DTOs.Search; -using API.DTOs.SeriesDetail; -using API.DTOs.Settings; -using API.DTOs.SideNav; -using API.DTOs.Stats; -using API.DTOs.Theme; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Entities.Progress; -using API.Entities.Scrobble; -using API.Entities.User; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; -using API.Helpers.Converters; using AutoMapper; -using EmailHistory = API.Entities.EmailHistory; -using MediaError = API.Entities.MediaError; -using PublicationStatus = API.Entities.Enums.PublicationStatus; -using SiteTheme = API.Entities.SiteTheme; +using Kavita.Common.Helpers; +using Kavita.Models.AutoMapper.Converters; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Annotations; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.DTOs.Email; +using Kavita.Models.DTOs.Font; +using Kavita.Models.DTOs.KavitaPlus.Manage; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.MediaErrors; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.Recommendation; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.Search; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.DTOs.Stats; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.Scrobble; +using Kavita.Models.Entities.User; -namespace API.Data.AutoMapper; -#nullable enable +namespace Kavita.Models.AutoMapper; public class AutoMapperProfiles : Profile { @@ -283,7 +277,7 @@ public class AutoMapperProfiles : Profile CreateMap() .ForMember(dest => dest.BodyJustText, opt => - opt.MapFrom(src => ReviewHelper.GetCharacters(src.Body))); + opt.MapFrom(src => HtmlHelper.GetCharacters(src.Body))); CreateMap(); CreateMap() @@ -305,12 +299,6 @@ public class AutoMapperProfiles : Profile CreateMap() .ForMember(dest => dest.ToUserName, opt => opt.MapFrom(src => src.AppUser.UserName)); - CreateMap() - .ForMember(dest => dest.SeriesId, opt => opt.MapFrom(src => src.Volume.SeriesId)) - .ForMember(dest => dest.VolumeTitle, opt => opt.MapFrom(src => src.Volume.Name)) - .ForMember(dest => dest.LibraryId, opt => opt.MapFrom(src => src.Volume.Series.LibraryId)) - .ForMember(dest => dest.LibraryType, opt => opt.MapFrom(src => src.Volume.Series.Library.Type)); - CreateMap(); CreateMap() diff --git a/API/Data/AutoMapper/AutoMapperReadingListProfile.cs b/Kavita.Models/AutoMapper/AutoMapperReadingListProfile.cs similarity index 96% rename from API/Data/AutoMapper/AutoMapperReadingListProfile.cs rename to Kavita.Models/AutoMapper/AutoMapperReadingListProfile.cs index 9aeda5cb6..f318796b1 100644 --- a/API/Data/AutoMapper/AutoMapperReadingListProfile.cs +++ b/Kavita.Models/AutoMapper/AutoMapperReadingListProfile.cs @@ -1,10 +1,10 @@ using System; using System.Linq; -using API.DTOs.ReadingLists; -using API.Entities; using AutoMapper; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities; -namespace API.Data.AutoMapper; +namespace Kavita.Models.AutoMapper; /// /// Maps ReadingList and ReadingListItem entities to DTOs with user progress attached at the DB level. diff --git a/API/Data/AutoMapper/AutoMapperSeriesProfile.cs b/Kavita.Models/AutoMapper/AutoMapperSeriesProfile.cs similarity index 94% rename from API/Data/AutoMapper/AutoMapperSeriesProfile.cs rename to Kavita.Models/AutoMapper/AutoMapperSeriesProfile.cs index 5825e2c82..d48bc5bfc 100644 --- a/API/Data/AutoMapper/AutoMapperSeriesProfile.cs +++ b/Kavita.Models/AutoMapper/AutoMapperSeriesProfile.cs @@ -1,10 +1,10 @@ using System; using System.Linq; -using API.DTOs; -using API.Entities; using AutoMapper; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; -namespace API.Data.AutoMapper; +namespace Kavita.Models.AutoMapper; /// /// This is a way to attach progress at the DB level via a JOIN. Critical for healthy response time. diff --git a/API/Data/AutoMapper/AutoMapperVolumeProfile.cs b/Kavita.Models/AutoMapper/AutoMapperVolumeProfile.cs similarity index 91% rename from API/Data/AutoMapper/AutoMapperVolumeProfile.cs rename to Kavita.Models/AutoMapper/AutoMapperVolumeProfile.cs index 3c17e0acd..a6841d047 100644 --- a/API/Data/AutoMapper/AutoMapperVolumeProfile.cs +++ b/Kavita.Models/AutoMapper/AutoMapperVolumeProfile.cs @@ -1,9 +1,9 @@ using System.Linq; -using API.DTOs; -using API.Entities; using AutoMapper; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; -namespace API.Data.AutoMapper; +namespace Kavita.Models.AutoMapper; /// /// Maps Volume entities to VolumeDto with user progress attached at the DB level via JOIN. diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/Kavita.Models/AutoMapper/Converters/ServerSettingConverter.cs similarity index 97% rename from API/Helpers/Converters/ServerSettingConverter.cs rename to Kavita.Models/AutoMapper/Converters/ServerSettingConverter.cs index 15a3d9c99..5a4f90faf 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/Kavita.Models/AutoMapper/Converters/ServerSettingConverter.cs @@ -1,14 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Text.Json; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; using AutoMapper; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.Helpers.Converters; -#nullable enable +namespace Kavita.Models.AutoMapper.Converters; public class ServerSettingConverter : ITypeConverter, ServerSettingDto> { diff --git a/API/Helpers/Builders/AppUserBuilder.cs b/Kavita.Models/Builders/AppUserBuilder.cs similarity index 85% rename from API/Helpers/Builders/AppUserBuilder.cs rename to Kavita.Models/Builders/AppUserBuilder.cs index 25c3cef3f..c7db2947f 100644 --- a/API/Helpers/Builders/AppUserBuilder.cs +++ b/Kavita.Models/Builders/AppUserBuilder.cs @@ -1,14 +1,9 @@ -using System; using System.Linq; -using API.Data; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.User; -using API.Entities.User; -using Kavita.Common; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.Helpers.Builders; -#nullable enable +namespace Kavita.Models.Builders; public class AppUserBuilder : IEntityBuilder { @@ -23,7 +18,7 @@ public class AppUserBuilder : IEntityBuilder Email = email, UserPreferences = new AppUserPreferences { - Theme = theme ?? Seed.DefaultThemes.First(), + Theme = theme ?? Defaults.DefaultThemes.First(), Locale = "en" }, ReadingLists = [], @@ -36,7 +31,7 @@ public class AppUserBuilder : IEntityBuilder DashboardStreams = [], SideNavStreams = [], ReadingProfiles = [], - AuthKeys = Seed.CreateDefaultAuthKeys() + AuthKeys = Defaults.CreateDefaultAuthKeys() }; } diff --git a/API/Helpers/Builders/AppUserChapterRatingBuilder.cs b/Kavita.Models/Builders/AppUserChapterRatingBuilder.cs similarity index 83% rename from API/Helpers/Builders/AppUserChapterRatingBuilder.cs rename to Kavita.Models/Builders/AppUserChapterRatingBuilder.cs index d524ed937..ef6ff5784 100644 --- a/API/Helpers/Builders/AppUserChapterRatingBuilder.cs +++ b/Kavita.Models/Builders/AppUserChapterRatingBuilder.cs @@ -1,11 +1,10 @@ #nullable enable using System; -using API.Entities; -using API.Entities.User; +using Kavita.Models.Entities.User; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; -public class ChapterRatingBuilder : IEntityBuilder +public class ChapterRatingBuilder : API.Helpers.Builders.IEntityBuilder { private readonly AppUserChapterRating _rating; public AppUserChapterRating Build() => _rating; diff --git a/API/Helpers/Builders/AppUserCollectionBuilder.cs b/Kavita.Models/Builders/AppUserCollectionBuilder.cs similarity index 91% rename from API/Helpers/Builders/AppUserCollectionBuilder.cs rename to Kavita.Models/Builders/AppUserCollectionBuilder.cs index e9bdcf977..113458dc4 100644 --- a/API/Helpers/Builders/AppUserCollectionBuilder.cs +++ b/Kavita.Models/Builders/AppUserCollectionBuilder.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Plus; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class AppUserCollectionBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/AppUserReadingProfileBuilder.cs b/Kavita.Models/Builders/AppUserReadingProfileBuilder.cs similarity index 89% rename from API/Helpers/Builders/AppUserReadingProfileBuilder.cs rename to Kavita.Models/Builders/AppUserReadingProfileBuilder.cs index e44fcebc5..a0e04c9f4 100644 --- a/API/Helpers/Builders/AppUserReadingProfileBuilder.cs +++ b/Kavita.Models/Builders/AppUserReadingProfileBuilder.cs @@ -1,8 +1,9 @@ -using API.Entities; -using API.Entities.Enums; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class AppUserReadingProfileBuilder { diff --git a/API/Helpers/Builders/DeviceBuilder.cs b/Kavita.Models/Builders/DeviceBuilder.cs similarity index 83% rename from API/Helpers/Builders/DeviceBuilder.cs rename to Kavita.Models/Builders/DeviceBuilder.cs index 0eb3e6600..aa591cae6 100644 --- a/API/Helpers/Builders/DeviceBuilder.cs +++ b/Kavita.Models/Builders/DeviceBuilder.cs @@ -1,7 +1,7 @@ -using API.Entities; -using API.Entities.Enums.Device; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.Device; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class DeviceBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/EntityBuilder.cs b/Kavita.Models/Builders/EntityBuilder.cs similarity index 100% rename from API/Helpers/Builders/EntityBuilder.cs rename to Kavita.Models/Builders/EntityBuilder.cs diff --git a/API/Helpers/Builders/ExternalSeriesMetadataBuilder.cs b/Kavita.Models/Builders/ExternalSeriesMetadataBuilder.cs similarity index 89% rename from API/Helpers/Builders/ExternalSeriesMetadataBuilder.cs rename to Kavita.Models/Builders/ExternalSeriesMetadataBuilder.cs index e716f5927..ae7976802 100644 --- a/API/Helpers/Builders/ExternalSeriesMetadataBuilder.cs +++ b/Kavita.Models/Builders/ExternalSeriesMetadataBuilder.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Metadata; +using Kavita.Models.Entities.Metadata; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class ExternalSeriesMetadataBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/FolderPathBuilder.cs b/Kavita.Models/Builders/FolderPathBuilder.cs similarity index 82% rename from API/Helpers/Builders/FolderPathBuilder.cs rename to Kavita.Models/Builders/FolderPathBuilder.cs index 07d6e2adf..698c6475a 100644 --- a/API/Helpers/Builders/FolderPathBuilder.cs +++ b/Kavita.Models/Builders/FolderPathBuilder.cs @@ -1,6 +1,6 @@ -using API.Entities; +using Kavita.Models.Entities; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class FolderPathBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/GenreBuilder.cs b/Kavita.Models/Builders/GenreBuilder.cs similarity index 80% rename from API/Helpers/Builders/GenreBuilder.cs rename to Kavita.Models/Builders/GenreBuilder.cs index e6265f078..a925edf50 100644 --- a/API/Helpers/Builders/GenreBuilder.cs +++ b/Kavita.Models/Builders/GenreBuilder.cs @@ -1,8 +1,8 @@ -using API.Entities; -using API.Entities.Metadata; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Metadata; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class GenreBuilder : IEntityBuilder { diff --git a/Kavita.Models/Builders/IEntityBuilder.cs b/Kavita.Models/Builders/IEntityBuilder.cs new file mode 100644 index 000000000..5c12ab777 --- /dev/null +++ b/Kavita.Models/Builders/IEntityBuilder.cs @@ -0,0 +1,6 @@ +namespace Kavita.Models.Builders; + +public interface IEntityBuilder +{ + public T Build(); +} diff --git a/API/Helpers/Builders/KoreaderBookDtoBuilder.cs b/Kavita.Models/Builders/KoreaderBookDtoBuilder.cs similarity index 95% rename from API/Helpers/Builders/KoreaderBookDtoBuilder.cs rename to Kavita.Models/Builders/KoreaderBookDtoBuilder.cs index 564f0ca33..a451ea1cd 100644 --- a/API/Helpers/Builders/KoreaderBookDtoBuilder.cs +++ b/Kavita.Models/Builders/KoreaderBookDtoBuilder.cs @@ -1,9 +1,9 @@ using System; using System.Security.Cryptography; using System.Text; -using API.DTOs.Koreader; +using Kavita.Models.DTOs.Koreader; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class KoreaderBookDtoBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/LibraryBuilder.cs b/Kavita.Models/Builders/LibraryBuilder.cs similarity index 95% rename from API/Helpers/Builders/LibraryBuilder.cs rename to Kavita.Models/Builders/LibraryBuilder.cs index 30181e37c..53d530c40 100644 --- a/API/Helpers/Builders/LibraryBuilder.cs +++ b/Kavita.Models/Builders/LibraryBuilder.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; using System.Linq; -using API.Entities; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class LibraryBuilder : IEntityBuilder { diff --git a/Kavita.Models/Builders/MediaErrorBuilder.cs b/Kavita.Models/Builders/MediaErrorBuilder.cs new file mode 100644 index 000000000..b5e91d3d9 --- /dev/null +++ b/Kavita.Models/Builders/MediaErrorBuilder.cs @@ -0,0 +1,28 @@ +using System.IO; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; + +namespace Kavita.Models.Builders; + +public class MediaErrorBuilder(string filePath): IEntityBuilder +{ + private readonly MediaError _mediaError = new() + { + FilePath = filePath.ToNormalized(), + Extension = Path.GetExtension(filePath).Replace(".", string.Empty).ToUpperInvariant() + }; + + public MediaError Build() => _mediaError; + + public MediaErrorBuilder WithComment(string comment) + { + _mediaError.Comment = comment.Trim(); + return this; + } + + public MediaErrorBuilder WithDetails(string details) + { + _mediaError.Details = details.Trim(); + return this; + } +} diff --git a/API/Helpers/Builders/PersonAliasBuilder.cs b/Kavita.Models/Builders/PersonAliasBuilder.cs similarity index 76% rename from API/Helpers/Builders/PersonAliasBuilder.cs rename to Kavita.Models/Builders/PersonAliasBuilder.cs index e54ea8975..6d4ed8d64 100644 --- a/API/Helpers/Builders/PersonAliasBuilder.cs +++ b/Kavita.Models/Builders/PersonAliasBuilder.cs @@ -1,7 +1,7 @@ -using API.Entities.Person; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities.Person; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class PersonAliasBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/PersonBuilder.cs b/Kavita.Models/Builders/PersonBuilder.cs similarity index 92% rename from API/Helpers/Builders/PersonBuilder.cs rename to Kavita.Models/Builders/PersonBuilder.cs index 3bf33e7a9..37eb49b35 100644 --- a/API/Helpers/Builders/PersonBuilder.cs +++ b/Kavita.Models/Builders/PersonBuilder.cs @@ -1,10 +1,10 @@ #nullable enable using System.Collections.Generic; using System.Linq; -using API.Entities.Person; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities.Person; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class PersonBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/RatingBuilder.cs b/Kavita.Models/Builders/RatingBuilder.cs similarity index 90% rename from API/Helpers/Builders/RatingBuilder.cs rename to Kavita.Models/Builders/RatingBuilder.cs index c0dc9e57a..fade4760e 100644 --- a/API/Helpers/Builders/RatingBuilder.cs +++ b/Kavita.Models/Builders/RatingBuilder.cs @@ -1,8 +1,8 @@ #nullable enable using System; -using API.Entities; +using Kavita.Models.Entities.User; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class RatingBuilder : IEntityBuilder { @@ -25,7 +25,7 @@ public class RatingBuilder : IEntityBuilder _rating.Rating = Math.Clamp(rating, 0, 5); return this; } - + public RatingBuilder WithBody(string body) { diff --git a/API/Helpers/Builders/ReadingListBuilder.cs b/Kavita.Models/Builders/ReadingListBuilder.cs similarity index 91% rename from API/Helpers/Builders/ReadingListBuilder.cs rename to Kavita.Models/Builders/ReadingListBuilder.cs index e05a92096..bcd057d67 100644 --- a/API/Helpers/Builders/ReadingListBuilder.cs +++ b/Kavita.Models/Builders/ReadingListBuilder.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class ReadingListBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/ReadingListItemBuilder.cs b/Kavita.Models/Builders/ReadingListItemBuilder.cs similarity index 87% rename from API/Helpers/Builders/ReadingListItemBuilder.cs rename to Kavita.Models/Builders/ReadingListItemBuilder.cs index 86ca4cfc8..5ef621353 100644 --- a/API/Helpers/Builders/ReadingListItemBuilder.cs +++ b/Kavita.Models/Builders/ReadingListItemBuilder.cs @@ -1,6 +1,6 @@ -using API.Entities; +using Kavita.Models.Entities; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class ReadingListItemBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/ScrobbleHoldBuilder.cs b/Kavita.Models/Builders/ScrobbleHoldBuilder.cs similarity index 86% rename from API/Helpers/Builders/ScrobbleHoldBuilder.cs rename to Kavita.Models/Builders/ScrobbleHoldBuilder.cs index cd03a08f0..4d5674e9f 100644 --- a/API/Helpers/Builders/ScrobbleHoldBuilder.cs +++ b/Kavita.Models/Builders/ScrobbleHoldBuilder.cs @@ -1,7 +1,6 @@ -using API.Entities.Scrobble; +using Kavita.Models.Entities.Scrobble; -namespace API.Helpers.Builders; -#nullable enable +namespace Kavita.Models.Builders; public class ScrobbleHoldBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/SeriesBuilder.cs b/Kavita.Models/Builders/SeriesBuilder.cs similarity index 93% rename from API/Helpers/Builders/SeriesBuilder.cs rename to Kavita.Models/Builders/SeriesBuilder.cs index 96e820659..6bced9f8d 100644 --- a/API/Helpers/Builders/SeriesBuilder.cs +++ b/Kavita.Models/Builders/SeriesBuilder.cs @@ -1,13 +1,13 @@ using System.Collections.Generic; using System.Linq; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; -public class SeriesBuilder : IEntityBuilder +public class SeriesBuilder : API.Helpers.Builders.IEntityBuilder { private readonly Series _series; public Series Build() diff --git a/API/Helpers/Builders/SeriesMetadataBuilder.cs b/Kavita.Models/Builders/SeriesMetadataBuilder.cs similarity index 95% rename from API/Helpers/Builders/SeriesMetadataBuilder.cs rename to Kavita.Models/Builders/SeriesMetadataBuilder.cs index 462bc4455..d5a121682 100644 --- a/API/Helpers/Builders/SeriesMetadataBuilder.cs +++ b/Kavita.Models/Builders/SeriesMetadataBuilder.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.Person; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Person; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class SeriesMetadataBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/TagBuilder.cs b/Kavita.Models/Builders/TagBuilder.cs similarity index 82% rename from API/Helpers/Builders/TagBuilder.cs rename to Kavita.Models/Builders/TagBuilder.cs index 623587fd1..d1ebd4dfb 100644 --- a/API/Helpers/Builders/TagBuilder.cs +++ b/Kavita.Models/Builders/TagBuilder.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.Entities; -using API.Entities.Metadata; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Metadata; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class TagBuilder : IEntityBuilder { diff --git a/API/Constants/CacheProfiles.cs b/Kavita.Models/Constants/CacheProfiles.cs similarity index 96% rename from API/Constants/CacheProfiles.cs rename to Kavita.Models/Constants/CacheProfiles.cs index afc82f19c..98f99f0a6 100644 --- a/API/Constants/CacheProfiles.cs +++ b/Kavita.Models/Constants/CacheProfiles.cs @@ -1,4 +1,4 @@ -namespace API.Constants; +namespace Kavita.Models.Constants; public static class EasyCacheProfiles { diff --git a/API/Constants/ControllerConstants.cs b/Kavita.Models/Constants/ControllerConstants.cs similarity index 72% rename from API/Constants/ControllerConstants.cs rename to Kavita.Models/Constants/ControllerConstants.cs index 34a2482ee..0d53d04dd 100644 --- a/API/Constants/ControllerConstants.cs +++ b/Kavita.Models/Constants/ControllerConstants.cs @@ -1,4 +1,4 @@ -namespace API.Constants; +namespace Kavita.Models.Constants; public abstract class ControllerConstants { diff --git a/Kavita.Models/Constants/ParserConstants.cs b/Kavita.Models/Constants/ParserConstants.cs new file mode 100644 index 000000000..6463db2bd --- /dev/null +++ b/Kavita.Models/Constants/ParserConstants.cs @@ -0,0 +1,14 @@ +namespace Kavita.Models.Constants; + +public static class ParserConstants +{ + public const string DefaultChapter = "-100000"; + public const string LooseLeafVolume = "-100000"; + public const int DefaultChapterNumber = -100_000; + public const int LooseLeafVolumeNumber = -100_000; + /// + /// The Volume Number of Specials to reside in + /// + public const int SpecialVolumeNumber = 100_000; + public const string SpecialVolume = "100000"; +} diff --git a/API/Constants/PolicyConstants.cs b/Kavita.Models/Constants/PolicyConstants.cs similarity index 98% rename from API/Constants/PolicyConstants.cs rename to Kavita.Models/Constants/PolicyConstants.cs index 734885806..618cd47f5 100644 --- a/API/Constants/PolicyConstants.cs +++ b/Kavita.Models/Constants/PolicyConstants.cs @@ -1,6 +1,6 @@ using System.Collections.Immutable; -namespace API.Constants; +namespace Kavita.Models.Constants; /// /// Role-based Security diff --git a/API/Constants/PolicyGroups.cs b/Kavita.Models/Constants/PolicyGroups.cs similarity index 74% rename from API/Constants/PolicyGroups.cs rename to Kavita.Models/Constants/PolicyGroups.cs index 47f65f798..27d6e0cde 100644 --- a/API/Constants/PolicyGroups.cs +++ b/Kavita.Models/Constants/PolicyGroups.cs @@ -1,4 +1,4 @@ -namespace API.Constants; +namespace Kavita.Models.Constants; /// /// Constants for Higher level policy roles @@ -17,4 +17,8 @@ public static class PolicyGroups /// Requires Admin or Change Password to execute /// public const string ChangePasswordPolicy = "RequireChangePasswordRole"; + /// + /// Requires Admin or Bookmark to execute + /// + public const string BookmarkPolicy = "RequireBookmarkRole"; } diff --git a/API/Constants/ResponseCacheProfiles.cs b/Kavita.Models/Constants/ResponseCacheProfiles.cs similarity index 93% rename from API/Constants/ResponseCacheProfiles.cs rename to Kavita.Models/Constants/ResponseCacheProfiles.cs index c68b49f62..f85ad7567 100644 --- a/API/Constants/ResponseCacheProfiles.cs +++ b/Kavita.Models/Constants/ResponseCacheProfiles.cs @@ -1,4 +1,4 @@ -namespace API.Constants; +namespace Kavita.Models.Constants; public static class ResponseCacheProfiles { diff --git a/Kavita.Models/Constants/TaskSchedulerConstants.cs b/Kavita.Models/Constants/TaskSchedulerConstants.cs new file mode 100644 index 000000000..af26b84e2 --- /dev/null +++ b/Kavita.Models/Constants/TaskSchedulerConstants.cs @@ -0,0 +1,26 @@ +namespace Kavita.Models.Constants; + +public static class TaskSchedulerConstants +{ + public const string ScanQueue = "scan"; + public const string DefaultQueue = "default"; + public const string RemoveFromWantToReadTaskId = "remove-from-want-to-read"; + public const string UpdateYearlyStatsTaskId = "update-yearly-stats"; + public const string SyncThemesTaskId = "sync-themes"; + public const string CheckForUpdateId = "check-updates"; + public const string CleanupDbTaskId = "cleanup-db"; + public const string CleanupTaskId = "cleanup"; + public const string BackupTaskId = "backup"; + public const string ScanLibrariesTaskId = "scan-libraries"; + public const string ReportStatsTaskId = "report-stats"; + public const string CheckScrobblingTokensId = "check-scrobbling-tokens"; + public const string ProcessScrobblingEventsId = "process-scrobbling-events"; + public const string ProcessProcessedScrobblingEventsId = "process-processed-scrobbling-events"; + public const string LicenseCheckId = "license-check"; + public const string KavitaPlusDataRefreshId = "kavita+-data-refresh"; + public const string KavitaPlusStackSyncId = "kavita+-stack-sync"; + public const string KavitaPlusWantToReadSyncId = "kavita+-want-to-read-sync"; + public const string ReadingHistoryAggregationId = "reading-history-aggregation"; + public const string AuthKeyExpirationId = "auth-key-expiration"; + public const string EnsureSideNavId = "ensure-sidenav"; +} diff --git a/API/DTOs/Account/AgeRestrictionDto.cs b/Kavita.Models/DTOs/Account/AgeRestrictionDto.cs similarity index 87% rename from API/DTOs/Account/AgeRestrictionDto.cs rename to Kavita.Models/DTOs/Account/AgeRestrictionDto.cs index 6505bdbff..06f7431af 100644 --- a/API/DTOs/Account/AgeRestrictionDto.cs +++ b/Kavita.Models/DTOs/Account/AgeRestrictionDto.cs @@ -1,6 +1,8 @@ -using API.Entities.Enums; + -namespace API.DTOs.Account; +using Kavita.Models.Entities.Enums; + +namespace Kavita.Models.DTOs.Account; public sealed record AgeRestrictionDto { diff --git a/API/DTOs/Account/AuthKeyDto.cs b/Kavita.Models/DTOs/Account/AuthKeyDto.cs similarity index 88% rename from API/DTOs/Account/AuthKeyDto.cs rename to Kavita.Models/DTOs/Account/AuthKeyDto.cs index 4f2625cbf..38e18623d 100644 --- a/API/DTOs/Account/AuthKeyDto.cs +++ b/Kavita.Models/DTOs/Account/AuthKeyDto.cs @@ -1,7 +1,8 @@ -using System; -using API.Entities.Enums.User; + +using System; +using Kavita.Models.Entities.Enums.User; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record AuthKeyDto { diff --git a/Kavita.Models/DTOs/Account/AuthKeyExpiresAtDto.cs b/Kavita.Models/DTOs/Account/AuthKeyExpiresAtDto.cs new file mode 100644 index 000000000..7e8b52937 --- /dev/null +++ b/Kavita.Models/DTOs/Account/AuthKeyExpiresAtDto.cs @@ -0,0 +1,8 @@ +using System; + +namespace Kavita.Models.DTOs.Account; + +public sealed record AuthKeyExpiresAtDto +{ + public required DateTime? ExpiresAt { get; set; } +} diff --git a/API/DTOs/Account/ConfirmEmailDto.cs b/Kavita.Models/DTOs/Account/ConfirmEmailDto.cs similarity index 91% rename from API/DTOs/Account/ConfirmEmailDto.cs rename to Kavita.Models/DTOs/Account/ConfirmEmailDto.cs index 413f9f34a..80f9a3e4c 100644 --- a/API/DTOs/Account/ConfirmEmailDto.cs +++ b/Kavita.Models/DTOs/Account/ConfirmEmailDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record ConfirmEmailDto { diff --git a/API/DTOs/Account/ConfirmEmailUpdateDto.cs b/Kavita.Models/DTOs/Account/ConfirmEmailUpdateDto.cs similarity index 85% rename from API/DTOs/Account/ConfirmEmailUpdateDto.cs rename to Kavita.Models/DTOs/Account/ConfirmEmailUpdateDto.cs index 2a0738e35..0ed1e4901 100644 --- a/API/DTOs/Account/ConfirmEmailUpdateDto.cs +++ b/Kavita.Models/DTOs/Account/ConfirmEmailUpdateDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record ConfirmEmailUpdateDto { diff --git a/API/DTOs/Account/ConfirmMigrationEmailDto.cs b/Kavita.Models/DTOs/Account/ConfirmMigrationEmailDto.cs similarity index 78% rename from API/DTOs/Account/ConfirmMigrationEmailDto.cs rename to Kavita.Models/DTOs/Account/ConfirmMigrationEmailDto.cs index cdfc1505c..a088fefcd 100644 --- a/API/DTOs/Account/ConfirmMigrationEmailDto.cs +++ b/Kavita.Models/DTOs/Account/ConfirmMigrationEmailDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record ConfirmMigrationEmailDto { diff --git a/API/DTOs/Account/ConfirmPasswordResetDto.cs b/Kavita.Models/DTOs/Account/ConfirmPasswordResetDto.cs similarity index 89% rename from API/DTOs/Account/ConfirmPasswordResetDto.cs rename to Kavita.Models/DTOs/Account/ConfirmPasswordResetDto.cs index 00aff301b..5bd43aeb7 100644 --- a/API/DTOs/Account/ConfirmPasswordResetDto.cs +++ b/Kavita.Models/DTOs/Account/ConfirmPasswordResetDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record ConfirmPasswordResetDto { diff --git a/API/DTOs/Account/InviteUserDto.cs b/Kavita.Models/DTOs/Account/InviteUserDto.cs similarity index 95% rename from API/DTOs/Account/InviteUserDto.cs rename to Kavita.Models/DTOs/Account/InviteUserDto.cs index c12bebc2b..1199b5cba 100644 --- a/API/DTOs/Account/InviteUserDto.cs +++ b/Kavita.Models/DTOs/Account/InviteUserDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record InviteUserDto { diff --git a/API/DTOs/Account/InviteUserResponse.cs b/Kavita.Models/DTOs/Account/InviteUserResponse.cs similarity index 92% rename from API/DTOs/Account/InviteUserResponse.cs rename to Kavita.Models/DTOs/Account/InviteUserResponse.cs index ed16bd05e..dbba7aafc 100644 --- a/API/DTOs/Account/InviteUserResponse.cs +++ b/Kavita.Models/DTOs/Account/InviteUserResponse.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record InviteUserResponse { diff --git a/API/DTOs/Account/LoginDto.cs b/Kavita.Models/DTOs/Account/LoginDto.cs similarity index 88% rename from API/DTOs/Account/LoginDto.cs rename to Kavita.Models/DTOs/Account/LoginDto.cs index 97338640b..51f3065f8 100644 --- a/API/DTOs/Account/LoginDto.cs +++ b/Kavita.Models/DTOs/Account/LoginDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; #nullable enable public sealed record LoginDto diff --git a/API/DTOs/Account/MemberDto.cs b/Kavita.Models/DTOs/Account/MemberDto.cs similarity index 91% rename from API/DTOs/Account/MemberDto.cs rename to Kavita.Models/DTOs/Account/MemberDto.cs index 4f0081c4f..c0aab2cc5 100644 --- a/API/DTOs/Account/MemberDto.cs +++ b/Kavita.Models/DTOs/Account/MemberDto.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Account; -#nullable enable +namespace Kavita.Models.DTOs.Account; /// /// Represents a member of a Kavita server. diff --git a/API/DTOs/Account/MemberInfoDto.cs b/Kavita.Models/DTOs/Account/MemberInfoDto.cs similarity index 80% rename from API/DTOs/Account/MemberInfoDto.cs rename to Kavita.Models/DTOs/Account/MemberInfoDto.cs index 0d3f4954e..56e2f0073 100644 --- a/API/DTOs/Account/MemberInfoDto.cs +++ b/Kavita.Models/DTOs/Account/MemberInfoDto.cs @@ -1,7 +1,7 @@ -using System; + +using System; -namespace API.DTOs.Account; -#nullable enable +namespace Kavita.Models.DTOs.Account; public sealed record MemberInfoDto { diff --git a/API/DTOs/Account/MigrateUserEmailDto.cs b/Kavita.Models/DTOs/Account/MigrateUserEmailDto.cs similarity index 83% rename from API/DTOs/Account/MigrateUserEmailDto.cs rename to Kavita.Models/DTOs/Account/MigrateUserEmailDto.cs index 4630c510f..2b7264d59 100644 --- a/API/DTOs/Account/MigrateUserEmailDto.cs +++ b/Kavita.Models/DTOs/Account/MigrateUserEmailDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record MigrateUserEmailDto { diff --git a/API/DTOs/Account/ResetPasswordDto.cs b/Kavita.Models/DTOs/Account/ResetPasswordDto.cs similarity index 94% rename from API/DTOs/Account/ResetPasswordDto.cs rename to Kavita.Models/DTOs/Account/ResetPasswordDto.cs index 545ca5ba6..c6c4449b0 100644 --- a/API/DTOs/Account/ResetPasswordDto.cs +++ b/Kavita.Models/DTOs/Account/ResetPasswordDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record ResetPasswordDto { diff --git a/API/DTOs/Account/RotateAuthKeyRequestDto.cs b/Kavita.Models/DTOs/Account/RotateAuthKeyRequestDto.cs similarity index 89% rename from API/DTOs/Account/RotateAuthKeyRequestDto.cs rename to Kavita.Models/DTOs/Account/RotateAuthKeyRequestDto.cs index ac7781dba..e654c3640 100644 --- a/API/DTOs/Account/RotateAuthKeyRequestDto.cs +++ b/Kavita.Models/DTOs/Account/RotateAuthKeyRequestDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; #nullable enable public sealed record RotateAuthKeyRequestDto diff --git a/API/DTOs/Account/TokenRequestDto.cs b/Kavita.Models/DTOs/Account/TokenRequestDto.cs similarity index 78% rename from API/DTOs/Account/TokenRequestDto.cs rename to Kavita.Models/DTOs/Account/TokenRequestDto.cs index 5c798721c..0f505d6bb 100644 --- a/API/DTOs/Account/TokenRequestDto.cs +++ b/Kavita.Models/DTOs/Account/TokenRequestDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record TokenRequestDto { diff --git a/API/DTOs/Account/UpdateAgeRestrictionDto.cs b/Kavita.Models/DTOs/Account/UpdateAgeRestrictionDto.cs similarity index 74% rename from API/DTOs/Account/UpdateAgeRestrictionDto.cs rename to Kavita.Models/DTOs/Account/UpdateAgeRestrictionDto.cs index 2fa9c89d2..435a461c3 100644 --- a/API/DTOs/Account/UpdateAgeRestrictionDto.cs +++ b/Kavita.Models/DTOs/Account/UpdateAgeRestrictionDto.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record UpdateAgeRestrictionDto { diff --git a/API/DTOs/Account/UpdateEmailDto.cs b/Kavita.Models/DTOs/Account/UpdateEmailDto.cs similarity index 77% rename from API/DTOs/Account/UpdateEmailDto.cs rename to Kavita.Models/DTOs/Account/UpdateEmailDto.cs index 873862ba1..fdd5b4f36 100644 --- a/API/DTOs/Account/UpdateEmailDto.cs +++ b/Kavita.Models/DTOs/Account/UpdateEmailDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record UpdateEmailDto { diff --git a/API/DTOs/Account/UpdateUserDto.cs b/Kavita.Models/DTOs/Account/UpdateUserDto.cs similarity index 77% rename from API/DTOs/Account/UpdateUserDto.cs rename to Kavita.Models/DTOs/Account/UpdateUserDto.cs index 1fb780d6d..5b52ea2f3 100644 --- a/API/DTOs/Account/UpdateUserDto.cs +++ b/Kavita.Models/DTOs/Account/UpdateUserDto.cs @@ -1,14 +1,15 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.DTOs.Account; -#nullable enable +namespace Kavita.Models.DTOs.Account; public sealed record UpdateUserDto { - /// + /// public int UserId { get; set; } - /// + /// public string Username { get; set; } = default!; /// /// List of Roles to assign to user. If admin not present, Pleb will be applied. @@ -23,7 +24,7 @@ public sealed record UpdateUserDto /// An Age Rating which will limit the account to seeing everything equal to or below said rating. /// public AgeRestrictionDto AgeRestriction { get; init; } = default!; - /// + /// public string? Email { get; set; } = default!; public IdentityProvider IdentityProvider { get; init; } = IdentityProvider.Kavita; } diff --git a/API/DTOs/Annotations/FullAnnotationDto.cs b/Kavita.Models/DTOs/Annotations/FullAnnotationDto.cs similarity index 95% rename from API/DTOs/Annotations/FullAnnotationDto.cs rename to Kavita.Models/DTOs/Annotations/FullAnnotationDto.cs index 17dbe0579..a395bfa26 100644 --- a/API/DTOs/Annotations/FullAnnotationDto.cs +++ b/Kavita.Models/DTOs/Annotations/FullAnnotationDto.cs @@ -1,7 +1,7 @@ using System; using System.Text.Json.Serialization; -namespace API.DTOs.Annotations; +namespace Kavita.Models.DTOs.Annotations; public sealed record FullAnnotationDto { diff --git a/API/DTOs/Archive/ArchiveLibrary.cs b/Kavita.Models/DTOs/Archive/ArchiveLibrary.cs similarity index 91% rename from API/DTOs/Archive/ArchiveLibrary.cs rename to Kavita.Models/DTOs/Archive/ArchiveLibrary.cs index a3beae9bc..5330f84a5 100644 --- a/API/DTOs/Archive/ArchiveLibrary.cs +++ b/Kavita.Models/DTOs/Archive/ArchiveLibrary.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Archive; +namespace Kavita.Models.DTOs.Archive; /// /// Represents which library should handle opening this library diff --git a/API/DTOs/BulkActionDto.cs b/Kavita.Models/DTOs/BulkActionDto.cs similarity index 88% rename from API/DTOs/BulkActionDto.cs rename to Kavita.Models/DTOs/BulkActionDto.cs index c26a73e9c..056813395 100644 --- a/API/DTOs/BulkActionDto.cs +++ b/Kavita.Models/DTOs/BulkActionDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record BulkActionDto { diff --git a/API/DTOs/ChapterDetailPlusDto.cs b/Kavita.Models/DTOs/ChapterDetailPlusDto.cs similarity index 81% rename from API/DTOs/ChapterDetailPlusDto.cs rename to Kavita.Models/DTOs/ChapterDetailPlusDto.cs index d99482e55..f9636aa8b 100644 --- a/API/DTOs/ChapterDetailPlusDto.cs +++ b/Kavita.Models/DTOs/ChapterDetailPlusDto.cs @@ -1,8 +1,8 @@ #nullable enable using System.Collections.Generic; -using API.DTOs.SeriesDetail; +using Kavita.Models.DTOs.SeriesDetail; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record ChapterDetailPlusDto { diff --git a/API/DTOs/ChapterDto.cs b/Kavita.Models/DTOs/ChapterDto.cs similarity index 64% rename from API/DTOs/ChapterDto.cs rename to Kavita.Models/DTOs/ChapterDto.cs index 98ef6c01e..e935fcd60 100644 --- a/API/DTOs/ChapterDto.cs +++ b/Kavita.Models/DTOs/ChapterDto.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.Entities.Enums; -using API.Entities.Interfaces; +using System.Runtime.InteropServices.JavaScript; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable /// @@ -15,24 +17,24 @@ namespace API.DTOs; /// public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage { - /// + /// public int Id { get; init; } - /// + /// public string Range { get; init; } = default!; - /// + /// [Obsolete("Use MinNumber and MaxNumber instead")] public string Number { get; init; } = default!; - /// + /// public float MinNumber { get; init; } - /// + /// public float MaxNumber { get; init; } - /// + /// public float SortOrder { get; set; } - /// + /// public int Pages { get; init; } - /// + /// public bool IsSpecial { get; init; } - /// + /// public string Title { get; set; } = default!; /// /// The files that represent this Chapter @@ -55,25 +57,25 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage /// The last time a chapter was read by current authenticated user /// public DateTime LastReadingProgress { get; set; } - /// + /// public bool CoverImageLocked { get; set; } - /// + /// public int VolumeId { get; init; } - /// + /// public DateTime CreatedUtc { get; set; } - /// + /// public DateTime LastModifiedUtc { get; set; } - /// + /// public DateTime Created { get; set; } - /// + /// public DateTime ReleaseDate { get; init; } - /// + /// public string TitleName { get; set; } = default!; - /// + /// public string Summary { get; init; } = default!; - /// + /// public AgeRating AgeRating { get; init; } - /// + /// public long WordCount { get; set; } = 0L; /// /// Formatted Volume title ie) Volume 2. @@ -86,9 +88,9 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage public int MaxHoursToRead { get; set; } /// public float AvgHoursToRead { get; set; } - /// + /// public string WebLinks { get; set; } - /// + /// public string ISBN { get; set; } #region Metadata @@ -114,64 +116,64 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage /// public ICollection Tags { get; set; } = new List(); public PublicationStatus PublicationStatus { get; set; } - /// + /// public string? Language { get; set; } - /// + /// public int Count { get; set; } - /// + /// public int TotalCount { get; set; } - /// + /// public bool LanguageLocked { get; set; } - /// + /// public bool SummaryLocked { get; set; } - /// + /// public bool AgeRatingLocked { get; set; } public bool PublicationStatusLocked { get; set; } - /// + /// public bool GenresLocked { get; set; } - /// + /// public bool TagsLocked { get; set; } - /// + /// public bool WriterLocked { get; set; } - /// + /// public bool CharacterLocked { get; set; } - /// + /// public bool ColoristLocked { get; set; } - /// + /// public bool EditorLocked { get; set; } - /// + /// public bool InkerLocked { get; set; } - /// + /// public bool ImprintLocked { get; set; } - /// + /// public bool LettererLocked { get; set; } - /// + /// public bool PencillerLocked { get; set; } - /// + /// public bool PublisherLocked { get; set; } - /// + /// public bool TranslatorLocked { get; set; } - /// + /// public bool TeamLocked { get; set; } - /// + /// public bool LocationLocked { get; set; } - /// + /// public bool CoverArtistLocked { get; set; } - /// + /// public bool ReleaseDateLocked { get; set; } - /// + /// public bool TitleNameLocked { get; set; } - /// + /// public bool SortOrderLocked { get; set; } #endregion - /// + /// public string? CoverImage { get; set; } - /// + /// public string? PrimaryColor { get; set; } = string.Empty; - /// + /// public string? SecondaryColor { get; set; } = string.Empty; public MangaFormat? Format => Files.FirstOrDefault()?.Format; diff --git a/API/DTOs/CheckForFilesInFolderRootsDto.cs b/Kavita.Models/DTOs/CheckForFilesInFolderRootsDto.cs similarity index 82% rename from API/DTOs/CheckForFilesInFolderRootsDto.cs rename to Kavita.Models/DTOs/CheckForFilesInFolderRootsDto.cs index 42d4e2747..a3e0d0d3b 100644 --- a/API/DTOs/CheckForFilesInFolderRootsDto.cs +++ b/Kavita.Models/DTOs/CheckForFilesInFolderRootsDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record CheckForFilesInFolderRootsDto { diff --git a/API/DTOs/Collection/AppUserCollectionDto.cs b/Kavita.Models/DTOs/Collection/AppUserCollectionDto.cs similarity index 94% rename from API/DTOs/Collection/AppUserCollectionDto.cs rename to Kavita.Models/DTOs/Collection/AppUserCollectionDto.cs index 0634b5d83..d51285dab 100644 --- a/API/DTOs/Collection/AppUserCollectionDto.cs +++ b/Kavita.Models/DTOs/Collection/AppUserCollectionDto.cs @@ -1,9 +1,8 @@ using System; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; -namespace API.DTOs.Collection; +namespace Kavita.Models.DTOs.Collection; #nullable enable public sealed record AppUserCollectionDto : IHasCoverImage diff --git a/API/DTOs/Collection/DeleteCollectionsDto.cs b/Kavita.Models/DTOs/Collection/DeleteCollectionsDto.cs similarity index 82% rename from API/DTOs/Collection/DeleteCollectionsDto.cs rename to Kavita.Models/DTOs/Collection/DeleteCollectionsDto.cs index c0b94e9a1..8322c0f9b 100644 --- a/API/DTOs/Collection/DeleteCollectionsDto.cs +++ b/Kavita.Models/DTOs/Collection/DeleteCollectionsDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Collection; +namespace Kavita.Models.DTOs.Collection; public class DeleteCollectionsDto { diff --git a/API/DTOs/Collection/MalStackDto.cs b/Kavita.Models/DTOs/Collection/MalStackDto.cs similarity index 93% rename from API/DTOs/Collection/MalStackDto.cs rename to Kavita.Models/DTOs/Collection/MalStackDto.cs index d9d902e88..d8160673e 100644 --- a/API/DTOs/Collection/MalStackDto.cs +++ b/Kavita.Models/DTOs/Collection/MalStackDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Collection; +namespace Kavita.Models.DTOs.Collection; #nullable enable /// diff --git a/API/DTOs/Collection/PromoteCollectionsDto.cs b/Kavita.Models/DTOs/Collection/PromoteCollectionsDto.cs similarity index 80% rename from API/DTOs/Collection/PromoteCollectionsDto.cs rename to Kavita.Models/DTOs/Collection/PromoteCollectionsDto.cs index 2e2ab793b..e94c6bf4e 100644 --- a/API/DTOs/Collection/PromoteCollectionsDto.cs +++ b/Kavita.Models/DTOs/Collection/PromoteCollectionsDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Collection; +namespace Kavita.Models.DTOs.Collection; public class PromoteCollectionsDto { diff --git a/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs b/Kavita.Models/DTOs/CollectionTags/CollectionTagBulkAddDto.cs similarity index 91% rename from API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs rename to Kavita.Models/DTOs/CollectionTags/CollectionTagBulkAddDto.cs index 0a2270fbf..0e5e41949 100644 --- a/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs +++ b/Kavita.Models/DTOs/CollectionTags/CollectionTagBulkAddDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.CollectionTags; +namespace Kavita.Models.DTOs.CollectionTags; public sealed record CollectionTagBulkAddDto { diff --git a/API/DTOs/CollectionTags/CollectionTagDto.cs b/Kavita.Models/DTOs/CollectionTags/CollectionTagDto.cs similarity index 95% rename from API/DTOs/CollectionTags/CollectionTagDto.cs rename to Kavita.Models/DTOs/CollectionTags/CollectionTagDto.cs index 911622051..d5524838c 100644 --- a/API/DTOs/CollectionTags/CollectionTagDto.cs +++ b/Kavita.Models/DTOs/CollectionTags/CollectionTagDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.CollectionTags; +namespace Kavita.Models.DTOs.CollectionTags; [Obsolete("Use AppUserCollectionDto")] public sealed record CollectionTagDto diff --git a/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs b/Kavita.Models/DTOs/CollectionTags/UpdateSeriesForTagDto.cs similarity index 73% rename from API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs rename to Kavita.Models/DTOs/CollectionTags/UpdateSeriesForTagDto.cs index becc7034f..ee3fb1d2d 100644 --- a/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs +++ b/Kavita.Models/DTOs/CollectionTags/UpdateSeriesForTagDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.DTOs.Collection; +using Kavita.Models.DTOs.Collection; -namespace API.DTOs.CollectionTags; +namespace Kavita.Models.DTOs.CollectionTags; public sealed record UpdateSeriesForTagDto { diff --git a/API/DTOs/ColorScape.cs b/Kavita.Models/DTOs/ColorScape.cs similarity index 86% rename from API/DTOs/ColorScape.cs rename to Kavita.Models/DTOs/ColorScape.cs index 5351f2351..fbe2f247d 100644 --- a/API/DTOs/ColorScape.cs +++ b/Kavita.Models/DTOs/ColorScape.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable /// diff --git a/API/DTOs/CopySettingsFromLibraryDto.cs b/Kavita.Models/DTOs/CopySettingsFromLibraryDto.cs similarity index 91% rename from API/DTOs/CopySettingsFromLibraryDto.cs rename to Kavita.Models/DTOs/CopySettingsFromLibraryDto.cs index 5ca5ead51..7c2df97f0 100644 --- a/API/DTOs/CopySettingsFromLibraryDto.cs +++ b/Kavita.Models/DTOs/CopySettingsFromLibraryDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record CopySettingsFromLibraryDto { diff --git a/API/DTOs/CoverDb/CoverDbAuthor.cs b/Kavita.Models/DTOs/CoverDb/CoverDbAuthor.cs similarity index 93% rename from API/DTOs/CoverDb/CoverDbAuthor.cs rename to Kavita.Models/DTOs/CoverDb/CoverDbAuthor.cs index ca924801f..316ea6479 100644 --- a/API/DTOs/CoverDb/CoverDbAuthor.cs +++ b/Kavita.Models/DTOs/CoverDb/CoverDbAuthor.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using YamlDotNet.Serialization; -namespace API.DTOs.CoverDb; +namespace Kavita.Models.DTOs.CoverDb; public sealed record CoverDbAuthor { diff --git a/API/DTOs/CoverDb/CoverDbPeople.cs b/Kavita.Models/DTOs/CoverDb/CoverDbPeople.cs similarity index 87% rename from API/DTOs/CoverDb/CoverDbPeople.cs rename to Kavita.Models/DTOs/CoverDb/CoverDbPeople.cs index 2e825eac7..0d591bcf0 100644 --- a/API/DTOs/CoverDb/CoverDbPeople.cs +++ b/Kavita.Models/DTOs/CoverDb/CoverDbPeople.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using YamlDotNet.Serialization; -namespace API.DTOs.CoverDb; +namespace Kavita.Models.DTOs.CoverDb; public sealed record CoverDbPeople { diff --git a/API/DTOs/CoverDb/CoverDbPersonIds.cs b/Kavita.Models/DTOs/CoverDb/CoverDbPersonIds.cs similarity index 95% rename from API/DTOs/CoverDb/CoverDbPersonIds.cs rename to Kavita.Models/DTOs/CoverDb/CoverDbPersonIds.cs index 5816bb479..11d43b19a 100644 --- a/API/DTOs/CoverDb/CoverDbPersonIds.cs +++ b/Kavita.Models/DTOs/CoverDb/CoverDbPersonIds.cs @@ -1,6 +1,6 @@ using YamlDotNet.Serialization; -namespace API.DTOs.CoverDb; +namespace Kavita.Models.DTOs.CoverDb; #nullable enable public sealed record CoverDbPersonIds diff --git a/API/DTOs/Dashboard/DashboardStreamDto.cs b/Kavita.Models/DTOs/Dashboard/DashboardStreamDto.cs similarity index 90% rename from API/DTOs/Dashboard/DashboardStreamDto.cs rename to Kavita.Models/DTOs/Dashboard/DashboardStreamDto.cs index 7ebbe6fb0..6234efb1e 100644 --- a/API/DTOs/Dashboard/DashboardStreamDto.cs +++ b/Kavita.Models/DTOs/Dashboard/DashboardStreamDto.cs @@ -1,6 +1,7 @@ -using API.Entities.Enums; + +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Dashboard; +namespace Kavita.Models.DTOs.Dashboard; public sealed record DashboardStreamDto { diff --git a/API/DTOs/Dashboard/GroupedSeriesDto.cs b/Kavita.Models/DTOs/Dashboard/GroupedSeriesDto.cs similarity index 93% rename from API/DTOs/Dashboard/GroupedSeriesDto.cs rename to Kavita.Models/DTOs/Dashboard/GroupedSeriesDto.cs index 940e42c40..d10778091 100644 --- a/API/DTOs/Dashboard/GroupedSeriesDto.cs +++ b/Kavita.Models/DTOs/Dashboard/GroupedSeriesDto.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Dashboard; +namespace Kavita.Models.DTOs.Dashboard; /// /// This is a representation of a Series with some amount of underlying files within it. This is used for Recently Updated Series section /// diff --git a/API/DTOs/Dashboard/SmartFilterDto.cs b/Kavita.Models/DTOs/Dashboard/SmartFilterDto.cs similarity index 79% rename from API/DTOs/Dashboard/SmartFilterDto.cs rename to Kavita.Models/DTOs/Dashboard/SmartFilterDto.cs index c1bc4d7e1..ee734861a 100644 --- a/API/DTOs/Dashboard/SmartFilterDto.cs +++ b/Kavita.Models/DTOs/Dashboard/SmartFilterDto.cs @@ -1,6 +1,6 @@ -using API.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Filtering.v2; -namespace API.DTOs.Dashboard; +namespace Kavita.Models.DTOs.Dashboard; public sealed record SmartFilterDto { diff --git a/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs b/Kavita.Models/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs similarity index 84% rename from API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs rename to Kavita.Models/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs index 476a0732e..e2c9d4e95 100644 --- a/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs +++ b/Kavita.Models/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Dashboard; +namespace Kavita.Models.DTOs.Dashboard; public sealed record UpdateDashboardStreamPositionDto { diff --git a/API/DTOs/Dashboard/UpdateStreamPositionDto.cs b/Kavita.Models/DTOs/Dashboard/UpdateStreamPositionDto.cs similarity index 89% rename from API/DTOs/Dashboard/UpdateStreamPositionDto.cs rename to Kavita.Models/DTOs/Dashboard/UpdateStreamPositionDto.cs index 33b939a39..69085c92e 100644 --- a/API/DTOs/Dashboard/UpdateStreamPositionDto.cs +++ b/Kavita.Models/DTOs/Dashboard/UpdateStreamPositionDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Dashboard; +namespace Kavita.Models.DTOs.Dashboard; public sealed record UpdateStreamPositionDto { diff --git a/API/DTOs/DeleteChaptersDto.cs b/Kavita.Models/DTOs/DeleteChaptersDto.cs similarity index 82% rename from API/DTOs/DeleteChaptersDto.cs rename to Kavita.Models/DTOs/DeleteChaptersDto.cs index 9fad2f1fb..f2742d7fa 100644 --- a/API/DTOs/DeleteChaptersDto.cs +++ b/Kavita.Models/DTOs/DeleteChaptersDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record DeleteChaptersDto { diff --git a/API/DTOs/DeleteSeriesDto.cs b/Kavita.Models/DTOs/DeleteSeriesDto.cs similarity index 82% rename from API/DTOs/DeleteSeriesDto.cs rename to Kavita.Models/DTOs/DeleteSeriesDto.cs index ec9ba0c68..80f56ca37 100644 --- a/API/DTOs/DeleteSeriesDto.cs +++ b/Kavita.Models/DTOs/DeleteSeriesDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record DeleteSeriesDto { diff --git a/API/DTOs/Device/ClientDevice/UpdateClientDeviceNameDto.cs b/Kavita.Models/DTOs/Device/ClientDevice/UpdateClientDeviceNameDto.cs similarity index 70% rename from API/DTOs/Device/ClientDevice/UpdateClientDeviceNameDto.cs rename to Kavita.Models/DTOs/Device/ClientDevice/UpdateClientDeviceNameDto.cs index 7d4180ad5..167348d1f 100644 --- a/API/DTOs/Device/ClientDevice/UpdateClientDeviceNameDto.cs +++ b/Kavita.Models/DTOs/Device/ClientDevice/UpdateClientDeviceNameDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Device.ClientDevice; +namespace Kavita.Models.DTOs.Device.ClientDevice; public sealed record UpdateClientDeviceNameDto { diff --git a/API/DTOs/Device/EmailDevice/CreateEmailDeviceDto.cs b/Kavita.Models/DTOs/Device/EmailDevice/CreateEmailDeviceDto.cs similarity index 81% rename from API/DTOs/Device/EmailDevice/CreateEmailDeviceDto.cs rename to Kavita.Models/DTOs/Device/EmailDevice/CreateEmailDeviceDto.cs index 7b084a2f1..e63af801b 100644 --- a/API/DTOs/Device/EmailDevice/CreateEmailDeviceDto.cs +++ b/Kavita.Models/DTOs/Device/EmailDevice/CreateEmailDeviceDto.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; -using API.Entities.Enums.Device; +using Kavita.Models.Entities.Enums.Device; -namespace API.DTOs.Device.EmailDevice; +namespace Kavita.Models.DTOs.Device.EmailDevice; public sealed record CreateEmailDeviceDto { diff --git a/API/DTOs/Device/EmailDevice/DeviceDto.cs b/Kavita.Models/DTOs/Device/EmailDevice/DeviceDto.cs similarity index 89% rename from API/DTOs/Device/EmailDevice/DeviceDto.cs rename to Kavita.Models/DTOs/Device/EmailDevice/DeviceDto.cs index a6b8ba3b4..3cdc1d6a4 100644 --- a/API/DTOs/Device/EmailDevice/DeviceDto.cs +++ b/Kavita.Models/DTOs/Device/EmailDevice/DeviceDto.cs @@ -1,6 +1,8 @@ -using API.Entities.Enums.Device; + -namespace API.DTOs.Device.EmailDevice; +using Kavita.Models.Entities.Enums.Device; + +namespace Kavita.Models.DTOs.Device.EmailDevice; /// /// A Device is an entity that can receive data from Kavita (kindle) diff --git a/API/DTOs/Device/EmailDevice/SendSeriesToEmailDeviceDto.cs b/Kavita.Models/DTOs/Device/EmailDevice/SendSeriesToEmailDeviceDto.cs similarity index 71% rename from API/DTOs/Device/EmailDevice/SendSeriesToEmailDeviceDto.cs rename to Kavita.Models/DTOs/Device/EmailDevice/SendSeriesToEmailDeviceDto.cs index a97864c66..0346a2da1 100644 --- a/API/DTOs/Device/EmailDevice/SendSeriesToEmailDeviceDto.cs +++ b/Kavita.Models/DTOs/Device/EmailDevice/SendSeriesToEmailDeviceDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Device.EmailDevice; +namespace Kavita.Models.DTOs.Device.EmailDevice; public sealed record SendSeriesToEmailDeviceDto { diff --git a/API/DTOs/Device/EmailDevice/SendToEmailDeviceDto.cs b/Kavita.Models/DTOs/Device/EmailDevice/SendToEmailDeviceDto.cs similarity index 79% rename from API/DTOs/Device/EmailDevice/SendToEmailDeviceDto.cs rename to Kavita.Models/DTOs/Device/EmailDevice/SendToEmailDeviceDto.cs index 313ac080a..3c8c0e896 100644 --- a/API/DTOs/Device/EmailDevice/SendToEmailDeviceDto.cs +++ b/Kavita.Models/DTOs/Device/EmailDevice/SendToEmailDeviceDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Device.EmailDevice; +namespace Kavita.Models.DTOs.Device.EmailDevice; public sealed record SendToEmailDeviceDto { diff --git a/API/DTOs/Device/EmailDevice/UpdateDeviceDto.cs b/Kavita.Models/DTOs/Device/EmailDevice/UpdateDeviceDto.cs similarity index 83% rename from API/DTOs/Device/EmailDevice/UpdateDeviceDto.cs rename to Kavita.Models/DTOs/Device/EmailDevice/UpdateDeviceDto.cs index 0346bd207..32e150036 100644 --- a/API/DTOs/Device/EmailDevice/UpdateDeviceDto.cs +++ b/Kavita.Models/DTOs/Device/EmailDevice/UpdateDeviceDto.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; -using API.Entities.Enums.Device; +using Kavita.Models.Entities.Enums.Device; -namespace API.DTOs.Device.EmailDevice; +namespace Kavita.Models.DTOs.Device.EmailDevice; public sealed record UpdateEmailDeviceDto { diff --git a/API/DTOs/Downloads/DownloadBookmarkDto.cs b/Kavita.Models/DTOs/Downloads/DownloadBookmarkDto.cs similarity index 74% rename from API/DTOs/Downloads/DownloadBookmarkDto.cs rename to Kavita.Models/DTOs/Downloads/DownloadBookmarkDto.cs index 00f763dac..8c162dfb0 100644 --- a/API/DTOs/Downloads/DownloadBookmarkDto.cs +++ b/Kavita.Models/DTOs/Downloads/DownloadBookmarkDto.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using API.DTOs.Reader; +using Kavita.Models.DTOs.Reader; -namespace API.DTOs.Downloads; +namespace Kavita.Models.DTOs.Downloads; public sealed record DownloadBookmarkDto { diff --git a/API/DTOs/Email/ConfirmationEmailDto.cs b/Kavita.Models/DTOs/Email/ConfirmationEmailDto.cs similarity index 90% rename from API/DTOs/Email/ConfirmationEmailDto.cs rename to Kavita.Models/DTOs/Email/ConfirmationEmailDto.cs index 197395794..bbf3d821a 100644 --- a/API/DTOs/Email/ConfirmationEmailDto.cs +++ b/Kavita.Models/DTOs/Email/ConfirmationEmailDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; public sealed record ConfirmationEmailDto { diff --git a/API/DTOs/Email/EmailHistoryDto.cs b/Kavita.Models/DTOs/Email/EmailHistoryDto.cs similarity index 90% rename from API/DTOs/Email/EmailHistoryDto.cs rename to Kavita.Models/DTOs/Email/EmailHistoryDto.cs index c2968d091..16ff4ec2f 100644 --- a/API/DTOs/Email/EmailHistoryDto.cs +++ b/Kavita.Models/DTOs/Email/EmailHistoryDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; public sealed record EmailHistoryDto { diff --git a/API/DTOs/Email/EmailMigrationDto.cs b/Kavita.Models/DTOs/Email/EmailMigrationDto.cs similarity index 90% rename from API/DTOs/Email/EmailMigrationDto.cs rename to Kavita.Models/DTOs/Email/EmailMigrationDto.cs index 5354afdaa..524b92a73 100644 --- a/API/DTOs/Email/EmailMigrationDto.cs +++ b/Kavita.Models/DTOs/Email/EmailMigrationDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; public sealed record EmailMigrationDto { diff --git a/API/DTOs/Email/EmailTestResultDto.cs b/Kavita.Models/DTOs/Email/EmailTestResultDto.cs similarity index 89% rename from API/DTOs/Email/EmailTestResultDto.cs rename to Kavita.Models/DTOs/Email/EmailTestResultDto.cs index 9be868eab..28dea1f30 100644 --- a/API/DTOs/Email/EmailTestResultDto.cs +++ b/Kavita.Models/DTOs/Email/EmailTestResultDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; /// /// Represents if Test Email Service URL was successful or not and if any error occured diff --git a/API/DTOs/Email/PasswordResetEmailDto.cs b/Kavita.Models/DTOs/Email/PasswordResetEmailDto.cs similarity index 88% rename from API/DTOs/Email/PasswordResetEmailDto.cs rename to Kavita.Models/DTOs/Email/PasswordResetEmailDto.cs index 9fda066a9..c98af7143 100644 --- a/API/DTOs/Email/PasswordResetEmailDto.cs +++ b/Kavita.Models/DTOs/Email/PasswordResetEmailDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; public sealed record PasswordResetEmailDto { diff --git a/API/DTOs/Email/SendToDto.cs b/Kavita.Models/DTOs/Email/SendToDto.cs similarity index 84% rename from API/DTOs/Email/SendToDto.cs rename to Kavita.Models/DTOs/Email/SendToDto.cs index eacd29449..0cb0d4ad7 100644 --- a/API/DTOs/Email/SendToDto.cs +++ b/Kavita.Models/DTOs/Email/SendToDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; public sealed record SendToDto { diff --git a/API/DTOs/Email/TestEmailDto.cs b/Kavita.Models/DTOs/Email/TestEmailDto.cs similarity index 69% rename from API/DTOs/Email/TestEmailDto.cs rename to Kavita.Models/DTOs/Email/TestEmailDto.cs index 44c11bd6c..93843ff6f 100644 --- a/API/DTOs/Email/TestEmailDto.cs +++ b/Kavita.Models/DTOs/Email/TestEmailDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; public sealed record TestEmailDto { diff --git a/API/DTOs/Filtering/FilterDto.cs b/Kavita.Models/DTOs/Filtering/FilterDto.cs similarity index 97% rename from API/DTOs/Filtering/FilterDto.cs rename to Kavita.Models/DTOs/Filtering/FilterDto.cs index cb3374838..e4d40b752 100644 --- a/API/DTOs/Filtering/FilterDto.cs +++ b/Kavita.Models/DTOs/Filtering/FilterDto.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.DTOs.Filtering; -#nullable enable +namespace Kavita.Models.DTOs.Filtering; public sealed record FilterDto { diff --git a/API/DTOs/Filtering/LanguageDto.cs b/Kavita.Models/DTOs/Filtering/LanguageDto.cs similarity index 75% rename from API/DTOs/Filtering/LanguageDto.cs rename to Kavita.Models/DTOs/Filtering/LanguageDto.cs index dde85f07e..1e116e957 100644 --- a/API/DTOs/Filtering/LanguageDto.cs +++ b/Kavita.Models/DTOs/Filtering/LanguageDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering; +namespace Kavita.Models.DTOs.Filtering; public sealed record LanguageDto { diff --git a/API/DTOs/Filtering/PersonSortField.cs b/Kavita.Models/DTOs/Filtering/PersonSortField.cs similarity index 67% rename from API/DTOs/Filtering/PersonSortField.cs rename to Kavita.Models/DTOs/Filtering/PersonSortField.cs index 5268a1bf9..079fefb1a 100644 --- a/API/DTOs/Filtering/PersonSortField.cs +++ b/Kavita.Models/DTOs/Filtering/PersonSortField.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering; +namespace Kavita.Models.DTOs.Filtering; public enum PersonSortField { diff --git a/API/DTOs/Filtering/Range.cs b/Kavita.Models/DTOs/Filtering/Range.cs similarity index 86% rename from API/DTOs/Filtering/Range.cs rename to Kavita.Models/DTOs/Filtering/Range.cs index e697f26e1..e80f49fa4 100644 --- a/API/DTOs/Filtering/Range.cs +++ b/Kavita.Models/DTOs/Filtering/Range.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering; +namespace Kavita.Models.DTOs.Filtering; #nullable enable /// diff --git a/API/DTOs/Filtering/ReadStatus.cs b/Kavita.Models/DTOs/Filtering/ReadStatus.cs similarity index 86% rename from API/DTOs/Filtering/ReadStatus.cs rename to Kavita.Models/DTOs/Filtering/ReadStatus.cs index 81498ecb5..9b1dbb4a5 100644 --- a/API/DTOs/Filtering/ReadStatus.cs +++ b/Kavita.Models/DTOs/Filtering/ReadStatus.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering; +namespace Kavita.Models.DTOs.Filtering; /// /// Represents the Reading Status. This is a flag and allows multiple statues diff --git a/API/DTOs/Filtering/SortField.cs b/Kavita.Models/DTOs/Filtering/SortField.cs similarity index 96% rename from API/DTOs/Filtering/SortField.cs rename to Kavita.Models/DTOs/Filtering/SortField.cs index eaecea0c9..c8346bd18 100644 --- a/API/DTOs/Filtering/SortField.cs +++ b/Kavita.Models/DTOs/Filtering/SortField.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering; +namespace Kavita.Models.DTOs.Filtering; public enum SortField { diff --git a/API/DTOs/Filtering/SortOptions.cs b/Kavita.Models/DTOs/Filtering/SortOptions.cs similarity index 94% rename from API/DTOs/Filtering/SortOptions.cs rename to Kavita.Models/DTOs/Filtering/SortOptions.cs index 864801e6b..96d91f147 100644 --- a/API/DTOs/Filtering/SortOptions.cs +++ b/Kavita.Models/DTOs/Filtering/SortOptions.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering; +namespace Kavita.Models.DTOs.Filtering; /// /// Sorting Options for a query diff --git a/API/DTOs/Filtering/v2/DecodeFilterDto.cs b/Kavita.Models/DTOs/Filtering/v2/DecodeFilterDto.cs similarity index 78% rename from API/DTOs/Filtering/v2/DecodeFilterDto.cs rename to Kavita.Models/DTOs/Filtering/v2/DecodeFilterDto.cs index db4c7ecce..821c99f92 100644 --- a/API/DTOs/Filtering/v2/DecodeFilterDto.cs +++ b/Kavita.Models/DTOs/Filtering/v2/DecodeFilterDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering.v2; +namespace Kavita.Models.DTOs.Filtering.v2; /// /// For requesting an encoded filter to be decoded diff --git a/API/DTOs/Filtering/v2/FilterCombination.cs b/Kavita.Models/DTOs/Filtering/v2/FilterCombination.cs similarity index 56% rename from API/DTOs/Filtering/v2/FilterCombination.cs rename to Kavita.Models/DTOs/Filtering/v2/FilterCombination.cs index d011cb000..a5e4da194 100644 --- a/API/DTOs/Filtering/v2/FilterCombination.cs +++ b/Kavita.Models/DTOs/Filtering/v2/FilterCombination.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering.v2; +namespace Kavita.Models.DTOs.Filtering.v2; public enum FilterCombination { diff --git a/API/DTOs/Filtering/v2/FilterComparision.cs b/Kavita.Models/DTOs/Filtering/v2/FilterComparision.cs similarity index 97% rename from API/DTOs/Filtering/v2/FilterComparision.cs rename to Kavita.Models/DTOs/Filtering/v2/FilterComparision.cs index 59bb86a8a..400ed32dd 100644 --- a/API/DTOs/Filtering/v2/FilterComparision.cs +++ b/Kavita.Models/DTOs/Filtering/v2/FilterComparision.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.DTOs.Filtering.v2; +namespace Kavita.Models.DTOs.Filtering.v2; public enum FilterComparison { diff --git a/API/DTOs/Filtering/v2/FilterField.cs b/Kavita.Models/DTOs/Filtering/v2/FilterField.cs similarity index 97% rename from API/DTOs/Filtering/v2/FilterField.cs rename to Kavita.Models/DTOs/Filtering/v2/FilterField.cs index 654fc64cf..a345873da 100644 --- a/API/DTOs/Filtering/v2/FilterField.cs +++ b/Kavita.Models/DTOs/Filtering/v2/FilterField.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering.v2; +namespace Kavita.Models.DTOs.Filtering.v2; /// /// Represents the field which will dictate the value type and the Extension used for filtering diff --git a/API/DTOs/Filtering/v2/FilterStatementDto.cs b/Kavita.Models/DTOs/Filtering/v2/FilterStatementDto.cs similarity index 93% rename from API/DTOs/Filtering/v2/FilterStatementDto.cs rename to Kavita.Models/DTOs/Filtering/v2/FilterStatementDto.cs index 47d87e94c..2e583a220 100644 --- a/API/DTOs/Filtering/v2/FilterStatementDto.cs +++ b/Kavita.Models/DTOs/Filtering/v2/FilterStatementDto.cs @@ -1,5 +1,5 @@  -namespace API.DTOs.Filtering.v2; +namespace Kavita.Models.DTOs.Filtering.v2; public sealed record FilterStatementDto { diff --git a/API/DTOs/Filtering/v2/FilterV2Dto.cs b/Kavita.Models/DTOs/Filtering/v2/FilterV2Dto.cs similarity index 94% rename from API/DTOs/Filtering/v2/FilterV2Dto.cs rename to Kavita.Models/DTOs/Filtering/v2/FilterV2Dto.cs index dc30b26e2..94d16f4f3 100644 --- a/API/DTOs/Filtering/v2/FilterV2Dto.cs +++ b/Kavita.Models/DTOs/Filtering/v2/FilterV2Dto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Filtering.v2; +namespace Kavita.Models.DTOs.Filtering.v2; #nullable enable /// diff --git a/API/DTOs/Font/EpubFontDto.cs b/Kavita.Models/DTOs/Font/EpubFontDto.cs similarity index 72% rename from API/DTOs/Font/EpubFontDto.cs rename to Kavita.Models/DTOs/Font/EpubFontDto.cs index 4a85916dc..b32e61640 100644 --- a/API/DTOs/Font/EpubFontDto.cs +++ b/Kavita.Models/DTOs/Font/EpubFontDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.Font; +using Kavita.Models.Entities.Enums.Font; -namespace API.DTOs.Font; +namespace Kavita.Models.DTOs.Font; public sealed record EpubFontDto { diff --git a/API/DTOs/ImportFieldMappings.cs b/Kavita.Models/DTOs/ImportFieldMappings.cs similarity index 96% rename from API/DTOs/ImportFieldMappings.cs rename to Kavita.Models/DTOs/ImportFieldMappings.cs index 41d7ab748..d961588d7 100644 --- a/API/DTOs/ImportFieldMappings.cs +++ b/Kavita.Models/DTOs/ImportFieldMappings.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.ComponentModel; -using API.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.KavitaPlus.Metadata; -namespace API.DTOs; +namespace Kavita.Models.DTOs; /// /// How Kavita should import the new settings diff --git a/API/DTOs/Internal/AppSettingsDto.cs b/Kavita.Models/DTOs/Internal/AppSettingsDto.cs similarity index 86% rename from API/DTOs/Internal/AppSettingsDto.cs rename to Kavita.Models/DTOs/Internal/AppSettingsDto.cs index 0aa36c7d7..2add8e7cb 100644 --- a/API/DTOs/Internal/AppSettingsDto.cs +++ b/Kavita.Models/DTOs/Internal/AppSettingsDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Internal; +namespace Kavita.Models.DTOs.Internal; #nullable enable public sealed record AppSettingsDto diff --git a/API/DTOs/Jobs/JobDto.cs b/Kavita.Models/DTOs/Jobs/JobDto.cs similarity index 94% rename from API/DTOs/Jobs/JobDto.cs rename to Kavita.Models/DTOs/Jobs/JobDto.cs index 55419811f..a962a445e 100644 --- a/API/DTOs/Jobs/JobDto.cs +++ b/Kavita.Models/DTOs/Jobs/JobDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Jobs; +namespace Kavita.Models.DTOs.Jobs; public sealed record JobDto { diff --git a/API/DTOs/JumpBar/JumpKeyDto.cs b/Kavita.Models/DTOs/JumpBar/JumpKeyDto.cs similarity index 91% rename from API/DTOs/JumpBar/JumpKeyDto.cs rename to Kavita.Models/DTOs/JumpBar/JumpKeyDto.cs index 8dc5b4a8e..9165905be 100644 --- a/API/DTOs/JumpBar/JumpKeyDto.cs +++ b/Kavita.Models/DTOs/JumpBar/JumpKeyDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.JumpBar; +namespace Kavita.Models.DTOs.JumpBar; /// /// Represents an individual button in a Jump Bar diff --git a/API/DTOs/KavitaLocale.cs b/Kavita.Models/DTOs/KavitaLocale.cs similarity index 90% rename from API/DTOs/KavitaLocale.cs rename to Kavita.Models/DTOs/KavitaLocale.cs index 51868605f..a71f665b6 100644 --- a/API/DTOs/KavitaLocale.cs +++ b/Kavita.Models/DTOs/KavitaLocale.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record KavitaLocale { diff --git a/API/DTOs/KavitaPlus/Account/AniListUpdateDto.cs b/Kavita.Models/DTOs/KavitaPlus/Account/AniListUpdateDto.cs similarity index 60% rename from API/DTOs/KavitaPlus/Account/AniListUpdateDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Account/AniListUpdateDto.cs index c053bd34e..1c76c6cc9 100644 --- a/API/DTOs/KavitaPlus/Account/AniListUpdateDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Account/AniListUpdateDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.Account; +namespace Kavita.Models.DTOs.KavitaPlus.Account; public sealed record AniListUpdateDto { diff --git a/API/DTOs/KavitaPlus/Account/UserTokenInfo.cs b/Kavita.Models/DTOs/KavitaPlus/Account/UserTokenInfo.cs similarity index 89% rename from API/DTOs/KavitaPlus/Account/UserTokenInfo.cs rename to Kavita.Models/DTOs/KavitaPlus/Account/UserTokenInfo.cs index 340ad0f4c..baa35203e 100644 --- a/API/DTOs/KavitaPlus/Account/UserTokenInfo.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Account/UserTokenInfo.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.KavitaPlus.Account; +namespace Kavita.Models.DTOs.KavitaPlus.Account; /// /// Represents information around a user's tokens and their status diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs similarity index 81% rename from API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs rename to Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs index c05ff0567..410a37228 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs @@ -1,6 +1,6 @@ -using API.DTOs.Scrobbling; +using Kavita.Models.DTOs.Scrobbling; -namespace API.DTOs.KavitaPlus.ExternalMetadata; +namespace Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; #nullable enable /// diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs similarity index 86% rename from API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs rename to Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs index a7359d69b..5178159aa 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.DTOs.Scrobbling; +using Kavita.Models.DTOs.Scrobbling; -namespace API.DTOs.KavitaPlus.ExternalMetadata; +namespace Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; #nullable enable /// diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs similarity index 71% rename from API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs rename to Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs index 48d2a2095..bc59e7fed 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Scrobbling; -using API.DTOs.SeriesDetail; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.SeriesDetail; -namespace API.DTOs.KavitaPlus.ExternalMetadata; +namespace Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; public sealed record SeriesDetailPlusApiDto { diff --git a/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs b/Kavita.Models/DTOs/KavitaPlus/License/EncryptLicenseDto.cs similarity index 82% rename from API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs rename to Kavita.Models/DTOs/KavitaPlus/License/EncryptLicenseDto.cs index dd85dd063..bd0f3e82a 100644 --- a/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/License/EncryptLicenseDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.License; +namespace Kavita.Models.DTOs.KavitaPlus.License; #nullable enable public sealed record EncryptLicenseDto diff --git a/API/DTOs/KavitaPlus/License/LicenseInfoDto.cs b/Kavita.Models/DTOs/KavitaPlus/License/LicenseInfoDto.cs similarity index 95% rename from API/DTOs/KavitaPlus/License/LicenseInfoDto.cs rename to Kavita.Models/DTOs/KavitaPlus/License/LicenseInfoDto.cs index aaf4eded8..f4015e773 100644 --- a/API/DTOs/KavitaPlus/License/LicenseInfoDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/License/LicenseInfoDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.KavitaPlus.License; +namespace Kavita.Models.DTOs.KavitaPlus.License; public sealed record LicenseInfoDto { diff --git a/API/DTOs/KavitaPlus/License/LicenseValidDto.cs b/Kavita.Models/DTOs/KavitaPlus/License/LicenseValidDto.cs similarity index 73% rename from API/DTOs/KavitaPlus/License/LicenseValidDto.cs rename to Kavita.Models/DTOs/KavitaPlus/License/LicenseValidDto.cs index a7bd476ce..25f112aeb 100644 --- a/API/DTOs/KavitaPlus/License/LicenseValidDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/License/LicenseValidDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.License; +namespace Kavita.Models.DTOs.KavitaPlus.License; public sealed record LicenseValidDto { diff --git a/API/DTOs/KavitaPlus/License/ResetLicenseDto.cs b/Kavita.Models/DTOs/KavitaPlus/License/ResetLicenseDto.cs similarity index 78% rename from API/DTOs/KavitaPlus/License/ResetLicenseDto.cs rename to Kavita.Models/DTOs/KavitaPlus/License/ResetLicenseDto.cs index d0fd9b666..5933b4f44 100644 --- a/API/DTOs/KavitaPlus/License/ResetLicenseDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/License/ResetLicenseDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.License; +namespace Kavita.Models.DTOs.KavitaPlus.License; public sealed record ResetLicenseDto { diff --git a/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs b/Kavita.Models/DTOs/KavitaPlus/License/UpdateLicenseDto.cs similarity index 88% rename from API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs rename to Kavita.Models/DTOs/KavitaPlus/License/UpdateLicenseDto.cs index 28b47efbe..cf06fae88 100644 --- a/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/License/UpdateLicenseDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.License; +namespace Kavita.Models.DTOs.KavitaPlus.License; #nullable enable public sealed record UpdateLicenseDto diff --git a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs b/Kavita.Models/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs similarity index 91% rename from API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs index c394cf8d4..6a7e4e256 100644 --- a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.Manage; +namespace Kavita.Models.DTOs.KavitaPlus.Manage; /// /// Represents an option in the UI layer for Filtering diff --git a/API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs b/Kavita.Models/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs similarity index 80% rename from API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs index a51e63ee9..bc18d74bb 100644 --- a/API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.KavitaPlus.Manage; +namespace Kavita.Models.DTOs.KavitaPlus.Manage; public sealed record ManageMatchSeriesDto { diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs b/Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs similarity index 90% rename from API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs index add9ca723..293585d17 100644 --- a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using API.DTOs.SeriesDetail; +using Kavita.Models.DTOs.SeriesDetail; -namespace API.DTOs.KavitaPlus.Metadata; +namespace Kavita.Models.DTOs.KavitaPlus.Metadata; #nullable enable /// diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs b/Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs similarity index 88% rename from API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs index 6704bf697..5c43c84d0 100644 --- a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs @@ -1,11 +1,11 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; -using API.DTOs.Recommendation; -using API.DTOs.Scrobbling; -using API.Services.Plus; +using Kavita.Models.DTOs.Recommendation; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.KavitaPlus.Metadata; -#nullable enable +namespace Kavita.Models.DTOs.KavitaPlus.Metadata; /// /// This is AniListSeries diff --git a/API/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs b/Kavita.Models/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs similarity index 87% rename from API/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs index a9debabd1..e9b7ff9a2 100644 --- a/API/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.KavitaPlus.Metadata; +namespace Kavita.Models.DTOs.KavitaPlus.Metadata; public sealed record MetadataFieldMappingDto { diff --git a/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs b/Kavita.Models/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs similarity index 96% rename from API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs index 97e743bce..481ac700f 100644 --- a/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.DTOs.Settings; -using API.Entities.Enums; -using API.Entities.MetadataMatching; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; -namespace API.DTOs.KavitaPlus.Metadata; +namespace Kavita.Models.DTOs.KavitaPlus.Metadata; public sealed record MetadataSettingsDto: FieldMappingsDto diff --git a/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs b/Kavita.Models/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs similarity index 87% rename from API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs rename to Kavita.Models/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs index 2b57548cd..fff85e9a6 100644 --- a/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.Metadata; +namespace Kavita.Models.DTOs.KavitaPlus.Metadata; #nullable enable public enum CharacterRole diff --git a/API/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs b/Kavita.Models/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs similarity index 79% rename from API/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs rename to Kavita.Models/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs index 6178c1d23..1b7a5ef2b 100644 --- a/API/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs @@ -1,8 +1,8 @@ -using API.DTOs.Scrobbling; -using API.Entities.Enums; -using API.Services.Plus; +#nullable enable +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.KavitaPlus.Metadata; +namespace Kavita.Models.DTOs.KavitaPlus.Metadata; public sealed record ALMediaTitle { diff --git a/API/DTOs/Koreader/KoreaderBookDto.cs b/Kavita.Models/DTOs/Koreader/KoreaderBookDto.cs similarity index 94% rename from API/DTOs/Koreader/KoreaderBookDto.cs rename to Kavita.Models/DTOs/Koreader/KoreaderBookDto.cs index 9bfc4adc3..7545ce057 100644 --- a/API/DTOs/Koreader/KoreaderBookDto.cs +++ b/Kavita.Models/DTOs/Koreader/KoreaderBookDto.cs @@ -1,6 +1,6 @@ -using API.DTOs.Progress; +using Kavita.Models.DTOs.Progress; -namespace API.DTOs.Koreader; +namespace Kavita.Models.DTOs.Koreader; /// /// This is the interface for receiving and sending updates to Koreader. The only fields diff --git a/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs b/Kavita.Models/DTOs/Koreader/KoreaderProgressUpdateDto.cs similarity index 89% rename from API/DTOs/Koreader/KoreaderProgressUpdateDto.cs rename to Kavita.Models/DTOs/Koreader/KoreaderProgressUpdateDto.cs index 52a1d6cbd..9d401d656 100644 --- a/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs +++ b/Kavita.Models/DTOs/Koreader/KoreaderProgressUpdateDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Koreader; +namespace Kavita.Models.DTOs.Koreader; public class KoreaderProgressUpdateDto { diff --git a/API/DTOs/LibraryDto.cs b/Kavita.Models/DTOs/LibraryDto.cs similarity index 97% rename from API/DTOs/LibraryDto.cs rename to Kavita.Models/DTOs/LibraryDto.cs index a493b3626..9dc5e6e3e 100644 --- a/API/DTOs/LibraryDto.cs +++ b/Kavita.Models/DTOs/LibraryDto.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable /// diff --git a/API/DTOs/MangaFileDto.cs b/Kavita.Models/DTOs/MangaFileDto.cs similarity index 92% rename from API/DTOs/MangaFileDto.cs rename to Kavita.Models/DTOs/MangaFileDto.cs index 645c9ad32..afd0ee9db 100644 --- a/API/DTOs/MangaFileDto.cs +++ b/Kavita.Models/DTOs/MangaFileDto.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record MangaFileDto diff --git a/API/DTOs/MediaErrors/MediaErrorDto.cs b/Kavita.Models/DTOs/MediaErrors/MediaErrorDto.cs similarity index 93% rename from API/DTOs/MediaErrors/MediaErrorDto.cs rename to Kavita.Models/DTOs/MediaErrors/MediaErrorDto.cs index b77ee88be..4bac5aab2 100644 --- a/API/DTOs/MediaErrors/MediaErrorDto.cs +++ b/Kavita.Models/DTOs/MediaErrors/MediaErrorDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.MediaErrors; +namespace Kavita.Models.DTOs.MediaErrors; public sealed record MediaErrorDto { diff --git a/API/DTOs/Metadata/AgeRatingDto.cs b/Kavita.Models/DTOs/Metadata/AgeRatingDto.cs similarity index 62% rename from API/DTOs/Metadata/AgeRatingDto.cs rename to Kavita.Models/DTOs/Metadata/AgeRatingDto.cs index bfa835ef5..ad444c433 100644 --- a/API/DTOs/Metadata/AgeRatingDto.cs +++ b/Kavita.Models/DTOs/Metadata/AgeRatingDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Metadata; +namespace Kavita.Models.DTOs.Metadata; public sealed record AgeRatingDto { diff --git a/API/DTOs/Metadata/Browse/BrowseGenreDto.cs b/Kavita.Models/DTOs/Metadata/Browse/BrowseGenreDto.cs similarity index 85% rename from API/DTOs/Metadata/Browse/BrowseGenreDto.cs rename to Kavita.Models/DTOs/Metadata/Browse/BrowseGenreDto.cs index 8044c7914..76b846d50 100644 --- a/API/DTOs/Metadata/Browse/BrowseGenreDto.cs +++ b/Kavita.Models/DTOs/Metadata/Browse/BrowseGenreDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Metadata.Browse; +namespace Kavita.Models.DTOs.Metadata.Browse; public sealed record BrowseGenreDto : GenreTagDto { diff --git a/API/DTOs/Metadata/Browse/BrowsePersonDto.cs b/Kavita.Models/DTOs/Metadata/Browse/BrowsePersonDto.cs similarity index 83% rename from API/DTOs/Metadata/Browse/BrowsePersonDto.cs rename to Kavita.Models/DTOs/Metadata/Browse/BrowsePersonDto.cs index 20f84b783..c69650e9e 100644 --- a/API/DTOs/Metadata/Browse/BrowsePersonDto.cs +++ b/Kavita.Models/DTOs/Metadata/Browse/BrowsePersonDto.cs @@ -1,6 +1,6 @@ -using API.DTOs.Person; +using Kavita.Models.DTOs.Person; -namespace API.DTOs.Metadata.Browse; +namespace Kavita.Models.DTOs.Metadata.Browse; /// /// Used to browse writers and click in to see their series diff --git a/API/DTOs/Metadata/Browse/BrowseTagDto.cs b/Kavita.Models/DTOs/Metadata/Browse/BrowseTagDto.cs similarity index 85% rename from API/DTOs/Metadata/Browse/BrowseTagDto.cs rename to Kavita.Models/DTOs/Metadata/Browse/BrowseTagDto.cs index 9a71876e3..f755188ff 100644 --- a/API/DTOs/Metadata/Browse/BrowseTagDto.cs +++ b/Kavita.Models/DTOs/Metadata/Browse/BrowseTagDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Metadata.Browse; +namespace Kavita.Models.DTOs.Metadata.Browse; public sealed record BrowseTagDto : TagDto { diff --git a/API/DTOs/Metadata/Browse/Requests/BrowseAnnotationFilterDto.cs b/Kavita.Models/DTOs/Metadata/Browse/Requests/BrowseAnnotationFilterDto.cs similarity index 84% rename from API/DTOs/Metadata/Browse/Requests/BrowseAnnotationFilterDto.cs rename to Kavita.Models/DTOs/Metadata/Browse/Requests/BrowseAnnotationFilterDto.cs index 4a0f08cda..42204bc0a 100644 --- a/API/DTOs/Metadata/Browse/Requests/BrowseAnnotationFilterDto.cs +++ b/Kavita.Models/DTOs/Metadata/Browse/Requests/BrowseAnnotationFilterDto.cs @@ -1,9 +1,9 @@ #nullable enable using System.Collections.Generic; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; -namespace API.DTOs.Metadata.Browse.Requests; +namespace Kavita.Models.DTOs.Metadata.Browse.Requests; public class BrowseAnnotationFilterDto { diff --git a/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs b/Kavita.Models/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs similarity index 84% rename from API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs rename to Kavita.Models/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs index 26377591f..d96df333d 100644 --- a/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs +++ b/Kavita.Models/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; -namespace API.DTOs.Metadata.Browse.Requests; +namespace Kavita.Models.DTOs.Metadata.Browse.Requests; #nullable enable public sealed record BrowsePersonFilterDto diff --git a/API/DTOs/Metadata/ChapterMetadataDto.cs b/Kavita.Models/DTOs/Metadata/ChapterMetadataDto.cs similarity index 95% rename from API/DTOs/Metadata/ChapterMetadataDto.cs rename to Kavita.Models/DTOs/Metadata/ChapterMetadataDto.cs index c79436e24..8c06a18c4 100644 --- a/API/DTOs/Metadata/ChapterMetadataDto.cs +++ b/Kavita.Models/DTOs/Metadata/ChapterMetadataDto.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using API.DTOs.Person; -using API.Entities.Enums; +using Kavita.Models.DTOs.Person; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Metadata; +namespace Kavita.Models.DTOs.Metadata; #nullable enable /// diff --git a/API/DTOs/Metadata/GenreTagDto.cs b/Kavita.Models/DTOs/Metadata/GenreTagDto.cs similarity index 72% rename from API/DTOs/Metadata/GenreTagDto.cs rename to Kavita.Models/DTOs/Metadata/GenreTagDto.cs index 13a339d38..a4cf3c34a 100644 --- a/API/DTOs/Metadata/GenreTagDto.cs +++ b/Kavita.Models/DTOs/Metadata/GenreTagDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Metadata; +namespace Kavita.Models.DTOs.Metadata; public record GenreTagDto { diff --git a/API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs b/Kavita.Models/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs similarity index 61% rename from API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs rename to Kavita.Models/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs index 209d1b4d6..a19738985 100644 --- a/API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs +++ b/Kavita.Models/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs @@ -1,6 +1,6 @@ -using API.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.KavitaPlus.Metadata; -namespace API.DTOs.Metadata.Matching; +namespace Kavita.Models.DTOs.Metadata.Matching; public sealed record ExternalSeriesMatchDto { diff --git a/API/DTOs/Metadata/Matching/MatchSeriesDto.cs b/Kavita.Models/DTOs/Metadata/Matching/MatchSeriesDto.cs similarity index 92% rename from API/DTOs/Metadata/Matching/MatchSeriesDto.cs rename to Kavita.Models/DTOs/Metadata/Matching/MatchSeriesDto.cs index bb497b9ab..5ad9e166d 100644 --- a/API/DTOs/Metadata/Matching/MatchSeriesDto.cs +++ b/Kavita.Models/DTOs/Metadata/Matching/MatchSeriesDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Metadata.Matching; +namespace Kavita.Models.DTOs.Metadata.Matching; /// /// Used for matching a series with Kavita+ for metadata and scrobbling diff --git a/API/DTOs/Metadata/PublicationStatusDto.cs b/Kavita.Models/DTOs/Metadata/PublicationStatusDto.cs similarity index 64% rename from API/DTOs/Metadata/PublicationStatusDto.cs rename to Kavita.Models/DTOs/Metadata/PublicationStatusDto.cs index b4f12500a..94b65df3a 100644 --- a/API/DTOs/Metadata/PublicationStatusDto.cs +++ b/Kavita.Models/DTOs/Metadata/PublicationStatusDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Metadata; +namespace Kavita.Models.DTOs.Metadata; public sealed record PublicationStatusDto { diff --git a/API/DTOs/Metadata/TagDto.cs b/Kavita.Models/DTOs/Metadata/TagDto.cs similarity index 71% rename from API/DTOs/Metadata/TagDto.cs rename to Kavita.Models/DTOs/Metadata/TagDto.cs index f5c925e1f..c0c2fbe8d 100644 --- a/API/DTOs/Metadata/TagDto.cs +++ b/Kavita.Models/DTOs/Metadata/TagDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Metadata; +namespace Kavita.Models.DTOs.Metadata; public record TagDto { diff --git a/API/DTOs/Misc/ParseBulkRequestDto.cs b/Kavita.Models/DTOs/Misc/ParseBulkRequestDto.cs similarity index 71% rename from API/DTOs/Misc/ParseBulkRequestDto.cs rename to Kavita.Models/DTOs/Misc/ParseBulkRequestDto.cs index 7e529e9ed..9b7a4537c 100644 --- a/API/DTOs/Misc/ParseBulkRequestDto.cs +++ b/Kavita.Models/DTOs/Misc/ParseBulkRequestDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Misc; +namespace Kavita.Models.DTOs.Misc; public sealed record ParseBulkRequestDto { diff --git a/API/DTOs/Misc/ParseBulkResponseDto.cs b/Kavita.Models/DTOs/Misc/ParseBulkResponseDto.cs similarity index 94% rename from API/DTOs/Misc/ParseBulkResponseDto.cs rename to Kavita.Models/DTOs/Misc/ParseBulkResponseDto.cs index c46f1b78b..6e7f1172f 100644 --- a/API/DTOs/Misc/ParseBulkResponseDto.cs +++ b/Kavita.Models/DTOs/Misc/ParseBulkResponseDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Misc; +namespace Kavita.Models.DTOs.Misc; public record ParseBulkResponseDto { diff --git a/API/DTOs/Misc/ParseResultDto.cs b/Kavita.Models/DTOs/Misc/ParseResultDto.cs similarity index 90% rename from API/DTOs/Misc/ParseResultDto.cs rename to Kavita.Models/DTOs/Misc/ParseResultDto.cs index dd2b8771e..0a8d55299 100644 --- a/API/DTOs/Misc/ParseResultDto.cs +++ b/Kavita.Models/DTOs/Misc/ParseResultDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Misc; +namespace Kavita.Models.DTOs.Misc; public sealed record ParseResultDto { diff --git a/API/DTOs/OPDS/Internal/Feed.cs b/Kavita.Models/DTOs/OPDS/Internal/Feed.cs similarity index 96% rename from API/DTOs/OPDS/Internal/Feed.cs rename to Kavita.Models/DTOs/OPDS/Internal/Feed.cs index 5663e8200..4be3a7f2f 100644 --- a/API/DTOs/OPDS/Internal/Feed.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/Feed.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; using System.Xml.Serialization; -using Newtonsoft.Json; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; [XmlRoot("feed", Namespace = "http://www.w3.org/2005/Atom")] public sealed record Feed diff --git a/API/DTOs/OPDS/Internal/FeedAuthor.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedAuthor.cs similarity index 84% rename from API/DTOs/OPDS/Internal/FeedAuthor.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedAuthor.cs index 4196997dd..71d2e7f55 100644 --- a/API/DTOs/OPDS/Internal/FeedAuthor.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedAuthor.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public sealed record FeedAuthor { diff --git a/API/DTOs/OPDS/Internal/FeedCategory.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedCategory.cs similarity index 92% rename from API/DTOs/OPDS/Internal/FeedCategory.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedCategory.cs index 2352b4af2..e20b5f9fe 100644 --- a/API/DTOs/OPDS/Internal/FeedCategory.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedCategory.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public sealed record FeedCategory { diff --git a/API/DTOs/OPDS/Internal/FeedEntry.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedEntry.cs similarity index 97% rename from API/DTOs/OPDS/Internal/FeedEntry.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedEntry.cs index 838ebd124..b9c44380e 100644 --- a/API/DTOs/OPDS/Internal/FeedEntry.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedEntry.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; #nullable enable public sealed record FeedEntry diff --git a/API/DTOs/OPDS/Internal/FeedEntryContent.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedEntryContent.cs similarity index 83% rename from API/DTOs/OPDS/Internal/FeedEntryContent.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedEntryContent.cs index 4de9b73bd..a65184834 100644 --- a/API/DTOs/OPDS/Internal/FeedEntryContent.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedEntryContent.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public sealed record FeedEntryContent { diff --git a/API/DTOs/OPDS/Internal/FeedLink.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedLink.cs similarity index 97% rename from API/DTOs/OPDS/Internal/FeedLink.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedLink.cs index 95d65f907..e0eaabf55 100644 --- a/API/DTOs/OPDS/Internal/FeedLink.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedLink.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public sealed record FeedLink { diff --git a/API/DTOs/OPDS/Internal/FeedLinkRelation.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedLinkRelation.cs similarity index 95% rename from API/DTOs/OPDS/Internal/FeedLinkRelation.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedLinkRelation.cs index 4c9ee2c94..903bf5bdd 100644 --- a/API/DTOs/OPDS/Internal/FeedLinkRelation.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedLinkRelation.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public static class FeedLinkRelation { diff --git a/API/DTOs/OPDS/Internal/FeedLinkType.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedLinkType.cs similarity index 91% rename from API/DTOs/OPDS/Internal/FeedLinkType.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedLinkType.cs index 6ae48bd52..4bb0cde7d 100644 --- a/API/DTOs/OPDS/Internal/FeedLinkType.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedLinkType.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public static class FeedLinkType { diff --git a/API/DTOs/OPDS/Internal/OpenSearchDescription.cs b/Kavita.Models/DTOs/OPDS/Internal/OpenSearchDescription.cs similarity index 98% rename from API/DTOs/OPDS/Internal/OpenSearchDescription.cs rename to Kavita.Models/DTOs/OPDS/Internal/OpenSearchDescription.cs index eba26572f..ff3d9eb2c 100644 --- a/API/DTOs/OPDS/Internal/OpenSearchDescription.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/OpenSearchDescription.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; [XmlRoot("OpenSearchDescription", Namespace = "http://a9.com/-/spec/opensearch/1.1/")] public sealed record OpenSearchDescription diff --git a/API/DTOs/OPDS/Internal/SearchLink.cs b/Kavita.Models/DTOs/OPDS/Internal/SearchLink.cs similarity index 89% rename from API/DTOs/OPDS/Internal/SearchLink.cs rename to Kavita.Models/DTOs/OPDS/Internal/SearchLink.cs index b4698c221..4673f8de8 100644 --- a/API/DTOs/OPDS/Internal/SearchLink.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/SearchLink.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public sealed record SearchLink { diff --git a/API/DTOs/OPDS/Requests/IOpdsPagination.cs b/Kavita.Models/DTOs/OPDS/Requests/IOpdsPagination.cs similarity index 62% rename from API/DTOs/OPDS/Requests/IOpdsPagination.cs rename to Kavita.Models/DTOs/OPDS/Requests/IOpdsPagination.cs index 7dba9cc8d..ac0644161 100644 --- a/API/DTOs/OPDS/Requests/IOpdsPagination.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/IOpdsPagination.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; public interface IOpdsPagination { diff --git a/API/DTOs/OPDS/Requests/IOpdsRequest.cs b/Kavita.Models/DTOs/OPDS/Requests/IOpdsRequest.cs similarity index 72% rename from API/DTOs/OPDS/Requests/IOpdsRequest.cs rename to Kavita.Models/DTOs/OPDS/Requests/IOpdsRequest.cs index 201431741..f66ca8582 100644 --- a/API/DTOs/OPDS/Requests/IOpdsRequest.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/IOpdsRequest.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; public interface IOpdsRequest { diff --git a/API/DTOs/OPDS/Requests/OpdsCatalogeRequest.cs b/Kavita.Models/DTOs/OPDS/Requests/OpdsCatalogeRequest.cs similarity index 74% rename from API/DTOs/OPDS/Requests/OpdsCatalogeRequest.cs rename to Kavita.Models/DTOs/OPDS/Requests/OpdsCatalogeRequest.cs index 5c0f9ffe9..890f4cf4c 100644 --- a/API/DTOs/OPDS/Requests/OpdsCatalogeRequest.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/OpdsCatalogeRequest.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; public sealed record OpdsCatalogueRequest : IOpdsRequest diff --git a/API/DTOs/OPDS/Requests/OpdsItemsFromCompoundEntityIdsRequest.cs b/Kavita.Models/DTOs/OPDS/Requests/OpdsItemsFromCompoundEntityIdsRequest.cs similarity index 87% rename from API/DTOs/OPDS/Requests/OpdsItemsFromCompoundEntityIdsRequest.cs rename to Kavita.Models/DTOs/OPDS/Requests/OpdsItemsFromCompoundEntityIdsRequest.cs index 3739bea4c..3434a7c22 100644 --- a/API/DTOs/OPDS/Requests/OpdsItemsFromCompoundEntityIdsRequest.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/OpdsItemsFromCompoundEntityIdsRequest.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; /// /// A special case for dealing with lower level entities (volume/chapter) which need higher level entity ids diff --git a/API/DTOs/OPDS/Requests/OpdsSearchRequest.cs b/Kavita.Models/DTOs/OPDS/Requests/OpdsSearchRequest.cs similarity index 76% rename from API/DTOs/OPDS/Requests/OpdsSearchRequest.cs rename to Kavita.Models/DTOs/OPDS/Requests/OpdsSearchRequest.cs index bcbe4d5bf..a59f141b7 100644 --- a/API/DTOs/OPDS/Requests/OpdsSearchRequest.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/OpdsSearchRequest.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; public sealed record OpdsSearchRequest : IOpdsRequest { diff --git a/API/DTOs/OPDS/Requests/OpdsSmartFilterCatalogueRequest.cs b/Kavita.Models/DTOs/OPDS/Requests/OpdsSmartFilterCatalogueRequest.cs similarity index 81% rename from API/DTOs/OPDS/Requests/OpdsSmartFilterCatalogueRequest.cs rename to Kavita.Models/DTOs/OPDS/Requests/OpdsSmartFilterCatalogueRequest.cs index f5e73a7bb..1e9848344 100644 --- a/API/DTOs/OPDS/Requests/OpdsSmartFilterCatalogueRequest.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/OpdsSmartFilterCatalogueRequest.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; /// /// A generic Catalogue request for a specific Entity diff --git a/API/DTOs/OPDS/Requests/OpdsSmartFilterRequest.cs b/Kavita.Models/DTOs/OPDS/Requests/OpdsSmartFilterRequest.cs similarity index 79% rename from API/DTOs/OPDS/Requests/OpdsSmartFilterRequest.cs rename to Kavita.Models/DTOs/OPDS/Requests/OpdsSmartFilterRequest.cs index cb33bbe9b..2aa9c3200 100644 --- a/API/DTOs/OPDS/Requests/OpdsSmartFilterRequest.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/OpdsSmartFilterRequest.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; public sealed record OpdsItemsFromEntityIdRequest : IOpdsRequest, IOpdsPagination { diff --git a/API/DTOs/Person/PersonAliasCheckDto.cs b/Kavita.Models/DTOs/Person/PersonAliasCheckDto.cs similarity index 94% rename from API/DTOs/Person/PersonAliasCheckDto.cs rename to Kavita.Models/DTOs/Person/PersonAliasCheckDto.cs index f0f09a7d4..0c4458c59 100644 --- a/API/DTOs/Person/PersonAliasCheckDto.cs +++ b/Kavita.Models/DTOs/Person/PersonAliasCheckDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record PersonAliasCheckDto { diff --git a/API/DTOs/Person/PersonDto.cs b/Kavita.Models/DTOs/Person/PersonDto.cs similarity index 95% rename from API/DTOs/Person/PersonDto.cs rename to Kavita.Models/DTOs/Person/PersonDto.cs index 2969561cc..807e694aa 100644 --- a/API/DTOs/Person/PersonDto.cs +++ b/Kavita.Models/DTOs/Person/PersonDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Person; +namespace Kavita.Models.DTOs.Person; #nullable enable public class PersonDto diff --git a/API/DTOs/Person/PersonMergeDto.cs b/Kavita.Models/DTOs/Person/PersonMergeDto.cs similarity index 93% rename from API/DTOs/Person/PersonMergeDto.cs rename to Kavita.Models/DTOs/Person/PersonMergeDto.cs index b5dc23375..fec3cfdbf 100644 --- a/API/DTOs/Person/PersonMergeDto.cs +++ b/Kavita.Models/DTOs/Person/PersonMergeDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record PersonMergeDto { diff --git a/API/DTOs/Person/UpdatePersonDto.cs b/Kavita.Models/DTOs/Person/UpdatePersonDto.cs similarity index 94% rename from API/DTOs/Person/UpdatePersonDto.cs rename to Kavita.Models/DTOs/Person/UpdatePersonDto.cs index b43a45e88..ce136c2d3 100644 --- a/API/DTOs/Person/UpdatePersonDto.cs +++ b/Kavita.Models/DTOs/Person/UpdatePersonDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record UpdatePersonDto diff --git a/API/DTOs/Progress/ClientDeviceDto.cs b/Kavita.Models/DTOs/Progress/ClientDeviceDto.cs similarity index 94% rename from API/DTOs/Progress/ClientDeviceDto.cs rename to Kavita.Models/DTOs/Progress/ClientDeviceDto.cs index cce9c056d..8b23c16da 100644 --- a/API/DTOs/Progress/ClientDeviceDto.cs +++ b/Kavita.Models/DTOs/Progress/ClientDeviceDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; public sealed record ClientDeviceDto { diff --git a/API/DTOs/Progress/ClientInfoDto.cs b/Kavita.Models/DTOs/Progress/ClientInfoDto.cs similarity index 94% rename from API/DTOs/Progress/ClientInfoDto.cs rename to Kavita.Models/DTOs/Progress/ClientInfoDto.cs index b354b034a..342e261b3 100644 --- a/API/DTOs/Progress/ClientInfoDto.cs +++ b/Kavita.Models/DTOs/Progress/ClientInfoDto.cs @@ -1,8 +1,7 @@ -using API.Constants; -using API.Entities.Enums; -using API.Entities.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; #nullable enable public sealed record ClientInfoDto diff --git a/API/DTOs/Progress/DailyReadingDataDto.cs b/Kavita.Models/DTOs/Progress/DailyReadingDataDto.cs similarity index 94% rename from API/DTOs/Progress/DailyReadingDataDto.cs rename to Kavita.Models/DTOs/Progress/DailyReadingDataDto.cs index d742503cd..a0df959e9 100644 --- a/API/DTOs/Progress/DailyReadingDataDto.cs +++ b/Kavita.Models/DTOs/Progress/DailyReadingDataDto.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Services; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; #nullable enable public class DailyReadingDataDto diff --git a/API/DTOs/Progress/FullProgressDto.cs b/Kavita.Models/DTOs/Progress/FullProgressDto.cs similarity index 93% rename from API/DTOs/Progress/FullProgressDto.cs rename to Kavita.Models/DTOs/Progress/FullProgressDto.cs index 4f97ab44a..d623b392e 100644 --- a/API/DTOs/Progress/FullProgressDto.cs +++ b/Kavita.Models/DTOs/Progress/FullProgressDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; /// /// A full progress Record from the DB (not all data, only what's needed for API) diff --git a/API/DTOs/Progress/ProgressDto.cs b/Kavita.Models/DTOs/Progress/ProgressDto.cs similarity index 93% rename from API/DTOs/Progress/ProgressDto.cs rename to Kavita.Models/DTOs/Progress/ProgressDto.cs index bf82fff35..78ac7b159 100644 --- a/API/DTOs/Progress/ProgressDto.cs +++ b/Kavita.Models/DTOs/Progress/ProgressDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; #nullable enable public sealed record ProgressDto diff --git a/API/DTOs/Progress/ReadingActivityDataDto.cs b/Kavita.Models/DTOs/Progress/ReadingActivityDataDto.cs similarity index 95% rename from API/DTOs/Progress/ReadingActivityDataDto.cs rename to Kavita.Models/DTOs/Progress/ReadingActivityDataDto.cs index fc5666a9c..dee47054c 100644 --- a/API/DTOs/Progress/ReadingActivityDataDto.cs +++ b/Kavita.Models/DTOs/Progress/ReadingActivityDataDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; #nullable enable public sealed record ReadingActivityDataDto diff --git a/API/DTOs/Progress/ReadingSessionDto.cs b/Kavita.Models/DTOs/Progress/ReadingSessionDto.cs similarity index 91% rename from API/DTOs/Progress/ReadingSessionDto.cs rename to Kavita.Models/DTOs/Progress/ReadingSessionDto.cs index ec39b1e07..9ef944e79 100644 --- a/API/DTOs/Progress/ReadingSessionDto.cs +++ b/Kavita.Models/DTOs/Progress/ReadingSessionDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; public sealed record ReadingSessionDto { diff --git a/API/DTOs/RatingDto.cs b/Kavita.Models/DTOs/RatingDto.cs similarity index 77% rename from API/DTOs/RatingDto.cs rename to Kavita.Models/DTOs/RatingDto.cs index c22e898c2..fe0bdcff8 100644 --- a/API/DTOs/RatingDto.cs +++ b/Kavita.Models/DTOs/RatingDto.cs @@ -1,8 +1,7 @@ -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record RatingDto diff --git a/API/DTOs/Reader/AnnotationDto.cs b/Kavita.Models/DTOs/Reader/AnnotationDto.cs similarity index 94% rename from API/DTOs/Reader/AnnotationDto.cs rename to Kavita.Models/DTOs/Reader/AnnotationDto.cs index 18cb563ea..33f0a0da0 100644 --- a/API/DTOs/Reader/AnnotationDto.cs +++ b/Kavita.Models/DTOs/Reader/AnnotationDto.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; /// /// Represents an annotation on a book diff --git a/API/DTOs/Reader/BookChapterItem.cs b/Kavita.Models/DTOs/Reader/BookChapterItem.cs similarity index 93% rename from API/DTOs/Reader/BookChapterItem.cs rename to Kavita.Models/DTOs/Reader/BookChapterItem.cs index 892e82e27..4523de198 100644 --- a/API/DTOs/Reader/BookChapterItem.cs +++ b/Kavita.Models/DTOs/Reader/BookChapterItem.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record BookChapterItem { diff --git a/API/DTOs/Reader/BookInfoDto.cs b/Kavita.Models/DTOs/Reader/BookInfoDto.cs similarity index 88% rename from API/DTOs/Reader/BookInfoDto.cs rename to Kavita.Models/DTOs/Reader/BookInfoDto.cs index 2473cd5dc..ca6584956 100644 --- a/API/DTOs/Reader/BookInfoDto.cs +++ b/Kavita.Models/DTOs/Reader/BookInfoDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record BookInfoDto : IChapterInfoDto { diff --git a/API/DTOs/Reader/BookResourceResultDto.cs b/Kavita.Models/DTOs/Reader/BookResourceResultDto.cs similarity index 93% rename from API/DTOs/Reader/BookResourceResultDto.cs rename to Kavita.Models/DTOs/Reader/BookResourceResultDto.cs index 9935341d9..7d89aa43b 100644 --- a/API/DTOs/Reader/BookResourceResultDto.cs +++ b/Kavita.Models/DTOs/Reader/BookResourceResultDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record BookResourceResultDto { diff --git a/API/DTOs/Reader/BookmarkDto.cs b/Kavita.Models/DTOs/Reader/BookmarkDto.cs similarity index 95% rename from API/DTOs/Reader/BookmarkDto.cs rename to Kavita.Models/DTOs/Reader/BookmarkDto.cs index b62271408..9537826da 100644 --- a/API/DTOs/Reader/BookmarkDto.cs +++ b/Kavita.Models/DTOs/Reader/BookmarkDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; #nullable enable public sealed record BookmarkDto diff --git a/API/DTOs/Reader/BookmarkInfoDto.cs b/Kavita.Models/DTOs/Reader/BookmarkInfoDto.cs similarity index 92% rename from API/DTOs/Reader/BookmarkInfoDto.cs rename to Kavita.Models/DTOs/Reader/BookmarkInfoDto.cs index c75c3d8bf..62fe20c94 100644 --- a/API/DTOs/Reader/BookmarkInfoDto.cs +++ b/Kavita.Models/DTOs/Reader/BookmarkInfoDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; #nullable enable public class BookmarkInfoDto diff --git a/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs b/Kavita.Models/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs similarity index 81% rename from API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs rename to Kavita.Models/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs index 51ccf5cc3..d862b2fc7 100644 --- a/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs +++ b/Kavita.Models/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record BulkRemoveBookmarkForSeriesDto { diff --git a/API/DTOs/Reader/ChapterInfoDto.cs b/Kavita.Models/DTOs/Reader/ChapterInfoDto.cs similarity index 97% rename from API/DTOs/Reader/ChapterInfoDto.cs rename to Kavita.Models/DTOs/Reader/ChapterInfoDto.cs index 4da08a31d..ca3a9602e 100644 --- a/API/DTOs/Reader/ChapterInfoDto.cs +++ b/Kavita.Models/DTOs/Reader/ChapterInfoDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; #nullable enable /// diff --git a/API/DTOs/Reader/CreatePersonalToCDto.cs b/Kavita.Models/DTOs/Reader/CreatePersonalToCDto.cs similarity index 91% rename from API/DTOs/Reader/CreatePersonalToCDto.cs rename to Kavita.Models/DTOs/Reader/CreatePersonalToCDto.cs index 545e17e47..ef66f86a1 100644 --- a/API/DTOs/Reader/CreatePersonalToCDto.cs +++ b/Kavita.Models/DTOs/Reader/CreatePersonalToCDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; #nullable enable public sealed record CreatePersonalToCDto diff --git a/API/DTOs/Reader/FileDimensionDto.cs b/Kavita.Models/DTOs/Reader/FileDimensionDto.cs similarity index 91% rename from API/DTOs/Reader/FileDimensionDto.cs rename to Kavita.Models/DTOs/Reader/FileDimensionDto.cs index 7a7d2978f..97bfd7692 100644 --- a/API/DTOs/Reader/FileDimensionDto.cs +++ b/Kavita.Models/DTOs/Reader/FileDimensionDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record FileDimensionDto { diff --git a/API/DTOs/Reader/HourEstimateRangeDto.cs b/Kavita.Models/DTOs/Reader/HourEstimateRangeDto.cs similarity index 92% rename from API/DTOs/Reader/HourEstimateRangeDto.cs rename to Kavita.Models/DTOs/Reader/HourEstimateRangeDto.cs index 3facf8e56..c2baeb25d 100644 --- a/API/DTOs/Reader/HourEstimateRangeDto.cs +++ b/Kavita.Models/DTOs/Reader/HourEstimateRangeDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; /// /// A range of time to read a selection (series, chapter, etc) diff --git a/API/DTOs/Reader/IChapterInfoDto.cs b/Kavita.Models/DTOs/Reader/IChapterInfoDto.cs similarity index 85% rename from API/DTOs/Reader/IChapterInfoDto.cs rename to Kavita.Models/DTOs/Reader/IChapterInfoDto.cs index 6a9a74a2c..ad3523070 100644 --- a/API/DTOs/Reader/IChapterInfoDto.cs +++ b/Kavita.Models/DTOs/Reader/IChapterInfoDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public interface IChapterInfoDto { diff --git a/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs b/Kavita.Models/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs similarity index 81% rename from API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs rename to Kavita.Models/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs index 4c39f7d76..70fe4c685 100644 --- a/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs +++ b/Kavita.Models/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record MarkMultipleSeriesAsReadDto { diff --git a/API/DTOs/Reader/MarkReadDto.cs b/Kavita.Models/DTOs/Reader/MarkReadDto.cs similarity index 65% rename from API/DTOs/Reader/MarkReadDto.cs rename to Kavita.Models/DTOs/Reader/MarkReadDto.cs index c6f7367c0..e0750bcd2 100644 --- a/API/DTOs/Reader/MarkReadDto.cs +++ b/Kavita.Models/DTOs/Reader/MarkReadDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record MarkReadDto { diff --git a/API/DTOs/Reader/MarkVolumeReadDto.cs b/Kavita.Models/DTOs/Reader/MarkVolumeReadDto.cs similarity index 75% rename from API/DTOs/Reader/MarkVolumeReadDto.cs rename to Kavita.Models/DTOs/Reader/MarkVolumeReadDto.cs index be95d2e98..00f05ddcd 100644 --- a/API/DTOs/Reader/MarkVolumeReadDto.cs +++ b/Kavita.Models/DTOs/Reader/MarkVolumeReadDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record MarkVolumeReadDto { diff --git a/API/DTOs/Reader/MarkVolumesReadDto.cs b/Kavita.Models/DTOs/Reader/MarkVolumesReadDto.cs similarity index 93% rename from API/DTOs/Reader/MarkVolumesReadDto.cs rename to Kavita.Models/DTOs/Reader/MarkVolumesReadDto.cs index b07bfbc67..b721c72f3 100644 --- a/API/DTOs/Reader/MarkVolumesReadDto.cs +++ b/Kavita.Models/DTOs/Reader/MarkVolumesReadDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; /// /// This is used for bulk updating a set of volume and or chapters in one go diff --git a/API/DTOs/Reader/PersonalToCDto.cs b/Kavita.Models/DTOs/Reader/PersonalToCDto.cs similarity index 96% rename from API/DTOs/Reader/PersonalToCDto.cs rename to Kavita.Models/DTOs/Reader/PersonalToCDto.cs index 66994a7ff..c6ab2b032 100644 --- a/API/DTOs/Reader/PersonalToCDto.cs +++ b/Kavita.Models/DTOs/Reader/PersonalToCDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; #nullable enable diff --git a/API/DTOs/Reader/ReReadDto.cs b/Kavita.Models/DTOs/Reader/ReReadDto.cs similarity index 94% rename from API/DTOs/Reader/ReReadDto.cs rename to Kavita.Models/DTOs/Reader/ReReadDto.cs index 97e746a9c..2943fabc1 100644 --- a/API/DTOs/Reader/ReReadDto.cs +++ b/Kavita.Models/DTOs/Reader/ReReadDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record RereadDto { diff --git a/API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs b/Kavita.Models/DTOs/Reader/RemoveBookmarkForSeriesDto.cs similarity index 69% rename from API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs rename to Kavita.Models/DTOs/Reader/RemoveBookmarkForSeriesDto.cs index ecbb744c8..1a7c2f4dd 100644 --- a/API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs +++ b/Kavita.Models/DTOs/Reader/RemoveBookmarkForSeriesDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record RemoveBookmarkForSeriesDto { diff --git a/API/DTOs/ReadingLists/CBL/CblBook.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblBook.cs similarity index 94% rename from API/DTOs/ReadingLists/CBL/CblBook.cs rename to Kavita.Models/DTOs/ReadingLists/CBL/CblBook.cs index d51795b8d..177423371 100644 --- a/API/DTOs/ReadingLists/CBL/CblBook.cs +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblBook.cs @@ -1,7 +1,6 @@ using System.Xml.Serialization; -using API.Data.Metadata; -namespace API.DTOs.ReadingLists.CBL; +namespace Kavita.Models.DTOs.ReadingLists.CBL; [XmlRoot(ElementName="Book")] diff --git a/API/DTOs/ReadingLists/CBL/CblConflictsDto.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblConflictsDto.cs similarity index 79% rename from API/DTOs/ReadingLists/CBL/CblConflictsDto.cs rename to Kavita.Models/DTOs/ReadingLists/CBL/CblConflictsDto.cs index 35234923f..a90b52988 100644 --- a/API/DTOs/ReadingLists/CBL/CblConflictsDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblConflictsDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.ReadingLists.CBL; +namespace Kavita.Models.DTOs.ReadingLists.CBL; public sealed record CblConflictQuestion diff --git a/API/DTOs/ReadingLists/CBL/CblImportSummary.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblImportSummary.cs similarity index 98% rename from API/DTOs/ReadingLists/CBL/CblImportSummary.cs rename to Kavita.Models/DTOs/ReadingLists/CBL/CblImportSummary.cs index b9716421e..3315837c1 100644 --- a/API/DTOs/ReadingLists/CBL/CblImportSummary.cs +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblImportSummary.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel; -namespace API.DTOs.ReadingLists.CBL; +namespace Kavita.Models.DTOs.ReadingLists.CBL; public enum CblImportResult { /// diff --git a/API/DTOs/ReadingLists/CBL/CblReadingList.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblReadingList.cs similarity index 97% rename from API/DTOs/ReadingLists/CBL/CblReadingList.cs rename to Kavita.Models/DTOs/ReadingLists/CBL/CblReadingList.cs index 15b349f42..aa368f09d 100644 --- a/API/DTOs/ReadingLists/CBL/CblReadingList.cs +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblReadingList.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace API.DTOs.ReadingLists.CBL; +namespace Kavita.Models.DTOs.ReadingLists.CBL; [XmlRoot(ElementName="Books")] diff --git a/API/DTOs/ReadingLists/CreateReadingListDto.cs b/Kavita.Models/DTOs/ReadingLists/CreateReadingListDto.cs similarity index 68% rename from API/DTOs/ReadingLists/CreateReadingListDto.cs rename to Kavita.Models/DTOs/ReadingLists/CreateReadingListDto.cs index 543215722..35de4fbda 100644 --- a/API/DTOs/ReadingLists/CreateReadingListDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/CreateReadingListDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record CreateReadingListDto { diff --git a/API/DTOs/ReadingLists/DeleteReadingListsDto.cs b/Kavita.Models/DTOs/ReadingLists/DeleteReadingListsDto.cs similarity index 82% rename from API/DTOs/ReadingLists/DeleteReadingListsDto.cs rename to Kavita.Models/DTOs/ReadingLists/DeleteReadingListsDto.cs index 8ce92f939..8feb44955 100644 --- a/API/DTOs/ReadingLists/DeleteReadingListsDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/DeleteReadingListsDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record DeleteReadingListsDto { diff --git a/API/DTOs/ReadingLists/PromoteReadingListsDto.cs b/Kavita.Models/DTOs/ReadingLists/PromoteReadingListsDto.cs similarity index 80% rename from API/DTOs/ReadingLists/PromoteReadingListsDto.cs rename to Kavita.Models/DTOs/ReadingLists/PromoteReadingListsDto.cs index 8915274de..6f3a6c729 100644 --- a/API/DTOs/ReadingLists/PromoteReadingListsDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/PromoteReadingListsDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record PromoteReadingListsDto { diff --git a/API/DTOs/ReadingLists/ReadingListCast.cs b/Kavita.Models/DTOs/ReadingLists/ReadingListCast.cs similarity index 92% rename from API/DTOs/ReadingLists/ReadingListCast.cs rename to Kavita.Models/DTOs/ReadingLists/ReadingListCast.cs index 855bb12b7..dc8533e78 100644 --- a/API/DTOs/ReadingLists/ReadingListCast.cs +++ b/Kavita.Models/DTOs/ReadingLists/ReadingListCast.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.DTOs.Person; +using Kavita.Models.DTOs.Person; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record ReadingListCast { diff --git a/API/DTOs/ReadingLists/ReadingListDto.cs b/Kavita.Models/DTOs/ReadingLists/ReadingListDto.cs similarity index 93% rename from API/DTOs/ReadingLists/ReadingListDto.cs rename to Kavita.Models/DTOs/ReadingLists/ReadingListDto.cs index b1296f87a..d5a274399 100644 --- a/API/DTOs/ReadingLists/ReadingListDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/ReadingListDto.cs @@ -1,7 +1,7 @@ -using API.Entities.Enums; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; #nullable enable public sealed record ReadingListDto : IHasCoverImage diff --git a/API/DTOs/ReadingLists/ReadingListInfoDto.cs b/Kavita.Models/DTOs/ReadingLists/ReadingListInfoDto.cs similarity index 89% rename from API/DTOs/ReadingLists/ReadingListInfoDto.cs rename to Kavita.Models/DTOs/ReadingLists/ReadingListInfoDto.cs index b1655f850..ef3875e12 100644 --- a/API/DTOs/ReadingLists/ReadingListInfoDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/ReadingListInfoDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record ReadingListInfoDto : IHasReadTimeEstimate { diff --git a/API/DTOs/ReadingLists/ReadingListItemDto.cs b/Kavita.Models/DTOs/ReadingLists/ReadingListItemDto.cs similarity index 95% rename from API/DTOs/ReadingLists/ReadingListItemDto.cs rename to Kavita.Models/DTOs/ReadingLists/ReadingListItemDto.cs index de3216a51..ed347579f 100644 --- a/API/DTOs/ReadingLists/ReadingListItemDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/ReadingListItemDto.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; #nullable enable public sealed record ReadingListItemDto diff --git a/API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs similarity index 79% rename from API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs index 6624c8a5c..a762c3d33 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record UpdateReadingListByChapterDto { diff --git a/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs similarity index 87% rename from API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs index ba7625088..5145026fe 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record UpdateReadingListByMultipleDto { diff --git a/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs similarity index 83% rename from API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs index 910a5744d..4a3ec3007 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record UpdateReadingListByMultipleSeriesDto { diff --git a/API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs similarity index 75% rename from API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs index 4bb4aa7bb..35a1969a3 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record UpdateReadingListBySeriesDto { diff --git a/API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs similarity index 79% rename from API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs index 422d1cc34..d43fddf80 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record UpdateReadingListByVolumeDto { diff --git a/API/DTOs/ReadingLists/UpdateReadingListDto.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListDto.cs similarity index 92% rename from API/DTOs/ReadingLists/UpdateReadingListDto.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListDto.cs index de273d825..fbc4f469c 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record UpdateReadingListDto { diff --git a/API/DTOs/ReadingLists/UpdateReadingListPosition.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListPosition.cs similarity index 90% rename from API/DTOs/ReadingLists/UpdateReadingListPosition.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListPosition.cs index 04f2501a8..e2fc14825 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListPosition.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListPosition.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; /// /// DTO for moving a reading list item to another position within the same list diff --git a/API/DTOs/Recommendation/ExternalSeriesDto.cs b/Kavita.Models/DTOs/Recommendation/ExternalSeriesDto.cs similarity index 82% rename from API/DTOs/Recommendation/ExternalSeriesDto.cs rename to Kavita.Models/DTOs/Recommendation/ExternalSeriesDto.cs index 752001a39..30f158a5c 100644 --- a/API/DTOs/Recommendation/ExternalSeriesDto.cs +++ b/Kavita.Models/DTOs/Recommendation/ExternalSeriesDto.cs @@ -1,6 +1,8 @@ -using API.Services.Plus; + -namespace API.DTOs.Recommendation; +using Kavita.Models.Entities.Enums; + +namespace Kavita.Models.DTOs.Recommendation; #nullable enable public sealed record ExternalSeriesDto diff --git a/API/DTOs/Recommendation/MetadataTagDto.cs b/Kavita.Models/DTOs/Recommendation/MetadataTagDto.cs similarity index 87% rename from API/DTOs/Recommendation/MetadataTagDto.cs rename to Kavita.Models/DTOs/Recommendation/MetadataTagDto.cs index a7eb76284..203854209 100644 --- a/API/DTOs/Recommendation/MetadataTagDto.cs +++ b/Kavita.Models/DTOs/Recommendation/MetadataTagDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Recommendation; +namespace Kavita.Models.DTOs.Recommendation; public sealed record MetadataTagDto { diff --git a/API/DTOs/Recommendation/RecommendationDto.cs b/Kavita.Models/DTOs/Recommendation/RecommendationDto.cs similarity index 85% rename from API/DTOs/Recommendation/RecommendationDto.cs rename to Kavita.Models/DTOs/Recommendation/RecommendationDto.cs index 387661324..e11fa894f 100644 --- a/API/DTOs/Recommendation/RecommendationDto.cs +++ b/Kavita.Models/DTOs/Recommendation/RecommendationDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Recommendation; +namespace Kavita.Models.DTOs.Recommendation; public sealed record RecommendationDto { diff --git a/API/DTOs/Recommendation/SeriesStaffDto.cs b/Kavita.Models/DTOs/Recommendation/SeriesStaffDto.cs similarity index 89% rename from API/DTOs/Recommendation/SeriesStaffDto.cs rename to Kavita.Models/DTOs/Recommendation/SeriesStaffDto.cs index e074e8625..6b3bbd032 100644 --- a/API/DTOs/Recommendation/SeriesStaffDto.cs +++ b/Kavita.Models/DTOs/Recommendation/SeriesStaffDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Recommendation; +namespace Kavita.Models.DTOs.Recommendation; #nullable enable public sealed record SeriesStaffDto diff --git a/API/DTOs/RefreshSeriesDto.cs b/Kavita.Models/DTOs/RefreshSeriesDto.cs similarity index 95% rename from API/DTOs/RefreshSeriesDto.cs rename to Kavita.Models/DTOs/RefreshSeriesDto.cs index ad26afba2..cf4afd260 100644 --- a/API/DTOs/RefreshSeriesDto.cs +++ b/Kavita.Models/DTOs/RefreshSeriesDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; /// /// Used for running some task against a Series. diff --git a/API/DTOs/RegisterDto.cs b/Kavita.Models/DTOs/RegisterDto.cs similarity index 93% rename from API/DTOs/RegisterDto.cs rename to Kavita.Models/DTOs/RegisterDto.cs index e117af872..64d626c54 100644 --- a/API/DTOs/RegisterDto.cs +++ b/Kavita.Models/DTOs/RegisterDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record RegisterDto diff --git a/API/DTOs/ScanFolderDto.cs b/Kavita.Models/DTOs/ScanFolderDto.cs similarity index 95% rename from API/DTOs/ScanFolderDto.cs rename to Kavita.Models/DTOs/ScanFolderDto.cs index bfa669eec..7416d6ccb 100644 --- a/API/DTOs/ScanFolderDto.cs +++ b/Kavita.Models/DTOs/ScanFolderDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; /// /// DTO for requesting a folder to be scanned diff --git a/API/DTOs/Scrobbling/MalUserInfoDto.cs b/Kavita.Models/DTOs/Scrobbling/MalUserInfoDto.cs similarity index 87% rename from API/DTOs/Scrobbling/MalUserInfoDto.cs rename to Kavita.Models/DTOs/Scrobbling/MalUserInfoDto.cs index b6fefc053..1e32d1b13 100644 --- a/API/DTOs/Scrobbling/MalUserInfoDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/MalUserInfoDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; /// /// Information about a User's MAL connection diff --git a/API/DTOs/Scrobbling/MediaRecommendationDto.cs b/Kavita.Models/DTOs/Scrobbling/MediaRecommendationDto.cs similarity index 86% rename from API/DTOs/Scrobbling/MediaRecommendationDto.cs rename to Kavita.Models/DTOs/Scrobbling/MediaRecommendationDto.cs index 476d77279..8c4041d09 100644 --- a/API/DTOs/Scrobbling/MediaRecommendationDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/MediaRecommendationDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; #nullable enable public sealed record MediaRecommendationDto diff --git a/API/DTOs/Scrobbling/PlusSeriesDto.cs b/Kavita.Models/DTOs/Scrobbling/PlusSeriesDto.cs similarity index 95% rename from API/DTOs/Scrobbling/PlusSeriesDto.cs rename to Kavita.Models/DTOs/Scrobbling/PlusSeriesDto.cs index e089ded72..c6be75cf1 100644 --- a/API/DTOs/Scrobbling/PlusSeriesDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/PlusSeriesDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; #nullable enable /// diff --git a/API/DTOs/Scrobbling/ScrobbleDto.cs b/Kavita.Models/DTOs/Scrobbling/ScrobbleDto.cs similarity index 98% rename from API/DTOs/Scrobbling/ScrobbleDto.cs rename to Kavita.Models/DTOs/Scrobbling/ScrobbleDto.cs index 7c440b61c..d937e34ee 100644 --- a/API/DTOs/Scrobbling/ScrobbleDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/ScrobbleDto.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel; -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; #nullable enable public enum ScrobbleEventType diff --git a/API/DTOs/Scrobbling/ScrobbleErrorDto.cs b/Kavita.Models/DTOs/Scrobbling/ScrobbleErrorDto.cs similarity index 90% rename from API/DTOs/Scrobbling/ScrobbleErrorDto.cs rename to Kavita.Models/DTOs/Scrobbling/ScrobbleErrorDto.cs index 7caaad1ca..31ba071f0 100644 --- a/API/DTOs/Scrobbling/ScrobbleErrorDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/ScrobbleErrorDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; public sealed record ScrobbleErrorDto { diff --git a/API/DTOs/Scrobbling/ScrobbleEventDto.cs b/Kavita.Models/DTOs/Scrobbling/ScrobbleEventDto.cs similarity index 94% rename from API/DTOs/Scrobbling/ScrobbleEventDto.cs rename to Kavita.Models/DTOs/Scrobbling/ScrobbleEventDto.cs index 562d923ff..42cfcc39c 100644 --- a/API/DTOs/Scrobbling/ScrobbleEventDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/ScrobbleEventDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; #nullable enable public sealed record ScrobbleEventDto diff --git a/API/DTOs/Scrobbling/ScrobbleHoldDto.cs b/Kavita.Models/DTOs/Scrobbling/ScrobbleHoldDto.cs similarity index 86% rename from API/DTOs/Scrobbling/ScrobbleHoldDto.cs rename to Kavita.Models/DTOs/Scrobbling/ScrobbleHoldDto.cs index 3e09e4799..77cd1b151 100644 --- a/API/DTOs/Scrobbling/ScrobbleHoldDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/ScrobbleHoldDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; public sealed record ScrobbleHoldDto { diff --git a/API/DTOs/Scrobbling/ScrobbleResponseDto.cs b/Kavita.Models/DTOs/Scrobbling/ScrobbleResponseDto.cs similarity index 87% rename from API/DTOs/Scrobbling/ScrobbleResponseDto.cs rename to Kavita.Models/DTOs/Scrobbling/ScrobbleResponseDto.cs index ad66729d0..8920b71e7 100644 --- a/API/DTOs/Scrobbling/ScrobbleResponseDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/ScrobbleResponseDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; #nullable enable /// diff --git a/API/DTOs/Search/BookmarkSearchResultDto.cs b/Kavita.Models/DTOs/Search/BookmarkSearchResultDto.cs similarity index 88% rename from API/DTOs/Search/BookmarkSearchResultDto.cs rename to Kavita.Models/DTOs/Search/BookmarkSearchResultDto.cs index c11d2a2b8..4c94547b3 100644 --- a/API/DTOs/Search/BookmarkSearchResultDto.cs +++ b/Kavita.Models/DTOs/Search/BookmarkSearchResultDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Search; +namespace Kavita.Models.DTOs.Search; public sealed record BookmarkSearchResultDto { diff --git a/API/DTOs/Search/SearchResultDto.cs b/Kavita.Models/DTOs/Search/SearchResultDto.cs similarity index 86% rename from API/DTOs/Search/SearchResultDto.cs rename to Kavita.Models/DTOs/Search/SearchResultDto.cs index c497b55dd..40837fa33 100644 --- a/API/DTOs/Search/SearchResultDto.cs +++ b/Kavita.Models/DTOs/Search/SearchResultDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Search; +namespace Kavita.Models.DTOs.Search; public sealed record SearchResultDto { diff --git a/API/DTOs/Search/SearchResultGroupDto.cs b/Kavita.Models/DTOs/Search/SearchResultGroupDto.cs similarity index 81% rename from API/DTOs/Search/SearchResultGroupDto.cs rename to Kavita.Models/DTOs/Search/SearchResultGroupDto.cs index e3edcaf29..cd0635ee7 100644 --- a/API/DTOs/Search/SearchResultGroupDto.cs +++ b/Kavita.Models/DTOs/Search/SearchResultGroupDto.cs @@ -1,11 +1,11 @@ using System.Collections.Generic; -using API.DTOs.Collection; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.Reader; -using API.DTOs.ReadingLists; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.ReadingLists; -namespace API.DTOs.Search; +namespace Kavita.Models.DTOs.Search; /// /// Represents all Search results for a query diff --git a/API/DTOs/SeriesByIdsDto.cs b/Kavita.Models/DTOs/SeriesByIdsDto.cs similarity index 74% rename from API/DTOs/SeriesByIdsDto.cs rename to Kavita.Models/DTOs/SeriesByIdsDto.cs index cb4c52b1e..971bbc517 100644 --- a/API/DTOs/SeriesByIdsDto.cs +++ b/Kavita.Models/DTOs/SeriesByIdsDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record SeriesByIdsDto { diff --git a/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs b/Kavita.Models/DTOs/SeriesDetail/NextExpectedChapterDto.cs similarity index 90% rename from API/DTOs/SeriesDetail/NextExpectedChapterDto.cs rename to Kavita.Models/DTOs/SeriesDetail/NextExpectedChapterDto.cs index 1bea81c84..41492b100 100644 --- a/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/NextExpectedChapterDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; public sealed record NextExpectedChapterDto { diff --git a/API/Data/Misc/RecentlyAddedSeries.cs b/Kavita.Models/DTOs/SeriesDetail/RecentlyAddedSeriesDto.cs similarity index 83% rename from API/Data/Misc/RecentlyAddedSeries.cs rename to Kavita.Models/DTOs/SeriesDetail/RecentlyAddedSeriesDto.cs index 1ea5b1d3e..c00897dbf 100644 --- a/API/Data/Misc/RecentlyAddedSeries.cs +++ b/Kavita.Models/DTOs/SeriesDetail/RecentlyAddedSeriesDto.cs @@ -1,10 +1,10 @@ -using System; -using API.Entities.Enums; - -namespace API.Data.Misc; #nullable enable +using System; +using Kavita.Models.Entities.Enums; -public class RecentlyAddedSeries +namespace Kavita.Models.DTOs.SeriesDetail; + +public class RecentlyAddedSeriesDto { public int LibraryId { get; init; } public LibraryType LibraryType { get; init; } diff --git a/API/DTOs/SeriesDetail/RelatedSeriesDto.cs b/Kavita.Models/DTOs/SeriesDetail/RelatedSeriesDto.cs similarity index 96% rename from API/DTOs/SeriesDetail/RelatedSeriesDto.cs rename to Kavita.Models/DTOs/SeriesDetail/RelatedSeriesDto.cs index a186dc295..7f34ca071 100644 --- a/API/DTOs/SeriesDetail/RelatedSeriesDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/RelatedSeriesDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; public sealed record RelatedSeriesDto { diff --git a/API/DTOs/SeriesDetail/SeriesDetailDto.cs b/Kavita.Models/DTOs/SeriesDetail/SeriesDetailDto.cs similarity index 96% rename from API/DTOs/SeriesDetail/SeriesDetailDto.cs rename to Kavita.Models/DTOs/SeriesDetail/SeriesDetailDto.cs index c4f15552d..43474a5f8 100644 --- a/API/DTOs/SeriesDetail/SeriesDetailDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/SeriesDetailDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; #nullable enable /// diff --git a/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs b/Kavita.Models/DTOs/SeriesDetail/SeriesDetailPlusDto.cs similarity index 78% rename from API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs rename to Kavita.Models/DTOs/SeriesDetail/SeriesDetailPlusDto.cs index 95f5f39bd..2e3952b2d 100644 --- a/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/SeriesDetailPlusDto.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Recommendation; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Recommendation; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; #nullable enable /// diff --git a/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs b/Kavita.Models/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs similarity index 95% rename from API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs rename to Kavita.Models/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs index a1bb2057e..1cd28ea10 100644 --- a/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; public sealed record UpdateRelatedSeriesDto { diff --git a/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs b/Kavita.Models/DTOs/SeriesDetail/UpdateUserReviewDto.cs similarity index 80% rename from API/DTOs/SeriesDetail/UpdateUserReviewDto.cs rename to Kavita.Models/DTOs/SeriesDetail/UpdateUserReviewDto.cs index 7af9441c1..fa679965d 100644 --- a/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/UpdateUserReviewDto.cs @@ -1,5 +1,5 @@  -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; #nullable enable public sealed record UpdateUserReviewDto diff --git a/API/DTOs/SeriesDetail/UserReviewDto.cs b/Kavita.Models/DTOs/SeriesDetail/UserReviewDto.cs similarity index 95% rename from API/DTOs/SeriesDetail/UserReviewDto.cs rename to Kavita.Models/DTOs/SeriesDetail/UserReviewDto.cs index 8d695d7e6..982868da5 100644 --- a/API/DTOs/SeriesDetail/UserReviewDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/UserReviewDto.cs @@ -1,7 +1,6 @@ -using API.Entities.Enums; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; #nullable enable /// diff --git a/API/DTOs/SeriesDetail/UserReviewExtendedDto.cs b/Kavita.Models/DTOs/SeriesDetail/UserReviewExtendedDto.cs similarity index 91% rename from API/DTOs/SeriesDetail/UserReviewExtendedDto.cs rename to Kavita.Models/DTOs/SeriesDetail/UserReviewExtendedDto.cs index 5a656e25e..6ecbcd673 100644 --- a/API/DTOs/SeriesDetail/UserReviewExtendedDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/UserReviewExtendedDto.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using API.DTOs.Person; +using Kavita.Models.DTOs.Person; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; #nullable enable diff --git a/API/DTOs/SeriesDto.cs b/Kavita.Models/DTOs/SeriesDto.cs similarity index 97% rename from API/DTOs/SeriesDto.cs rename to Kavita.Models/DTOs/SeriesDto.cs index 03f0d04b3..4a3edd417 100644 --- a/API/DTOs/SeriesDto.cs +++ b/Kavita.Models/DTOs/SeriesDto.cs @@ -1,8 +1,8 @@ using System; -using API.Entities.Enums; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record SeriesDto : IHasReadTimeEstimate, IHasCoverImage diff --git a/API/DTOs/SeriesMetadataDto.cs b/Kavita.Models/DTOs/SeriesMetadataDto.cs similarity index 96% rename from API/DTOs/SeriesMetadataDto.cs rename to Kavita.Models/DTOs/SeriesMetadataDto.cs index 562335da1..75f646b36 100644 --- a/API/DTOs/SeriesMetadataDto.cs +++ b/Kavita.Models/DTOs/SeriesMetadataDto.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.Entities.Enums; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.Entities.Enums; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record SeriesMetadataDto diff --git a/API/DTOs/Settings/AuthorityValidationDto.cs b/Kavita.Models/DTOs/Settings/AuthorityValidationDto.cs similarity index 60% rename from API/DTOs/Settings/AuthorityValidationDto.cs rename to Kavita.Models/DTOs/Settings/AuthorityValidationDto.cs index e7ea2ae18..3414eea7b 100644 --- a/API/DTOs/Settings/AuthorityValidationDto.cs +++ b/Kavita.Models/DTOs/Settings/AuthorityValidationDto.cs @@ -1,3 +1,3 @@ -namespace API.DTOs.Settings; +namespace Kavita.Models.DTOs.Settings; public sealed record AuthorityValidationDto(string Authority); diff --git a/API/DTOs/Settings/ImportFieldMappingsDto.cs b/Kavita.Models/DTOs/Settings/ImportFieldMappingsDto.cs similarity index 75% rename from API/DTOs/Settings/ImportFieldMappingsDto.cs rename to Kavita.Models/DTOs/Settings/ImportFieldMappingsDto.cs index 2699292b2..b7bb4f8e9 100644 --- a/API/DTOs/Settings/ImportFieldMappingsDto.cs +++ b/Kavita.Models/DTOs/Settings/ImportFieldMappingsDto.cs @@ -1,6 +1,6 @@ -using API.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.KavitaPlus.Metadata; -namespace API.DTOs.Settings; +namespace Kavita.Models.DTOs.Settings; public sealed record ImportFieldMappingsDto { diff --git a/API/DTOs/Settings/OidcConfigDto.cs b/Kavita.Models/DTOs/Settings/OidcConfigDto.cs similarity index 97% rename from API/DTOs/Settings/OidcConfigDto.cs rename to Kavita.Models/DTOs/Settings/OidcConfigDto.cs index db065b5f0..dc4be353e 100644 --- a/API/DTOs/Settings/OidcConfigDto.cs +++ b/Kavita.Models/DTOs/Settings/OidcConfigDto.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Security.Claims; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Settings; +namespace Kavita.Models.DTOs.Settings; /// /// All configuration regarding OIDC diff --git a/API/DTOs/Settings/OidcPublicConfigDto.cs b/Kavita.Models/DTOs/Settings/OidcPublicConfigDto.cs similarity index 94% rename from API/DTOs/Settings/OidcPublicConfigDto.cs rename to Kavita.Models/DTOs/Settings/OidcPublicConfigDto.cs index 6843adcca..4d94d60a0 100644 --- a/API/DTOs/Settings/OidcPublicConfigDto.cs +++ b/Kavita.Models/DTOs/Settings/OidcPublicConfigDto.cs @@ -1,6 +1,6 @@ #nullable enable -namespace API.DTOs.Settings; +namespace Kavita.Models.DTOs.Settings; /** * The part of the OIDC configuration that is returned by the API without authentication diff --git a/API/DTOs/Settings/SMTPConfigDto.cs b/Kavita.Models/DTOs/Settings/SMTPConfigDto.cs similarity index 94% rename from API/DTOs/Settings/SMTPConfigDto.cs rename to Kavita.Models/DTOs/Settings/SMTPConfigDto.cs index c14140062..065e91ba0 100644 --- a/API/DTOs/Settings/SMTPConfigDto.cs +++ b/Kavita.Models/DTOs/Settings/SMTPConfigDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Settings; +namespace Kavita.Models.DTOs.Settings; public sealed record SmtpConfigDto { diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/Kavita.Models/DTOs/Settings/ServerSettingDTO.cs similarity index 98% rename from API/DTOs/Settings/ServerSettingDTO.cs rename to Kavita.Models/DTOs/Settings/ServerSettingDTO.cs index dbc894412..b00b241f3 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/Kavita.Models/DTOs/Settings/ServerSettingDTO.cs @@ -1,8 +1,7 @@ using System; -using API.Entities.Enums; -using API.Services; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Settings; +namespace Kavita.Models.DTOs.Settings; #nullable enable public sealed record ServerSettingDto diff --git a/API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs b/Kavita.Models/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs similarity index 84% rename from API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs rename to Kavita.Models/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs index ae1d927a9..a84c5bdb5 100644 --- a/API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs +++ b/Kavita.Models/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.SideNav; +namespace Kavita.Models.DTOs.SideNav; public sealed record BulkUpdateSideNavStreamVisibilityDto { diff --git a/API/DTOs/SideNav/ExternalSourceDto.cs b/Kavita.Models/DTOs/SideNav/ExternalSourceDto.cs similarity index 84% rename from API/DTOs/SideNav/ExternalSourceDto.cs rename to Kavita.Models/DTOs/SideNav/ExternalSourceDto.cs index ef79f1d89..618fd0838 100644 --- a/API/DTOs/SideNav/ExternalSourceDto.cs +++ b/Kavita.Models/DTOs/SideNav/ExternalSourceDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.SideNav; +namespace Kavita.Models.DTOs.SideNav; public sealed record ExternalSourceDto { diff --git a/API/DTOs/SideNav/SideNavStreamDto.cs b/Kavita.Models/DTOs/SideNav/SideNavStreamDto.cs similarity index 93% rename from API/DTOs/SideNav/SideNavStreamDto.cs rename to Kavita.Models/DTOs/SideNav/SideNavStreamDto.cs index ec6cdf5a7..11ccd7fd0 100644 --- a/API/DTOs/SideNav/SideNavStreamDto.cs +++ b/Kavita.Models/DTOs/SideNav/SideNavStreamDto.cs @@ -1,6 +1,6 @@ -using API.Entities; +using Kavita.Models.Entities; -namespace API.DTOs.SideNav; +namespace Kavita.Models.DTOs.SideNav; #nullable enable public sealed record SideNavStreamDto diff --git a/API/SignalR/MessageFactory.cs b/Kavita.Models/DTOs/SignalR/MessageFactory.cs similarity index 97% rename from API/SignalR/MessageFactory.cs rename to Kavita.Models/DTOs/SignalR/MessageFactory.cs index bb0e7c776..7597a3346 100644 --- a/API/SignalR/MessageFactory.cs +++ b/Kavita.Models/DTOs/SignalR/MessageFactory.cs @@ -1,12 +1,11 @@ using System; -using API.DTOs.Account; -using API.DTOs.Reader; -using API.DTOs.Update; -using API.Entities.Person; -using API.Extensions; -using API.Services.Plus; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.Update; +using Kavita.Models.Entities.Enums; -namespace API.SignalR; +namespace Kavita.Models.DTOs.SignalR; public static class MessageFactoryEntityTypes { @@ -148,6 +147,10 @@ public static class MessageFactory /// public const string ChapterRemoved = "ChapterRemoved"; /// + /// Chapter is updated + /// + public const string ChapterUpdated = "ChapterUpdated"; + /// /// Volume is removed from server /// public const string VolumeRemoved = "VolumeRemoved"; @@ -269,6 +272,19 @@ public static class MessageFactory }; } + public static SignalRMessage ChapterUpdatedEvent(int chapterId, int seriesId) + { + return new SignalRMessage + { + Name = ChapterUpdated, + Body = new + { + SeriesId = seriesId, + ChapterId = chapterId + } + }; + } + public static SignalRMessage VolumeRemovedEvent(int volumeId, int seriesId) { return new SignalRMessage() @@ -703,7 +719,7 @@ public static class MessageFactory }; } - public static SignalRMessage PersonMergedMessage(Person dst, Person src) + public static SignalRMessage PersonMergedMessage(Entities.Person.Person dst, Entities.Person.Person src) { return new SignalRMessage() { diff --git a/API/SignalR/ProgressEventType.cs b/Kavita.Models/DTOs/SignalR/ProgressEventType.cs similarity index 89% rename from API/SignalR/ProgressEventType.cs rename to Kavita.Models/DTOs/SignalR/ProgressEventType.cs index 89ba758c5..34cc9ebc4 100644 --- a/API/SignalR/ProgressEventType.cs +++ b/Kavita.Models/DTOs/SignalR/ProgressEventType.cs @@ -1,4 +1,4 @@ -namespace API.SignalR; +namespace Kavita.Models.DTOs.SignalR; public static class ProgressEventType { diff --git a/API/SignalR/ProgressType.cs b/Kavita.Models/DTOs/SignalR/ProgressType.cs similarity index 92% rename from API/SignalR/ProgressType.cs rename to Kavita.Models/DTOs/SignalR/ProgressType.cs index b0fbe341d..9cac78616 100644 --- a/API/SignalR/ProgressType.cs +++ b/Kavita.Models/DTOs/SignalR/ProgressType.cs @@ -1,4 +1,4 @@ -namespace API.SignalR; +namespace Kavita.Models.DTOs.SignalR; /// /// How progress should be represented on the UI diff --git a/API/SignalR/SignalRMessage.cs b/Kavita.Models/DTOs/SignalR/SignalRMessage.cs similarity index 96% rename from API/SignalR/SignalRMessage.cs rename to Kavita.Models/DTOs/SignalR/SignalRMessage.cs index f00a677b9..0dfc11b17 100644 --- a/API/SignalR/SignalRMessage.cs +++ b/Kavita.Models/DTOs/SignalR/SignalRMessage.cs @@ -1,6 +1,6 @@ using System; -namespace API.SignalR; +namespace Kavita.Models.DTOs.SignalR; #nullable enable /// diff --git a/API/DTOs/StandaloneChapterDto.cs b/Kavita.Models/DTOs/StandaloneChapterDto.cs similarity index 81% rename from API/DTOs/StandaloneChapterDto.cs rename to Kavita.Models/DTOs/StandaloneChapterDto.cs index 2f4cd2ee1..8d49c0c8a 100644 --- a/API/DTOs/StandaloneChapterDto.cs +++ b/Kavita.Models/DTOs/StandaloneChapterDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable /// diff --git a/API/DTOs/Statistics/BreakDownDto.cs b/Kavita.Models/DTOs/Statistics/BreakDownDto.cs similarity index 85% rename from API/DTOs/Statistics/BreakDownDto.cs rename to Kavita.Models/DTOs/Statistics/BreakDownDto.cs index f364dd4b3..3d5889f5f 100644 --- a/API/DTOs/Statistics/BreakDownDto.cs +++ b/Kavita.Models/DTOs/Statistics/BreakDownDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record BreakDownDto { diff --git a/API/DTOs/Statistics/Count.cs b/Kavita.Models/DTOs/Statistics/Count.cs similarity index 75% rename from API/DTOs/Statistics/Count.cs rename to Kavita.Models/DTOs/Statistics/Count.cs index 1577e682c..de2bf5232 100644 --- a/API/DTOs/Statistics/Count.cs +++ b/Kavita.Models/DTOs/Statistics/Count.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record StatCount : ICount { diff --git a/API/DTOs/Statistics/FileExtensionBreakdownDto.cs b/Kavita.Models/DTOs/Statistics/FileExtensionBreakdownDto.cs similarity index 86% rename from API/DTOs/Statistics/FileExtensionBreakdownDto.cs rename to Kavita.Models/DTOs/Statistics/FileExtensionBreakdownDto.cs index 7a248caef..84f3294be 100644 --- a/API/DTOs/Statistics/FileExtensionBreakdownDto.cs +++ b/Kavita.Models/DTOs/Statistics/FileExtensionBreakdownDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable public sealed record FileExtensionDto diff --git a/API/DTOs/Statistics/ICount.cs b/Kavita.Models/DTOs/Statistics/ICount.cs similarity index 69% rename from API/DTOs/Statistics/ICount.cs rename to Kavita.Models/DTOs/Statistics/ICount.cs index 7f8b5b2ed..9c66f801e 100644 --- a/API/DTOs/Statistics/ICount.cs +++ b/Kavita.Models/DTOs/Statistics/ICount.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public interface ICount { diff --git a/API/DTOs/Statistics/MostActiveUserDto.cs b/Kavita.Models/DTOs/Statistics/MostActiveUserDto.cs similarity index 92% rename from API/DTOs/Statistics/MostActiveUserDto.cs rename to Kavita.Models/DTOs/Statistics/MostActiveUserDto.cs index 2f90ed429..043321e4a 100644 --- a/API/DTOs/Statistics/MostActiveUserDto.cs +++ b/Kavita.Models/DTOs/Statistics/MostActiveUserDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable diff --git a/API/DTOs/Statistics/MostReadAuthorsDto.cs b/Kavita.Models/DTOs/Statistics/MostReadAuthorsDto.cs similarity index 91% rename from API/DTOs/Statistics/MostReadAuthorsDto.cs rename to Kavita.Models/DTOs/Statistics/MostReadAuthorsDto.cs index 9fe2de530..94b742070 100644 --- a/API/DTOs/Statistics/MostReadAuthorsDto.cs +++ b/Kavita.Models/DTOs/Statistics/MostReadAuthorsDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record MostReadAuthorsDto { diff --git a/API/DTOs/Statistics/PagesReadOnADayCount.cs b/Kavita.Models/DTOs/Statistics/PagesReadOnADayCount.cs similarity index 82% rename from API/DTOs/Statistics/PagesReadOnADayCount.cs rename to Kavita.Models/DTOs/Statistics/PagesReadOnADayCount.cs index 08a1f404c..fb8120040 100644 --- a/API/DTOs/Statistics/PagesReadOnADayCount.cs +++ b/Kavita.Models/DTOs/Statistics/PagesReadOnADayCount.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record StatCountWithFormat : ICount { diff --git a/API/DTOs/Statistics/ProfileStatBarDto.cs b/Kavita.Models/DTOs/Statistics/ProfileStatBarDto.cs similarity index 88% rename from API/DTOs/Statistics/ProfileStatBarDto.cs rename to Kavita.Models/DTOs/Statistics/ProfileStatBarDto.cs index 5cba4021d..17d93ea61 100644 --- a/API/DTOs/Statistics/ProfileStatBarDto.cs +++ b/Kavita.Models/DTOs/Statistics/ProfileStatBarDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record ProfileStatBarDto { diff --git a/API/DTOs/Statistics/ReadHistoryEvent.cs b/Kavita.Models/DTOs/Statistics/ReadHistoryEvent.cs similarity index 93% rename from API/DTOs/Statistics/ReadHistoryEvent.cs rename to Kavita.Models/DTOs/Statistics/ReadHistoryEvent.cs index 5d8262aef..53021f924 100644 --- a/API/DTOs/Statistics/ReadHistoryEvent.cs +++ b/Kavita.Models/DTOs/Statistics/ReadHistoryEvent.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable /// diff --git a/API/DTOs/Statistics/ReadTimeByHourDto.cs b/Kavita.Models/DTOs/Statistics/ReadTimeByHourDto.cs similarity index 82% rename from API/DTOs/Statistics/ReadTimeByHourDto.cs rename to Kavita.Models/DTOs/Statistics/ReadTimeByHourDto.cs index ee94302f6..f3226757e 100644 --- a/API/DTOs/Statistics/ReadTimeByHourDto.cs +++ b/Kavita.Models/DTOs/Statistics/ReadTimeByHourDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record ReadTimeByHourDto { diff --git a/API/DTOs/Statistics/ReadingActivityGraphDto.cs b/Kavita.Models/DTOs/Statistics/ReadingActivityGraphDto.cs similarity index 91% rename from API/DTOs/Statistics/ReadingActivityGraphDto.cs rename to Kavita.Models/DTOs/Statistics/ReadingActivityGraphDto.cs index e11435c27..b1275a6c6 100644 --- a/API/DTOs/Statistics/ReadingActivityGraphDto.cs +++ b/Kavita.Models/DTOs/Statistics/ReadingActivityGraphDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable public sealed record ReadingActivityGraphEntryDto diff --git a/API/DTOs/Statistics/ReadingHistoryItemDto.cs b/Kavita.Models/DTOs/Statistics/ReadingHistoryItemDto.cs similarity index 94% rename from API/DTOs/Statistics/ReadingHistoryItemDto.cs rename to Kavita.Models/DTOs/Statistics/ReadingHistoryItemDto.cs index fdadc92a9..2cbac4d33 100644 --- a/API/DTOs/Statistics/ReadingHistoryItemDto.cs +++ b/Kavita.Models/DTOs/Statistics/ReadingHistoryItemDto.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record ReadingHistoryItemDto { diff --git a/API/DTOs/Statistics/ReadingPaceDto.cs b/Kavita.Models/DTOs/Statistics/ReadingPaceDto.cs similarity index 86% rename from API/DTOs/Statistics/ReadingPaceDto.cs rename to Kavita.Models/DTOs/Statistics/ReadingPaceDto.cs index d8a1df50d..9bc7c65a8 100644 --- a/API/DTOs/Statistics/ReadingPaceDto.cs +++ b/Kavita.Models/DTOs/Statistics/ReadingPaceDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record ReadingPaceDto { diff --git a/API/DTOs/Statistics/ServerStatisticsDto.cs b/Kavita.Models/DTOs/Statistics/ServerStatisticsDto.cs similarity index 90% rename from API/DTOs/Statistics/ServerStatisticsDto.cs rename to Kavita.Models/DTOs/Statistics/ServerStatisticsDto.cs index c67a30069..15b11e6ef 100644 --- a/API/DTOs/Statistics/ServerStatisticsDto.cs +++ b/Kavita.Models/DTOs/Statistics/ServerStatisticsDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable public sealed record ServerStatisticsDto diff --git a/API/DTOs/Statistics/SpreadStatsDto.cs b/Kavita.Models/DTOs/Statistics/SpreadStatsDto.cs similarity index 80% rename from API/DTOs/Statistics/SpreadStatsDto.cs rename to Kavita.Models/DTOs/Statistics/SpreadStatsDto.cs index 88a705ba6..c62fa2760 100644 --- a/API/DTOs/Statistics/SpreadStatsDto.cs +++ b/Kavita.Models/DTOs/Statistics/SpreadStatsDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record SpreadStatsDto { diff --git a/API/DTOs/Statistics/StatBucketDto.cs b/Kavita.Models/DTOs/Statistics/StatBucketDto.cs similarity index 90% rename from API/DTOs/Statistics/StatBucketDto.cs rename to Kavita.Models/DTOs/Statistics/StatBucketDto.cs index ebd791bcd..bee8cd550 100644 --- a/API/DTOs/Statistics/StatBucketDto.cs +++ b/Kavita.Models/DTOs/Statistics/StatBucketDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; /// /// A bucket of items (fixed) from 0-X, X-X*2 diff --git a/API/DTOs/Statistics/StatsFilterDto.cs b/Kavita.Models/DTOs/Statistics/StatsFilterDto.cs similarity index 93% rename from API/DTOs/Statistics/StatsFilterDto.cs rename to Kavita.Models/DTOs/Statistics/StatsFilterDto.cs index a0c76524b..1c57b792b 100644 --- a/API/DTOs/Statistics/StatsFilterDto.cs +++ b/Kavita.Models/DTOs/Statistics/StatsFilterDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable public sealed record StatsFilterDto diff --git a/API/DTOs/Statistics/TopReadsDto.cs b/Kavita.Models/DTOs/Statistics/TopReadsDto.cs similarity index 90% rename from API/DTOs/Statistics/TopReadsDto.cs rename to Kavita.Models/DTOs/Statistics/TopReadsDto.cs index d11594dca..b5bb19146 100644 --- a/API/DTOs/Statistics/TopReadsDto.cs +++ b/Kavita.Models/DTOs/Statistics/TopReadsDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable public sealed record TopReadDto diff --git a/API/DTOs/Statistics/UserReadStatistics.cs b/Kavita.Models/DTOs/Statistics/UserReadStatistics.cs similarity index 94% rename from API/DTOs/Statistics/UserReadStatistics.cs rename to Kavita.Models/DTOs/Statistics/UserReadStatistics.cs index 2ff96fb61..f299fb618 100644 --- a/API/DTOs/Statistics/UserReadStatistics.cs +++ b/Kavita.Models/DTOs/Statistics/UserReadStatistics.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable public sealed record UserReadStatistics diff --git a/API/DTOs/Statistics/YearMonthGroupingDto.cs b/Kavita.Models/DTOs/Statistics/YearMonthGroupingDto.cs similarity index 73% rename from API/DTOs/Statistics/YearMonthGroupingDto.cs rename to Kavita.Models/DTOs/Statistics/YearMonthGroupingDto.cs index b080baac9..cf59e7a9d 100644 --- a/API/DTOs/Statistics/YearMonthGroupingDto.cs +++ b/Kavita.Models/DTOs/Statistics/YearMonthGroupingDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record YearMonthGroupingDto { diff --git a/API/DTOs/Stats/FileExtensionExportDto.cs b/Kavita.Models/DTOs/Stats/FileExtensionExportDto.cs similarity index 89% rename from API/DTOs/Stats/FileExtensionExportDto.cs rename to Kavita.Models/DTOs/Stats/FileExtensionExportDto.cs index e881960a5..ff80d9e58 100644 --- a/API/DTOs/Stats/FileExtensionExportDto.cs +++ b/Kavita.Models/DTOs/Stats/FileExtensionExportDto.cs @@ -1,6 +1,6 @@ using CsvHelper.Configuration.Attributes; -namespace API.DTOs.Stats; +namespace Kavita.Models.DTOs.Stats; /// /// Excel export for File Extension Report diff --git a/API/DTOs/Stats/ServerInfoSlimDto.cs b/Kavita.Models/DTOs/Stats/ServerInfoSlimDto.cs similarity index 95% rename from API/DTOs/Stats/ServerInfoSlimDto.cs rename to Kavita.Models/DTOs/Stats/ServerInfoSlimDto.cs index f1abb2e1d..cdc07fbc2 100644 --- a/API/DTOs/Stats/ServerInfoSlimDto.cs +++ b/Kavita.Models/DTOs/Stats/ServerInfoSlimDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Stats; +namespace Kavita.Models.DTOs.Stats; #nullable enable /// diff --git a/API/DTOs/Stats/V3/ClientDevice/DeviceClientBreakdownDto.cs b/Kavita.Models/DTOs/Stats/V3/ClientDevice/DeviceClientBreakdownDto.cs similarity index 61% rename from API/DTOs/Stats/V3/ClientDevice/DeviceClientBreakdownDto.cs rename to Kavita.Models/DTOs/Stats/V3/ClientDevice/DeviceClientBreakdownDto.cs index aca049e3c..f35ecbabf 100644 --- a/API/DTOs/Stats/V3/ClientDevice/DeviceClientBreakdownDto.cs +++ b/Kavita.Models/DTOs/Stats/V3/ClientDevice/DeviceClientBreakdownDto.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using API.DTOs.Statistics; -using API.Entities.Enums; +using Kavita.Models.DTOs.Statistics; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Stats.V3.ClientDevice; +namespace Kavita.Models.DTOs.Stats.V3.ClientDevice; public sealed record DeviceClientBreakdownDto { diff --git a/API/DTOs/Stats/V3/LibraryStatV3.cs b/Kavita.Models/DTOs/Stats/V3/LibraryStatV3.cs similarity index 94% rename from API/DTOs/Stats/V3/LibraryStatV3.cs rename to Kavita.Models/DTOs/Stats/V3/LibraryStatV3.cs index 461792666..e85877917 100644 --- a/API/DTOs/Stats/V3/LibraryStatV3.cs +++ b/Kavita.Models/DTOs/Stats/V3/LibraryStatV3.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Stats.V3; +namespace Kavita.Models.DTOs.Stats.V3; public sealed record LibraryStatV3 { diff --git a/API/DTOs/Stats/V3/RelationshipStatV3.cs b/Kavita.Models/DTOs/Stats/V3/RelationshipStatV3.cs similarity index 73% rename from API/DTOs/Stats/V3/RelationshipStatV3.cs rename to Kavita.Models/DTOs/Stats/V3/RelationshipStatV3.cs index 37b63cb9a..f7f22cdd6 100644 --- a/API/DTOs/Stats/V3/RelationshipStatV3.cs +++ b/Kavita.Models/DTOs/Stats/V3/RelationshipStatV3.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Stats.V3; +namespace Kavita.Models.DTOs.Stats.V3; /// /// KavitaStats - Information about Series Relationships diff --git a/API/DTOs/Stats/V3/ServerInfoV3Dto.cs b/Kavita.Models/DTOs/Stats/V3/ServerInfoV3Dto.cs similarity index 98% rename from API/DTOs/Stats/V3/ServerInfoV3Dto.cs rename to Kavita.Models/DTOs/Stats/V3/ServerInfoV3Dto.cs index 464179ca7..5614593b9 100644 --- a/API/DTOs/Stats/V3/ServerInfoV3Dto.cs +++ b/Kavita.Models/DTOs/Stats/V3/ServerInfoV3Dto.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Stats.V3; +namespace Kavita.Models.DTOs.Stats.V3; /// /// Represents information about a Kavita Installation for Kavita Stats v3 API diff --git a/API/DTOs/Stats/V3/UserStatV3.cs b/Kavita.Models/DTOs/Stats/V3/UserStatV3.cs similarity index 95% rename from API/DTOs/Stats/V3/UserStatV3.cs rename to Kavita.Models/DTOs/Stats/V3/UserStatV3.cs index de04f4113..8bad2b6db 100644 --- a/API/DTOs/Stats/V3/UserStatV3.cs +++ b/Kavita.Models/DTOs/Stats/V3/UserStatV3.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using API.Data.Misc; -using API.Entities.Enums; -using API.Entities.Enums.Device; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.Device; -namespace API.DTOs.Stats.V3; +namespace Kavita.Models.DTOs.Stats.V3; public sealed record UserStatV3 { diff --git a/API/DTOs/System/DirectoryDto.cs b/Kavita.Models/DTOs/System/DirectoryDto.cs similarity index 87% rename from API/DTOs/System/DirectoryDto.cs rename to Kavita.Models/DTOs/System/DirectoryDto.cs index 3b1408f7f..e0a5ea96c 100644 --- a/API/DTOs/System/DirectoryDto.cs +++ b/Kavita.Models/DTOs/System/DirectoryDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.System; +namespace Kavita.Models.DTOs.System; public sealed record DirectoryDto { diff --git a/API/DTOs/TachiyomiChapterDto.cs b/Kavita.Models/DTOs/TachiyomiChapterDto.cs similarity index 91% rename from API/DTOs/TachiyomiChapterDto.cs rename to Kavita.Models/DTOs/TachiyomiChapterDto.cs index ecdd5115c..91d5b7969 100644 --- a/API/DTOs/TachiyomiChapterDto.cs +++ b/Kavita.Models/DTOs/TachiyomiChapterDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable /// diff --git a/API/DTOs/Theme/ColorScapeDto.cs b/Kavita.Models/DTOs/Theme/ColorScapeDto.cs similarity index 91% rename from API/DTOs/Theme/ColorScapeDto.cs rename to Kavita.Models/DTOs/Theme/ColorScapeDto.cs index 2ebd96e2b..be035c1b2 100644 --- a/API/DTOs/Theme/ColorScapeDto.cs +++ b/Kavita.Models/DTOs/Theme/ColorScapeDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Theme; +namespace Kavita.Models.DTOs.Theme; #nullable enable /// diff --git a/API/DTOs/Theme/DownloadableSiteThemeDto.cs b/Kavita.Models/DTOs/Theme/DownloadableSiteThemeDto.cs similarity index 97% rename from API/DTOs/Theme/DownloadableSiteThemeDto.cs rename to Kavita.Models/DTOs/Theme/DownloadableSiteThemeDto.cs index 9f5991158..087f879fe 100644 --- a/API/DTOs/Theme/DownloadableSiteThemeDto.cs +++ b/Kavita.Models/DTOs/Theme/DownloadableSiteThemeDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Theme; +namespace Kavita.Models.DTOs.Theme; public sealed record DownloadableSiteThemeDto diff --git a/API/DTOs/Theme/SiteThemeDto.cs b/Kavita.Models/DTOs/Theme/SiteThemeDto.cs similarity index 95% rename from API/DTOs/Theme/SiteThemeDto.cs rename to Kavita.Models/DTOs/Theme/SiteThemeDto.cs index 7ae8369e9..6dc9d5695 100644 --- a/API/DTOs/Theme/SiteThemeDto.cs +++ b/Kavita.Models/DTOs/Theme/SiteThemeDto.cs @@ -1,8 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums.Theme; -using API.Services; +using Kavita.Models.Entities.Enums.Theme; -namespace API.DTOs.Theme; +namespace Kavita.Models.DTOs.Theme; /// /// Represents a set of css overrides the user can upload to Kavita and will load into webui diff --git a/API/DTOs/Theme/UpdateDefaultThemeDto.cs b/Kavita.Models/DTOs/Theme/UpdateDefaultThemeDto.cs similarity index 68% rename from API/DTOs/Theme/UpdateDefaultThemeDto.cs rename to Kavita.Models/DTOs/Theme/UpdateDefaultThemeDto.cs index aac0858c3..845dec922 100644 --- a/API/DTOs/Theme/UpdateDefaultThemeDto.cs +++ b/Kavita.Models/DTOs/Theme/UpdateDefaultThemeDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Theme; +namespace Kavita.Models.DTOs.Theme; public sealed record UpdateDefaultThemeDto { diff --git a/API/DTOs/Update/UpdateNotificationDto.cs b/Kavita.Models/DTOs/Update/UpdateNotificationDto.cs similarity index 98% rename from API/DTOs/Update/UpdateNotificationDto.cs rename to Kavita.Models/DTOs/Update/UpdateNotificationDto.cs index e1d2b81fe..67a5740f3 100644 --- a/API/DTOs/Update/UpdateNotificationDto.cs +++ b/Kavita.Models/DTOs/Update/UpdateNotificationDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Update; +namespace Kavita.Models.DTOs.Update; /// /// Update Notification denoting a new release available for user to update to diff --git a/API/DTOs/UpdateChapterDto.cs b/Kavita.Models/DTOs/UpdateChapterDto.cs similarity index 96% rename from API/DTOs/UpdateChapterDto.cs rename to Kavita.Models/DTOs/UpdateChapterDto.cs index 9ead8adc8..32fad623e 100644 --- a/API/DTOs/UpdateChapterDto.cs +++ b/Kavita.Models/DTOs/UpdateChapterDto.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.Entities.Enums; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.Entities.Enums; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record UpdateChapterDto { diff --git a/API/DTOs/UpdateLibraryDto.cs b/Kavita.Models/DTOs/UpdateLibraryDto.cs similarity index 95% rename from API/DTOs/UpdateLibraryDto.cs rename to Kavita.Models/DTOs/UpdateLibraryDto.cs index ab7d01c36..96240a08b 100644 --- a/API/DTOs/UpdateLibraryDto.cs +++ b/Kavita.Models/DTOs/UpdateLibraryDto.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using API.Entities; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record UpdateLibraryDto { diff --git a/API/DTOs/UpdateLibraryForUserDto.cs b/Kavita.Models/DTOs/UpdateLibraryForUserDto.cs similarity index 88% rename from API/DTOs/UpdateLibraryForUserDto.cs rename to Kavita.Models/DTOs/UpdateLibraryForUserDto.cs index 4ce8d0df8..4b1c78fcf 100644 --- a/API/DTOs/UpdateLibraryForUserDto.cs +++ b/Kavita.Models/DTOs/UpdateLibraryForUserDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record UpdateLibraryForUserDto { diff --git a/API/DTOs/UpdateRBSDto.cs b/Kavita.Models/DTOs/UpdateRBSDto.cs similarity index 86% rename from API/DTOs/UpdateRBSDto.cs rename to Kavita.Models/DTOs/UpdateRBSDto.cs index fa8bb78f9..7615f4ea7 100644 --- a/API/DTOs/UpdateRBSDto.cs +++ b/Kavita.Models/DTOs/UpdateRBSDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record UpdateRbsDto diff --git a/API/DTOs/UpdateRatingDto.cs b/Kavita.Models/DTOs/UpdateRatingDto.cs similarity index 83% rename from API/DTOs/UpdateRatingDto.cs rename to Kavita.Models/DTOs/UpdateRatingDto.cs index 472a94fe9..f0b756860 100644 --- a/API/DTOs/UpdateRatingDto.cs +++ b/Kavita.Models/DTOs/UpdateRatingDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record UpdateRatingDto { diff --git a/API/DTOs/UpdateSeriesDto.cs b/Kavita.Models/DTOs/UpdateSeriesDto.cs similarity index 90% rename from API/DTOs/UpdateSeriesDto.cs rename to Kavita.Models/DTOs/UpdateSeriesDto.cs index a4a9baf8c..6c99d0bd3 100644 --- a/API/DTOs/UpdateSeriesDto.cs +++ b/Kavita.Models/DTOs/UpdateSeriesDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record UpdateSeriesDto diff --git a/API/DTOs/UpdateSeriesMetadataDto.cs b/Kavita.Models/DTOs/UpdateSeriesMetadataDto.cs similarity index 78% rename from API/DTOs/UpdateSeriesMetadataDto.cs rename to Kavita.Models/DTOs/UpdateSeriesMetadataDto.cs index 5225f5486..c9e9783ee 100644 --- a/API/DTOs/UpdateSeriesMetadataDto.cs +++ b/Kavita.Models/DTOs/UpdateSeriesMetadataDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record UpdateSeriesMetadataDto { diff --git a/API/DTOs/Uploads/UploadFileDto.cs b/Kavita.Models/DTOs/Uploads/UploadFileDto.cs similarity index 90% rename from API/DTOs/Uploads/UploadFileDto.cs rename to Kavita.Models/DTOs/Uploads/UploadFileDto.cs index 8d5cdf4cb..29608fbd0 100644 --- a/API/DTOs/Uploads/UploadFileDto.cs +++ b/Kavita.Models/DTOs/Uploads/UploadFileDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Uploads; +namespace Kavita.Models.DTOs.Uploads; public sealed record UploadFileDto { diff --git a/API/DTOs/Uploads/UploadUrlDto.cs b/Kavita.Models/DTOs/Uploads/UploadUrlDto.cs similarity index 84% rename from API/DTOs/Uploads/UploadUrlDto.cs rename to Kavita.Models/DTOs/Uploads/UploadUrlDto.cs index 3f4e625c3..1860068c2 100644 --- a/API/DTOs/Uploads/UploadUrlDto.cs +++ b/Kavita.Models/DTOs/Uploads/UploadUrlDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Uploads; +namespace Kavita.Models.DTOs.Uploads; public sealed record UploadUrlDto { diff --git a/API/DTOs/UserDto.cs b/Kavita.Models/DTOs/UserDto.cs similarity index 83% rename from API/DTOs/UserDto.cs rename to Kavita.Models/DTOs/UserDto.cs index ee32802f0..f948aa5c4 100644 --- a/API/DTOs/UserDto.cs +++ b/Kavita.Models/DTOs/UserDto.cs @@ -1,14 +1,15 @@  using System; using System.Collections.Generic; -using API.DTOs.Account; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.User; -using API.Entities.Interfaces; +using Kavita.Models.DTOs.Account; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; using NotImplementedException = System.NotImplementedException; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record UserDto : IHasCoverImage diff --git a/API/DTOs/UserPreferencesDto.cs b/Kavita.Models/DTOs/UserPreferencesDto.cs similarity index 93% rename from API/DTOs/UserPreferencesDto.cs rename to Kavita.Models/DTOs/UserPreferencesDto.cs index 319438fef..244c74843 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/Kavita.Models/DTOs/UserPreferencesDto.cs @@ -1,11 +1,11 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using API.DTOs.Theme; -using API.Entities; -using API.Entities.Enums.UserPreferences; -using API.Entities.User; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.User; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record UserPreferencesDto diff --git a/API/DTOs/UserReadingProfileDto.cs b/Kavita.Models/DTOs/UserReadingProfileDto.cs similarity index 96% rename from API/DTOs/UserReadingProfileDto.cs rename to Kavita.Models/DTOs/UserReadingProfileDto.cs index 1e59a5e85..484194af6 100644 --- a/API/DTOs/UserReadingProfileDto.cs +++ b/Kavita.Models/DTOs/UserReadingProfileDto.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.User; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record UserReadingProfileDto { diff --git a/API/DTOs/VolumeDto.cs b/Kavita.Models/DTOs/VolumeDto.cs similarity index 83% rename from API/DTOs/VolumeDto.cs rename to Kavita.Models/DTOs/VolumeDto.cs index b2c56ae0b..80c422289 100644 --- a/API/DTOs/VolumeDto.cs +++ b/Kavita.Models/DTOs/VolumeDto.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using API.Entities.Interfaces; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities.Interfaces; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record VolumeDto : IHasReadTimeEstimate, IHasCoverImage { @@ -49,24 +47,6 @@ public sealed record VolumeDto : IHasReadTimeEstimate, IHasCoverImage public float AvgHoursToRead { get; set; } public long WordCount { get; set; } - /// - /// Is this a loose leaf volume - /// - /// - public bool IsLooseLeaf() - { - return MinNumber.Is(Parser.LooseLeafVolumeNumber); - } - - /// - /// Does this volume hold only specials - /// - /// - public bool IsSpecial() - { - return MinNumber.Is(Parser.SpecialVolumeNumber); - } - /// public string CoverImage { get; set; } /// diff --git a/API/DTOs/WantToRead/UpdateWantToReadDto.cs b/Kavita.Models/DTOs/WantToRead/UpdateWantToReadDto.cs similarity index 89% rename from API/DTOs/WantToRead/UpdateWantToReadDto.cs rename to Kavita.Models/DTOs/WantToRead/UpdateWantToReadDto.cs index a5be26857..a82d49f46 100644 --- a/API/DTOs/WantToRead/UpdateWantToReadDto.cs +++ b/Kavita.Models/DTOs/WantToRead/UpdateWantToReadDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.DTOs.WantToRead; +namespace Kavita.Models.DTOs.WantToRead; /// /// A list of Series to pass when working with Want To Read APIs diff --git a/Kavita.Models/Defaults.cs b/Kavita.Models/Defaults.cs new file mode 100644 index 000000000..e0da65b9a --- /dev/null +++ b/Kavita.Models/Defaults.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.Font; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Entities.User; + +namespace Kavita.Models; + +public static class Defaults +{ + public static readonly string DefaultFont = "Default"; + + /// + /// Generated on Startup. Seed.SeedSettings must run before + /// + public static ImmutableArray DefaultSettings; + + public static readonly ImmutableArray DefaultHighlightSlots = + [ + new() + { + Id = 1, + SlotNumber = 0, + Color = new RgbaColor { R = 0, G = 255, B = 255, A = 0.4f } + }, + new() + { + Id = 2, + SlotNumber = 1, + Color = new RgbaColor { R = 0, G = 255, B = 0, A = 0.4f } + }, + new() + { + Id = 3, + SlotNumber = 2, + Color = new RgbaColor { R = 255, G = 255, B = 0, A = 0.4f } + }, + new() + { + Id = 4, + SlotNumber = 3, + Color = new RgbaColor { R = 255, G = 165, B = 0, A = 0.4f } + }, + new() + { + Id = 5, + SlotNumber = 4, + Color = new RgbaColor { R = 255, G = 0, B = 255, A = 0.4f } + } + ]; + + public static readonly ImmutableArray DefaultFonts = + [ + new () + { + Name = DefaultFont, + NormalizedName = DefaultFont.ToNormalized(), + Provider = FontProvider.System, + FileName = string.Empty, + }, + new () + { + Name = "Merriweather", + NormalizedName = "Merriweather".ToNormalized(), + Provider = FontProvider.System, + FileName = "Merriweather-Regular.woff2", + }, + new () + { + Name = "EB Garamond", + NormalizedName = "EB Garamond".ToNormalized(), + Provider = FontProvider.System, + FileName = "EBGaramond-VariableFont_wght.woff2", + }, + new () + { + Name = "Fira Sans", + NormalizedName = "Fira Sans".ToNormalized(), + Provider = FontProvider.System, + FileName = "FiraSans-Regular.woff2", + }, + new () + { + Name = "Lato", + NormalizedName = "Lato".ToNormalized(), + Provider = FontProvider.System, + FileName = "Lato-Regular.woff2", + }, + new () + { + Name = "Libre Baskerville", + NormalizedName = "Libre Baskerville".ToNormalized(), + Provider = FontProvider.System, + FileName = "LibreBaskerville-Regular.woff2", + }, + new () + { + Name = "Nanum Gothic", + NormalizedName = ("Nanum Gothic").ToNormalized(), + Provider = FontProvider.System, + FileName = "NanumGothic-Regular.woff2", + }, + new () + { + Name = "Open Dyslexic", + NormalizedName = ("Open Dyslexic").ToNormalized(), + Provider = FontProvider.System, + FileName = "OpenDyslexic-Regular.woff2", + }, + new () + { + Name = "RocknRoll One", + NormalizedName = ("RocknRoll One").ToNormalized(), + Provider = FontProvider.System, + FileName = "RocknRollOne-Regular.woff2", + }, + new () + { + Name = "Fast Font Serif", + NormalizedName = ("Fast Font Serif").ToNormalized(), + Provider = FontProvider.System, + FileName = "Fast_Serif.woff2", + }, + new () + { + Name = "Fast Font Sans", + NormalizedName = ("Fast Font Sans").ToNormalized(), + Provider = FontProvider.System, + FileName = "Fast_Sans.woff2", + } + ]; + + public static readonly ImmutableArray DefaultThemes = [ + ..new List + { + SiteTheme.DefaultTheme, + }.ToArray() + ]; + + public static readonly ImmutableArray DefaultStreams = [ + ..new List + { + new() + { + Name = "on-deck", + StreamType = DashboardStreamType.OnDeck, + Order = 0, + IsProvided = true, + Visible = true + }, + new() + { + Name = "recently-updated", + StreamType = DashboardStreamType.RecentlyUpdated, + Order = 1, + IsProvided = true, + Visible = true + }, + new() + { + Name = "newly-added", + StreamType = DashboardStreamType.NewlyAdded, + Order = 2, + IsProvided = true, + Visible = true + }, + new() + { + Name = "more-in-genre", + StreamType = DashboardStreamType.MoreInGenre, + Order = 3, + IsProvided = true, + Visible = false + }, + }.ToArray() + ]; + + public static readonly ImmutableArray DefaultSideNavStreams = + [ + new() + { + Name = "want-to-read", + StreamType = SideNavStreamType.WantToRead, + Order = 1, + IsProvided = true, + Visible = true + }, new() + { + Name = "collections", + StreamType = SideNavStreamType.Collections, + Order = 2, + IsProvided = true, + Visible = true + }, new() + { + Name = "reading-lists", + StreamType = SideNavStreamType.ReadingLists, + Order = 3, + IsProvided = true, + Visible = true + }, new() + { + Name = "bookmarks", + StreamType = SideNavStreamType.Bookmarks, + Order = 4, + IsProvided = true, + Visible = true + }, new() + { + Name = "all-series", + StreamType = SideNavStreamType.AllSeries, + Order = 5, + IsProvided = true, + Visible = true + }, + new() + { + Name = "browse-authors", + StreamType = SideNavStreamType.BrowsePeople, + Order = 6, + IsProvided = true, + Visible = true + } + ]; + + public static List CreateDefaultAuthKeys() + { + return + [ + new AppUserAuthKey() + { + Name = AuthKeyHelper.OpdsKeyName, + Key = AuthKeyHelper.GenerateKey(32), + CreatedAtUtc = DateTime.UtcNow, + ExpiresAtUtc = null, + Provider = AuthKeyProvider.System, + }, + new AppUserAuthKey() + { + Name = AuthKeyHelper.ImageOnlyKeyName, + Key = AuthKeyHelper.GenerateKey(32), + CreatedAtUtc = DateTime.UtcNow, + ExpiresAtUtc = null, + Provider = AuthKeyProvider.System, + } + ]; + } +} diff --git a/API/Data/Misc/AgeRestriction.cs b/Kavita.Models/Entities/AgeRestriction.cs similarity index 63% rename from API/Data/Misc/AgeRestriction.cs rename to Kavita.Models/Entities/AgeRestriction.cs index 90c3c5888..72c2ce705 100644 --- a/API/Data/Misc/AgeRestriction.cs +++ b/Kavita.Models/Entities/AgeRestriction.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Data.Misc; +namespace Kavita.Models.Entities; public class AgeRestriction { diff --git a/API/Entities/Chapter.cs b/Kavita.Models/Entities/Chapter.cs similarity index 77% rename from API/Entities/Chapter.cs rename to Kavita.Models/Entities/Chapter.cs index e4cd398ea..fdb6f478e 100644 --- a/API/Entities/Chapter.cs +++ b/Kavita.Models/Entities/Chapter.cs @@ -1,17 +1,15 @@ using System; using System.Collections.Generic; using System.Globalization; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Entities.Progress; -using API.Entities.User; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; -namespace API.Entities; +namespace Kavita.Models.Entities; public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKPlusMetadata { @@ -185,66 +183,6 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKP public ICollection ExternalReviews { get; set; } = []; public ICollection ExternalRatings { get; set; } = null!; - - public void UpdateFrom(ParserInfo info) - { - Files ??= new List(); - IsSpecial = info.IsSpecialInfo(); - if (IsSpecial) - { - Number = Parser.DefaultChapter; - MinNumber = Parser.DefaultChapterNumber; - MaxNumber = Parser.DefaultChapterNumber; - } - Title = (IsSpecial && info.Format is MangaFormat.Epub or MangaFormat.Pdf) - ? info.Title - : Parser.RemoveExtensionIfSupported(Range); - - var specialTreatment = info.IsSpecialInfo(); - Range = specialTreatment ? info.Filename : info.Chapters; - } - - /// - /// Returns the Chapter Number. If the chapter is a range, returns that, formatted. - /// - /// - public string GetNumberTitle() - { - try - { - if (MinNumber.Is(MaxNumber)) - { - if (MinNumber.Is(Parser.DefaultChapterNumber) && IsSpecial) - { - return Parser.RemoveExtensionIfSupported(Title); - } - - if (MinNumber.Is(0f) && !float.TryParse(Range, CultureInfo.InvariantCulture, out _)) - { - return $"{Range.ToString(CultureInfo.InvariantCulture)}"; - } - - return $"{MinNumber.ToString(CultureInfo.InvariantCulture)}"; - - } - - return $"{MinNumber.ToString(CultureInfo.InvariantCulture)}-{MaxNumber.ToString(CultureInfo.InvariantCulture)}"; - } - catch (Exception) - { - return MinNumber.ToString(CultureInfo.InvariantCulture); - } - } - - /// - /// Is the Chapter representing a single Volume (volume 1.cbz). If so, Min/Max will be Default and will not be special - /// - /// - public bool IsSingleVolumeChapter() - { - return MinNumber.Is(Parser.DefaultChapterNumber) && !IsSpecial; - } - public void ResetColorScape() { PrimaryColor = string.Empty; diff --git a/API/Entities/CollectionTag.cs b/Kavita.Models/Entities/CollectionTag.cs similarity index 96% rename from API/Entities/CollectionTag.cs rename to Kavita.Models/Entities/CollectionTag.cs index e23d0154c..0284ca7b3 100644 --- a/API/Entities/CollectionTag.cs +++ b/Kavita.Models/Entities/CollectionTag.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using API.Entities.Metadata; +using Kavita.Models.Entities.Metadata; using Microsoft.EntityFrameworkCore; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// Represents a user entered field that is used as a tagging and grouping mechanism diff --git a/API/Entities/Device.cs b/Kavita.Models/Entities/Device.cs similarity index 90% rename from API/Entities/Device.cs rename to Kavita.Models/Entities/Device.cs index 2f2ffa8c0..b5568860b 100644 --- a/API/Entities/Device.cs +++ b/Kavita.Models/Entities/Device.cs @@ -1,8 +1,9 @@ using System; -using API.Entities.Enums.Device; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Enums.Device; +using Kavita.Models.Entities.User; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// A Device is an entity that can receive data from Kavita (kindle) diff --git a/API/Entities/EmailHistory.cs b/Kavita.Models/Entities/EmailHistory.cs similarity index 88% rename from API/Entities/EmailHistory.cs rename to Kavita.Models/Entities/EmailHistory.cs index f1ab95ca5..59bf5f6b4 100644 --- a/API/Entities/EmailHistory.cs +++ b/Kavita.Models/Entities/EmailHistory.cs @@ -1,8 +1,9 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// Records all emails that are sent from Kavita diff --git a/API/Entities/Enums/AgeRating.cs b/Kavita.Models/Entities/Enums/AgeRating.cs similarity index 96% rename from API/Entities/Enums/AgeRating.cs rename to Kavita.Models/Entities/Enums/AgeRating.cs index 9eefb9fa7..109fc22b0 100644 --- a/API/Entities/Enums/AgeRating.cs +++ b/Kavita.Models/Entities/Enums/AgeRating.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Represents Age Rating for content. diff --git a/API/Entities/Enums/BookPageLayoutMode.cs b/Kavita.Models/Entities/Enums/BookPageLayoutMode.cs similarity index 83% rename from API/Entities/Enums/BookPageLayoutMode.cs rename to Kavita.Models/Entities/Enums/BookPageLayoutMode.cs index dc61b5a1e..3dfc12eaa 100644 --- a/API/Entities/Enums/BookPageLayoutMode.cs +++ b/Kavita.Models/Entities/Enums/BookPageLayoutMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum BookPageLayoutMode { diff --git a/API/Entities/Enums/ClientDevicePlatform.cs b/Kavita.Models/Entities/Enums/ClientDevicePlatform.cs similarity index 89% rename from API/Entities/Enums/ClientDevicePlatform.cs rename to Kavita.Models/Entities/Enums/ClientDevicePlatform.cs index d78c0171c..31154d500 100644 --- a/API/Entities/Enums/ClientDevicePlatform.cs +++ b/Kavita.Models/Entities/Enums/ClientDevicePlatform.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum ClientDevicePlatform { diff --git a/API/Entities/Enums/ClientDeviceType.cs b/Kavita.Models/Entities/Enums/ClientDeviceType.cs similarity index 93% rename from API/Entities/Enums/ClientDeviceType.cs rename to Kavita.Models/Entities/Enums/ClientDeviceType.cs index 9ab694043..5aed7a0e5 100644 --- a/API/Entities/Enums/ClientDeviceType.cs +++ b/Kavita.Models/Entities/Enums/ClientDeviceType.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum ClientDeviceType { diff --git a/API/Entities/Enums/CoverImageSize.cs b/Kavita.Models/Entities/Enums/CoverImageSize.cs similarity index 94% rename from API/Entities/Enums/CoverImageSize.cs rename to Kavita.Models/Entities/Enums/CoverImageSize.cs index d2d0eebb6..955eb5d07 100644 --- a/API/Entities/Enums/CoverImageSize.cs +++ b/Kavita.Models/Entities/Enums/CoverImageSize.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum CoverImageSize { diff --git a/API/Entities/Enums/DashboardStreamType.cs b/Kavita.Models/Entities/Enums/DashboardStreamType.cs similarity index 82% rename from API/Entities/Enums/DashboardStreamType.cs rename to Kavita.Models/Entities/Enums/DashboardStreamType.cs index 27a7d67ca..43f293741 100644 --- a/API/Entities/Enums/DashboardStreamType.cs +++ b/Kavita.Models/Entities/Enums/DashboardStreamType.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum DashboardStreamType { diff --git a/API/Entities/Enums/Device/DevicePlatform.cs b/Kavita.Models/Entities/Enums/Device/DevicePlatform.cs similarity index 91% rename from API/Entities/Enums/Device/DevicePlatform.cs rename to Kavita.Models/Entities/Enums/Device/DevicePlatform.cs index 41e68584b..a9999c402 100644 --- a/API/Entities/Enums/Device/DevicePlatform.cs +++ b/Kavita.Models/Entities/Enums/Device/DevicePlatform.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.Device; +namespace Kavita.Models.Entities.Enums.Device; public enum EmailDevicePlatform { diff --git a/API/Entities/Enums/EncodeFormat.cs b/Kavita.Models/Entities/Enums/EncodeFormat.cs similarity index 81% rename from API/Entities/Enums/EncodeFormat.cs rename to Kavita.Models/Entities/Enums/EncodeFormat.cs index 70345f1db..4a4810788 100644 --- a/API/Entities/Enums/EncodeFormat.cs +++ b/Kavita.Models/Entities/Enums/EncodeFormat.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum EncodeFormat { diff --git a/API/Entities/Enums/EpubPageCalculationMethod.cs b/Kavita.Models/Entities/Enums/EpubPageCalculationMethod.cs similarity index 88% rename from API/Entities/Enums/EpubPageCalculationMethod.cs rename to Kavita.Models/Entities/Enums/EpubPageCalculationMethod.cs index 1fd2cb1de..42123d83b 100644 --- a/API/Entities/Enums/EpubPageCalculationMethod.cs +++ b/Kavita.Models/Entities/Enums/EpubPageCalculationMethod.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Due to a bleeding text bug in the Epub reader with 1/2 column layout, multiple calculation modes are present diff --git a/API/Entities/Enums/FileTypeGroup.cs b/Kavita.Models/Entities/Enums/FileTypeGroup.cs similarity index 88% rename from API/Entities/Enums/FileTypeGroup.cs rename to Kavita.Models/Entities/Enums/FileTypeGroup.cs index eda039fc9..e3932ae73 100644 --- a/API/Entities/Enums/FileTypeGroup.cs +++ b/Kavita.Models/Entities/Enums/FileTypeGroup.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Represents a set of file types that can be scanned diff --git a/API/Entities/Enums/Font/FontProvider.cs b/Kavita.Models/Entities/Enums/Font/FontProvider.cs similarity index 82% rename from API/Entities/Enums/Font/FontProvider.cs rename to Kavita.Models/Entities/Enums/Font/FontProvider.cs index ee944844a..4c79a52cc 100644 --- a/API/Entities/Enums/Font/FontProvider.cs +++ b/Kavita.Models/Entities/Enums/Font/FontProvider.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums.Font; +namespace Kavita.Models.Entities.Enums.Font; public enum FontProvider { diff --git a/API/Entities/Enums/IdentityProvider.cs b/Kavita.Models/Entities/Enums/IdentityProvider.cs similarity index 85% rename from API/Entities/Enums/IdentityProvider.cs rename to Kavita.Models/Entities/Enums/IdentityProvider.cs index 8ae814882..3c691fb5d 100644 --- a/API/Entities/Enums/IdentityProvider.cs +++ b/Kavita.Models/Entities/Enums/IdentityProvider.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Who provides the identity of the user diff --git a/API/Entities/Enums/LayoutMode.cs b/Kavita.Models/Entities/Enums/LayoutMode.cs similarity index 83% rename from API/Entities/Enums/LayoutMode.cs rename to Kavita.Models/Entities/Enums/LayoutMode.cs index 37fc69293..21dfa2b6b 100644 --- a/API/Entities/Enums/LayoutMode.cs +++ b/Kavita.Models/Entities/Enums/LayoutMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum LayoutMode { diff --git a/API/Entities/Enums/LibraryType.cs b/Kavita.Models/Entities/Enums/LibraryType.cs similarity index 96% rename from API/Entities/Enums/LibraryType.cs rename to Kavita.Models/Entities/Enums/LibraryType.cs index 2e2bd235b..542587928 100644 --- a/API/Entities/Enums/LibraryType.cs +++ b/Kavita.Models/Entities/Enums/LibraryType.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum LibraryType { diff --git a/API/Entities/Enums/MangaFormat.cs b/Kavita.Models/Entities/Enums/MangaFormat.cs similarity index 95% rename from API/Entities/Enums/MangaFormat.cs rename to Kavita.Models/Entities/Enums/MangaFormat.cs index 26f744b9b..7f269cdc8 100644 --- a/API/Entities/Enums/MangaFormat.cs +++ b/Kavita.Models/Entities/Enums/MangaFormat.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Represents the format of the file diff --git a/Kavita.Models/Entities/Enums/MediaErrorProducer.cs b/Kavita.Models/Entities/Enums/MediaErrorProducer.cs new file mode 100644 index 000000000..aed11169f --- /dev/null +++ b/Kavita.Models/Entities/Enums/MediaErrorProducer.cs @@ -0,0 +1,7 @@ +namespace Kavita.Models.Entities.Enums; + +public enum MediaErrorProducer +{ + BookService = 0, + ArchiveService = 1 +} diff --git a/API/Entities/Enums/MetadataFieldType.cs b/Kavita.Models/Entities/Enums/MetadataFieldType.cs similarity index 59% rename from API/Entities/Enums/MetadataFieldType.cs rename to Kavita.Models/Entities/Enums/MetadataFieldType.cs index 0052b6599..e0dc39561 100644 --- a/API/Entities/Enums/MetadataFieldType.cs +++ b/Kavita.Models/Entities/Enums/MetadataFieldType.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum MetadataFieldType { diff --git a/API/Entities/Enums/PageSplitOption.cs b/Kavita.Models/Entities/Enums/PageSplitOption.cs similarity index 73% rename from API/Entities/Enums/PageSplitOption.cs rename to Kavita.Models/Entities/Enums/PageSplitOption.cs index 7b421240c..864f5adb3 100644 --- a/API/Entities/Enums/PageSplitOption.cs +++ b/Kavita.Models/Entities/Enums/PageSplitOption.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum PageSplitOption { diff --git a/API/Entities/Enums/PdfRenderResolution.cs b/Kavita.Models/Entities/Enums/PdfRenderResolution.cs similarity index 86% rename from API/Entities/Enums/PdfRenderResolution.cs rename to Kavita.Models/Entities/Enums/PdfRenderResolution.cs index b6d0fec93..84f31ea50 100644 --- a/API/Entities/Enums/PdfRenderResolution.cs +++ b/Kavita.Models/Entities/Enums/PdfRenderResolution.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum PdfRenderResolution { diff --git a/API/Entities/Enums/PdfRenderResolutionExtensions.cs b/Kavita.Models/Entities/Enums/PdfRenderResolutionExtensions.cs similarity index 90% rename from API/Entities/Enums/PdfRenderResolutionExtensions.cs rename to Kavita.Models/Entities/Enums/PdfRenderResolutionExtensions.cs index cadb82abc..4d721cc1c 100644 --- a/API/Entities/Enums/PdfRenderResolutionExtensions.cs +++ b/Kavita.Models/Entities/Enums/PdfRenderResolutionExtensions.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public static class PdfRenderResolutionExtensions { diff --git a/API/Entities/Enums/PersonRole.cs b/Kavita.Models/Entities/Enums/PersonRole.cs similarity index 93% rename from API/Entities/Enums/PersonRole.cs rename to Kavita.Models/Entities/Enums/PersonRole.cs index f7ad45021..5519f2add 100644 --- a/API/Entities/Enums/PersonRole.cs +++ b/Kavita.Models/Entities/Enums/PersonRole.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum PersonRole { diff --git a/API/Entities/Enums/PublicationStatus.cs b/Kavita.Models/Entities/Enums/PublicationStatus.cs similarity index 95% rename from API/Entities/Enums/PublicationStatus.cs rename to Kavita.Models/Entities/Enums/PublicationStatus.cs index 614bc0604..542e08919 100644 --- a/API/Entities/Enums/PublicationStatus.cs +++ b/Kavita.Models/Entities/Enums/PublicationStatus.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum PublicationStatus { diff --git a/API/Entities/Enums/RatingAuthority.cs b/Kavita.Models/Entities/Enums/RatingAuthority.cs similarity index 88% rename from API/Entities/Enums/RatingAuthority.cs rename to Kavita.Models/Entities/Enums/RatingAuthority.cs index 0f358a9a7..2bb0cc860 100644 --- a/API/Entities/Enums/RatingAuthority.cs +++ b/Kavita.Models/Entities/Enums/RatingAuthority.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum RatingAuthority { diff --git a/API/Entities/Enums/ReaderMode.cs b/Kavita.Models/Entities/Enums/ReaderMode.cs similarity index 84% rename from API/Entities/Enums/ReaderMode.cs rename to Kavita.Models/Entities/Enums/ReaderMode.cs index e1353ad59..cc85911f2 100644 --- a/API/Entities/Enums/ReaderMode.cs +++ b/Kavita.Models/Entities/Enums/ReaderMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum ReaderMode { diff --git a/API/Entities/Enums/ReadingDirection.cs b/Kavita.Models/Entities/Enums/ReadingDirection.cs similarity index 63% rename from API/Entities/Enums/ReadingDirection.cs rename to Kavita.Models/Entities/Enums/ReadingDirection.cs index 8804ca6d4..016e35e1b 100644 --- a/API/Entities/Enums/ReadingDirection.cs +++ b/Kavita.Models/Entities/Enums/ReadingDirection.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum ReadingDirection { diff --git a/API/Entities/Enums/ReadingProfileKind.cs b/Kavita.Models/Entities/Enums/ReadingProfileKind.cs similarity index 91% rename from API/Entities/Enums/ReadingProfileKind.cs rename to Kavita.Models/Entities/Enums/ReadingProfileKind.cs index 0f9cfa20b..b1b0b71e6 100644 --- a/API/Entities/Enums/ReadingProfileKind.cs +++ b/Kavita.Models/Entities/Enums/ReadingProfileKind.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum ReadingProfileKind { diff --git a/API/Entities/Enums/RelationKind.cs b/Kavita.Models/Entities/Enums/RelationKind.cs similarity index 98% rename from API/Entities/Enums/RelationKind.cs rename to Kavita.Models/Entities/Enums/RelationKind.cs index 61516ec0d..d04dacfbd 100644 --- a/API/Entities/Enums/RelationKind.cs +++ b/Kavita.Models/Entities/Enums/RelationKind.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Represents a relationship between Series diff --git a/API/Entities/Enums/ScalingOption.cs b/Kavita.Models/Entities/Enums/ScalingOption.cs similarity index 71% rename from API/Entities/Enums/ScalingOption.cs rename to Kavita.Models/Entities/Enums/ScalingOption.cs index f0b357898..940442b38 100644 --- a/API/Entities/Enums/ScalingOption.cs +++ b/Kavita.Models/Entities/Enums/ScalingOption.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum ScalingOption { diff --git a/Kavita.Models/Entities/Enums/ScrobbleProvider.cs b/Kavita.Models/Entities/Enums/ScrobbleProvider.cs new file mode 100644 index 000000000..556244983 --- /dev/null +++ b/Kavita.Models/Entities/Enums/ScrobbleProvider.cs @@ -0,0 +1,20 @@ +using System; + +namespace Kavita.Models.Entities.Enums; + +/// +/// Misleading name but is the source of data (like a review coming from AniList) +/// +public enum ScrobbleProvider +{ + /// + /// For now, this means data comes from within this instance of Kavita + /// + Kavita = 0, + AniList = 1, + Mal = 2, + [Obsolete("No longer supported")] + GoogleBooks = 3, + Cbr = 4, + Hardcover = 5, +} diff --git a/API/Entities/Enums/ServerSettingKey.cs b/Kavita.Models/Entities/Enums/ServerSettingKey.cs similarity index 99% rename from API/Entities/Enums/ServerSettingKey.cs rename to Kavita.Models/Entities/Enums/ServerSettingKey.cs index 43fc63ed2..175048bc8 100644 --- a/API/Entities/Enums/ServerSettingKey.cs +++ b/Kavita.Models/Entities/Enums/ServerSettingKey.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// 15 is blocked as it was EnableSwaggerUi, which is no longer used diff --git a/API/Entities/Enums/SyncKey.cs b/Kavita.Models/Entities/Enums/SyncKey.cs similarity index 81% rename from API/Entities/Enums/SyncKey.cs rename to Kavita.Models/Entities/Enums/SyncKey.cs index 6e5346ab8..7e7c80a7d 100644 --- a/API/Entities/Enums/SyncKey.cs +++ b/Kavita.Models/Entities/Enums/SyncKey.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum SyncKey { diff --git a/API/Entities/Enums/Theme/ThemeProvider.cs b/Kavita.Models/Entities/Enums/Theme/ThemeProvider.cs similarity index 88% rename from API/Entities/Enums/Theme/ThemeProvider.cs rename to Kavita.Models/Entities/Enums/Theme/ThemeProvider.cs index cc12a552e..e3dd64dc4 100644 --- a/API/Entities/Enums/Theme/ThemeProvider.cs +++ b/Kavita.Models/Entities/Enums/Theme/ThemeProvider.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.Theme; +namespace Kavita.Models.Entities.Enums.Theme; public enum ThemeProvider { diff --git a/API/Entities/Enums/User/AuthKeyProvider.cs b/Kavita.Models/Entities/Enums/User/AuthKeyProvider.cs similarity index 86% rename from API/Entities/Enums/User/AuthKeyProvider.cs rename to Kavita.Models/Entities/Enums/User/AuthKeyProvider.cs index 4a2da9ada..578b22204 100644 --- a/API/Entities/Enums/User/AuthKeyProvider.cs +++ b/Kavita.Models/Entities/Enums/User/AuthKeyProvider.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.User; +namespace Kavita.Models.Entities.Enums.User; public enum AuthKeyProvider { diff --git a/API/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs b/Kavita.Models/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs similarity index 87% rename from API/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs rename to Kavita.Models/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs index 5fdb2f5f4..347bbe07a 100644 --- a/API/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public class AppUserOpdsPreferences { diff --git a/API/Entities/Enums/UserPreferences/AppUserSocialPreferences.cs b/Kavita.Models/Entities/Enums/UserPreferences/AppUserSocialPreferences.cs similarity index 96% rename from API/Entities/Enums/UserPreferences/AppUserSocialPreferences.cs rename to Kavita.Models/Entities/Enums/UserPreferences/AppUserSocialPreferences.cs index 4b873ebed..221a03026 100644 --- a/API/Entities/Enums/UserPreferences/AppUserSocialPreferences.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/AppUserSocialPreferences.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public class AppUserSocialPreferences { diff --git a/API/Entities/Enums/UserPreferences/KeyBind.cs b/Kavita.Models/Entities/Enums/UserPreferences/KeyBind.cs similarity index 85% rename from API/Entities/Enums/UserPreferences/KeyBind.cs rename to Kavita.Models/Entities/Enums/UserPreferences/KeyBind.cs index 5a15ec5ec..d23cec012 100644 --- a/API/Entities/Enums/UserPreferences/KeyBind.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/KeyBind.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; #nullable enable public sealed record KeyBind diff --git a/API/Entities/Enums/UserPreferences/KeyBindTarget.cs b/Kavita.Models/Entities/Enums/UserPreferences/KeyBindTarget.cs similarity index 94% rename from API/Entities/Enums/UserPreferences/KeyBindTarget.cs rename to Kavita.Models/Entities/Enums/UserPreferences/KeyBindTarget.cs index 666f77c73..91c784617 100644 --- a/API/Entities/Enums/UserPreferences/KeyBindTarget.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/KeyBindTarget.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public enum KeyBindTarget { diff --git a/API/Entities/Enums/UserPreferences/PageLayoutMode.cs b/Kavita.Models/Entities/Enums/UserPreferences/PageLayoutMode.cs similarity index 72% rename from API/Entities/Enums/UserPreferences/PageLayoutMode.cs rename to Kavita.Models/Entities/Enums/UserPreferences/PageLayoutMode.cs index 328e90d35..182da3e34 100644 --- a/API/Entities/Enums/UserPreferences/PageLayoutMode.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/PageLayoutMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public enum PageLayoutMode { diff --git a/API/Entities/Enums/UserPreferences/PdfBookMode.cs b/Kavita.Models/Entities/Enums/UserPreferences/PdfBookMode.cs similarity index 89% rename from API/Entities/Enums/UserPreferences/PdfBookMode.cs rename to Kavita.Models/Entities/Enums/UserPreferences/PdfBookMode.cs index 5946e17c5..1a0c719a6 100644 --- a/API/Entities/Enums/UserPreferences/PdfBookMode.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/PdfBookMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public enum PdfLayoutMode { diff --git a/API/Entities/Enums/UserPreferences/PdfScrollMode.cs b/Kavita.Models/Entities/Enums/UserPreferences/PdfScrollMode.cs similarity index 87% rename from API/Entities/Enums/UserPreferences/PdfScrollMode.cs rename to Kavita.Models/Entities/Enums/UserPreferences/PdfScrollMode.cs index 93cc5bd2e..81c54bd7a 100644 --- a/API/Entities/Enums/UserPreferences/PdfScrollMode.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/PdfScrollMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; /// /// Enum values match PdfViewer's enums diff --git a/API/Entities/Enums/UserPreferences/PdfSpreadMode.cs b/Kavita.Models/Entities/Enums/UserPreferences/PdfSpreadMode.cs similarity index 76% rename from API/Entities/Enums/UserPreferences/PdfSpreadMode.cs rename to Kavita.Models/Entities/Enums/UserPreferences/PdfSpreadMode.cs index 412239d4a..50f4bd99c 100644 --- a/API/Entities/Enums/UserPreferences/PdfSpreadMode.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/PdfSpreadMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public enum PdfSpreadMode { diff --git a/API/Entities/Enums/UserPreferences/PdfTheme.cs b/Kavita.Models/Entities/Enums/UserPreferences/PdfTheme.cs similarity index 71% rename from API/Entities/Enums/UserPreferences/PdfTheme.cs rename to Kavita.Models/Entities/Enums/UserPreferences/PdfTheme.cs index 0efe1dfde..8e4292cdf 100644 --- a/API/Entities/Enums/UserPreferences/PdfTheme.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/PdfTheme.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public enum PdfTheme { diff --git a/API/Entities/Enums/WritingStyle.cs b/Kavita.Models/Entities/Enums/WritingStyle.cs similarity index 91% rename from API/Entities/Enums/WritingStyle.cs rename to Kavita.Models/Entities/Enums/WritingStyle.cs index 37d50c160..0f515e12b 100644 --- a/API/Entities/Enums/WritingStyle.cs +++ b/Kavita.Models/Entities/Enums/WritingStyle.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Represents the writing styles for the book-reader diff --git a/API/Entities/EpubFont.cs b/Kavita.Models/Entities/EpubFont.cs similarity index 85% rename from API/Entities/EpubFont.cs rename to Kavita.Models/Entities/EpubFont.cs index 0cf745db6..dc9373d02 100644 --- a/API/Entities/EpubFont.cs +++ b/Kavita.Models/Entities/EpubFont.cs @@ -1,9 +1,8 @@ using System; -using API.Entities.Enums.Font; -using API.Entities.Interfaces; -using API.Services; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Enums.Font; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// Represents a user provider font to be used in the epub reader @@ -34,4 +33,6 @@ public class EpubFont: IEntityDate public DateTime CreatedUtc { get; set; } public DateTime LastModified { get; set; } public DateTime LastModifiedUtc { get; set; } + + public static readonly string DefaultFont = "Default"; } diff --git a/API/Entities/FolderPath.cs b/Kavita.Models/Entities/FolderPath.cs similarity index 95% rename from API/Entities/FolderPath.cs rename to Kavita.Models/Entities/FolderPath.cs index 2d5684ba9..4d0e31a2f 100644 --- a/API/Entities/FolderPath.cs +++ b/Kavita.Models/Entities/FolderPath.cs @@ -1,7 +1,7 @@  using System; -namespace API.Entities; +namespace Kavita.Models.Entities; public class FolderPath { diff --git a/API/Entities/Genre.cs b/Kavita.Models/Entities/Genre.cs similarity index 85% rename from API/Entities/Genre.cs rename to Kavita.Models/Entities/Genre.cs index 56cb446b2..5750bb471 100644 --- a/API/Entities/Genre.cs +++ b/Kavita.Models/Entities/Genre.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using API.Entities.Metadata; +using Kavita.Models.Entities.Metadata; using Microsoft.EntityFrameworkCore; -namespace API.Entities; +namespace Kavita.Models.Entities; [Index(nameof(NormalizedTitle), IsUnique = true)] public class Genre diff --git a/API/Entities/HighlightSlot.cs b/Kavita.Models/Entities/HighlightSlot.cs similarity index 89% rename from API/Entities/HighlightSlot.cs rename to Kavita.Models/Entities/HighlightSlot.cs index 2f951b290..4720b3871 100644 --- a/API/Entities/HighlightSlot.cs +++ b/Kavita.Models/Entities/HighlightSlot.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities; public sealed record HighlightSlot { diff --git a/API/Entities/History/KavitaPlusHistory.cs b/Kavita.Models/Entities/History/KavitaPlusHistory.cs similarity index 73% rename from API/Entities/History/KavitaPlusHistory.cs rename to Kavita.Models/Entities/History/KavitaPlusHistory.cs index 81b7e5e40..faa8dbdee 100644 --- a/API/Entities/History/KavitaPlusHistory.cs +++ b/Kavita.Models/Entities/History/KavitaPlusHistory.cs @@ -1,4 +1,4 @@ -namespace API.Entities.History; +namespace Kavita.Models.Entities.History; /// /// Records history of actions Kavita+ takes diff --git a/API/Entities/History/ManualMigrationHistory.cs b/Kavita.Models/Entities/History/ManualMigrationHistory.cs similarity index 91% rename from API/Entities/History/ManualMigrationHistory.cs rename to Kavita.Models/Entities/History/ManualMigrationHistory.cs index 4e22d0f0c..34cce7d26 100644 --- a/API/Entities/History/ManualMigrationHistory.cs +++ b/Kavita.Models/Entities/History/ManualMigrationHistory.cs @@ -1,7 +1,7 @@ using System; using Kavita.Common.EnvironmentInfo; -namespace API.Entities.History; +namespace Kavita.Models.Entities.History; /// /// This will track manual migrations so that I can use simple selects to check if a Manual Migration is needed diff --git a/API/Entities/Interfaces/IEntityDate.cs b/Kavita.Models/Entities/Interfaces/IEntityDate.cs similarity index 82% rename from API/Entities/Interfaces/IEntityDate.cs rename to Kavita.Models/Entities/Interfaces/IEntityDate.cs index 3ffcebfd2..ca8aabc3c 100644 --- a/API/Entities/Interfaces/IEntityDate.cs +++ b/Kavita.Models/Entities/Interfaces/IEntityDate.cs @@ -1,6 +1,6 @@ using System; -namespace API.Entities.Interfaces; +namespace Kavita.Models.Entities.Interfaces; public interface IEntityDate { diff --git a/API/Entities/Interfaces/IHasConcurrencyToken.cs b/Kavita.Models/Entities/Interfaces/IHasConcurrencyToken.cs similarity index 89% rename from API/Entities/Interfaces/IHasConcurrencyToken.cs rename to Kavita.Models/Entities/Interfaces/IHasConcurrencyToken.cs index 3cd3f1adf..b469ca3e0 100644 --- a/API/Entities/Interfaces/IHasConcurrencyToken.cs +++ b/Kavita.Models/Entities/Interfaces/IHasConcurrencyToken.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Interfaces; +namespace Kavita.Models.Entities.Interfaces; /// /// An interface abstracting an entity that has a concurrency token. diff --git a/API/Entities/Interfaces/IHasCoverImage.cs b/Kavita.Models/Entities/Interfaces/IHasCoverImage.cs similarity index 93% rename from API/Entities/Interfaces/IHasCoverImage.cs rename to Kavita.Models/Entities/Interfaces/IHasCoverImage.cs index 5570e37eb..5696da153 100644 --- a/API/Entities/Interfaces/IHasCoverImage.cs +++ b/Kavita.Models/Entities/Interfaces/IHasCoverImage.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Interfaces; +namespace Kavita.Models.Entities.Interfaces; #nullable enable diff --git a/API/Entities/Interfaces/IHasKPlusMetadata.cs b/Kavita.Models/Entities/Interfaces/IHasKPlusMetadata.cs similarity index 71% rename from API/Entities/Interfaces/IHasKPlusMetadata.cs rename to Kavita.Models/Entities/Interfaces/IHasKPlusMetadata.cs index 062afd7e1..717372cb0 100644 --- a/API/Entities/Interfaces/IHasKPlusMetadata.cs +++ b/Kavita.Models/Entities/Interfaces/IHasKPlusMetadata.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.MetadataMatching; +using Kavita.Models.Entities.MetadataMatching; -namespace API.Entities.Interfaces; +namespace Kavita.Models.Entities.Interfaces; public interface IHasKPlusMetadata { diff --git a/API/Entities/Interfaces/IHasReadTimeEstimate.cs b/Kavita.Models/Entities/Interfaces/IHasReadTimeEstimate.cs similarity index 92% rename from API/Entities/Interfaces/IHasReadTimeEstimate.cs rename to Kavita.Models/Entities/Interfaces/IHasReadTimeEstimate.cs index 7816da054..f41c35509 100644 --- a/API/Entities/Interfaces/IHasReadTimeEstimate.cs +++ b/Kavita.Models/Entities/Interfaces/IHasReadTimeEstimate.cs @@ -1,6 +1,5 @@ -using API.Services.Reading; - -namespace API.Entities.Interfaces; + +namespace Kavita.Models.Entities.Interfaces; /// /// Entity has read time estimate properties to estimate time to read diff --git a/API/Entities/Interfaces/ITheme.cs b/Kavita.Models/Entities/Interfaces/ITheme.cs similarity index 76% rename from API/Entities/Interfaces/ITheme.cs rename to Kavita.Models/Entities/Interfaces/ITheme.cs index 216136569..c33565610 100644 --- a/API/Entities/Interfaces/ITheme.cs +++ b/Kavita.Models/Entities/Interfaces/ITheme.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.Theme; +using Kavita.Models.Entities.Enums.Theme; -namespace API.Entities.Interfaces; +namespace Kavita.Models.Entities.Interfaces; /// /// A theme in some kind diff --git a/API/Entities/Library.cs b/Kavita.Models/Entities/Library.cs similarity index 96% rename from API/Entities/Library.cs rename to Kavita.Models/Entities/Library.cs index 66fd0eda9..0881a5171 100644 --- a/API/Entities/Library.cs +++ b/Kavita.Models/Entities/Library.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; -namespace API.Entities; +namespace Kavita.Models.Entities; public class Library : IEntityDate, IHasCoverImage { diff --git a/API/Entities/LibraryExcludedGlob.cs b/Kavita.Models/Entities/LibraryExcludedGlob.cs similarity index 84% rename from API/Entities/LibraryExcludedGlob.cs rename to Kavita.Models/Entities/LibraryExcludedGlob.cs index 69bc86342..9998ad63c 100644 --- a/API/Entities/LibraryExcludedGlob.cs +++ b/Kavita.Models/Entities/LibraryExcludedGlob.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities; public class LibraryExcludePattern { diff --git a/API/Entities/LibraryFileTypeGroup.cs b/Kavita.Models/Entities/LibraryFileTypeGroup.cs similarity index 74% rename from API/Entities/LibraryFileTypeGroup.cs rename to Kavita.Models/Entities/LibraryFileTypeGroup.cs index a3af30d80..c75c55e54 100644 --- a/API/Entities/LibraryFileTypeGroup.cs +++ b/Kavita.Models/Entities/LibraryFileTypeGroup.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Entities; +namespace Kavita.Models.Entities; public class LibraryFileTypeGroup { diff --git a/API/Entities/MangaFile.cs b/Kavita.Models/Entities/MangaFile.cs similarity index 95% rename from API/Entities/MangaFile.cs rename to Kavita.Models/Entities/MangaFile.cs index 2f1708226..909fcc312 100644 --- a/API/Entities/MangaFile.cs +++ b/Kavita.Models/Entities/MangaFile.cs @@ -1,10 +1,10 @@  using System; using System.IO; -using API.Entities.Enums; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// Represents a wrapper to the underlying file. This provides information around file, like number of pages, format, etc. diff --git a/API/Entities/MediaError.cs b/Kavita.Models/Entities/MediaError.cs similarity index 92% rename from API/Entities/MediaError.cs rename to Kavita.Models/Entities/MediaError.cs index 33e55ed8e..ecff2e2d5 100644 --- a/API/Entities/MediaError.cs +++ b/Kavita.Models/Entities/MediaError.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// Represents issues found during scanning or interacting with media. For example) Can't open file, corrupt media, missing content in epub. diff --git a/API/Entities/Metadata/ExternalRating.cs b/Kavita.Models/Entities/Metadata/ExternalRating.cs similarity index 89% rename from API/Entities/Metadata/ExternalRating.cs rename to Kavita.Models/Entities/Metadata/ExternalRating.cs index 7fc2b9353..b836c4094 100644 --- a/API/Entities/Metadata/ExternalRating.cs +++ b/Kavita.Models/Entities/Metadata/ExternalRating.cs @@ -1,8 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; public class ExternalRating { diff --git a/API/Entities/Metadata/ExternalRecommendation.cs b/Kavita.Models/Entities/Metadata/ExternalRecommendation.cs similarity index 91% rename from API/Entities/Metadata/ExternalRecommendation.cs rename to Kavita.Models/Entities/Metadata/ExternalRecommendation.cs index c5bb98f20..a5733af7e 100644 --- a/API/Entities/Metadata/ExternalRecommendation.cs +++ b/Kavita.Models/Entities/Metadata/ExternalRecommendation.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; using Microsoft.EntityFrameworkCore; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; [Index(nameof(SeriesId), IsUnique = false)] public class ExternalRecommendation diff --git a/API/Entities/Metadata/ExternalReview.cs b/Kavita.Models/Entities/Metadata/ExternalReview.cs similarity index 93% rename from API/Entities/Metadata/ExternalReview.cs rename to Kavita.Models/Entities/Metadata/ExternalReview.cs index 73c71e5ee..55be48f9b 100644 --- a/API/Entities/Metadata/ExternalReview.cs +++ b/Kavita.Models/Entities/Metadata/ExternalReview.cs @@ -1,8 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; /// /// Represents an Externally supplied Review for a given Series diff --git a/API/Entities/Metadata/ExternalSeriesMetadata.cs b/Kavita.Models/Entities/Metadata/ExternalSeriesMetadata.cs similarity index 96% rename from API/Entities/Metadata/ExternalSeriesMetadata.cs rename to Kavita.Models/Entities/Metadata/ExternalSeriesMetadata.cs index 1ab37ba3c..2e31b3924 100644 --- a/API/Entities/Metadata/ExternalSeriesMetadata.cs +++ b/Kavita.Models/Entities/Metadata/ExternalSeriesMetadata.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; /// /// External Metadata from Kavita+ for a Series diff --git a/API/Entities/Metadata/SeriesBlacklist.cs b/Kavita.Models/Entities/Metadata/SeriesBlacklist.cs similarity index 89% rename from API/Entities/Metadata/SeriesBlacklist.cs rename to Kavita.Models/Entities/Metadata/SeriesBlacklist.cs index 3d262eeb4..c231ab6de 100644 --- a/API/Entities/Metadata/SeriesBlacklist.cs +++ b/Kavita.Models/Entities/Metadata/SeriesBlacklist.cs @@ -1,6 +1,6 @@ using System; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; /// /// A blacklist of Series for Kavita+ diff --git a/API/Entities/Metadata/SeriesMetadata.cs b/Kavita.Models/Entities/Metadata/SeriesMetadata.cs similarity index 96% rename from API/Entities/Metadata/SeriesMetadata.cs rename to Kavita.Models/Entities/Metadata/SeriesMetadata.cs index e304dee6c..f5fdad0c8 100644 --- a/API/Entities/Metadata/SeriesMetadata.cs +++ b/Kavita.Models/Entities/Metadata/SeriesMetadata.cs @@ -2,13 +2,13 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Entities.MetadataMatching; -using API.Entities.Person; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; using Microsoft.EntityFrameworkCore; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; [Index(nameof(Id), nameof(SeriesId), IsUnique = true)] public class SeriesMetadata : IHasConcurrencyToken, IHasKPlusMetadata diff --git a/API/Entities/Metadata/SeriesRelation.cs b/Kavita.Models/Entities/Metadata/SeriesRelation.cs similarity index 87% rename from API/Entities/Metadata/SeriesRelation.cs rename to Kavita.Models/Entities/Metadata/SeriesRelation.cs index 7493f945b..08aea5cf5 100644 --- a/API/Entities/Metadata/SeriesRelation.cs +++ b/Kavita.Models/Entities/Metadata/SeriesRelation.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; /// /// A relation flows between one series and another. diff --git a/API/Entities/MetadataMatching/MetadataFieldMapping.cs b/Kavita.Models/Entities/MetadataMatching/MetadataFieldMapping.cs similarity index 85% rename from API/Entities/MetadataMatching/MetadataFieldMapping.cs rename to Kavita.Models/Entities/MetadataMatching/MetadataFieldMapping.cs index e7dd88c03..abf18203b 100644 --- a/API/Entities/MetadataMatching/MetadataFieldMapping.cs +++ b/Kavita.Models/Entities/MetadataMatching/MetadataFieldMapping.cs @@ -1,7 +1,7 @@ -using API.Entities.Enums; -using API.Entities.MetadataMatching; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; -namespace API.Entities; +namespace Kavita.Models.Entities; public class MetadataFieldMapping { diff --git a/API/Entities/MetadataMatching/MetadataSettingField.cs b/Kavita.Models/Entities/MetadataMatching/MetadataSettingField.cs similarity index 90% rename from API/Entities/MetadataMatching/MetadataSettingField.cs rename to Kavita.Models/Entities/MetadataMatching/MetadataSettingField.cs index 9333c269e..acc14313b 100644 --- a/API/Entities/MetadataMatching/MetadataSettingField.cs +++ b/Kavita.Models/Entities/MetadataMatching/MetadataSettingField.cs @@ -1,4 +1,4 @@ -namespace API.Entities.MetadataMatching; +namespace Kavita.Models.Entities.MetadataMatching; /// /// Represents which field that can be written to as an override when already locked diff --git a/API/Entities/MetadataMatching/MetadataSettings.cs b/Kavita.Models/Entities/MetadataMatching/MetadataSettings.cs similarity index 97% rename from API/Entities/MetadataMatching/MetadataSettings.cs rename to Kavita.Models/Entities/MetadataMatching/MetadataSettings.cs index b72329342..6d4caf64d 100644 --- a/API/Entities/MetadataMatching/MetadataSettings.cs +++ b/Kavita.Models/Entities/MetadataMatching/MetadataSettings.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Entities.MetadataMatching; +namespace Kavita.Models.Entities.MetadataMatching; /// /// Handles the metadata settings for Kavita+ diff --git a/API/Entities/Person/ChapterPeople.cs b/Kavita.Models/Entities/Person/ChapterPeople.cs similarity index 88% rename from API/Entities/Person/ChapterPeople.cs rename to Kavita.Models/Entities/Person/ChapterPeople.cs index c6a08a7dd..e366b7816 100644 --- a/API/Entities/Person/ChapterPeople.cs +++ b/Kavita.Models/Entities/Person/ChapterPeople.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Entities.Person; +namespace Kavita.Models.Entities.Person; public class ChapterPeople { diff --git a/API/Entities/Person/Person.cs b/Kavita.Models/Entities/Person/Person.cs similarity index 95% rename from API/Entities/Person/Person.cs rename to Kavita.Models/Entities/Person/Person.cs index ed57fd6d3..8a360d154 100644 --- a/API/Entities/Person/Person.cs +++ b/Kavita.Models/Entities/Person/Person.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities.Person; +namespace Kavita.Models.Entities.Person; public class Person : IHasCoverImage { diff --git a/API/Entities/Person/PersonAlias.cs b/Kavita.Models/Entities/Person/PersonAlias.cs similarity index 85% rename from API/Entities/Person/PersonAlias.cs rename to Kavita.Models/Entities/Person/PersonAlias.cs index f053f608d..d2f2027c1 100644 --- a/API/Entities/Person/PersonAlias.cs +++ b/Kavita.Models/Entities/Person/PersonAlias.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Person; +namespace Kavita.Models.Entities.Person; public class PersonAlias { diff --git a/API/Entities/Person/SeriesMetadataPeople.cs b/Kavita.Models/Entities/Person/SeriesMetadataPeople.cs similarity index 84% rename from API/Entities/Person/SeriesMetadataPeople.cs rename to Kavita.Models/Entities/Person/SeriesMetadataPeople.cs index caea10cd6..3de668e0e 100644 --- a/API/Entities/Person/SeriesMetadataPeople.cs +++ b/Kavita.Models/Entities/Person/SeriesMetadataPeople.cs @@ -1,7 +1,7 @@ -using API.Entities.Enums; -using API.Entities.Metadata; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; -namespace API.Entities.Person; +namespace Kavita.Models.Entities.Person; public class SeriesMetadataPeople { diff --git a/API/Entities/Progress/AppUserProgress.cs b/Kavita.Models/Entities/Progress/AppUserProgress.cs similarity index 94% rename from API/Entities/Progress/AppUserProgress.cs rename to Kavita.Models/Entities/Progress/AppUserProgress.cs index bf4283aa0..6e0458056 100644 --- a/API/Entities/Progress/AppUserProgress.cs +++ b/Kavita.Models/Entities/Progress/AppUserProgress.cs @@ -1,7 +1,8 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; -namespace API.Entities.Progress; +namespace Kavita.Models.Entities.Progress; /// /// Represents the progress a single user has on a given Chapter. diff --git a/API/Entities/Progress/AppUserReadingHistory.cs b/Kavita.Models/Entities/Progress/AppUserReadingHistory.cs similarity index 85% rename from API/Entities/Progress/AppUserReadingHistory.cs rename to Kavita.Models/Entities/Progress/AppUserReadingHistory.cs index 009687732..3190217db 100644 --- a/API/Entities/Progress/AppUserReadingHistory.cs +++ b/Kavita.Models/Entities/Progress/AppUserReadingHistory.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; -using API.DTOs.Progress; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Entities.Progress; +namespace Kavita.Models.Entities.Progress; /// /// Represents a single day's worth of Reading Sessions diff --git a/API/Entities/Progress/AppUserReadingSession.cs b/Kavita.Models/Entities/Progress/AppUserReadingSession.cs similarity index 89% rename from API/Entities/Progress/AppUserReadingSession.cs rename to Kavita.Models/Entities/Progress/AppUserReadingSession.cs index be81d69d3..34a6b2f33 100644 --- a/API/Entities/Progress/AppUserReadingSession.cs +++ b/Kavita.Models/Entities/Progress/AppUserReadingSession.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using API.Entities.Interfaces; -using API.Services.Reading; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Entities.Progress; +namespace Kavita.Models.Entities.Progress; /// /// Represents a reading session for a user. See diff --git a/API/Entities/Progress/AppUserReadingSessionActivityData.cs b/Kavita.Models/Entities/Progress/AppUserReadingSessionActivityData.cs similarity index 95% rename from API/Entities/Progress/AppUserReadingSessionActivityData.cs rename to Kavita.Models/Entities/Progress/AppUserReadingSessionActivityData.cs index fae6d1c69..3c5bdc387 100644 --- a/API/Entities/Progress/AppUserReadingSessionActivityData.cs +++ b/Kavita.Models/Entities/Progress/AppUserReadingSessionActivityData.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using API.DTOs.Progress; -using API.Entities.Enums; -using Microsoft.EntityFrameworkCore; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; -namespace API.Entities.Progress; +namespace Kavita.Models.Entities.Progress; #nullable enable public class AppUserReadingSessionActivityData diff --git a/API/Entities/Progress/ClientInfoData.cs b/Kavita.Models/Entities/Progress/ClientInfoData.cs similarity index 96% rename from API/Entities/Progress/ClientInfoData.cs rename to Kavita.Models/Entities/Progress/ClientInfoData.cs index 3c11678fd..f25420f98 100644 --- a/API/Entities/Progress/ClientInfoData.cs +++ b/Kavita.Models/Entities/Progress/ClientInfoData.cs @@ -1,8 +1,7 @@ using System; -using API.Constants; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Entities.Progress; +namespace Kavita.Models.Entities.Progress; #nullable enable public class ClientInfoData diff --git a/API/Entities/ReadingList.cs b/Kavita.Models/Entities/ReadingList.cs similarity index 93% rename from API/Entities/ReadingList.cs rename to Kavita.Models/Entities/ReadingList.cs index 4a11845af..c4916aa1d 100644 --- a/API/Entities/ReadingList.cs +++ b/Kavita.Models/Entities/ReadingList.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; -namespace API.Entities; +namespace Kavita.Models.Entities; #nullable enable diff --git a/API/Entities/ReadingListItem.cs b/Kavita.Models/Entities/ReadingListItem.cs similarity index 94% rename from API/Entities/ReadingListItem.cs rename to Kavita.Models/Entities/ReadingListItem.cs index c9d1de5db..6dfa1cc9d 100644 --- a/API/Entities/ReadingListItem.cs +++ b/Kavita.Models/Entities/ReadingListItem.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities; public class ReadingListItem { diff --git a/API/Entities/Scrobble/ScrobbleError.cs b/Kavita.Models/Entities/Scrobble/ScrobbleError.cs similarity index 90% rename from API/Entities/Scrobble/ScrobbleError.cs rename to Kavita.Models/Entities/Scrobble/ScrobbleError.cs index 5db780bfc..32e416c51 100644 --- a/API/Entities/Scrobble/ScrobbleError.cs +++ b/Kavita.Models/Entities/Scrobble/ScrobbleError.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities.Scrobble; +namespace Kavita.Models.Entities.Scrobble; /// /// When a series is not found, we report it here diff --git a/API/Entities/Scrobble/ScrobbleEvent.cs b/Kavita.Models/Entities/Scrobble/ScrobbleEvent.cs similarity index 93% rename from API/Entities/Scrobble/ScrobbleEvent.cs rename to Kavita.Models/Entities/Scrobble/ScrobbleEvent.cs index 89d2701bc..977631f7c 100644 --- a/API/Entities/Scrobble/ScrobbleEvent.cs +++ b/Kavita.Models/Entities/Scrobble/ScrobbleEvent.cs @@ -1,8 +1,9 @@ using System; -using API.DTOs.Scrobbling; -using API.Entities.Interfaces; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; -namespace API.Entities.Scrobble; +namespace Kavita.Models.Entities.Scrobble; #nullable enable /// diff --git a/API/Entities/Scrobble/ScrobbleEventFilter.cs b/Kavita.Models/Entities/Scrobble/ScrobbleEventFilter.cs similarity index 93% rename from API/Entities/Scrobble/ScrobbleEventFilter.cs rename to Kavita.Models/Entities/Scrobble/ScrobbleEventFilter.cs index 1153e90e9..a029c6c70 100644 --- a/API/Entities/Scrobble/ScrobbleEventFilter.cs +++ b/Kavita.Models/Entities/Scrobble/ScrobbleEventFilter.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Scrobble; +namespace Kavita.Models.Entities.Scrobble; public class ScrobbleEventFilter { diff --git a/API/Entities/Scrobble/ScrobbleEventSortField.cs b/Kavita.Models/Entities/Scrobble/ScrobbleEventSortField.cs similarity index 78% rename from API/Entities/Scrobble/ScrobbleEventSortField.cs rename to Kavita.Models/Entities/Scrobble/ScrobbleEventSortField.cs index 51b3a2146..fd65e47ae 100644 --- a/API/Entities/Scrobble/ScrobbleEventSortField.cs +++ b/Kavita.Models/Entities/Scrobble/ScrobbleEventSortField.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Scrobble; +namespace Kavita.Models.Entities.Scrobble; public enum ScrobbleEventSortField { diff --git a/API/Entities/Scrobble/ScrobbleHold.cs b/Kavita.Models/Entities/Scrobble/ScrobbleHold.cs similarity index 78% rename from API/Entities/Scrobble/ScrobbleHold.cs rename to Kavita.Models/Entities/Scrobble/ScrobbleHold.cs index c6f3afdb1..073723a94 100644 --- a/API/Entities/Scrobble/ScrobbleHold.cs +++ b/Kavita.Models/Entities/Scrobble/ScrobbleHold.cs @@ -1,7 +1,8 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; -namespace API.Entities.Scrobble; +namespace Kavita.Models.Entities.Scrobble; public class ScrobbleHold : IEntityDate { diff --git a/API/Entities/Series.cs b/Kavita.Models/Entities/Series.cs similarity index 96% rename from API/Entities/Series.cs rename to Kavita.Models/Entities/Series.cs index 395236ae3..f9d4f760d 100644 --- a/API/Entities/Series.cs +++ b/Kavita.Models/Entities/Series.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Entities.Metadata; -using API.Entities.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; -namespace API.Entities; +namespace Kavita.Models.Entities; public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage { diff --git a/API/Entities/ServerSetting.cs b/Kavita.Models/Entities/ServerSetting.cs similarity index 82% rename from API/Entities/ServerSetting.cs rename to Kavita.Models/Entities/ServerSetting.cs index 37e85efae..95bcd83e7 100644 --- a/API/Entities/ServerSetting.cs +++ b/Kavita.Models/Entities/ServerSetting.cs @@ -1,8 +1,8 @@ using System.ComponentModel.DataAnnotations; -using API.Entities.Enums; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities; public class ServerSetting : IHasConcurrencyToken { diff --git a/API/Entities/ServerStatistics.cs b/Kavita.Models/Entities/ServerStatistics.cs similarity index 92% rename from API/Entities/ServerStatistics.cs rename to Kavita.Models/Entities/ServerStatistics.cs index 159b7ef4c..f4fa46d28 100644 --- a/API/Entities/ServerStatistics.cs +++ b/Kavita.Models/Entities/ServerStatistics.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities; public class ServerStatistics { diff --git a/API/Entities/SideNavStreamType.cs b/Kavita.Models/Entities/SideNavStreamType.cs similarity index 85% rename from API/Entities/SideNavStreamType.cs rename to Kavita.Models/Entities/SideNavStreamType.cs index 62f429889..104a6c350 100644 --- a/API/Entities/SideNavStreamType.cs +++ b/Kavita.Models/Entities/SideNavStreamType.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities; public enum SideNavStreamType { diff --git a/API/Entities/SiteTheme.cs b/Kavita.Models/Entities/SiteTheme.cs similarity index 81% rename from API/Entities/SiteTheme.cs rename to Kavita.Models/Entities/SiteTheme.cs index 107dca556..852732907 100644 --- a/API/Entities/SiteTheme.cs +++ b/Kavita.Models/Entities/SiteTheme.cs @@ -1,9 +1,9 @@ using System; -using API.Entities.Enums.Theme; -using API.Entities.Interfaces; -using API.Services; +using Kavita.Common.Extensions; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Enums.Theme; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// Represents a set of css overrides the user can upload to Kavita and will load into webui /// @@ -63,4 +63,14 @@ public class SiteTheme : IEntityDate, ITheme public string CompatibleVersion { get; set; } #endregion + + public static readonly SiteTheme DefaultTheme = new() + { + Name = "Dark", + NormalizedName = "Dark".ToNormalized(), + Provider = ThemeProvider.System, + FileName = "dark.scss", + IsDefault = true, + Description = "Default theme shipped with Kavita" + }; } diff --git a/API/Entities/Tag.cs b/Kavita.Models/Entities/Tag.cs similarity index 85% rename from API/Entities/Tag.cs rename to Kavita.Models/Entities/Tag.cs index 277422713..d95356831 100644 --- a/API/Entities/Tag.cs +++ b/Kavita.Models/Entities/Tag.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using API.Entities.Metadata; +using Kavita.Models.Entities.Metadata; using Microsoft.EntityFrameworkCore; -namespace API.Entities; +namespace Kavita.Models.Entities; [Index(nameof(NormalizedTitle), IsUnique = true)] public class Tag diff --git a/API/Entities/User/AppRole.cs b/Kavita.Models/Entities/User/AppRole.cs similarity index 82% rename from API/Entities/User/AppRole.cs rename to Kavita.Models/Entities/User/AppRole.cs index ca46d1bb0..0d9ae0cc2 100644 --- a/API/Entities/User/AppRole.cs +++ b/Kavita.Models/Entities/User/AppRole.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Identity; -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppRole : IdentityRole { diff --git a/API/Entities/User/AppUser.cs b/Kavita.Models/Entities/User/AppUser.cs similarity index 96% rename from API/Entities/User/AppUser.cs rename to Kavita.Models/Entities/User/AppUser.cs index d3d6eb3c4..cb59f1916 100644 --- a/API/Entities/User/AppUser.cs +++ b/Kavita.Models/Entities/User/AppUser.cs @@ -3,16 +3,14 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Entities.Progress; -using API.Entities.Scrobble; -using API.Entities.User; -using API.Helpers; +using Kavita.Common.Helpers; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.Scrobble; using Microsoft.AspNetCore.Identity; - -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUser : IdentityUser, IHasConcurrencyToken, IHasCoverImage { diff --git a/API/Entities/User/AppUserAnnotation.cs b/Kavita.Models/Entities/User/AppUserAnnotation.cs similarity index 96% rename from API/Entities/User/AppUserAnnotation.cs rename to Kavita.Models/Entities/User/AppUserAnnotation.cs index e6b3b33af..c26742e52 100644 --- a/API/Entities/User/AppUserAnnotation.cs +++ b/Kavita.Models/Entities/User/AppUserAnnotation.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities.User; /// /// Represents an annotation in the Epub reader diff --git a/API/Entities/User/AppUserAuthKey.cs b/Kavita.Models/Entities/User/AppUserAuthKey.cs similarity index 92% rename from API/Entities/User/AppUserAuthKey.cs rename to Kavita.Models/Entities/User/AppUserAuthKey.cs index 811a80e60..9537824cf 100644 --- a/API/Entities/User/AppUserAuthKey.cs +++ b/Kavita.Models/Entities/User/AppUserAuthKey.cs @@ -1,8 +1,8 @@ using System; -using API.Entities.Enums.User; +using Kavita.Models.Entities.Enums.User; using Microsoft.EntityFrameworkCore; -namespace API.Entities.User; +namespace Kavita.Models.Entities.User; [Index(nameof(Key), IsUnique = true)] [Index(nameof(ExpiresAtUtc), IsUnique = false)] diff --git a/API/Entities/User/AppUserBookmark.cs b/Kavita.Models/Entities/User/AppUserBookmark.cs similarity index 86% rename from API/Entities/User/AppUserBookmark.cs rename to Kavita.Models/Entities/User/AppUserBookmark.cs index c62f8685e..8bb53c236 100644 --- a/API/Entities/User/AppUserBookmark.cs +++ b/Kavita.Models/Entities/User/AppUserBookmark.cs @@ -1,9 +1,14 @@ using System; using System.Text.Json.Serialization; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities.User; +public class BookmarkSeriesPair +{ + public AppUserBookmark Bookmark { get; init; } = null!; + public Series Series { get; init; } = null!; +} /// /// Represents a saved page in a Chapter entity for a given user. diff --git a/API/Entities/User/AppUserChapterRating.cs b/Kavita.Models/Entities/User/AppUserChapterRating.cs similarity index 93% rename from API/Entities/User/AppUserChapterRating.cs rename to Kavita.Models/Entities/User/AppUserChapterRating.cs index 777862b49..1543a862f 100644 --- a/API/Entities/User/AppUserChapterRating.cs +++ b/Kavita.Models/Entities/User/AppUserChapterRating.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities.User; +namespace Kavita.Models.Entities.User; #nullable enable public class AppUserChapterRating : IEntityDate diff --git a/API/Entities/User/AppUserCollection.cs b/Kavita.Models/Entities/User/AppUserCollection.cs similarity index 95% rename from API/Entities/User/AppUserCollection.cs rename to Kavita.Models/Entities/User/AppUserCollection.cs index 2a6d8faff..f15e47a64 100644 --- a/API/Entities/User/AppUserCollection.cs +++ b/Kavita.Models/Entities/User/AppUserCollection.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; - -namespace API.Entities; +namespace Kavita.Models.Entities.User; /// /// Represents a Collection of Series for a given User diff --git a/API/Entities/User/AppUserDashboardStream.cs b/Kavita.Models/Entities/User/AppUserDashboardStream.cs similarity index 90% rename from API/Entities/User/AppUserDashboardStream.cs rename to Kavita.Models/Entities/User/AppUserDashboardStream.cs index a3554b277..113b064cf 100644 --- a/API/Entities/User/AppUserDashboardStream.cs +++ b/Kavita.Models/Entities/User/AppUserDashboardStream.cs @@ -1,7 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; - -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUserDashboardStream { diff --git a/API/Entities/User/AppUserExternalSource.cs b/Kavita.Models/Entities/User/AppUserExternalSource.cs similarity index 87% rename from API/Entities/User/AppUserExternalSource.cs rename to Kavita.Models/Entities/User/AppUserExternalSource.cs index 502204831..7e1389f5f 100644 --- a/API/Entities/User/AppUserExternalSource.cs +++ b/Kavita.Models/Entities/User/AppUserExternalSource.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUserExternalSource { diff --git a/API/Entities/User/AppUserOnDeckRemoval.cs b/Kavita.Models/Entities/User/AppUserOnDeckRemoval.cs similarity index 84% rename from API/Entities/User/AppUserOnDeckRemoval.cs rename to Kavita.Models/Entities/User/AppUserOnDeckRemoval.cs index 3b7b16f80..d627b462b 100644 --- a/API/Entities/User/AppUserOnDeckRemoval.cs +++ b/Kavita.Models/Entities/User/AppUserOnDeckRemoval.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUserOnDeckRemoval { diff --git a/API/Entities/User/AppUserPreferences.cs b/Kavita.Models/Entities/User/AppUserPreferences.cs similarity index 96% rename from API/Entities/User/AppUserPreferences.cs rename to Kavita.Models/Entities/User/AppUserPreferences.cs index ab2d1b081..8091f4c7c 100644 --- a/API/Entities/User/AppUserPreferences.cs +++ b/Kavita.Models/Entities/User/AppUserPreferences.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using API.Data; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; -using API.Services.Tasks; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.Entities.User; +namespace Kavita.Models.Entities.User; public class AppUserPreferences { @@ -81,7 +79,7 @@ public class AppUserPreferences /// /// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override /// - public string BookReaderFontFamily { get; set; } = FontService.DefaultFont; + public string BookReaderFontFamily { get; set; } = EpubFont.DefaultFont; /// /// Book Reader Option: Allows tapping on side of screens to paginate /// @@ -140,7 +138,7 @@ public class AppUserPreferences /// UI Site Global Setting: The UI theme the user should use. /// /// Should default to Dark - public required SiteTheme Theme { get; set; } = Seed.DefaultThemes[0]; + public required SiteTheme Theme { get; set; } = SiteTheme.DefaultTheme; /// /// Global Site Option: If the UI should layout items as Cards or List items /// diff --git a/API/Entities/User/AppUserRating.cs b/Kavita.Models/Entities/User/AppUserRating.cs similarity index 90% rename from API/Entities/User/AppUserRating.cs rename to Kavita.Models/Entities/User/AppUserRating.cs index d49f9a3fd..d7530c49e 100644 --- a/API/Entities/User/AppUserRating.cs +++ b/Kavita.Models/Entities/User/AppUserRating.cs @@ -1,8 +1,7 @@ - -using System; -using API.Entities.Interfaces; +using System; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities.User; #nullable enable public class AppUserRating : IEntityDate diff --git a/API/Entities/User/AppUserReadingProfile.cs b/Kavita.Models/Entities/User/AppUserReadingProfile.cs similarity index 96% rename from API/Entities/User/AppUserReadingProfile.cs rename to Kavita.Models/Entities/User/AppUserReadingProfile.cs index 4a68db4d5..439b27e64 100644 --- a/API/Entities/User/AppUserReadingProfile.cs +++ b/Kavita.Models/Entities/User/AppUserReadingProfile.cs @@ -1,10 +1,9 @@ using System.Collections.Generic; using System.ComponentModel; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; -using API.Services.Tasks; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.Entities; +namespace Kavita.Models.Entities.User; public enum BreakPoint { @@ -111,7 +110,7 @@ public class AppUserReadingProfile /// /// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override /// - public string BookReaderFontFamily { get; set; } = FontService.DefaultFont; + public string BookReaderFontFamily { get; set; } = EpubFont.DefaultFont; /// /// Book Reader Option: Allows tapping on side of screens to paginate /// diff --git a/API/Entities/User/AppUserRole.cs b/Kavita.Models/Entities/User/AppUserRole.cs similarity index 82% rename from API/Entities/User/AppUserRole.cs rename to Kavita.Models/Entities/User/AppUserRole.cs index 9ee798e6b..895162098 100644 --- a/API/Entities/User/AppUserRole.cs +++ b/Kavita.Models/Entities/User/AppUserRole.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUserRole : IdentityUserRole { diff --git a/API/Entities/User/AppUserSideNavStream.cs b/Kavita.Models/Entities/User/AppUserSideNavStream.cs similarity index 95% rename from API/Entities/User/AppUserSideNavStream.cs rename to Kavita.Models/Entities/User/AppUserSideNavStream.cs index a164b0a1f..963048e06 100644 --- a/API/Entities/User/AppUserSideNavStream.cs +++ b/Kavita.Models/Entities/User/AppUserSideNavStream.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUserSideNavStream { diff --git a/API/Entities/User/AppUserSmartFilter.cs b/Kavita.Models/Entities/User/AppUserSmartFilter.cs similarity index 88% rename from API/Entities/User/AppUserSmartFilter.cs rename to Kavita.Models/Entities/User/AppUserSmartFilter.cs index e9f58fb5c..6933fa55d 100644 --- a/API/Entities/User/AppUserSmartFilter.cs +++ b/Kavita.Models/Entities/User/AppUserSmartFilter.cs @@ -1,6 +1,4 @@ -using API.DTOs.Filtering.v2; - -namespace API.Entities; +namespace Kavita.Models.Entities.User; /// /// Represents a Saved user Filter diff --git a/API/Entities/User/AppUserTableOfContent.cs b/Kavita.Models/Entities/User/AppUserTableOfContent.cs similarity index 95% rename from API/Entities/User/AppUserTableOfContent.cs rename to Kavita.Models/Entities/User/AppUserTableOfContent.cs index 5d110b8b6..18968b9dc 100644 --- a/API/Entities/User/AppUserTableOfContent.cs +++ b/Kavita.Models/Entities/User/AppUserTableOfContent.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities.User; /// /// A personal table of contents for a given user linked with a given book diff --git a/API/Entities/User/AppUserWantToRead.cs b/Kavita.Models/Entities/User/AppUserWantToRead.cs similarity index 91% rename from API/Entities/User/AppUserWantToRead.cs rename to Kavita.Models/Entities/User/AppUserWantToRead.cs index d41e44962..f133d491c 100644 --- a/API/Entities/User/AppUserWantToRead.cs +++ b/Kavita.Models/Entities/User/AppUserWantToRead.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUserWantToRead { diff --git a/API/Entities/User/ClientDevice.cs b/Kavita.Models/Entities/User/ClientDevice.cs similarity index 95% rename from API/Entities/User/ClientDevice.cs rename to Kavita.Models/Entities/User/ClientDevice.cs index ea8cdcc1c..fff09e687 100644 --- a/API/Entities/User/ClientDevice.cs +++ b/Kavita.Models/Entities/User/ClientDevice.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using API.Constants; -using API.Entities.Progress; +using Kavita.Models.Entities.Progress; -namespace API.Entities.User; +namespace Kavita.Models.Entities.User; #nullable enable public class ClientDevice diff --git a/API/Entities/User/ClientDeviceHistory.cs b/Kavita.Models/Entities/User/ClientDeviceHistory.cs similarity index 85% rename from API/Entities/User/ClientDeviceHistory.cs rename to Kavita.Models/Entities/User/ClientDeviceHistory.cs index 8981073a8..cd4c2c879 100644 --- a/API/Entities/User/ClientDeviceHistory.cs +++ b/Kavita.Models/Entities/User/ClientDeviceHistory.cs @@ -1,8 +1,7 @@ using System; -using API.Entities.Progress; -using API.Entities.User; +using Kavita.Models.Entities.Progress; -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class ClientDeviceHistory { diff --git a/API/Entities/Volume.cs b/Kavita.Models/Entities/Volume.cs similarity index 97% rename from API/Entities/Volume.cs rename to Kavita.Models/Entities/Volume.cs index d5e9cf1c2..22faa2173 100644 --- a/API/Entities/Volume.cs +++ b/Kavita.Models/Entities/Volume.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Globalization; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities; public class Volume : IEntityDate, IHasReadTimeEstimate, IHasCoverImage { diff --git a/Kavita.Models/Extensions/AppUserExtensions.cs b/Kavita.Models/Extensions/AppUserExtensions.cs new file mode 100644 index 000000000..25b28db36 --- /dev/null +++ b/Kavita.Models/Extensions/AppUserExtensions.cs @@ -0,0 +1,53 @@ +using System.Linq; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; +using Kavita.Models.Helpers; + +namespace Kavita.Models.Extensions; + +public static class AppUserExtensions +{ + /// + extension(AppUser user) + { + /// + /// Adds a new SideNavStream to the user's SideNavStreams. This user should have these streams already loaded + /// + /// + public void CreateSideNavFromLibrary(Library library) + { + var maxCount = user.SideNavStreams.Select(s => s.Order).DefaultIfEmpty().Max(); + + if (user.SideNavStreams.FirstOrDefault(s => s.LibraryId == library.Id) != null) return; + + user.SideNavStreams.Add(new AppUserSideNavStream + { + Name = library.Name, + Order = maxCount + 1, + IsProvided = false, + StreamType = SideNavStreamType.Library, + LibraryId = library.Id, + Visible = true, + }); + } + + public void RemoveSideNavFromLibrary(Library library) + { + // Find the library and remove it + var item = user.SideNavStreams.FirstOrDefault(s => s.LibraryId == library.Id); + if (item == null) return; + user.SideNavStreams.Remove(item); + + OrderableHelper.ReorderItems(user.SideNavStreams); + } + + public AgeRestriction GetAgeRestriction() + { + return new AgeRestriction + { + AgeRating = user.AgeRestriction, + IncludeUnknowns = user.AgeRestrictionIncludeUnknowns, + }; + } + } +} diff --git a/Kavita.Models/Extensions/ApplicationServiceExtensions.cs b/Kavita.Models/Extensions/ApplicationServiceExtensions.cs new file mode 100644 index 000000000..fe83e2e6b --- /dev/null +++ b/Kavita.Models/Extensions/ApplicationServiceExtensions.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Kavita.Models.Extensions; + +public static class ApplicationServiceExtensions +{ + public static void AddMappings(this IServiceCollection services) + { + services.AddAutoMapper(typeof(ApplicationServiceExtensions).Assembly); + } +} diff --git a/API/Extensions/EncodeFormatExtensions.cs b/Kavita.Models/Extensions/EncodeFormatExtensions.cs similarity index 82% rename from API/Extensions/EncodeFormatExtensions.cs rename to Kavita.Models/Extensions/EncodeFormatExtensions.cs index 924ae8b89..9413925b9 100644 --- a/API/Extensions/EncodeFormatExtensions.cs +++ b/Kavita.Models/Extensions/EncodeFormatExtensions.cs @@ -1,8 +1,7 @@ -using System; -using API.Entities.Enums; +using System; +using Kavita.Models.Entities.Enums; -namespace API.Extensions; -#nullable enable +namespace Kavita.Models.Extensions; public static class EncodeFormatExtensions { diff --git a/Kavita.Models/Extensions/EnumerableExtensions.cs b/Kavita.Models/Extensions/EnumerableExtensions.cs new file mode 100644 index 000000000..579198d57 --- /dev/null +++ b/Kavita.Models/Extensions/EnumerableExtensions.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; + +namespace Kavita.Models.Extensions; + +public static class EnumerableExtensions +{ + public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) + { + if (restriction.AgeRating == AgeRating.NotApplicable) return items; + var q = items.Where(s => s.AgeRating <= restriction.AgeRating); + if (!restriction.IncludeUnknowns) + { + return q.Where(s => s.AgeRating != AgeRating.Unknown); + } + + return q; + } + + public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) + { + if (restriction.AgeRating == AgeRating.NotApplicable) return items; + var q = items.Where(s => s.AgeRating <= restriction.AgeRating); + if (!restriction.IncludeUnknowns) + { + return q.Where(s => s.AgeRating != AgeRating.Unknown); + } + + return q; + } + + public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) + { + if (restriction.AgeRating == AgeRating.NotApplicable) return items; + var q = items.Where(s => s.AgeRating <= restriction.AgeRating); + if (!restriction.IncludeUnknowns) + { + return q.Where(s => s.AgeRating != AgeRating.Unknown); + } + + return q; + } +} diff --git a/API/Extensions/FilterDtoExtensions.cs b/Kavita.Models/Extensions/FilterDtoExtensions.cs similarity index 76% rename from API/Extensions/FilterDtoExtensions.cs rename to Kavita.Models/Extensions/FilterDtoExtensions.cs index 7a55f7db9..385b921a3 100644 --- a/API/Extensions/FilterDtoExtensions.cs +++ b/Kavita.Models/Extensions/FilterDtoExtensions.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.Collections.Generic; -using API.DTOs.Filtering; -using API.Entities.Enums; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.Entities.Enums; -namespace API.Extensions; -#nullable enable +namespace Kavita.Models.Extensions; public static class FilterDtoExtensions { diff --git a/API/Extensions/PlusMediaFormatExtensions.cs b/Kavita.Models/Extensions/PlusMediaFormatExtensions.cs similarity index 95% rename from API/Extensions/PlusMediaFormatExtensions.cs rename to Kavita.Models/Extensions/PlusMediaFormatExtensions.cs index a88b9c2f9..e6534da6e 100644 --- a/API/Extensions/PlusMediaFormatExtensions.cs +++ b/Kavita.Models/Extensions/PlusMediaFormatExtensions.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using API.DTOs.Scrobbling; -using API.Entities.Enums; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Enums; -namespace API.Extensions; +namespace Kavita.Models.Extensions; public static class PlusMediaFormatExtensions { diff --git a/API/Helpers/OrderableHelper.cs b/Kavita.Models/Helpers/OrderableHelper.cs similarity index 94% rename from API/Helpers/OrderableHelper.cs rename to Kavita.Models/Helpers/OrderableHelper.cs index d4ff89573..9561fd2dc 100644 --- a/API/Helpers/OrderableHelper.cs +++ b/Kavita.Models/Helpers/OrderableHelper.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; -using API.Entities; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; -namespace API.Helpers; +namespace Kavita.Models.Helpers; #nullable enable public static class OrderableHelper diff --git a/Kavita.Models/Kavita.Models.csproj b/Kavita.Models/Kavita.Models.csproj new file mode 100644 index 000000000..c858a6446 --- /dev/null +++ b/Kavita.Models/Kavita.Models.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + disable + disable + + + + + + + + + + + + + + + diff --git a/API/Data/Metadata/ComicInfo.cs b/Kavita.Models/Metadata/ComicInfo.cs similarity index 65% rename from API/Data/Metadata/ComicInfo.cs rename to Kavita.Models/Metadata/ComicInfo.cs index 5c65f368b..708e54e91 100644 --- a/API/Data/Metadata/ComicInfo.cs +++ b/Kavita.Models/Metadata/ComicInfo.cs @@ -2,14 +2,11 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Services; using Kavita.Common.Extensions; -using Nager.ArticleNumber; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.Data.Metadata; +namespace Kavita.Models.Metadata; #nullable enable /// @@ -135,23 +132,26 @@ public class ComicInfo public IList GetPeopleForRole(PersonRole role) => role switch { + PersonRole.Writer => SplitNames(Writer), + PersonRole.Penciller => SplitNames(Penciller), + PersonRole.Inker => SplitNames(Inker), + PersonRole.Colorist => SplitNames(Colorist), + PersonRole.Letterer => SplitNames(Letterer), + PersonRole.CoverArtist => SplitNames(CoverArtist), + PersonRole.Editor => SplitNames(Editor), + PersonRole.Publisher => SplitNames(Publisher), + PersonRole.Translator => SplitNames(Translator), + PersonRole.Imprint => SplitNames(Imprint), + PersonRole.Character => SplitNames(Characters), + PersonRole.Team => SplitNames(Teams), + PersonRole.Location => SplitNames(Locations), PersonRole.Other => [], - PersonRole.Writer => TagHelper.GetTagValues(Writer), - PersonRole.Penciller => TagHelper.GetTagValues(Penciller), - PersonRole.Inker => TagHelper.GetTagValues(Inker), - PersonRole.Colorist => TagHelper.GetTagValues(Colorist), - PersonRole.Letterer => TagHelper.GetTagValues(Letterer), - PersonRole.CoverArtist => TagHelper.GetTagValues(CoverArtist), - PersonRole.Editor => TagHelper.GetTagValues(Editor), - PersonRole.Publisher => TagHelper.GetTagValues(Publisher), - PersonRole.Character => TagHelper.GetTagValues(Characters), - PersonRole.Translator => TagHelper.GetTagValues(Translator), - PersonRole.Imprint => TagHelper.GetTagValues(Imprint), - PersonRole.Team => TagHelper.GetTagValues(Teams), - PersonRole.Location => TagHelper.GetTagValues(Locations), _ => throw new ArgumentOutOfRangeException(nameof(role), role, null) }; + private static string[] SplitNames(string value) => + value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + public static AgeRating ConvertAgeRatingToEnum(string value) { if (string.IsNullOrEmpty(value)) return Entities.Enums.AgeRating.Unknown; @@ -159,41 +159,6 @@ public class ComicInfo .SingleOrDefault(t => t.ToDescription().ToUpperInvariant().Equals(value.ToUpperInvariant()), Entities.Enums.AgeRating.Unknown); } - public static void CleanComicInfo(ComicInfo? info) - { - if (info == null) return; - - info.Series = info.Series.Trim(); - info.SeriesSort = info.SeriesSort.Trim(); - info.LocalizedSeries = info.LocalizedSeries.Trim(); - - info.Writer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Writer); - info.Colorist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Colorist); - info.Editor = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Editor); - info.Inker = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Inker); - info.Letterer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Letterer); - info.Penciller = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Penciller); - info.Publisher = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Publisher); - info.Imprint = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Imprint); - info.Characters = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Characters); - info.Translator = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Translator); - info.CoverArtist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.CoverArtist); - info.Teams = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Teams); - info.Locations = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Locations); - - // We need to convert GTIN to ISBN - info.Isbn = ParseGtin(info.GTIN); - - if (!string.IsNullOrEmpty(info.Number)) - { - info.Number = info.Number.Trim().Replace(",", "."); // Corrective measure for non English OSes - } - - if (!string.IsNullOrEmpty(info.Volume)) - { - info.Volume = info.Volume.Trim(); - } - } /// /// Uses both Volume and Number to make an educated guess as to what count refers to and it's highest number. @@ -221,34 +186,5 @@ public class ComicInfo return 0; } - /// - /// For a given GTIN, attempts to parse out an ISBN and set the Isbn property. - /// - /// - /// - public static string ParseGtin(string? gtin) - { - if (string.IsNullOrEmpty(gtin)) return string.Empty; - - - // This is likely a valid ISBN - if (gtin[0] == '0') - { - var offset = gtin[1] == '-' ? 0 : 1; - var potentialIsbn = gtin[offset..]; - if (ArticleNumberHelper.IsValidIsbn13(potentialIsbn)) - { - return potentialIsbn; - } - } - - if (ArticleNumberHelper.IsValidIsbn10(gtin) || ArticleNumberHelper.IsValidIsbn13(gtin)) - { - return gtin; - } - - return string.Empty; - } - } diff --git a/API/Data/Scanner/Chunk.cs b/Kavita.Models/Misc/Chunk.cs similarity index 93% rename from API/Data/Scanner/Chunk.cs rename to Kavita.Models/Misc/Chunk.cs index 78091200d..1639bd893 100644 --- a/API/Data/Scanner/Chunk.cs +++ b/Kavita.Models/Misc/Chunk.cs @@ -1,4 +1,4 @@ -namespace API.Data.Scanner; +namespace Kavita.Models.Misc; /// /// Represents a set of Entities which is broken up and iterated on diff --git a/Kavita.Models/Parser/ParseScannedFiles.cs b/Kavita.Models/Parser/ParseScannedFiles.cs new file mode 100644 index 000000000..070ed6d68 --- /dev/null +++ b/Kavita.Models/Parser/ParseScannedFiles.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using Kavita.Models.Entities.Enums; + +namespace Kavita.Models.Parser; + +public class ParsedSeries +{ + /// + /// Name of the Series + /// + public required string Name { get; init; } + /// + /// Normalized Name of the Series + /// + public required string NormalizedName { get; init; } + /// + /// Format of the Series + /// + public required MangaFormat Format { get; init; } + /// + /// Has this Series changed or not aka do we need to process it or not. + /// + public bool HasChanged { get; set; } +} + +public class ScanResult +{ + /// + /// A list of files in the Folder. Empty if HasChanged = false + /// + public IList Files { get; set; } + /// + /// A nested folder from Library Root (at any level) + /// + public string Folder { get; set; } + /// + /// The library root + /// + public string LibraryRoot { get; set; } + /// + /// Was the Folder scanned or not. If not modified since last scan, this will be false and Files empty + /// + public bool HasChanged { get; set; } + /// + /// Set in Stage 2: Parsed Info from the Files + /// + public IList ParserInfos { get; set; } +} + +/// +/// The final product of ParseScannedFiles. This has all the processed parserInfo and is ready for tracking/processing into entities +/// +public class ScannedSeriesResult +{ + /// + /// Was the Folder scanned or not. If not modified since last scan, this will be false and indicates that upstream should count this as skipped + /// + public bool HasChanged { get; set; } + /// + /// The Parsed Series information used for tracking + /// + public ParsedSeries ParsedSeries { get; set; } + /// + /// Parsed files + /// + public IList ParsedInfos { get; set; } +} + +public class SeriesModified +{ + public required string? FolderPath { get; set; } + public required string? LowestFolderPath { get; set; } + public required string SeriesName { get; set; } + public DateTime LastScanned { get; set; } + public MangaFormat Format { get; set; } + public IEnumerable LibraryRoots { get; set; } = ArraySegment.Empty; +} diff --git a/API/Services/Tasks/Scanner/Parser/ParserInfo.cs b/Kavita.Models/Parser/ParserInfo.cs similarity index 71% rename from API/Services/Tasks/Scanner/Parser/ParserInfo.cs rename to Kavita.Models/Parser/ParserInfo.cs index 524547d8b..759c2795f 100644 --- a/API/Services/Tasks/Scanner/Parser/ParserInfo.cs +++ b/Kavita.Models/Parser/ParserInfo.cs @@ -1,8 +1,8 @@ -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; -namespace API.Services.Tasks.Scanner.Parser; -#nullable enable +namespace Kavita.Models.Parser; /// /// This represents all parsed information from a single file @@ -19,11 +19,11 @@ public class ParserInfo /// public required string Series { get; set; } = string.Empty; /// - /// This can be filled in from ComicInfo.xml/Epub during scanning. Will update the SortName field on + /// This can be filled in from ComicInfo.xml/Epub during scanning. Will update the SortName field on /// public string SeriesSort { get; set; } = string.Empty; /// - /// This can be filled in from ComicInfo.xml/Epub during scanning. Will update the LocalizedName field on + /// This can be filled in from ComicInfo.xml/Epub during scanning. Will update the LocalizedName field on /// public string LocalizedSeries { get; set; } = string.Empty; /// @@ -72,39 +72,16 @@ public class ParserInfo public string Title { get; set; } = string.Empty; /// - /// This can be filled in from ComicInfo.xml during scanning. Will update the SortOrder field on . + /// This can be filled in from ComicInfo.xml during scanning. Will update the SortOrder field on . /// Falls back to Parsed Chapter number /// public float IssueOrder { get; set; } - /// - /// If the ParserInfo has the IsSpecial tag or both volumes and chapters are default aka 0 - /// - /// - public bool IsSpecialInfo() - { - return IsSpecial || (Parser.IsLooseLeafVolume(Volumes) && Parser.IsDefaultChapter(Chapters)); - } - /// /// This will contain any EXTRA comicInfo information parsed from the epub or archive. If there is an archive with comicInfo.xml AND it contains /// series, volume information, that will override what we parsed. /// public ComicInfo? ComicInfo { get; set; } - /// - /// Merges non-empty/null properties from info2 into this entity. - /// - /// This does not merge ComicInfo as they should always be the same - /// - public void Merge(ParserInfo? info2) - { - if (info2 == null) return; - Chapters = Parser.IsDefaultChapter(Chapters) ? info2.Chapters: Chapters; - Volumes = Parser.IsLooseLeafVolume(Volumes) ? info2.Volumes : Volumes; - Edition = string.IsNullOrEmpty(Edition) ? info2.Edition : Edition; - Title = string.IsNullOrEmpty(Title) ? info2.Title : Title; - Series = string.IsNullOrEmpty(Series) ? info2.Series : Series; - IsSpecial = IsSpecial || info2.IsSpecial; - } + } diff --git a/API.Tests/Helpers/BrowserHelperTests.cs b/Kavita.Server.Tests/Helpers/BrowserHelperTests.cs similarity index 99% rename from API.Tests/Helpers/BrowserHelperTests.cs rename to Kavita.Server.Tests/Helpers/BrowserHelperTests.cs index e6a9568a2..68ee857ed 100644 --- a/API.Tests/Helpers/BrowserHelperTests.cs +++ b/Kavita.Server.Tests/Helpers/BrowserHelperTests.cs @@ -1,9 +1,7 @@ -using API.Constants; -using API.Entities.Enums; -using API.Helpers; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Server.Tests.Helpers; public class BrowserHelperTests { diff --git a/Kavita.Server.Tests/Kavita.Server.Tests.csproj b/Kavita.Server.Tests/Kavita.Server.Tests.csproj new file mode 100644 index 000000000..967f6059e --- /dev/null +++ b/Kavita.Server.Tests/Kavita.Server.Tests.csproj @@ -0,0 +1,34 @@ + + + + net10.0 + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + \ No newline at end of file diff --git a/Kavita.Server.Tests/ManualMigrations/MigrateSmartFilterEncodingTests.cs b/Kavita.Server.Tests/ManualMigrations/MigrateSmartFilterEncodingTests.cs new file mode 100644 index 000000000..2ebc1b0da --- /dev/null +++ b/Kavita.Server.Tests/ManualMigrations/MigrateSmartFilterEncodingTests.cs @@ -0,0 +1,36 @@ +using Kavita.Server.ManualMigrations.v0._7._11; + +namespace Kavita.Server.Tests.ManualMigrations; + +public class MigrateSmartFilterEncodingTests +{ + + [Theory] + [InlineData("", false)] + [InlineData("name=DC%20-%20On%20Deck&stmts=comparison%3D1%26field%3D20%26value%3D0,comparison%3D9%26field%3D20%26value%3D100,comparison%3D0%26field%3D19%26value%3D274&sortOptions=sortField%3D1&isAscending=True&limitTo=0&combination=1", true)] + [InlineData("name=English%20In%20Progress&stmts=comparison%253D8%252Cfield%253D7%252Cvalue%253D4%25252C3,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D8%252Cfield%253D3%252Cvalue%253Dja,comparison%253D1%252Cfield%253D20%252Cvalue%253D0&sortOptions=sortField%3D7,isAscending%3DFalse&limitTo=0&combination=1", true)] + [InlineData("name=Unread%20Isekai%20Light%20Novels&stmts=comparison%253D0%25C2%25A6field%253D20%25C2%25A6value%253D0%EF%BF%BDcomparison%253D5%25C2%25A6field%253D6%25C2%25A6value%253D230%EF%BF%BDcomparison%253D8%25C2%25A6field%253D7%25C2%25A6value%253D4%EF%BF%BDcomparison%253D0%25C2%25A6field%253D19%25C2%25A6value%253D14&sortOptions=sortField%3D5%C2%A6isAscending%3DFalse&limitTo=0&combination=1", false)] + [InlineData("name=Zero&stmts=comparison%3d7%26field%3d1%26value%3d0&sortOptions=sortField=2&isAscending=False&limitTo=0&combination=1", true)] + public void Test_ShouldMigrateFilter(string filter, bool expected) + { + Assert.Equal(expected, MigrateSmartFilterEncoding.ShouldMigrateFilter(filter)); + } + + [Theory] + [InlineData("name=DC%20-%20On%20Deck&stmts=comparison%3D1%26field%3D20%26value%3D0,comparison%3D9%26field%3D20%26value%3D100,comparison%3D0%26field%3D19%26value%3D274&sortOptions=sortField%3D1&isAscending=True&limitTo=0&combination=1")] + [InlineData("name=Manga%20-%20On%20Deck&stmts=comparison%253D1%252Cfield%253D20%252Cvalue%253D0,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D0%252Cfield%253D19%252Cvalue%253D2&sortOptions=sortField%3D1,isAscending%3DTrue&limitTo=0&combination=1")] + [InlineData("name=English%20In%20Progress&stmts=comparison%253D8%252Cfield%253D7%252Cvalue%253D4%25252C3,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D8%252Cfield%253D3%252Cvalue%253Dja,comparison%253D1%252Cfield%253D20%252Cvalue%253D0&sortOptions=sortField%3D7,isAscending%3DFalse&limitTo=0&combination=1")] + public void MigrationWorks(string filter) + { + try + { + var updatedFilter = MigrateSmartFilterEncoding.EncodeFix(filter); + Assert.NotNull(updatedFilter); + } + catch (Exception ex) + { + Assert.Fail("Exception thrown: " + ex.Message); + } + + } +} diff --git a/API.Tests/Middleware/ClientInfoMiddlewareTests.cs b/Kavita.Server.Tests/Middleware/ClientInfoMiddlewareTests.cs similarity index 98% rename from API.Tests/Middleware/ClientInfoMiddlewareTests.cs rename to Kavita.Server.Tests/Middleware/ClientInfoMiddlewareTests.cs index a002e9f87..508269f1f 100644 --- a/API.Tests/Middleware/ClientInfoMiddlewareTests.cs +++ b/Kavita.Server.Tests/Middleware/ClientInfoMiddlewareTests.cs @@ -1,18 +1,13 @@ -using System; -using System.Threading.Tasks; -using API.Constants; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Middleware; -using API.Services.Reading; -using API.Services.Store; +using Kavita.API.Store; +using Kavita.Common.Constants; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Server.Middleware; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Middleware; -#nullable enable +namespace Kavita.Server.Tests.Middleware; public class ClientInfoMiddlewareTests { diff --git a/API/Assets/anilist-no-image-placeholder.jpg b/Kavita.Server/Assets/anilist-no-image-placeholder.jpg similarity index 100% rename from API/Assets/anilist-no-image-placeholder.jpg rename to Kavita.Server/Assets/anilist-no-image-placeholder.jpg diff --git a/API/Middleware/Attribute/DisallowRoleAttribute.cs b/Kavita.Server/Attributes/DisallowRoleAttribute.cs similarity index 94% rename from API/Middleware/Attribute/DisallowRoleAttribute.cs rename to Kavita.Server/Attributes/DisallowRoleAttribute.cs index 7cc1a7be2..92cf3dd89 100644 --- a/API/Middleware/Attribute/DisallowRoleAttribute.cs +++ b/Kavita.Server/Attributes/DisallowRoleAttribute.cs @@ -1,14 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Extensions; -using API.Services; +using Kavita.API.Services; +using Kavita.Common.Extensions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; -namespace API.Middleware; +namespace Kavita.Server.Attributes; /// /// An attribute to prevent users with certain roles to access resources, or do actions. diff --git a/Kavita.Server/Attributes/EntityAccessAttribute.cs b/Kavita.Server/Attributes/EntityAccessAttribute.cs new file mode 100644 index 000000000..f5f8bf70b --- /dev/null +++ b/Kavita.Server/Attributes/EntityAccessAttribute.cs @@ -0,0 +1,150 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace Kavita.Server.Attributes; + +/// +/// An attribute restricting access to entities based on the user's access to the reading list. +/// Returns 404 Not Found on failure +/// +/// +/// +public class ReadingListAccessAttribute(bool failOnMissing = true, string readingListIdKey = "readingListId") + : AccessAttribute(readingListIdKey, failOnMissing, false) +{ + protected override Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct) + { + return unitOfWork.UserRepository.HasAccessToReadingList(userId, entityId, ct); + } +} + +/// +/// An attribute restricting access to entities based on the user's access to the person. +/// Returns 404 Not Found on failure +/// +/// +/// +public class PersonAccessAttribute(bool failOnMissing = true, string personIdKey = "personId") + : AccessAttribute(personIdKey, failOnMissing) +{ + protected override Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct) + { + return unitOfWork.UserRepository.HasAccessToPerson(userId, entityId, ct); + } +} + +/// +/// An attribute restricting access to entities based on the user's access to the library. +/// Returns 404 Not Found on failure +/// +/// +/// +public class LibraryAccessAttribute(bool failOnMissing = true, string libraryIdKey = "libraryId") + : AccessAttribute(libraryIdKey, failOnMissing) +{ + + protected override Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct) + { + return unitOfWork.UserRepository.HasAccessToLibrary(userId, entityId, ct); + } +} + +/// +/// An attribute restricting access to entities based on the user's access to the series. +/// Returns 404 Not Found on failure +/// +/// +/// +public class SeriesAccessAttribute(bool failOnMissing = true, string seriesIdKey = "seriesId") + : AccessAttribute(seriesIdKey, failOnMissing) +{ + + protected override Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct) + { + return unitOfWork.UserRepository.HasAccessToSeries(userId, entityId, ct); + } +} + +/// +/// An attribute restricting access to entities based on the user's access to the volume. +/// Returns 404 Not Found on failure +/// +/// +/// +public class VolumeAccessAttribute(bool failOnMissing = true, string volumeIdKey = "volumeId") + : AccessAttribute(volumeIdKey, failOnMissing) +{ + + protected override Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct) + { + return unitOfWork.UserRepository.HasAccessToVolume(userId, entityId, ct); + } +} + +/// +/// An attribute restricting access to entities based on the user's access to the chapter. +/// Returns 404 Not Found on failure +/// +/// +/// +public class ChapterAccessAttribute(bool failOnMissing = true, string chapterIdKey = "chapterId") + : AccessAttribute(chapterIdKey, failOnMissing) +{ + + protected override Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct) + { + return unitOfWork.UserRepository.HasAccessToChapter(userId, entityId, ct); + } +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +public abstract class AccessAttribute(string idKey, bool failOnMissing = true, bool alwaysAllowAdmin = true) : Attribute, IAsyncAuthorizationFilter +{ + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + var user = context.HttpContext.User; + if (alwaysAllowAdmin && user.IsInRole(PolicyConstants.AdminRole)) return; + + var userId = user.GetUserId(); + + var entityId = ExtractId(context.HttpContext, idKey); + + if (entityId == null) + { + if (failOnMissing) + { + context.Result = new NotFoundResult(); + } + return; + } + + var unitOfWork = context.HttpContext.RequestServices.GetRequiredService(); + + var hasAccess = await CheckAccess(unitOfWork, userId, entityId.Value, context.HttpContext.RequestAborted); + if (!hasAccess) + { + context.Result = new NotFoundResult(); + } + } + + protected abstract Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct); + + private static int? ExtractId(HttpContext httpContext, string key) + { + if (httpContext.Request.RouteValues.TryGetValue(key, out var pathVal) && + int.TryParse(pathVal?.ToString(), out var pId)) return pId; + + if (int.TryParse(httpContext.Request.Query[key], out var qId)) return qId; + + return null; + } +} diff --git a/Kavita.Server/Attributes/KPlusAttribute.cs b/Kavita.Server/Attributes/KPlusAttribute.cs new file mode 100644 index 000000000..27ac84b19 --- /dev/null +++ b/Kavita.Server/Attributes/KPlusAttribute.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Store; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace Kavita.Server.Attributes; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +public class KPlusAttribute : Attribute, IAsyncAuthorizationFilter +{ + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + var userContext = context.HttpContext.RequestServices.GetRequiredService(); + if (!userContext.IsAuthenticated) + { + context.Result = new UnauthorizedResult(); + return; + } + + var licenseService = context.HttpContext.RequestServices.GetRequiredService(); + + if (!await licenseService.HasActiveLicense(ct: context.HttpContext.RequestAborted)) + { + var localizationService = context.HttpContext.RequestServices.GetRequiredService(); + var message = localizationService.Translate(userContext.GetUserIdOrThrow(), "kavitaplus-restricted"); + + context.Result = new BadRequestObjectResult(new {Message = message}); + } + + } +} diff --git a/API/Middleware/ProfilePrivacyAttribute.cs b/Kavita.Server/Attributes/ProfilePrivacyAttribute.cs similarity index 93% rename from API/Middleware/ProfilePrivacyAttribute.cs rename to Kavita.Server/Attributes/ProfilePrivacyAttribute.cs index bf46ac11c..07a6e1a35 100644 --- a/API/Middleware/ProfilePrivacyAttribute.cs +++ b/Kavita.Server/Attributes/ProfilePrivacyAttribute.cs @@ -1,15 +1,15 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.Extensions; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; -namespace API.Middleware; +namespace Kavita.Server.Attributes; /// /// An attribute to restrict endpoint usage to either the user itself (authenticated user == userId) or the diff --git a/API/Controllers/AccountController.cs b/Kavita.Server/Controllers/AccountController.cs similarity index 51% rename from API/Controllers/AccountController.cs rename to Kavita.Server/Controllers/AccountController.cs index b142d628e..8841890d5 100644 --- a/API/Controllers/AccountController.cs +++ b/Kavita.Server/Controllers/AccountController.cs @@ -3,29 +3,33 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Account; -using API.DTOs.Email; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.User; -using API.Entities.User; -using API.Errors; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Middleware; -using API.Services; -using API.Services.Caching; -using API.SignalR; using AutoMapper; using Hangfire; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Errors; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Email; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; +using Kavita.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -34,56 +38,25 @@ using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// All Account matters /// -public class AccountController : BaseApiController +/// +public class AccountController(UserManager userManager, + SignInManager signInManager, + ITokenService tokenService, IUnitOfWork unitOfWork, + ILogger logger, + IMapper mapper, IAccountService accountService, + IEmailService emailService, IEventHub eventHub, + ILocalizationService localizationService, + IAuthenticationSchemeProvider authenticationSchemeProvider, + IAuthKeyService authKeyService) : BaseApiController { // Hardcoded to avoid localization multiple enumeration: https://github.com/Kareadita/Kavita/issues/2829 private const string BadCredentialsMessage = "Your credentials are not correct"; - private readonly UserManager _userManager; - private readonly SignInManager _signInManager; - private readonly ITokenService _tokenService; - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IMapper _mapper; - private readonly IAccountService _accountService; - private readonly IEmailService _emailService; - private readonly IEventHub _eventHub; - private readonly ILocalizationService _localizationService; - private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; - private readonly IAuthKeyCacheInvalidator _authKeyCacheInvalidator; - - /// - public AccountController(UserManager userManager, - SignInManager signInManager, - ITokenService tokenService, IUnitOfWork unitOfWork, - ILogger logger, - IMapper mapper, IAccountService accountService, - IEmailService emailService, IEventHub eventHub, - ILocalizationService localizationService, - IAuthenticationSchemeProvider authenticationSchemeProvider, - IAuthKeyCacheInvalidator authKeyCacheInvalidator) - { - _userManager = userManager; - _signInManager = signInManager; - _tokenService = tokenService; - _unitOfWork = unitOfWork; - _logger = logger; - _mapper = mapper; - _accountService = accountService; - _emailService = emailService; - _eventHub = eventHub; - _localizationService = localizationService; - _authenticationSchemeProvider = authenticationSchemeProvider; - _authKeyCacheInvalidator = authKeyCacheInvalidator; - } - /// /// Returns true if OIDC authentication cookies are present and the /// scheme has been registered @@ -94,7 +67,7 @@ public class AccountController : BaseApiController [HttpGet("oidc-authenticated")] public async Task> OidcAuthenticated() { - var oidcScheme = await _authenticationSchemeProvider.GetSchemeAsync(IdentityServiceExtensions.OpenIdConnect); + var oidcScheme = await authenticationSchemeProvider.GetSchemeAsync(IdentityServiceExtensions.OpenIdConnect); return Ok(oidcScheme != null && HttpContext.Request.Cookies.ContainsKey(OidcService.CookieName)); } @@ -107,11 +80,11 @@ public class AccountController : BaseApiController [HttpGet] public async Task> GetCurrentUserAsync() { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams | AppUserIncludes.AuthKeys); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams | AppUserIncludes.AuthKeys); if (user == null) throw new UnauthorizedAccessException(); - var roles = await _userManager.GetRolesAsync(user); - if (!roles.Contains(PolicyConstants.LoginRole) && !roles.Contains(PolicyConstants.AdminRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account")); + var roles = await userManager.GetRolesAsync(user); + if (!roles.Contains(PolicyConstants.LoginRole) && !roles.Contains(PolicyConstants.AdminRole)) return Unauthorized(await localizationService.Translate(user.Id, "disabled-account")); return Ok(await ConstructUserDto(user, roles, false)); } @@ -125,43 +98,43 @@ public class AccountController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdatePassword(ResetPasswordDto resetPasswordDto) { - var user = await _userManager.Users.SingleOrDefaultAsync(x => x.UserName == resetPasswordDto.UserName); + var user = await userManager.Users.SingleOrDefaultAsync(x => x.UserName == resetPasswordDto.UserName); if (user == null) return Ok(); // Don't report BadRequest as that would allow brute forcing to find accounts on system - _logger.LogInformation("{UserName} is changing {ResetUser}'s password", Username!, resetPasswordDto.UserName.Sanitize()); + logger.LogInformation("{UserName} is changing {ResetUser}'s password", Username!, resetPasswordDto.UserName.Sanitize()); var isAdmin = User.IsInRole(PolicyConstants.AdminRole); if (resetPasswordDto.UserName == Username! && !(User.IsInRole(PolicyConstants.ChangePasswordRole) || isAdmin)) - return Unauthorized(await _localizationService.Translate(UserId, "permission-denied")); + return Unauthorized(await localizationService.Translate(UserId, "permission-denied")); if (resetPasswordDto.UserName != Username! && !isAdmin) - return Unauthorized(await _localizationService.Translate(UserId, "permission-denied")); + return Unauthorized(await localizationService.Translate(UserId, "permission-denied")); if (string.IsNullOrEmpty(resetPasswordDto.OldPassword) && !isAdmin) return BadRequest( new ApiException(400, - await _localizationService.Translate(UserId, "password-required"))); + await localizationService.Translate(UserId, "password-required"))); - var oidcConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var oidcConfig = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; if (user.IdentityProvider == IdentityProvider.OpenIdConnect && oidcConfig is {Enabled: true, SyncUserSettings: true}) { - return BadRequest(await _localizationService.Translate(user.Id, "oidc-managed")); + return BadRequest(await localizationService.Translate(user.Id, "oidc-managed")); } // If you're an admin and the username isn't yours, you don't need to validate the password var isResettingOtherUser = (resetPasswordDto.UserName != Username! && isAdmin); - if (!isResettingOtherUser && !await _userManager.CheckPasswordAsync(user, resetPasswordDto.OldPassword)) + if (!isResettingOtherUser && !await userManager.CheckPasswordAsync(user, resetPasswordDto.OldPassword)) { - return BadRequest(await _localizationService.Translate(UserId, "invalid-password")); + return BadRequest(await localizationService.Translate(UserId, "invalid-password")); } - var errors = await _accountService.ChangeUserPassword(user, resetPasswordDto.Password); + var errors = await accountService.ChangeUserPassword(user, resetPasswordDto.Password); if (errors.Any()) { return BadRequest(errors); } - _logger.LogInformation("{User}'s Password has been reset", resetPasswordDto.UserName); + logger.LogInformation("{User}'s Password has been reset", user.UserName); return Ok(); } @@ -174,12 +147,12 @@ public class AccountController : BaseApiController [HttpPost("register")] public async Task> RegisterFirstUser(RegisterDto registerDto) { - var admins = await _userManager.GetUsersInRoleAsync("Admin"); - if (admins.Count > 0) return BadRequest(await _localizationService.Get("en", "denied")); + var admins = await userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole); + if (admins.Count > 0) return BadRequest(await localizationService.Get("en", "denied")); try { - var usernameValidation = await _accountService.ValidateUsername(registerDto.Username); + var usernameValidation = await accountService.ValidateUsername(registerDto.Username); if (usernameValidation.Any()) { return BadRequest(usernameValidation); @@ -192,43 +165,43 @@ public class AccountController : BaseApiController } var user = new AppUserBuilder(registerDto.Username, registerDto.Email, - await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build(); + await unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build(); - var result = await _userManager.CreateAsync(user, registerDto.Password); + var result = await userManager.CreateAsync(user, registerDto.Password); if (!result.Succeeded) return BadRequest(result.Errors); - await _accountService.SeedUser(user); + await accountService.SeedUser(user); - var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); - if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Get("en", "confirm-token-gen")); - if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Get("en", "validate-email", token)); + var token = await userManager.GenerateEmailConfirmationTokenAsync(user); + if (string.IsNullOrEmpty(token)) return BadRequest(await localizationService.Get("en", "confirm-token-gen")); + if (!await ConfirmEmailToken(token, user)) return BadRequest(await localizationService.Get("en", "validate-email", token)); - var roleResult = await _userManager.AddToRoleAsync(user, PolicyConstants.AdminRole); + var roleResult = await userManager.AddToRoleAsync(user, PolicyConstants.AdminRole); if (!roleResult.Succeeded) return BadRequest(result.Errors); - await _userManager.AddToRoleAsync(user, PolicyConstants.LoginRole); + await userManager.AddToRoleAsync(user, PolicyConstants.LoginRole); return new UserDto { Username = user.UserName, Email = user.Email, - Token = await _tokenService.CreateToken(user), - RefreshToken = await _tokenService.CreateRefreshToken(user), + Token = await tokenService.CreateToken(user), + RefreshToken = await tokenService.CreateRefreshToken(user), ApiKey = user.GetOpdsAuthKey(), - Preferences = _mapper.Map(user.UserPreferences), - KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value, + Preferences = mapper.Map(user.UserPreferences), + KavitaVersion = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value, }; } catch (Exception ex) { - _logger.LogError(ex, "Something went wrong when registering user"); + logger.LogError(ex, "Something went wrong when registering user"); // We need to manually delete the User as we've already committed - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(registerDto.Username); - _unitOfWork.UserRepository.Delete(user); - await _unitOfWork.CommitAsync(); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(registerDto.Username); + unitOfWork.UserRepository.Delete(user); + await unitOfWork.CommitAsync(); } - return BadRequest(await _localizationService.Get("en", "register-user")); + return BadRequest(await localizationService.Get("en", "register-user")); } @@ -244,60 +217,60 @@ public class AccountController : BaseApiController AppUser? user; if (!string.IsNullOrEmpty(loginDto.ApiKey)) { - user = await _unitOfWork.UserRepository.GetUserByAuthKey(loginDto.ApiKey); + user = await unitOfWork.UserRepository.GetUserByAuthKey(loginDto.ApiKey); } else { - user = await _userManager.Users + user = await userManager.Users .Include(u => u.UserPreferences) .Include(u => u.AuthKeys) .AsSplitQuery() .SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpperInvariant()); } - _logger.LogInformation("{UserName} attempting to login from {IpAddress}", loginDto.Username, HttpContext.Connection.RemoteIpAddress?.ToString()); + logger.LogInformation("{UserName} attempting to login from {IpAddress}", loginDto.Username.Sanitize(), HttpContext.Connection.RemoteIpAddress?.ToString()); if (user == null) { - _logger.LogWarning("Attempted login by {UserName} failed due to unable to find account", loginDto.Username); + logger.LogWarning("Attempted login by {UserName} failed due to unable to find account", loginDto.Username.Sanitize()); return Unauthorized(BadCredentialsMessage); } - var roles = await _userManager.GetRolesAsync(user); - if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account")); + var roles = await userManager.GetRolesAsync(user); + if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await localizationService.Translate(user.Id, "disabled-account")); - var oidcConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var oidcConfig = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; // Setting only takes effect if OIDC is functional, and if we're not logging in via ApiKey var disablePasswordAuthentication = oidcConfig is {Enabled: true, DisablePasswordAuthentication: true} && string.IsNullOrEmpty(loginDto.ApiKey); - if (disablePasswordAuthentication && !roles.Contains(PolicyConstants.AdminRole)) return Unauthorized(await _localizationService.Translate(user.Id, "password-authentication-disabled")); + if (disablePasswordAuthentication && !roles.Contains(PolicyConstants.AdminRole)) return Unauthorized(await localizationService.Translate(user.Id, "password-authentication-disabled")); if (string.IsNullOrEmpty(loginDto.ApiKey)) { - var result = await _signInManager + var result = await signInManager .CheckPasswordSignInAsync(user, loginDto.Password, true); if (result.IsLockedOut) { - await _userManager.UpdateSecurityStampAsync(user); - var errorStr = await _localizationService.Translate(user.Id, "locked-out"); - _logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive, + await userManager.UpdateSecurityStampAsync(user); + var errorStr = await localizationService.Translate(user.Id, "locked-out"); + logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive, errorStr); return Unauthorized(errorStr); } if (!result.Succeeded) { - string errorStr = result.IsNotAllowed - ? await _localizationService.Translate(user.Id, "confirm-email") + var errorStr = result.IsNotAllowed + ? await localizationService.Translate(user.Id, "confirm-email") : BadCredentialsMessage; - _logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive, errorStr); + logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive, errorStr); return Unauthorized(errorStr); } } - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); - _logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive); + logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive); return Ok(await ConstructUserDto(user, roles)); } @@ -305,23 +278,23 @@ public class AccountController : BaseApiController private async Task ConstructUserDto(AppUser user, IList roles, bool includeTokens = true) { // TODO: Clean this up to be streamlined - var dto = _mapper.Map(user); + var dto = mapper.Map(user); if (includeTokens) { - dto.Token = await _tokenService.CreateToken(user); - dto.RefreshToken = await _tokenService.CreateRefreshToken(user); + dto.Token = await tokenService.CreateToken(user); + dto.RefreshToken = await tokenService.CreateRefreshToken(user); } dto.Roles = roles; - dto.KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value; // Why are we getting this from the DB? + dto.KavitaVersion = BuildInfo.Version.ToString(); - var pref = await _unitOfWork.UserRepository.GetPreferencesAsync(user.UserName!); + var pref = await unitOfWork.UserRepository.GetPreferencesAsync(user.UserName!); if (pref == null) return dto; - pref.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); - dto.Preferences = _mapper.Map(pref); - dto.AuthKeys = _mapper.Map>(user.AuthKeys); + pref.Theme ??= await unitOfWork.SiteThemeRepository.GetDefaultTheme(); + dto.Preferences = mapper.Map(pref); + dto.AuthKeys = mapper.Map>(user.AuthKeys); return dto; } @@ -333,10 +306,10 @@ public class AccountController : BaseApiController [HttpGet("refresh-account")] public async Task> RefreshAccount() { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.UserPreferences | AppUserIncludes.AuthKeys); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.UserPreferences | AppUserIncludes.AuthKeys); if (user == null) return Unauthorized(); - var roles = await _userManager.GetRolesAsync(user); + var roles = await userManager.GetRolesAsync(user); return Ok(await ConstructUserDto(user, roles, !HttpContext.Request.Cookies.ContainsKey(OidcService.CookieName))); } @@ -351,10 +324,10 @@ public class AccountController : BaseApiController [HttpPost("refresh-token")] public async Task> RefreshToken([FromBody] TokenRequestDto tokenRequestDto) { - var token = await _tokenService.ValidateRefreshToken(tokenRequestDto); + var token = await tokenService.ValidateRefreshToken(tokenRequestDto); if (token == null) { - return Unauthorized(new { message = await _localizationService.Get("en", "invalid-token") }); + return Unauthorized(new { message = await localizationService.Get("en", "invalid-token") }); } return Ok(token); @@ -387,56 +360,56 @@ public class AccountController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateEmail(UpdateEmailDto? dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (user == null || dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password)) - return BadRequest(await _localizationService.Translate(UserId, "invalid-payload")); + return BadRequest(await localizationService.Translate(UserId, "invalid-payload")); - var oidcConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var oidcConfig = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; if (user.IdentityProvider == IdentityProvider.OpenIdConnect && oidcConfig is {Enabled: true, SyncUserSettings: true}) { - return BadRequest(await _localizationService.Translate(user.Id, "oidc-managed")); + return BadRequest(await localizationService.Translate(user.Id, "oidc-managed")); } // Validate this user's password - if (! await _userManager.CheckPasswordAsync(user, dto.Password)) + if (! await userManager.CheckPasswordAsync(user, dto.Password)) { - _logger.LogWarning("A user tried to change {UserName}'s email, but password didn't validate", user.UserName); - return BadRequest(await _localizationService.Translate(UserId, "permission-denied")); + logger.LogWarning("A user tried to change {UserName}'s email, but password didn't validate", user.UserName); + return BadRequest(await localizationService.Translate(UserId, "permission-denied")); } // Validate no other users exist with this email if (user.Email!.Equals(dto.Email)) - return BadRequest(await _localizationService.Translate(UserId, "nothing-to-do")); + return BadRequest(await localizationService.Translate(UserId, "nothing-to-do")); // Check if email is used by another user - var existingUserEmail = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); + var existingUserEmail = await unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); if (existingUserEmail != null) { - return BadRequest(await _localizationService.Translate(UserId, "share-multiple-emails")); + return BadRequest(await localizationService.Translate(UserId, "share-multiple-emails")); } // All validations complete, generate a new token and email it to the user at the new address. Confirm email link will update the email - var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var token = await userManager.GenerateEmailConfirmationTokenAsync(user); if (string.IsNullOrEmpty(token)) { - _logger.LogError("There was an issue generating a token for the email"); - return BadRequest(await _localizationService.Translate(UserId, "generate-token")); + logger.LogError("There was an issue generating a token for the email"); + return BadRequest(await localizationService.Translate(UserId, "generate-token")); } - var isValidEmailAddress = _emailService.IsValidEmail(user.Email); - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var isValidEmailAddress = emailService.IsValidEmail(user.Email); + var serverSettings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var shouldEmailUser = serverSettings.IsEmailSetup() || !isValidEmailAddress; user.EmailConfirmed = !shouldEmailUser; user.ConfirmationToken = token; - await _userManager.UpdateAsync(user); + await userManager.UpdateAsync(user); - var emailLink = await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email); - _logger.LogCritical("[Update Email]: Email Link for {UserName}: {Link}", user.UserName, emailLink); + var emailLink = await emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email); + logger.LogCritical("[Update Email]: Email Link for {UserName}: {Link}", user.UserName, emailLink); if (!shouldEmailUser) { - _logger.LogInformation("Cannot email admin, email not setup or admin email invalid"); + logger.LogInformation("Cannot email admin, email not setup or admin email invalid"); return Ok(new InviteUserResponse { EmailLink = string.Empty, @@ -451,7 +424,7 @@ public class AccountController : BaseApiController { if (!isValidEmailAddress) { - _logger.LogCritical("[Update Email]: User is trying to update their email, but their existing email ({Email}) isn't valid. No email will be send", user.Email); + logger.LogCritical("[Update Email]: User is trying to update their email, but their existing email ({Email}) isn't valid. No email will be send", user.Email); return Ok(new InviteUserResponse { EmailLink = string.Empty, @@ -463,9 +436,9 @@ public class AccountController : BaseApiController try { - var invitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!; + var invitingUser = (await unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!; // Email the old address of the update change - BackgroundJob.Enqueue(() => _emailService.SendEmailChangeEmail(new ConfirmationEmailDto() + BackgroundJob.Enqueue(() => emailService.SendEmailChangeEmail(new ConfirmationEmailDto() { EmailAddress = string.IsNullOrEmpty(user.Email) ? dto.Email : user.Email, InstallId = BuildInfo.Version.ToString(), @@ -487,10 +460,10 @@ public class AccountController : BaseApiController } catch (Exception ex) { - _logger.LogError(ex, "There was an error during invite user flow, unable to send an email"); + logger.LogError(ex, "There was an error during invite user flow, unable to send an email"); } - await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); + await eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); return Ok(); } @@ -504,36 +477,36 @@ public class AccountController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateAgeRestriction(UpdateAgeRestrictionDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); - if (user == null) return Unauthorized(await _localizationService.Translate(UserId, "permission-denied")); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + if (user == null) return Unauthorized(await localizationService.Translate(UserId, "permission-denied")); - var oidcConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var oidcConfig = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; if (user.IdentityProvider == IdentityProvider.OpenIdConnect && oidcConfig is {Enabled: true, SyncUserSettings: true}) { - return BadRequest(await _localizationService.Translate(user.Id, "oidc-managed")); + return BadRequest(await localizationService.Translate(user.Id, "oidc-managed")); } - var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); - var hasRole = await _accountService.CanChangeAgeRestriction(user); - if (!hasRole) return BadRequest(await _localizationService.Translate(UserId, "permission-denied")); + var isAdmin = await unitOfWork.UserRepository.IsUserAdminAsync(user); + var hasRole = await accountService.CanChangeAgeRestriction(user); + if (!hasRole) return BadRequest(await localizationService.Translate(UserId, "permission-denied")); user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating; user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns; - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - if (!_unitOfWork.HasChanges()) return Ok(); + if (!unitOfWork.HasChanges()) return Ok(); try { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); } catch (Exception ex) { - _logger.LogError(ex, "There was an error updating the age restriction"); - return BadRequest(await _localizationService.Translate(UserId, "age-restriction-update")); + logger.LogError(ex, "There was an error updating the age restriction"); + return BadRequest(await localizationService.Translate(UserId, "age-restriction-update")); } - await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); + await eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); return Ok(); } @@ -549,16 +522,16 @@ public class AccountController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateAccount(UpdateUserDto dto) { - var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var adminUser = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (adminUser == null) return Unauthorized(); - if (!await _unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized(await _localizationService.Translate(UserId, "permission-denied")); + if (!await unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized(await localizationService.Translate(UserId, "permission-denied")); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(dto.UserId, AppUserIncludes.SideNavStreams); - if (user == null) return BadRequest(await _localizationService.Translate(UserId, "no-user")); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(dto.UserId, AppUserIncludes.SideNavStreams); + if (user == null) return BadRequest(await localizationService.Translate(UserId, "no-user")); try { - if (await _accountService.ChangeIdentityProvider(UserId, user, dto.IdentityProvider)) return Ok(); + if (await accountService.ChangeIdentityProvider(UserId, user, dto.IdentityProvider)) return Ok(); } catch (KavitaException exception) { @@ -569,11 +542,11 @@ public class AccountController : BaseApiController if (!user.UserName!.Equals(dto.Username)) { // Validate username change - var errors = await _accountService.ValidateUsername(dto.Username); - if (errors.Any()) return BadRequest(await _localizationService.Translate(UserId, "username-taken")); + var errors = await accountService.ValidateUsername(dto.Username); + if (errors.Any()) return BadRequest(await localizationService.Translate(UserId, "username-taken")); user.UserName = dto.Username; - await _userManager.UpdateNormalizedUserNameAsync(user); - _unitOfWork.UserRepository.Update(user); + await userManager.UpdateNormalizedUserNameAsync(user); + unitOfWork.UserRepository.Update(user); } // Check if email is changing for a non-admin user @@ -581,18 +554,18 @@ public class AccountController : BaseApiController if (isUpdatingAnotherAccount && !string.IsNullOrEmpty(dto.Email) && user.Email != dto.Email) { // Validate username change - var errors = await _accountService.ValidateEmail(dto.Email); - if (errors.Any()) return BadRequest(await _localizationService.Translate(UserId, "email-taken")); + var errors = await accountService.ValidateEmail(dto.Email); + if (errors.Any()) return BadRequest(await localizationService.Translate(UserId, "email-taken")); user.Email = dto.Email; user.EmailConfirmed = true; // When an admin performs the flow, we assume the email address is able to receive data - await _userManager.UpdateNormalizedEmailAsync(user); - _unitOfWork.UserRepository.Update(user); + await userManager.UpdateNormalizedEmailAsync(user); + unitOfWork.UserRepository.Update(user); } // Update roles - var existingRoles = await _userManager.GetRolesAsync(user); + var existingRoles = await userManager.GetRolesAsync(user); var hasAdminRole = dto.Roles.Contains(PolicyConstants.AdminRole); if (!hasAdminRole) { @@ -603,9 +576,9 @@ public class AccountController : BaseApiController { var roles = dto.Roles; - var roleResult = await _userManager.RemoveFromRolesAsync(user, existingRoles); + var roleResult = await userManager.RemoveFromRolesAsync(user, existingRoles); if (!roleResult.Succeeded) return BadRequest(roleResult.Errors); - roleResult = await _userManager.AddToRolesAsync(user, roles); + roleResult = await userManager.AddToRolesAsync(user, roles); if (!roleResult.Succeeded) return BadRequest(roleResult.Errors); } @@ -613,12 +586,12 @@ public class AccountController : BaseApiController // await _userManager.UpdateSecurityStampAsync(user); to force them to re-authenticate - var allLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); + var allLibraries = (await unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); List libraries; if (hasAdminRole) { - _logger.LogInformation("{UserName} is being registered as admin. Granting access to all libraries", - user.UserName); + logger.LogInformation("{UserId} is being registered as admin. Granting access to all libraries", + user.Id); libraries = allLibraries; } else @@ -631,7 +604,7 @@ public class AccountController : BaseApiController user.RemoveSideNavFromLibrary(lib); } - libraries = (await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.Libraries, LibraryIncludes.AppUser)).ToList(); + libraries = (await unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.Libraries, LibraryIncludes.AppUser)).ToList(); } foreach (var lib in libraries) @@ -644,19 +617,19 @@ public class AccountController : BaseApiController user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating; user.AgeRestrictionIncludeUnknowns = hasAdminRole || dto.AgeRestriction.IncludeUnknowns; - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) + if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync()) { - await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id); - await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(user.Id), user.Id); + await eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id); + await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(user.Id), user.Id); // If we adjust library access, dashboards should re-render - await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id), user.Id); + await eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id), user.Id); return Ok(); } - await _unitOfWork.RollbackAsync(); - return BadRequest(await _localizationService.Translate(UserId, "generic-user-update")); + await unitOfWork.RollbackAsync(); + return BadRequest(await localizationService.Translate(UserId, "generic-user-update")); } /// @@ -669,14 +642,14 @@ public class AccountController : BaseApiController [HttpGet("invite-url")] public async Task> GetInviteUrl(int userId, bool withBaseUrl) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); if (user == null) return Unauthorized(); if (user.EmailConfirmed) - return BadRequest(await _localizationService.Translate(UserId, "user-already-confirmed")); + return BadRequest(await localizationService.Translate(UserId, "user-already-confirmed")); if (string.IsNullOrEmpty(user.ConfirmationToken)) - return BadRequest(await _localizationService.Translate(UserId, "manual-setup-fail")); + return BadRequest(await localizationService.Translate(UserId, "manual-setup-fail")); - return await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl); + return await emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl); } @@ -690,34 +663,35 @@ public class AccountController : BaseApiController public async Task> InviteUser(InviteUserDto dto) { var userId = UserId; - var adminUser = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (adminUser == null) return Unauthorized(await _localizationService.Translate(userId, "permission-denied")); + var adminUser = await unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (adminUser == null) return Unauthorized(await localizationService.Translate(userId, "permission-denied")); dto.Email = dto.Email.Trim(); - if (string.IsNullOrEmpty(dto.Email)) return BadRequest(await _localizationService.Translate(userId, "invalid-payload")); + if (string.IsNullOrEmpty(dto.Email)) return BadRequest(await localizationService.Translate(userId, "invalid-payload")); - _logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email); + logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email); // Check if there is an existing invite - var emailValidationErrors = await _accountService.ValidateEmail(dto.Email); + var emailValidationErrors = await accountService.ValidateEmail(dto.Email); if (emailValidationErrors.Any()) { - var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); - if (await _userManager.IsEmailConfirmedAsync(invitedUser!)) - return BadRequest(await _localizationService.Translate(UserId, "user-already-registered", invitedUser!.UserName)); - return BadRequest(await _localizationService.Translate(UserId, "user-already-invited")); + var invitedUser = await unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); + if (await userManager.IsEmailConfirmedAsync(invitedUser!)) + return BadRequest(await localizationService.Translate(UserId, "user-already-registered", invitedUser!.UserName)); + return BadRequest(await localizationService.Translate(UserId, "user-already-invited")); } // Create a new user var user = new AppUserBuilder(dto.Email, dto.Email, - await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build(); - _unitOfWork.UserRepository.Add(user); + await unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build(); + unitOfWork.UserRepository.Add(user); + try { - var result = await _userManager.CreateAsync(user, AccountService.DefaultPassword); + var result = await userManager.CreateAsync(user, AccountService.DefaultPassword); if (!result.Succeeded) return BadRequest(result.Errors); - await _accountService.SeedUser(user); + await accountService.SeedUser(user); // Assign Roles var roles = dto.Roles; @@ -734,7 +708,7 @@ public class AccountController : BaseApiController foreach (var role in roles) { if (!PolicyConstants.ValidRoles.Contains(role)) continue; - var roleResult = await _userManager.AddToRoleAsync(user, role); + var roleResult = await userManager.AddToRoleAsync(user, role); if (!roleResult.Succeeded) return BadRequest(roleResult.Errors); @@ -744,13 +718,13 @@ public class AccountController : BaseApiController List libraries; if (hasAdminRole) { - _logger.LogInformation("{UserName} is being registered as admin. Granting access to all libraries", - user.UserName); - libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync(LibraryIncludes.AppUser)).ToList(); + logger.LogInformation("{UserName} is being registered as admin. Granting access to all libraries", + user.UserName?.Sanitize()); + libraries = (await unitOfWork.LibraryRepository.GetLibrariesAsync(LibraryIncludes.AppUser)).ToList(); } else { - libraries = (await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.Libraries, LibraryIncludes.AppUser)).ToList(); + libraries = (await unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.Libraries, LibraryIncludes.AppUser)).ToList(); } foreach (var lib in libraries) @@ -763,34 +737,34 @@ public class AccountController : BaseApiController user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating; user.AgeRestrictionIncludeUnknowns = hasAdminRole || dto.AgeRestriction.IncludeUnknowns; - var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var token = await userManager.GenerateEmailConfirmationTokenAsync(user); if (string.IsNullOrEmpty(token)) { - _logger.LogError("There was an issue generating a token for the email"); - return BadRequest(await _localizationService.Translate(UserId, "generic-invite-user")); + logger.LogError("There was an issue generating a token for the email"); + return BadRequest(await localizationService.Translate(UserId, "generic-invite-user")); } user.ConfirmationToken = token; - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); } catch (Exception ex) { - _logger.LogError(ex, "There was an error during invite user flow, unable to create user. Deleting user for retry"); - _unitOfWork.UserRepository.Delete(user); - await _unitOfWork.CommitAsync(); - return BadRequest(await _localizationService.Translate(UserId, "generic-invite-user")); + logger.LogError(ex, "There was an error during invite user flow, unable to create user. Deleting user for retry"); + unitOfWork.UserRepository.Delete(user); + await unitOfWork.CommitAsync(); + return BadRequest(await localizationService.Translate(UserId, "generic-invite-user")); } try { - var emailLink = await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email); - _logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink); + var emailLink = await emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email); + logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName?.Sanitize(), emailLink); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (!_emailService.IsValidEmail(dto.Email) || !settings.IsEmailSetup()) + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + if (!emailService.IsValidEmail(dto.Email) || !settings.IsEmailSetup()) { - _logger.LogInformation("[Invite User] {Email} doesn't appear to be an email or email is not setup", dto.Email.Replace(Environment.NewLine, string.Empty)); + logger.LogInformation("[Invite User] {Email} doesn't appear to be an email or email is not setup", dto.Email.Replace(Environment.NewLine, string.Empty)); return Ok(new InviteUserResponse { EmailLink = emailLink, @@ -799,7 +773,7 @@ public class AccountController : BaseApiController }); } - BackgroundJob.Enqueue(() => _emailService.SendInviteEmail(new ConfirmationEmailDto() + BackgroundJob.Enqueue(() => emailService.SendInviteEmail(new ConfirmationEmailDto() { EmailAddress = dto.Email, InvitingUser = adminUser.UserName, @@ -814,10 +788,10 @@ public class AccountController : BaseApiController } catch (Exception ex) { - _logger.LogError(ex, "There was an error during invite user flow, unable to send an email"); + logger.LogError(ex, "There was an error during invite user flow, unable to send an email"); } - return BadRequest(await _localizationService.Translate(UserId, "generic-invite-user")); + return BadRequest(await localizationService.Translate(UserId, "generic-invite-user")); } /// @@ -829,12 +803,12 @@ public class AccountController : BaseApiController [HttpPost("confirm-email")] public async Task> ConfirmEmail(ConfirmEmailDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); + var user = await unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); if (user == null) { - _logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email); - return BadRequest(await _localizationService.Get("en", "invalid-email-confirmation")); + logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email); + return BadRequest(await localizationService.Get("en", "invalid-email-confirmation")); } // Validate Password and Username @@ -842,10 +816,10 @@ public class AccountController : BaseApiController // This allows users that use a fake email with the same username to continue setting up the account if (!dto.Username.Equals(dto.Email) && !user.UserName!.Equals(dto.Username)) { - validationErrors.AddRange(await _accountService.ValidateUsername(dto.Username)); + validationErrors.AddRange(await accountService.ValidateUsername(dto.Username)); } - validationErrors.AddRange(await _accountService.ValidatePassword(user, dto.Password)); + validationErrors.AddRange(await accountService.ValidatePassword(user, dto.Password)); if (validationErrors.Any()) { @@ -855,21 +829,21 @@ public class AccountController : BaseApiController if (!await ConfirmEmailToken(dto.Token, user)) { - _logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token); - return BadRequest(await _localizationService.Translate(user.Id, "invalid-email-confirmation")); + logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token.Sanitize()); + return BadRequest(await localizationService.Translate(user.Id, "invalid-email-confirmation")); } user.UserName = dto.Username; user.ConfirmationToken = null; - var errors = await _accountService.ChangeUserPassword(user, dto.Password); + var errors = await accountService.ChangeUserPassword(user, dto.Password); if (errors.Any()) { return BadRequest(errors); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); - user = (await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName, + user = (await unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName, AppUserIncludes.UserPreferences | AppUserIncludes.AuthKeys))!; // Perform Login code @@ -877,11 +851,11 @@ public class AccountController : BaseApiController { Username = user.UserName!, Email = user.Email!, - Token = await _tokenService.CreateToken(user), - RefreshToken = await _tokenService.CreateRefreshToken(user), + Token = await tokenService.CreateToken(user), + RefreshToken = await tokenService.CreateRefreshToken(user), ApiKey = user.GetOpdsAuthKey(), - Preferences = _mapper.Map(user.UserPreferences), - KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value, + Preferences = mapper.Map(user.UserPreferences), + KavitaVersion = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value, }; } @@ -895,33 +869,33 @@ public class AccountController : BaseApiController [HttpPost("confirm-email-update")] public async Task ConfirmEmailUpdate(ConfirmEmailUpdateDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByConfirmationToken(dto.Token); + var user = await unitOfWork.UserRepository.GetUserByConfirmationToken(dto.Token); if (user == null) { - _logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email); - return BadRequest(await _localizationService.Get("en", "invalid-email-confirmation")); + logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email.Sanitize()); + return BadRequest(await localizationService.Get("en", "invalid-email-confirmation")); } if (!await ConfirmEmailToken(dto.Token, user)) { - _logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token); - return BadRequest(await _localizationService.Translate(user.Id, "invalid-email-confirmation")); + logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token.Sanitize()); + return BadRequest(await localizationService.Translate(user.Id, "invalid-email-confirmation")); } - _logger.LogInformation("User is updating email from {OldEmail} to {NewEmail}", user.Email, dto.Email); - var result = await _userManager.SetEmailAsync(user, dto.Email); + logger.LogInformation("User is updating email from {OldEmail} to {NewEmail}", user.Email, dto.Email.Sanitize()); + var result = await userManager.SetEmailAsync(user, dto.Email); if (!result.Succeeded) { - _logger.LogError("Unable to update email for users: {Errors}", result.Errors.Select(e => e.Description)); - return BadRequest(await _localizationService.Translate(user.Id, "generic-user-email-update")); + logger.LogError("Unable to update email for users: {Errors}", result.Errors.Select(e => e.Description)); + return BadRequest(await localizationService.Translate(user.Id, "generic-user-email-update")); } user.ConfirmationToken = null; user.EmailConfirmed = true; - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); // For the user's connected devices to pull the new information in - await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, + await eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); return Ok(); @@ -931,7 +905,7 @@ public class AccountController : BaseApiController [HttpPost("confirm-password-reset")] public async Task> ConfirmForgotPassword(ConfirmPasswordResetDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); + var user = await unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); if (user == null) { return BadRequest(BadCredentialsMessage); @@ -939,21 +913,21 @@ public class AccountController : BaseApiController try { - var result = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, + var result = await userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword", dto.Token); if (!result) { - _logger.LogInformation("Unable to reset password, your email token is not correct: {@Dto}", dto); + logger.LogInformation("Unable to reset password, your email token is not correct: {Token}", dto.Token.Sanitize()); return BadRequest(BadCredentialsMessage); } - var errors = await _accountService.ChangeUserPassword(user, dto.Password); - return errors.Any() ? BadRequest(errors) : Ok(await _localizationService.Translate(user.Id, "password-updated")); + var errors = await accountService.ChangeUserPassword(user, dto.Password); + return errors.Any() ? BadRequest(errors) : Ok(await localizationService.Translate(user.Id, "password-updated")); } catch (Exception ex) { - _logger.LogError(ex, "There was an unexpected error when confirming new password"); - return BadRequest(await _localizationService.Translate(user.Id, "generic-password-update")); + logger.LogError(ex, "There was an unexpected error when confirming new password"); + return BadRequest(await localizationService.Translate(user.Id, "generic-password-update")); } } @@ -968,58 +942,58 @@ public class AccountController : BaseApiController [EnableRateLimiting("Authentication")] public async Task> ForgotPassword([FromQuery] string email) { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var user = await unitOfWork.UserRepository.GetUserByEmailAsync(email); if (user == null) { - _logger.LogError("There are no users with email: {Email} but user is requesting password reset", email); - return Ok(await _localizationService.Get("en", "forgot-password-generic")); + logger.LogError("There are no users with email: {Email} but user is requesting password reset", email.Sanitize().Censor()); + return Ok(await localizationService.Get("en", "forgot-password-generic")); } - var oidcConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var oidcConfig = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; if (user.IdentityProvider == IdentityProvider.OpenIdConnect && oidcConfig is {Enabled: true, SyncUserSettings: true}) { - return BadRequest(await _localizationService.Translate(user.Id, "oidc-managed")); + return BadRequest(await localizationService.Translate(user.Id, "oidc-managed")); } - var roles = await _userManager.GetRolesAsync(user); + var roles = await userManager.GetRolesAsync(user); if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole or PolicyConstants.ReadOnlyRole)) - return Unauthorized(await _localizationService.Translate(user.Id, "permission-denied")); + return Unauthorized(await localizationService.Translate(user.Id, "permission-denied")); if (string.IsNullOrEmpty(user.Email) || !user.EmailConfirmed) - return BadRequest(await _localizationService.Translate(user.Id, "confirm-email")); + return BadRequest(await localizationService.Translate(user.Id, "confirm-email")); - var token = await _userManager.GeneratePasswordResetTokenAsync(user); - var emailLink = await _emailService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email); + var token = await userManager.GeneratePasswordResetTokenAsync(user); + var emailLink = await emailService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email); user.ConfirmationToken = token; - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - _logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); + logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink); - if (!settings.IsEmailSetup()) return Ok(await _localizationService.Get("en", "email-not-enabled")); - if (!_emailService.IsValidEmail(user.Email)) + if (!settings.IsEmailSetup()) return Ok(await localizationService.Get("en", "email-not-enabled")); + if (!emailService.IsValidEmail(user.Email)) { - _logger.LogCritical("[Forgot Password]: User is trying to do a forgot password flow, but their email ({Email}) isn't valid. No email will be send. Admin must change it in UI or from url above", user.Email); - return Ok(await _localizationService.Translate(user.Id, "invalid-email")); + logger.LogCritical("[Forgot Password]: User is trying to do a forgot password flow, but their email ({Email}) isn't valid. No email will be send. Admin must change it in UI or from url above", user.Email); + return Ok(await localizationService.Translate(user.Id, "invalid-email")); } - var installId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value; - BackgroundJob.Enqueue(() => _emailService.SendForgotPasswordEmail(new PasswordResetEmailDto() + var installId = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value; + BackgroundJob.Enqueue(() => emailService.SendForgotPasswordEmail(new PasswordResetEmailDto() { EmailAddress = user.Email, ServerConfirmationLink = emailLink, InstallId = installId })); - return Ok(await _localizationService.Translate(user.Id, "email-sent")); + return Ok(await localizationService.Translate(user.Id, "email-sent")); } [HttpGet("email-confirmed")] public async Task> IsEmailConfirmed() { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (user == null) return Unauthorized(); return Ok(user.EmailConfirmed); @@ -1029,18 +1003,18 @@ public class AccountController : BaseApiController [HttpPost("confirm-migration-email")] public async Task> ConfirmMigrationEmail(ConfirmMigrationEmailDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); + var user = await unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); if (user == null) return BadRequest(BadCredentialsMessage); if (!await ConfirmEmailToken(dto.Token, user)) { - _logger.LogInformation("confirm-migration-email email token is invalid"); + logger.LogInformation("confirm-migration-email email token is invalid"); return BadRequest(BadCredentialsMessage); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); - user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName!, + user = await unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName!, AppUserIncludes.UserPreferences | AppUserIncludes.AuthKeys); // Perform Login code @@ -1048,12 +1022,12 @@ public class AccountController : BaseApiController { Username = user!.UserName!, Email = user.Email!, - Token = await _tokenService.CreateToken(user), - RefreshToken = await _tokenService.CreateRefreshToken(user), + Token = await tokenService.CreateToken(user), + RefreshToken = await tokenService.CreateRefreshToken(user), ApiKey = user.GetOpdsAuthKey(), - AuthKeys = _mapper.Map>(user.AuthKeys), - Preferences = _mapper.Map(user.UserPreferences), - KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value, + AuthKeys = mapper.Map>(user.AuthKeys), + Preferences = mapper.Map(user.UserPreferences), + KavitaVersion = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value, }; } @@ -1065,33 +1039,32 @@ public class AccountController : BaseApiController [Authorize(PolicyGroups.AdminPolicy)] [HttpPost("resend-confirmation-email")] [EnableRateLimiting("Authentication")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> ResendConfirmationSendEmail([FromQuery] int userId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user == null) return BadRequest(await _localizationService.Get("en", "no-user")); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (user == null) return BadRequest(await localizationService.Get("en", "no-user")); if (string.IsNullOrEmpty(user.Email)) - return BadRequest( - await _localizationService.Translate(user.Id, "user-migration-needed")); - if (user.EmailConfirmed) return BadRequest(await _localizationService.Translate(user.Id, "user-already-confirmed")); + return BadRequest(await localizationService.Translate(user.Id, "user-migration-needed")); - // TODO: If the target user is read only, we might want to just forgo this + if (user.EmailConfirmed) return BadRequest(await localizationService.Translate(user.Id, "user-already-confirmed")); - var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var token = await userManager.GenerateEmailConfirmationTokenAsync(user); user.ConfirmationToken = token; - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - var emailLink = await _emailService.GenerateEmailLink(Request, token, "confirm-email-update", user.Email); - _logger.LogCritical("[Email Migration]: Email Link for {UserName}: {Link}", user.UserName, emailLink); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); + var emailLink = await emailService.GenerateEmailLink(Request, token, "confirm-email-update", user.Email); + logger.LogCritical("[Email Migration]: Email Link for {UserName}: {Link}", user.UserName, emailLink); - if (!_emailService.IsValidEmail(user.Email)) + if (!emailService.IsValidEmail(user.Email)) { - _logger.LogCritical("[Email Migration]: User {UserName} is trying to resend an invite flow, but their email ({Email}) isn't valid. No email will be send", user.UserName, user.Email); + logger.LogCritical("[Email Migration]: User {UserName} is trying to resend an invite flow, but their email ({Email}) isn't valid. No email will be send", user.UserName, user.Email); } - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var shouldEmailUser = serverSettings.IsEmailSetup() || !_emailService.IsValidEmail(user.Email); + var serverSettings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var shouldEmailUser = serverSettings.IsEmailSetup() || !emailService.IsValidEmail(user.Email); if (!shouldEmailUser) { @@ -1099,11 +1072,11 @@ public class AccountController : BaseApiController { EmailLink = emailLink, EmailSent = false, - InvalidEmail = !_emailService.IsValidEmail(user.Email) + InvalidEmail = !emailService.IsValidEmail(user.Email) }); } - BackgroundJob.Enqueue(() => _emailService.SendInviteEmail(new ConfirmationEmailDto() + BackgroundJob.Enqueue(() => emailService.SendInviteEmail(new ConfirmationEmailDto() { EmailAddress = user.Email!, InvitingUser = Username!, @@ -1115,21 +1088,21 @@ public class AccountController : BaseApiController { EmailLink = emailLink, EmailSent = true, - InvalidEmail = !_emailService.IsValidEmail(user.Email) + InvalidEmail = !emailService.IsValidEmail(user.Email) }); } private async Task ConfirmEmailToken(string token, AppUser user) { - var result = await _userManager.ConfirmEmailAsync(user, token); + var result = await userManager.ConfirmEmailAsync(user, token); if (result.Succeeded) return true; - _logger.LogCritical("[Account] Email validation failed"); + logger.LogCritical("[Account] Email validation failed"); if (!result.Errors.Any()) return false; foreach (var error in result.Errors) { - _logger.LogCritical("[Account] Email validation error: {Message}", error.Description); + logger.LogCritical("[Account] Email validation error: {Message}", error.Description); } return false; @@ -1142,8 +1115,8 @@ public class AccountController : BaseApiController [HttpGet("opds-url")] public async Task> GetOpdsUrl() { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId); - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId); + var serverSettings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var origin = HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value; if (!string.IsNullOrEmpty(serverSettings.HostName)) origin = serverSettings.HostName; @@ -1163,7 +1136,7 @@ public class AccountController : BaseApiController } } - var opdsAuthKey = (await _unitOfWork.UserRepository.GetAuthKeysForUserId(UserId)) + var opdsAuthKey = (await unitOfWork.UserRepository.GetAuthKeysForUserId(UserId)) .Where(k => k is {Name: AuthKeyHelper.OpdsKeyName, Provider: AuthKeyProvider.System}) .Select(k => k.Key) .FirstOrDefault(); @@ -1179,11 +1152,11 @@ public class AccountController : BaseApiController [HttpGet("is-email-valid")] public async Task> IsEmailValid() { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId); if (user == null) return Unauthorized(); if (string.IsNullOrEmpty(user.Email)) return Ok(false); - return Ok(_emailService.IsValidEmail(user.Email)); + return Ok(emailService.IsValidEmail(user.Email)); } /// @@ -1193,7 +1166,7 @@ public class AccountController : BaseApiController [HttpGet("auth-keys")] public async Task>> GetAuthKeys() { - return Ok(await _unitOfWork.UserRepository.GetAuthKeysForUserId(UserId)); + return Ok(await unitOfWork.UserRepository.GetAuthKeysForUserId(UserId)); } /// @@ -1206,7 +1179,7 @@ public class AccountController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> RotateAuthKey([FromQuery] int authKeyId, RotateAuthKeyRequestDto dto) { - var authKey = await _unitOfWork.UserRepository.GetAuthKeyById(authKeyId); + var authKey = await unitOfWork.UserRepository.GetAuthKeyById(authKeyId); if (authKey?.AppUserId != UserId) return BadRequest(); var oldKeyValue = authKey.Key; @@ -1220,13 +1193,13 @@ public class AccountController : BaseApiController authKey.Key = AuthKeyHelper.GenerateKey(dto.KeyLength); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); - await _authKeyCacheInvalidator.InvalidateAsync(oldKeyValue); + await authKeyService.InvalidateAsync(oldKeyValue); - var newDto = _mapper.Map(authKey); + var newDto = mapper.Map(authKey); - await _eventHub.SendMessageToAsync(MessageFactory.AuthKeyUpdate, MessageFactory.AuthKeyUpdatedEvent(newDto), UserId); + await eventHub.SendMessageToAsync(MessageFactory.AuthKeyUpdate, MessageFactory.AuthKeyUpdatedEvent(newDto), UserId); return Ok(newDto); } @@ -1241,10 +1214,10 @@ public class AccountController : BaseApiController public async Task> CreateAuthKey(RotateAuthKeyRequestDto dto) { // Validate the name doesn't collide - var authKeys = await _unitOfWork.UserRepository.GetAuthKeysForUserId(UserId); + var authKeys = await unitOfWork.UserRepository.GetAuthKeysForUserId(UserId); if (authKeys.Any(k => string.Equals(k.Name, dto.Name, StringComparison.InvariantCultureIgnoreCase))) { - return BadRequest(await _localizationService.Translate(UserId, "auth-key-unique")); + return BadRequest(await localizationService.Translate(UserId, "auth-key-unique")); } var newKey = new AppUserAuthKey() @@ -1256,12 +1229,12 @@ public class AccountController : BaseApiController ExpiresAtUtc = string.IsNullOrEmpty(dto?.ExpiresUtc) ? null : DateTime.Parse(dto.ExpiresUtc), Provider = AuthKeyProvider.User, }; - _unitOfWork.UserRepository.Add(newKey); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Add(newKey); + await unitOfWork.CommitAsync(); - var newDto = _mapper.Map(newKey); + var newDto = mapper.Map(newKey); - await _eventHub.SendMessageToAsync(MessageFactory.AuthKeyUpdate, MessageFactory.AuthKeyUpdatedEvent(newDto), UserId); + await eventHub.SendMessageToAsync(MessageFactory.AuthKeyUpdate, MessageFactory.AuthKeyUpdatedEvent(newDto), UserId); return Ok(newDto); } @@ -1275,14 +1248,14 @@ public class AccountController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteAuthKey(int authKeyId) { - var authKey = await _unitOfWork.UserRepository.GetAuthKeyById(authKeyId); + var authKey = await unitOfWork.UserRepository.GetAuthKeyById(authKeyId); if (authKey?.AppUserId != UserId) return BadRequest(); if (authKey.Provider != AuthKeyProvider.User) return BadRequest(); - _unitOfWork.UserRepository.Delete(authKey); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Delete(authKey); + await unitOfWork.CommitAsync(); - await _eventHub.SendMessageToAsync(MessageFactory.AuthKeyDeleted, MessageFactory.AuthKeyDeletedEvent(authKeyId), UserId); + await eventHub.SendMessageToAsync(MessageFactory.AuthKeyDeleted, MessageFactory.AuthKeyDeletedEvent(authKeyId), UserId); return Ok(); } diff --git a/API/Controllers/ActivityController.cs b/Kavita.Server/Controllers/ActivityController.cs similarity index 82% rename from API/Controllers/ActivityController.cs rename to Kavita.Server/Controllers/ActivityController.cs index 35bb6bb26..ce38665d4 100644 --- a/API/Controllers/ActivityController.cs +++ b/Kavita.Server/Controllers/ActivityController.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Progress; +using Kavita.API.Database; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Progress; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; public class ActivityController(IUnitOfWork unitOfWork) : BaseApiController { diff --git a/API/Controllers/AdminController.cs b/Kavita.Server/Controllers/AdminController.cs similarity index 84% rename from API/Controllers/AdminController.cs rename to Kavita.Server/Controllers/AdminController.cs index 3af60100b..083c47114 100644 --- a/API/Controllers/AdminController.cs +++ b/Kavita.Server/Controllers/AdminController.cs @@ -1,12 +1,12 @@ using System.Threading.Tasks; -using API.Constants; -using API.Entities; -using API.Middleware; +using Kavita.API.Attributes; +using Kavita.Models.Constants; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; #nullable enable diff --git a/API/Controllers/AnnotationController.cs b/Kavita.Server/Controllers/AnnotationController.cs similarity index 94% rename from API/Controllers/AnnotationController.cs rename to Kavita.Server/Controllers/AnnotationController.cs index 82bcfc4c5..c2a8698e1 100644 --- a/API/Controllers/AnnotationController.cs +++ b/Kavita.Server/Controllers/AnnotationController.cs @@ -1,29 +1,25 @@ -#nullable enable -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Metadata.Browse.Requests; -using API.DTOs.Reader; -using API.Extensions; -using API.Helpers; -using API.Middleware; -using API.Services; -using API.SignalR; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.DTOs.Reader; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -namespace API.Controllers; +namespace Kavita.Server.Controllers; public class AnnotationController( IUnitOfWork unitOfWork, - ILogger logger, ILocalizationService localizationService, - IEventHub eventHub, IAnnotationService annotationService) : BaseApiController { @@ -226,6 +222,7 @@ public class AnnotationController( /// /// [HttpPost("export-filter")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task ExportAnnotationsFilter(BrowseAnnotationFilterDto filter, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; @@ -247,6 +244,7 @@ public class AnnotationController( /// Export annotations with the given ids /// [HttpPost("export")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task ExportAnnotations(IList? annotations = null) { var json = await annotationService.ExportAnnotations(UserId, annotations); diff --git a/API/Controllers/BaseApiController.cs b/Kavita.Server/Controllers/BaseApiController.cs similarity index 98% rename from API/Controllers/BaseApiController.cs rename to Kavita.Server/Controllers/BaseApiController.cs index 87e9b9f42..624abbbe3 100644 --- a/API/Controllers/BaseApiController.cs +++ b/Kavita.Server/Controllers/BaseApiController.cs @@ -1,16 +1,14 @@ using System; -using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; -using System.Text; -using API.Services.Store; +using Kavita.API.Store; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using MimeTypes; -namespace API.Controllers; +namespace Kavita.Server.Controllers; #nullable enable diff --git a/API/Controllers/BookController.cs b/Kavita.Server/Controllers/BookController.cs similarity index 56% rename from API/Controllers/BookController.cs rename to Kavita.Server/Controllers/BookController.cs index e7985c9a0..07323728c 100644 --- a/API/Controllers/BookController.cs +++ b/Kavita.Server/Controllers/BookController.cs @@ -2,38 +2,28 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Reader; -using API.Entities.Enums; -using API.Middleware; -using API.Services; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; +using Kavita.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using VersOne.Epub; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class BookController : BaseApiController +public class BookController( + IBookService bookService, + IUnitOfWork unitOfWork, + ICacheService cacheService, + ILocalizationService localizationService) + : BaseApiController { - private readonly IBookService _bookService; - private readonly IUnitOfWork _unitOfWork; - private readonly ICacheService _cacheService; - private readonly ILocalizationService _localizationService; - - public BookController(IBookService bookService, - IUnitOfWork unitOfWork, ICacheService cacheService, - ILocalizationService localizationService) - { - _bookService = bookService; - _unitOfWork = unitOfWork; - _cacheService = cacheService; - _localizationService = localizationService; - } - /// /// Retrieves information for the PDF and Epub reader. This will cache the file. /// @@ -44,8 +34,8 @@ public class BookController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId"])] public async Task> GetBookInfo(int chapterId) { - var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId); - if (dto == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); + var dto = await unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId); + if (dto == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); var bookTitle = string.Empty; @@ -53,19 +43,23 @@ public class BookController : BaseApiController { case MangaFormat.Epub: { - var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0]; - await _cacheService.Ensure(chapterId); - var file = _cacheService.GetCachedFile(chapterId, mangaFile.FilePath); + var mangaFile = (await unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0]; + await cacheService.Ensure(chapterId); + + var file = cacheService.GetCachedFile(chapterId, mangaFile.FilePath); using var book = await EpubReader.OpenBookAsync(file, BookService.LenientBookReaderOptions); + if (book == null) return NotFound(); + bookTitle = book.Title; break; } case MangaFormat.Pdf: { - var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0]; - await _cacheService.Ensure(chapterId); - var file = _cacheService.GetCachedFile(chapterId, mangaFile.FilePath); + var mangaFile = (await unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0]; + await cacheService.Ensure(chapterId); + + var file = cacheService.GetCachedFile(chapterId, mangaFile.FilePath); if (string.IsNullOrEmpty(bookTitle)) { // Override with filename @@ -105,21 +99,21 @@ public class BookController : BaseApiController /// /// /// - [AllowAnonymous] + [ChapterAccess] [SkipDeviceTracking] [HttpGet("{chapterId}/book-resources")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["chapterId", "file"])] public async Task GetBookPageResources(int chapterId, [FromQuery] string file) { - if (chapterId <= 0) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist")); + if (chapterId <= 0) return BadRequest(await localizationService.Get("en", "chapter-doesnt-exist")); - var chapter = await _cacheService.Ensure(chapterId); - if (chapter == null) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist")); + var chapter = await cacheService.Ensure(chapterId); + if (chapter == null) return BadRequest(await localizationService.Get("en", "chapter-doesnt-exist")); - var cachedFilePath = Path.Join(_cacheService.GetCachePath(chapterId), Path.GetFileName(chapter.Files.ElementAt(0).FilePath)); - var result = await _bookService.GetResourceAsync(cachedFilePath, file); + var cachedFilePath = Path.Join(cacheService.GetCachePath(chapterId), Path.GetFileName(chapter.Files.ElementAt(0).FilePath)); + var result = await bookService.GetResourceAsync(cachedFilePath, file); - if (!result.IsSuccess) return BadRequest(await _localizationService.Get("en", result.ErrorMessage)); + if (!result.IsSuccess) return BadRequest(await localizationService.Get("en", result.ErrorMessage)); return File(result.Content, result.ContentType, $"{chapterId}-{file}"); } @@ -134,13 +128,14 @@ public class BookController : BaseApiController [HttpGet("{chapterId}/chapters")] public async Task>> GetBookChapters(int chapterId) { - if (chapterId <= 0) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); - if (chapter == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); + if (chapterId <= 0) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); + + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(chapterId); + if (chapter == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); try { - return Ok(await _bookService.GenerateTableOfContents(chapter)); + return Ok(await bookService.GenerateTableOfContents(chapter)); } catch (KavitaException ex) { @@ -159,23 +154,23 @@ public class BookController : BaseApiController [HttpGet("{chapterId}/book-page")] public async Task> GetBookPage(int chapterId, [FromQuery] int page) { - var chapter = await _cacheService.Ensure(chapterId); - if (chapter == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); - var path = _cacheService.GetCachedFile(chapter); + var chapter = await cacheService.Ensure(chapterId); + if (chapter == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); + var path = cacheService.GetCachedFile(chapter); var baseUrl = "//" + Request.Host + Request.PathBase + "/api/"; try { var ptocBookmarks = - await _unitOfWork.UserTableOfContentRepository.GetPersonalToCForPage(UserId, chapterId, page); - var annotations = await _unitOfWork.UserRepository.GetAnnotationsByPage(UserId, chapter.Id, page); + await unitOfWork.UserTableOfContentRepository.GetPersonalToCForPage(UserId, chapterId, page); + var annotations = await unitOfWork.UserRepository.GetAnnotationsByPage(UserId, chapter.Id, page); - return Ok(await _bookService.GetBookPage(page, chapterId, path, baseUrl, ptocBookmarks, annotations)); + return Ok(await bookService.GetBookPage(UserId, page, chapterId, path, baseUrl, ptocBookmarks, annotations)); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } } } diff --git a/API/Controllers/CBLController.cs b/Kavita.Server/Controllers/CBLController.cs similarity index 67% rename from API/Controllers/CBLController.cs rename to Kavita.Server/Controllers/CBLController.cs index 298681a80..1e0159674 100644 --- a/API/Controllers/CBLController.cs +++ b/Kavita.Server/Controllers/CBLController.cs @@ -2,34 +2,27 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using API.Constants; -using API.DTOs.ReadingLists.CBL; -using API.Middleware; -using API.Services; +using Kavita.API.Attributes; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.ReadingLists.CBL; +using Kavita.Server.Attributes; +using Kavita.Services.Reading; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// Responsible for the CBL import flow /// -public class CblController : BaseApiController +public class CblController( + IReadingListService readingListService, + IDirectoryService directoryService) + : BaseApiController { - private readonly IReadingListService _readingListService; - private readonly IDirectoryService _directoryService; - private readonly ILocalizationService _localizationService; - - public CblController(IReadingListService readingListService, IDirectoryService directoryService, ILocalizationService localizationService) - { - _readingListService = readingListService; - _directoryService = directoryService; - _localizationService = localizationService; - } - /// /// The first step in a cbl import. This validates the cbl file that if an import occured, would it be successful. /// If this returns errors, the cbl will always be rejected by Kavita. @@ -45,39 +38,39 @@ public class CblController : BaseApiController try { var cblReadingList = await SaveAndLoadCblFile(cbl); - var importSummary = await _readingListService.ValidateCblFile(userId, cblReadingList, useComicVineMatching); + var importSummary = await readingListService.ValidateCblFile(userId, cblReadingList, useComicVineMatching); importSummary.FileName = cbl.FileName; return Ok(importSummary); } catch (ArgumentNullException) { - return Ok(new CblImportSummaryDto() + return Ok(new CblImportSummaryDto { FileName = cbl.FileName, Success = CblImportResult.Fail, - Results = new List() - { - new CblBookResult() + Results = + [ + new CblBookResult { Reason = CblImportReason.InvalidFile } - } + ] }); } catch (InvalidOperationException) { - return Ok(new CblImportSummaryDto() + return Ok(new CblImportSummaryDto { FileName = cbl.FileName, Success = CblImportResult.Fail, - Results = new List() - { - new CblBookResult() + Results = + [ + new CblBookResult { Reason = CblImportReason.InvalidFile } - } + ] }); } } @@ -99,38 +92,38 @@ public class CblController : BaseApiController { var userId = UserId; var cblReadingList = await SaveAndLoadCblFile(cbl); - var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cblReadingList, dryRun, useComicVineMatching); + var importSummary = await readingListService.CreateReadingListFromCbl(userId, cblReadingList, dryRun, useComicVineMatching); importSummary.FileName = cbl.FileName; return Ok(importSummary); } catch (ArgumentNullException) { - return Ok(new CblImportSummaryDto() + return Ok(new CblImportSummaryDto { FileName = cbl.FileName, Success = CblImportResult.Fail, - Results = new List() - { - new CblBookResult() + Results = + [ + new CblBookResult { Reason = CblImportReason.InvalidFile } - } + ] }); } catch (InvalidOperationException) { - return Ok(new CblImportSummaryDto() + return Ok(new CblImportSummaryDto { FileName = cbl.FileName, Success = CblImportResult.Fail, - Results = new List() - { - new CblBookResult() + Results = + [ + new CblBookResult { Reason = CblImportReason.InvalidFile } - } + ] }); } @@ -139,7 +132,7 @@ public class CblController : BaseApiController private async Task SaveAndLoadCblFile(IFormFile file) { var filename = Path.GetRandomFileName(); - var outputFile = Path.Join(_directoryService.TempDirectory, filename); + var outputFile = Path.Join(directoryService.TempDirectory, filename); await using var stream = System.IO.File.Create(outputFile); await file.CopyToAsync(stream); stream.Close(); diff --git a/API/Controllers/ChapterController.cs b/Kavita.Server/Controllers/ChapterController.cs similarity index 70% rename from API/Controllers/ChapterController.cs rename to Kavita.Server/Controllers/ChapterController.cs index 081df91b9..b18e9c8b0 100644 --- a/API/Controllers/ChapterController.cs +++ b/Kavita.Server/Controllers/ChapterController.cs @@ -2,42 +2,33 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Entities.Enums; -using API.Entities.MetadataMatching; -using API.Extensions; -using API.Helpers; -using API.Middleware; -using API.Services; -using API.SignalR; -using AutoMapper; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Server.Attributes; +using Kavita.Services.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Nager.ArticleNumber; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -public class ChapterController : BaseApiController +public class ChapterController( + IUnitOfWork unitOfWork, + ILocalizationService localizationService, + IEventHub eventHub, + ILogger logger) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IEventHub _eventHub; - private readonly ILogger _logger; - private readonly IMapper _mapper; - - public ChapterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub, ILogger logger, - IMapper mapper) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _eventHub = eventHub; - _logger = logger; - _mapper = mapper; - } /// /// Gets a single chapter @@ -45,9 +36,10 @@ public class ChapterController : BaseApiController /// /// [HttpGet] + [ChapterAccess] public async Task> GetChapter(int chapterId) { - var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, UserId); + var chapter = await unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, UserId); return Ok(chapter); } @@ -62,46 +54,46 @@ public class ChapterController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> DeleteChapter(int chapterId) { - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId, + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(chapterId, ChapterIncludes.Files | ChapterIncludes.ExternalReviews | ChapterIncludes.ExternalRatings); if (chapter == null) - return BadRequest(_localizationService.Translate(UserId, "chapter-doesnt-exist")); + return BadRequest(localizationService.Translate(UserId, "chapter-doesnt-exist")); - var vol = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId, VolumeIncludes.Chapters); - if (vol == null) return BadRequest(_localizationService.Translate(UserId, "volume-doesnt-exist")); + var vol = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId, VolumeIncludes.Chapters); + if (vol == null) return BadRequest(localizationService.Translate(UserId, "volume-doesnt-exist")); // If there is only 1 chapter within the volume, then we need to remove the volume var needToRemoveVolume = vol.Chapters.Count == 1; if (needToRemoveVolume) { - _unitOfWork.VolumeRepository.Remove(vol); + unitOfWork.VolumeRepository.Remove(vol); } else { - _unitOfWork.ChapterRepository.Remove(chapter); + unitOfWork.ChapterRepository.Remove(chapter); } // If we removed the volume, do an additional check if we need to delete the actual series as well or not - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(vol.SeriesId, SeriesIncludes.ExternalData | SeriesIncludes.Volumes); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(vol.SeriesId, SeriesIncludes.ExternalData | SeriesIncludes.Volumes); var needToRemoveSeries = needToRemoveVolume && series != null && series.Volumes.Count <= 1; if (needToRemoveSeries) { - _unitOfWork.SeriesRepository.Remove(series!); + unitOfWork.SeriesRepository.Remove(series!); } - if (!await _unitOfWork.CommitAsync()) return Ok(false); + if (!await unitOfWork.CommitAsync()) return Ok(false); - await _eventHub.SendMessageAsync(MessageFactory.ChapterRemoved, MessageFactory.ChapterRemovedEvent(chapter.Id, vol.SeriesId), false); + await eventHub.SendMessageAsync(MessageFactory.ChapterRemoved, MessageFactory.ChapterRemovedEvent(chapter.Id, vol.SeriesId), false); if (needToRemoveVolume) { - await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(chapter.VolumeId, vol.SeriesId), false); + await eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(chapter.VolumeId, vol.SeriesId), false); } if (needToRemoveSeries) { - await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, + await eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, MessageFactory.SeriesRemovedEvent(series!.Id, series.Name, series.LibraryId), false); } @@ -114,8 +106,8 @@ public class ChapterController : BaseApiController /// The ID of the series /// The IDs of the chapters to be deleted /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("delete-multiple")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> DeleteMultipleChapters([FromQuery] int seriesId, DeleteChaptersDto dto) { try @@ -127,7 +119,7 @@ public class ChapterController : BaseApiController } // Fetch all chapters to be deleted - var chapters = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds)).ToList(); + var chapters = (await unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds)).ToList(); // Group chapters by their volume var volumesToUpdate = chapters.GroupBy(c => c.VolumeId).ToList(); @@ -139,38 +131,38 @@ public class ChapterController : BaseApiController var chaptersToDelete = volumeGroup.ToList(); // Fetch the volume - var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId, VolumeIncludes.Chapters); + var volume = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId, VolumeIncludes.Chapters); if (volume == null) - return BadRequest(_localizationService.Translate(UserId, "volume-doesnt-exist")); + return BadRequest(localizationService.Translate(UserId, "volume-doesnt-exist")); // Check if all chapters in the volume are being deleted var isVolumeToBeRemoved = volume.Chapters.Count == chaptersToDelete.Count; if (isVolumeToBeRemoved) { - _unitOfWork.VolumeRepository.Remove(volume); + unitOfWork.VolumeRepository.Remove(volume); removedVolumes.Add(volume.Id); } else { // Remove only the specified chapters - _unitOfWork.ChapterRepository.Remove(chaptersToDelete); + unitOfWork.ChapterRepository.Remove(chaptersToDelete); } } - if (!await _unitOfWork.CommitAsync()) return Ok(false); + if (!await unitOfWork.CommitAsync()) return Ok(false); // Send events for removed chapters foreach (var chapter in chapters) { - await _eventHub.SendMessageAsync(MessageFactory.ChapterRemoved, + await eventHub.SendMessageAsync(MessageFactory.ChapterRemoved, MessageFactory.ChapterRemovedEvent(chapter.Id, seriesId), false); } // Send events for removed volumes foreach (var volumeId in removedVolumes) { - await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, + await eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volumeId, seriesId), false); } @@ -178,8 +170,8 @@ public class ChapterController : BaseApiController } catch (Exception ex) { - _logger.LogError(ex, "An error occured while deleting chapters"); - return BadRequest(_localizationService.Translate(UserId, "generic-error")); + logger.LogError(ex, "An error occured while deleting chapters"); + return BadRequest(localizationService.Translate(UserId, "generic-error")); } } @@ -190,14 +182,16 @@ public class ChapterController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("update")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task UpdateChapterMetadata(UpdateChapterDto dto) { - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(dto.Id, - ChapterIncludes.People | ChapterIncludes.Genres | ChapterIncludes.Tags); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(dto.Id, + ChapterIncludes.People | ChapterIncludes.Genres | ChapterIncludes.Tags, HttpContext.RequestAborted); if (chapter == null) - return BadRequest(_localizationService.Translate(UserId, "chapter-doesnt-exist")); + return BadRequest(localizationService.Translate(UserId, "chapter-doesnt-exist")); + + var seriesId = await unitOfWork.ChapterRepository.GetSeriesIdForChapter(chapter.Id, HttpContext.RequestAborted); if (chapter.AgeRating != dto.AgeRating) { @@ -256,12 +250,12 @@ public class ChapterController : BaseApiController #region Genres chapter.Genres ??= []; - await GenreHelper.UpdateChapterGenres(chapter, dto.Genres.Select(t => t.Title), _unitOfWork); + await GenreHelper.UpdateChapterGenres(chapter, dto.Genres.Select(t => t.Title), unitOfWork); #endregion #region Tags chapter.Tags ??= []; - await TagHelper.UpdateChapterTags(chapter, dto.Tags.Select(t => t.Title), _unitOfWork); + await TagHelper.UpdateChapterTags(chapter, dto.Tags.Select(t => t.Title), unitOfWork); #endregion #region People @@ -272,7 +266,7 @@ public class ChapterController : BaseApiController chapter, dto.Writers.Select(p => p.Name).ToList(), PersonRole.Writer, - _unitOfWork + unitOfWork ); // Update characters @@ -280,7 +274,7 @@ public class ChapterController : BaseApiController chapter, dto.Characters.Select(p => p.Name).ToList(), PersonRole.Character, - _unitOfWork + unitOfWork ); // Update pencillers @@ -288,7 +282,7 @@ public class ChapterController : BaseApiController chapter, dto.Pencillers.Select(p => p.Name).ToList(), PersonRole.Penciller, - _unitOfWork + unitOfWork ); // Update inkers @@ -296,7 +290,7 @@ public class ChapterController : BaseApiController chapter, dto.Inkers.Select(p => p.Name).ToList(), PersonRole.Inker, - _unitOfWork + unitOfWork ); // Update colorists @@ -304,7 +298,7 @@ public class ChapterController : BaseApiController chapter, dto.Colorists.Select(p => p.Name).ToList(), PersonRole.Colorist, - _unitOfWork + unitOfWork ); // Update letterers @@ -312,7 +306,7 @@ public class ChapterController : BaseApiController chapter, dto.Letterers.Select(p => p.Name).ToList(), PersonRole.Letterer, - _unitOfWork + unitOfWork ); // Update cover artists @@ -320,7 +314,7 @@ public class ChapterController : BaseApiController chapter, dto.CoverArtists.Select(p => p.Name).ToList(), PersonRole.CoverArtist, - _unitOfWork + unitOfWork ); // Update editors @@ -328,25 +322,26 @@ public class ChapterController : BaseApiController chapter, dto.Editors.Select(p => p.Name).ToList(), PersonRole.Editor, - _unitOfWork + unitOfWork ); - // TODO: Only remove field if changes were made - chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterPublisher); // Update publishers - await PersonHelper.UpdateChapterPeopleAsync( + var updatedPublishers = await PersonHelper.UpdateChapterPeopleAsync( chapter, dto.Publishers.Select(p => p.Name).ToList(), PersonRole.Publisher, - _unitOfWork + unitOfWork ); + if (updatedPublishers) + chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterPublisher); + // Update translators await PersonHelper.UpdateChapterPeopleAsync( chapter, dto.Translators.Select(p => p.Name).ToList(), PersonRole.Translator, - _unitOfWork + unitOfWork ); // Update imprints @@ -354,7 +349,7 @@ public class ChapterController : BaseApiController chapter, dto.Imprints.Select(p => p.Name).ToList(), PersonRole.Imprint, - _unitOfWork + unitOfWork ); // Update teams @@ -362,7 +357,7 @@ public class ChapterController : BaseApiController chapter, dto.Teams.Select(p => p.Name).ToList(), PersonRole.Team, - _unitOfWork + unitOfWork ); // Update locations @@ -370,7 +365,7 @@ public class ChapterController : BaseApiController chapter, dto.Locations.Select(p => p.Name).ToList(), PersonRole.Location, - _unitOfWork + unitOfWork ); #endregion @@ -398,17 +393,21 @@ public class ChapterController : BaseApiController #endregion - _unitOfWork.ChapterRepository.Update(chapter); + unitOfWork.ChapterRepository.Update(chapter); - if (!_unitOfWork.HasChanges()) + if (!unitOfWork.HasChanges()) { return Ok(); } - // TODO: Emit a ChapterMetadataUpdate out - - await _unitOfWork.CommitAsync(); + if (seriesId.HasValue) + { + await eventHub.SendMessageAsync(MessageFactory.ChapterUpdated, + MessageFactory.ChapterUpdatedEvent(chapter.Id, seriesId.Value), + false, HttpContext.RequestAborted); + } + await unitOfWork.CommitAsync(); return Ok(); } @@ -418,24 +417,25 @@ public class ChapterController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("chapter-detail-plus")] public async Task> ChapterDetailPlus([FromQuery] int chapterId) { var ret = new ChapterDetailPlusDto(); - var userReviews = (await _unitOfWork.UserRepository.GetUserRatingDtosForChapterAsync(chapterId, UserId)) + var userReviews = (await unitOfWork.UserRepository.GetUserRatingDtosForChapterAsync(chapterId, UserId)) .Where(r => !string.IsNullOrEmpty(r.Body)) .OrderByDescending(review => review.Username.Equals(Username!) ? 1 : 0) .ToList(); - var ownRating = await _unitOfWork.UserRepository.GetUserChapterRatingAsync(UserId, chapterId); + var ownRating = await unitOfWork.UserRepository.GetUserChapterRatingAsync(UserId, chapterId); if (ownRating != null) { ret.Rating = ownRating.Rating; ret.HasBeenRated = ownRating.HasBeenRated; } - var externalReviews = await _unitOfWork.ChapterRepository.GetExternalChapterReviewDtos(chapterId); + var externalReviews = await unitOfWork.ChapterRepository.GetExternalChapterReviewDtos(chapterId); if (externalReviews.Count > 0) { userReviews.AddRange(ReviewHelper.SelectSpectrumOfReviews(externalReviews)); @@ -443,7 +443,7 @@ public class ChapterController : BaseApiController ret.Reviews = userReviews; - ret.Ratings = await _unitOfWork.ChapterRepository.GetExternalChapterRatingDtos(chapterId); + ret.Ratings = await unitOfWork.ChapterRepository.GetExternalChapterRatingDtos(chapterId); return Ok(ret); } diff --git a/API/Controllers/CollectionController.cs b/Kavita.Server/Controllers/CollectionController.cs similarity index 56% rename from API/Controllers/CollectionController.cs rename to Kavita.Server/Controllers/CollectionController.cs index e333bd42a..06a5bd29e 100644 --- a/API/Controllers/CollectionController.cs +++ b/Kavita.Server/Controllers/CollectionController.cs @@ -2,53 +2,36 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Collection; -using API.DTOs.CollectionTags; -using API.Entities; -using API.Helpers.Builders; -using API.Middleware; -using API.Services; -using API.Services.Plus; -using API.SignalR; using Hangfire; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.CollectionTags; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// APIs for Collections /// -public class CollectionController : BaseApiController +/// +public class CollectionController(IUnitOfWork unitOfWork, ICollectionTagService collectionService, + ILocalizationService localizationService, IExternalMetadataService externalMetadataService, + ISmartCollectionSyncService collectionSyncService, ILogger logger, + IEventHub eventHub) : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ICollectionTagService _collectionService; - private readonly ILocalizationService _localizationService; - private readonly IExternalMetadataService _externalMetadataService; - private readonly ISmartCollectionSyncService _collectionSyncService; - private readonly ILogger _logger; - private readonly IEventHub _eventHub; - - /// - public CollectionController(IUnitOfWork unitOfWork, ICollectionTagService collectionService, - ILocalizationService localizationService, IExternalMetadataService externalMetadataService, - ISmartCollectionSyncService collectionSyncService, ILogger logger, - IEventHub eventHub) - { - _unitOfWork = unitOfWork; - _collectionService = collectionService; - _localizationService = localizationService; - _externalMetadataService = externalMetadataService; - _collectionSyncService = collectionSyncService; - _logger = logger; - _eventHub = eventHub; - } /// /// Returns all Collection tags for a given User @@ -57,7 +40,7 @@ public class CollectionController : BaseApiController [HttpGet] public async Task>> GetAllTags(bool ownedOnly = false) { - return Ok(await _unitOfWork.CollectionTagRepository.GetCollectionDtosAsync(UserId, !ownedOnly)); + return Ok(await unitOfWork.CollectionTagRepository.GetCollectionDtosAsync(UserId, !ownedOnly)); } /// @@ -68,8 +51,8 @@ public class CollectionController : BaseApiController [HttpGet("single")] public async Task> GetTag(int collectionId) { - var result = await _unitOfWork.CollectionTagRepository.GetCollectionDtoAsync(collectionId, UserId); - if (result == null) return NotFound(); // TODO: Figure out how to best handle restrictions/not found across the codebase + var result = await unitOfWork.CollectionTagRepository.GetCollectionDtoAsync(collectionId, UserId); + if (result == null) return NotFound(); return Ok(result); } @@ -83,7 +66,7 @@ public class CollectionController : BaseApiController [HttpGet("all-series")] public async Task>> GetCollectionsBySeries(int seriesId, bool ownedOnly = false) { - return Ok(await _unitOfWork.CollectionTagRepository.GetCollectionDtosBySeriesAsync(UserId, seriesId, !ownedOnly)); + return Ok(await unitOfWork.CollectionTagRepository.GetCollectionDtosBySeriesAsync(UserId, seriesId, !ownedOnly)); } @@ -95,7 +78,7 @@ public class CollectionController : BaseApiController [HttpGet("name-exists")] public async Task> DoesNameExists(string name) { - return Ok(await _unitOfWork.CollectionTagRepository.CollectionExists(name, UserId)); + return Ok(await unitOfWork.CollectionTagRepository.CollectionExists(name, UserId)); } /// @@ -110,19 +93,19 @@ public class CollectionController : BaseApiController { try { - if (await _collectionService.UpdateTag(updatedTag, UserId)) + if (await collectionService.UpdateTag(updatedTag, UserId)) { - await _eventHub.SendMessageAsync(MessageFactory.CollectionUpdated, + await eventHub.SendMessageAsync(MessageFactory.CollectionUpdated, MessageFactory.CollectionUpdatedEvent(updatedTag.Id), false); - return Ok(await _unitOfWork.CollectionTagRepository.GetCollectionDtoAsync(updatedTag.Id, UserId)); + return Ok(await unitOfWork.CollectionTagRepository.GetCollectionDtoAsync(updatedTag.Id, UserId)); } } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } /// @@ -135,23 +118,23 @@ public class CollectionController : BaseApiController public async Task PromoteMultipleCollections(PromoteCollectionsDto dto) { // This needs to take into account owner as I can select other users cards - var collections = await _unitOfWork.CollectionTagRepository.GetCollectionsByIds(dto.CollectionIds); + var collections = await unitOfWork.CollectionTagRepository.GetCollectionsByIds(dto.CollectionIds); var userId = UserId; if (!User.IsInRole(PolicyConstants.PromoteRole) && !User.IsInRole(PolicyConstants.AdminRole)) { - return BadRequest(await _localizationService.Translate(userId, "permission-denied")); + return BadRequest(await localizationService.Translate(userId, "permission-denied")); } foreach (var collection in collections) { if (collection.AppUserId != userId) continue; collection.Promoted = dto.Promoted; - _unitOfWork.CollectionTagRepository.Update(collection); + unitOfWork.CollectionTagRepository.Update(collection); } - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return Ok(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -167,14 +150,15 @@ public class CollectionController : BaseApiController public async Task DeleteMultipleCollections(DeleteCollectionsDto dto) { // This needs to take into account owner as I can select other users cards - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); if (user == null) return Unauthorized(); + user.Collections = user.Collections.Where(uc => !dto.CollectionIds.Contains(uc.Id)).ToList(); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return Ok(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -189,7 +173,7 @@ public class CollectionController : BaseApiController public async Task AddToMultipleSeries(CollectionTagBulkAddDto dto) { // Create a new tag and save - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); if (user == null) return Unauthorized(); AppUserCollection? tag; @@ -206,19 +190,19 @@ public class CollectionController : BaseApiController if (tag == null) { - return BadRequest(_localizationService.Translate(UserId, "collection-doesnt-exists")); + return BadRequest(localizationService.Translate(UserId, "collection-doesnt-exists")); } - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(dto.SeriesIds.ToList(), false); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdsAsync(dto.SeriesIds.ToList(), false); foreach (var s in series) { if (tag.Items.Contains(s)) continue; tag.Items.Add(s); } - _unitOfWork.UserRepository.Update(user); - if (await _unitOfWork.CommitAsync()) return Ok(); + unitOfWork.UserRepository.Update(user); + if (await unitOfWork.CommitAsync()) return Ok(); - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } /// @@ -232,18 +216,18 @@ public class CollectionController : BaseApiController { try { - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(updateSeriesForTagDto.Tag.Id, CollectionIncludes.Series); - if (tag == null) return BadRequest(await _localizationService.Translate(UserId, "collection-doesnt-exist")); + var tag = await unitOfWork.CollectionTagRepository.GetCollectionAsync(updateSeriesForTagDto.Tag.Id, CollectionIncludes.Series); + if (tag == null) return BadRequest(await localizationService.Translate(UserId, "collection-doesnt-exist")); - if (await _collectionService.RemoveTagFromSeries(tag, updateSeriesForTagDto.SeriesIdsToRemove)) - return Ok(await _localizationService.Translate(UserId, "collection-updated")); + if (await collectionService.RemoveTagFromSeries(tag, updateSeriesForTagDto.SeriesIdsToRemove)) + return Ok(await localizationService.Translate(UserId, "collection-updated")); } catch (Exception) { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } /// @@ -257,23 +241,24 @@ public class CollectionController : BaseApiController { try { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); if (user == null) return Unauthorized(); - if (user.Collections.All(c => c.Id != tagId)) - return BadRequest(await _localizationService.Translate(user.Id, "access-denied")); - if (await _collectionService.DeleteTag(tagId, user)) + if (user.Collections.All(c => c.Id != tagId)) + return BadRequest(await localizationService.Translate(user.Id, "access-denied")); + + if (await collectionService.DeleteTag(tagId, user)) { - return Ok(await _localizationService.Translate(UserId, "collection-deleted")); + return Ok(await localizationService.Translate(UserId, "collection-deleted")); } } catch (Exception ex) { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } /// @@ -285,7 +270,7 @@ public class CollectionController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task>> GetMalStacksForUser() { - return Ok(await _externalMetadataService.GetStacksForUser(UserId)); + return Ok(await externalMetadataService.GetStacksForUser(UserId)); } /// @@ -297,13 +282,13 @@ public class CollectionController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task ImportMalStack(MalStackDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); if (user == null) return Unauthorized(); // Validation check to ensure stack doesn't exist already - if (await _unitOfWork.CollectionTagRepository.CollectionExists(dto.Title, user.Id)) + if (await unitOfWork.CollectionTagRepository.CollectionExists(dto.Title, user.Id)) { - return BadRequest(_localizationService.Translate(user.Id, "collection-already-exists")); + return BadRequest(localizationService.Translate(user.Id, "collection-already-exists")); } try @@ -315,18 +300,18 @@ public class CollectionController : BaseApiController .Build(); user.Collections.Add(newCollection); - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); // Trigger Stack Refresh for just one stack (not all) - BackgroundJob.Enqueue(() => _collectionSyncService.Sync(newCollection.Id)); + BackgroundJob.Enqueue(() => collectionSyncService.Sync(newCollection.Id)); return Ok(); } catch (Exception ex) { - _logger.LogError(ex, "There was an issue importing MAL Stack"); + logger.LogError(ex, "There was an issue importing MAL Stack"); } - return BadRequest(_localizationService.Translate(user.Id, "error-import-stack")); + return BadRequest(localizationService.Translate(user.Id, "error-import-stack")); } } diff --git a/API/Controllers/ColorScapeController.cs b/Kavita.Server/Controllers/ColorScapeController.cs similarity index 68% rename from API/Controllers/ColorScapeController.cs rename to Kavita.Server/Controllers/ColorScapeController.cs index 850e94122..7fd64434a 100644 --- a/API/Controllers/ColorScapeController.cs +++ b/Kavita.Server/Controllers/ColorScapeController.cs @@ -1,31 +1,26 @@ using System.Threading.Tasks; -using API.Data; -using API.DTOs.Theme; -using API.Entities.Interfaces; +using Kavita.API.Database; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities.Interfaces; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; [Authorize] -public class ColorScapeController : BaseApiController +public class ColorScapeController(IUnitOfWork unitOfWork) : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - - public ColorScapeController(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - /// /// Returns the color scape for a series /// /// /// + [SeriesAccess] [HttpGet("series")] public async Task> GetColorScapeForSeries(int id) { - var entity = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(id, UserId); + var entity = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(id, UserId); return GetColorSpaceDto(entity); } @@ -34,10 +29,11 @@ public class ColorScapeController : BaseApiController /// /// /// + [VolumeAccess] [HttpGet("volume")] public async Task> GetColorScapeForVolume(int id) { - var entity = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(id, UserId); + var entity = await unitOfWork.VolumeRepository.GetVolumeDtoAsync(id, UserId); return GetColorSpaceDto(entity); } @@ -46,15 +42,16 @@ public class ColorScapeController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("chapter")] public async Task> GetColorScapeForChapter(int id) { - var entity = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(id, UserId); + var entity = await unitOfWork.ChapterRepository.GetChapterDtoAsync(id, UserId); return GetColorSpaceDto(entity); } - private ActionResult GetColorSpaceDto(IHasCoverImage entity) + private ActionResult GetColorSpaceDto(IHasCoverImage? entity) { if (entity == null) return Ok(ColorScapeDto.Empty); return Ok(new ColorScapeDto(entity.PrimaryColor, entity.SecondaryColor)); diff --git a/API/Controllers/DeprecatedController.cs b/Kavita.Server/Controllers/DeprecatedController.cs similarity index 74% rename from API/Controllers/DeprecatedController.cs rename to Kavita.Server/Controllers/DeprecatedController.cs index 55e0c19a0..e340013a8 100644 --- a/API/Controllers/DeprecatedController.cs +++ b/Kavita.Server/Controllers/DeprecatedController.cs @@ -2,49 +2,38 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs; -using API.DTOs.Account; -using API.DTOs.Filtering; -using API.DTOs.Metadata; -using API.DTOs.Progress; -using API.DTOs.Statistics; -using API.DTOs.Uploads; -using API.Extensions; -using API.Helpers; -using API.Services; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.Statistics; +using Kavita.Models.DTOs.Uploads; +using Kavita.Server.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; +namespace Kavita.Server.Controllers; /// /// All APIs here are subject to be removed and are no longer maintained. Will be removed v0.9.0 /// [Route("api/")] -public class DeprecatedController : BaseApiController +public class DeprecatedController( + IUnitOfWork unitOfWork, + ILocalizationService localizationService, + ITaskScheduler taskScheduler, + ILogger logger, + IStatisticService statService, + IMapper mapper) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly ITaskScheduler _taskScheduler; - private readonly ILogger _logger; - private readonly IStatisticService _statService; - private readonly IMapper _mapper; - - public DeprecatedController(IUnitOfWork unitOfWork, ILocalizationService localizationService, ITaskScheduler taskScheduler, - ILogger logger, IStatisticService statService, IMapper mapper) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _taskScheduler = taskScheduler; - _logger = logger; - _statService = statService; - _mapper = mapper; - } - /// /// Return all Series that are in the current logged-in user's Want to Read list, filtered (deprecated, use v2) /// @@ -57,7 +46,7 @@ public class DeprecatedController : BaseApiController public async Task>> GetWantToRead([FromQuery] UserParams? userParams, FilterDto filterDto) { userParams ??= new UserParams(); - var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(UserId, userParams, filterDto); + var pagedList = await unitOfWork.SeriesRepository.GetWantToReadForUserAsync(UserId, userParams, filterDto); Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages); return Ok(pagedList); @@ -72,7 +61,7 @@ public class DeprecatedController : BaseApiController [HttpGet("series/chapter-metadata")] public async Task> GetChapterMetadata(int chapterId) { - return Ok(await _unitOfWork.ChapterRepository.GetChapterMetadataDtoAsync(chapterId)); + return Ok(await unitOfWork.ChapterRepository.GetChapterMetadataDtoAsync(chapterId)); } /// @@ -89,7 +78,7 @@ public class DeprecatedController : BaseApiController { var userId = UserId; var series = - await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto); + await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -110,7 +99,7 @@ public class DeprecatedController : BaseApiController { var userId = UserId; var series = - await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto); + await unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -130,7 +119,7 @@ public class DeprecatedController : BaseApiController { var userId = UserId; var series = - await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto); + await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -149,36 +138,36 @@ public class DeprecatedController : BaseApiController { try { - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id); - if (chapter == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id); + if (chapter == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); var originalFile = chapter.CoverImage; chapter.CoverImage = string.Empty; chapter.CoverImageLocked = false; - _unitOfWork.ChapterRepository.Update(chapter); + unitOfWork.ChapterRepository.Update(chapter); - var volume = (await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId))!; + var volume = (await unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId))!; volume.CoverImage = chapter.CoverImage; - _unitOfWork.VolumeRepository.Update(volume); + unitOfWork.VolumeRepository.Update(volume); - var series = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId))!; + var series = (await unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId))!; - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); if (originalFile != null) System.IO.File.Delete(originalFile); - await _taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true); + await taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true); return Ok(); } } catch (Exception e) { - _logger.LogError(e, "There was an issue resetting cover lock for Chapter {Id}", uploadFileDto.Id); - await _unitOfWork.RollbackAsync(); + logger.LogError(e, "There was an issue resetting cover lock for Chapter {Id}", uploadFileDto.Id); + await unitOfWork.RollbackAsync(); } - return BadRequest(await _localizationService.Translate(UserId, "reset-chapter-lock")); + return BadRequest(await localizationService.Translate(UserId, "reset-chapter-lock")); } @@ -187,11 +176,11 @@ public class DeprecatedController : BaseApiController [Obsolete("Will be removed in v0.9.0")] public async Task>> GetReadingHistory(int userId) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); var isAdmin = User.IsInRole(PolicyConstants.AdminRole); if (!isAdmin && userId != user!.Id) return BadRequest(); - return Ok(await _statService.GetReadingHistory(userId)); + return Ok(await statService.GetReadingHistory(userId)); } [Authorize(PolicyGroups.AdminPolicy)] @@ -200,7 +189,7 @@ public class DeprecatedController : BaseApiController [Obsolete("Will be removed in v0.9.0")] public async Task>>> GetTopYears() { - return Ok(await _statService.GetTopYears()); + return Ok(await statService.GetTopYears()); } /// @@ -214,11 +203,11 @@ public class DeprecatedController : BaseApiController [Obsolete("Will be removed in v0.9.0")] public async Task>>> ReadCountByDay(int userId = 0, int days = 0) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); var isAdmin = User.IsInRole(PolicyConstants.AdminRole); if (!isAdmin && userId != user!.Id) return BadRequest(); - return Ok(await _statService.ReadCountByDay(userId, days)); + return Ok(await statService.ReadCountByDay(userId, days)); } [Authorize(PolicyGroups.AdminPolicy)] @@ -227,7 +216,7 @@ public class DeprecatedController : BaseApiController [Obsolete("Will be removed in v0.9.0")] public async Task>>> GetYearStatistics() { - return Ok(await _statService.GetYearCount()); + return Ok(await statService.GetYearCount()); } /// @@ -241,7 +230,7 @@ public class DeprecatedController : BaseApiController [Obsolete("Will be removed in v0.9.0")] public async Task>> GetTopReads(int days = 0) { - return Ok(await _statService.GetTopUsers(days)); + return Ok(await statService.GetTopUsers(days)); } /// @@ -254,7 +243,7 @@ public class DeprecatedController : BaseApiController public async Task>> GetProgressForChapter(int chapterId) { var userId = User.IsInRole(PolicyConstants.AdminRole) ? 0 : UserId; - return Ok(await _unitOfWork.AppUserProgressRepository.GetUserProgressForChapter(chapterId, userId)); + return Ok(await unitOfWork.AppUserProgressRepository.GetUserProgressForChapter(chapterId, userId)); } /// @@ -268,7 +257,7 @@ public class DeprecatedController : BaseApiController public async Task>> GetQuickReads(int libraryId, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; - var series = await _unitOfWork.SeriesRepository.GetQuickReads(UserId, libraryId, userParams); + var series = await unitOfWork.SeriesRepository.GetQuickReads(UserId, libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); @@ -285,7 +274,7 @@ public class DeprecatedController : BaseApiController public async Task>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; - var series = await _unitOfWork.SeriesRepository.GetQuickCatchupReads(UserId, libraryId, userParams); + var series = await unitOfWork.SeriesRepository.GetQuickCatchupReads(UserId, libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); @@ -304,7 +293,7 @@ public class DeprecatedController : BaseApiController var userId = UserId; userParams ??= UserParams.Default; - var series = await _unitOfWork.SeriesRepository.GetHighlyRated(userId, libraryId, userParams); + var series = await unitOfWork.SeriesRepository.GetHighlyRated(userId, libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -325,7 +314,7 @@ public class DeprecatedController : BaseApiController var userId = UserId; userParams ??= UserParams.Default; - var series = await _unitOfWork.SeriesRepository.GetMoreIn(userId, libraryId, genreId, userParams); + var series = await unitOfWork.SeriesRepository.GetMoreIn(userId, libraryId, genreId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); @@ -342,7 +331,7 @@ public class DeprecatedController : BaseApiController public async Task>> GetRediscover(int libraryId, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; - var series = await _unitOfWork.SeriesRepository.GetRediscover(UserId, libraryId, userParams); + var series = await unitOfWork.SeriesRepository.GetRediscover(UserId, libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); @@ -352,8 +341,8 @@ public class DeprecatedController : BaseApiController [HttpGet("users/myself")] public async Task>> GetMyself() { - var users = await _unitOfWork.UserRepository.GetAllUsersAsync(); - return Ok(users.Where(u => u.UserName == Username!).DefaultIfEmpty().Select(u => _mapper.Map(u)).SingleOrDefault()); + var users = await unitOfWork.UserRepository.GetAllUsersAsync(); + return Ok(users.Where(u => u.UserName == Username!).DefaultIfEmpty().Select(u => mapper.Map(u)).SingleOrDefault()); } } diff --git a/Kavita.Server/Controllers/DeviceController.cs b/Kavita.Server/Controllers/DeviceController.cs new file mode 100644 index 000000000..2bedd6c7b --- /dev/null +++ b/Kavita.Server/Controllers/DeviceController.cs @@ -0,0 +1,246 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Device.ClientDevice; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.SignalR; +using Kavita.Server.Attributes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Kavita.Server.Controllers; + +/// +/// Responsible for interacting and creating Devices +/// +public class DeviceController( + IUnitOfWork unitOfWork, + IDeviceService deviceService, + IEventHub eventHub, + ILocalizationService localizationService, + IMapper mapper, + IClientDeviceService clientDeviceService) + : BaseApiController +{ + /// + /// Creates a new Device + /// + /// + /// + [HttpPost("create")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task> CreateOrUpdateDevice(CreateEmailDeviceDto dto) + { + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); + if (user == null) return Unauthorized(); + try + { + var device = await deviceService.Create(dto, user); + if (device == null) + return BadRequest(await localizationService.Translate(UserId, "generic-device-create")); + + return Ok(mapper.Map(device)); + } + catch (KavitaException ex) + { + return BadRequest(await localizationService.Translate(UserId, ex.Message)); + } + } + + /// + /// Updates an existing Device + /// + /// + /// + [HttpPost("update")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task> UpdateDevice(UpdateEmailDeviceDto dto) + { + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); + if (user == null) return Unauthorized(); + + var device = await deviceService.Update(dto, user); + if (device == null) return BadRequest(await localizationService.Translate(UserId, "generic-device-update")); + + return Ok(mapper.Map(device)); + } + + /// + /// Deletes the device from the user + /// + /// + /// + [HttpDelete] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task DeleteDevice(int deviceId) + { + if (deviceId <= 0) return BadRequest(await localizationService.Translate(UserId, "device-doesnt-exist")); + + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); + if (user == null) return Unauthorized(); + + if (await deviceService.Delete(user, deviceId)) return Ok(); + + return BadRequest(await localizationService.Translate(UserId, "generic-device-delete")); + } + + [HttpGet] + public async Task>> GetDevices() + { + return Ok(await unitOfWork.DeviceRepository.GetDevicesForUserAsync(UserId)); + } + + /// + /// Sends a collection of chapters to the user's device + /// + /// + /// + [HttpPost("send-to")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task SendToDevice(SendToEmailDeviceDto dto) + { + var userId = UserId; + if (dto.ChapterIds.Any(i => i < 0)) return BadRequest(await localizationService.Translate(userId, "greater-0", "ChapterIds")); + if (dto.DeviceId < 0) return BadRequest(await localizationService.Translate(userId, "greater-0", "DeviceId")); + + var isEmailSetup = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); + if (!isEmailSetup) + return BadRequest(await localizationService.Translate(userId, "send-to-kavita-email")); + + // // Validate that the device belongs to the user + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.Devices); + if (user == null || user.Devices.All(d => d.Id != dto.DeviceId)) return BadRequest(await localizationService.Translate(userId, "send-to-unallowed")); + + await eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, + MessageFactory.SendingToDeviceEvent(await localizationService.Translate(userId, "send-to-device-status"), + "started"), userId); + try + { + var success = await deviceService.SendTo(dto.ChapterIds, dto.DeviceId); + if (success) return Ok(); + } + catch (KavitaException ex) + { + return BadRequest(await localizationService.Translate(userId, ex.Message)); + } + finally + { + await eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, + MessageFactory.SendingToDeviceEvent(await localizationService.Translate(userId, "send-to-device-status"), + "ended"), userId); + } + + return BadRequest(await localizationService.Translate(userId, "generic-send-to")); + } + + + /// + /// Attempts to send a whole series to a device. + /// + /// + /// + [HttpPost("send-series-to")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task SendSeriesToDevice(SendSeriesToEmailDeviceDto dto) + { + var userId = UserId; + if (dto.SeriesId <= 0) return BadRequest(await localizationService.Translate(userId, "greater-0", "SeriesId")); + if (dto.DeviceId < 0) return BadRequest(await localizationService.Translate(userId, "greater-0", "DeviceId")); + + var isEmailSetup = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); + if (!isEmailSetup) + return BadRequest(await localizationService.Translate(userId, "send-to-kavita-email")); + + await eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, + MessageFactory.SendingToDeviceEvent(await localizationService.Translate(userId, "send-to-device-status"), + "started"), userId); + + var series = + await unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, + SeriesIncludes.Volumes | SeriesIncludes.Chapters); + if (series == null) return BadRequest(await localizationService.Translate(userId, "series-doesnt-exist")); + var chapterIds = series.Volumes.SelectMany(v => v.Chapters.Select(c => c.Id)).ToList(); + try + { + var success = await deviceService.SendTo(chapterIds, dto.DeviceId); + if (success) return Ok(); + } + catch (KavitaException ex) + { + return BadRequest(await localizationService.Translate(userId, ex.Message)); + } + finally + { + await eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, + MessageFactory.SendingToDeviceEvent(await localizationService.Translate(userId, "send-to-device-status"), + "ended"), userId); + } + + return BadRequest(await localizationService.Translate(userId, "generic-send-to")); + } + + #region Client Devices + /// + /// Get my client devices + /// + /// + /// + [HttpGet("client/devices")] + public async Task>> GetMyClientDevices(bool includeInactive = false) + { + return Ok(await unitOfWork.ClientDeviceRepository.GetUserDeviceDtosAsync(UserId, includeInactive)); + } + + /// + /// Get All user client devices + /// + /// + /// + [Authorize(PolicyGroups.AdminPolicy)] + [HttpGet("client/all-devices")] + public async Task>> GetAllClientDevices(bool includeInactive = false) + { + return Ok(await unitOfWork.ClientDeviceRepository.GetAllUserDeviceDtos(includeInactive)); + } + + + /// + /// Removes the client device from DB + /// + /// + /// + [HttpDelete("client/device")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task> DeleteClientDevice(int clientDeviceId) + { + return Ok(await clientDeviceService.DeleteDeviceAsync(UserId, clientDeviceId)); + } + + /// + /// Update the friendly name of the Device + /// + /// + /// + [HttpPost("client/update-name")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task UpdateClientDeviceName(UpdateClientDeviceNameDto dto) + { + await clientDeviceService.UpdateFriendlyNameAsync(UserId, dto); + return Ok(); + } + + + + #endregion Client Devices + +} + + diff --git a/API/Controllers/DownloadController.cs b/Kavita.Server/Controllers/DownloadController.cs similarity index 54% rename from API/Controllers/DownloadController.cs rename to Kavita.Server/Controllers/DownloadController.cs index 9729441ad..08df315e0 100644 --- a/API/Controllers/DownloadController.cs +++ b/Kavita.Server/Controllers/DownloadController.cs @@ -3,63 +3,49 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Downloads; -using API.Entities; -using API.Entities.Enums; -using API.Services; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Downloads; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; +using Kavita.Services.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// All APIs related to downloading entities from the system. Requires Download Role or Admin Role. /// [Authorize(PolicyGroups.DownloadPolicy)] -public class DownloadController : BaseApiController +public class DownloadController( + IUnitOfWork unitOfWork, + IArchiveService archiveService, + IDownloadService downloadService, + IEventHub eventHub, + ILogger logger, + IBookmarkService bookmarkService, + ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IArchiveService _archiveService; - private readonly IDirectoryService _directoryService; - private readonly IDownloadService _downloadService; - private readonly IEventHub _eventHub; - private readonly ILogger _logger; - private readonly IBookmarkService _bookmarkService; - private readonly IAccountService _accountService; - private readonly ILocalizationService _localizationService; private const string DefaultContentType = "application/octet-stream"; - public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService, - IDownloadService downloadService, IEventHub eventHub, ILogger logger, IBookmarkService bookmarkService, - IAccountService accountService, ILocalizationService localizationService) - { - _unitOfWork = unitOfWork; - _archiveService = archiveService; - _directoryService = directoryService; - _downloadService = downloadService; - _eventHub = eventHub; - _logger = logger; - _bookmarkService = bookmarkService; - _accountService = accountService; - _localizationService = localizationService; - } - /// /// For a given volume, return the size in bytes /// /// /// + [VolumeAccess] [HttpGet("volume-size")] public async Task> GetVolumeSize(int volumeId) { - return Ok(await _unitOfWork.VolumeRepository.GetFilesizeForVolumeAsync(volumeId)); + return Ok(await unitOfWork.VolumeRepository.GetFilesizeForVolumeAsync(volumeId)); } /// @@ -70,7 +56,7 @@ public class DownloadController : BaseApiController [HttpPost("bulk-volume-size")] public async Task>> GetBulkVolumeSize([FromBody] IList volumeIds) { - return Ok(await _unitOfWork.VolumeRepository.GetFilesizeForVolumesAsync(volumeIds)); + return Ok(await unitOfWork.VolumeRepository.GetFilesizeForVolumesAsync(volumeIds)); } /// @@ -78,10 +64,11 @@ public class DownloadController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("chapter-size")] public async Task> GetChapterSize(int chapterId) { - return Ok(await _unitOfWork.ChapterRepository.GetFilesizeForChapterAsync(chapterId)); + return Ok(await unitOfWork.ChapterRepository.GetFilesizeForChapterAsync(chapterId)); } /// @@ -93,7 +80,7 @@ public class DownloadController : BaseApiController public async Task>> GetChapterSizeInBulk([FromBody] IList chapterIds) { // If there are more than 50 chapterIds, we need to break up into multiple calls - return Ok(await _unitOfWork.ChapterRepository.GetFilesizeForChaptersAsync(chapterIds)); + return Ok(await unitOfWork.ChapterRepository.GetFilesizeForChaptersAsync(chapterIds)); } /// @@ -101,10 +88,11 @@ public class DownloadController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("series-size")] public async Task> GetSeriesSize(int seriesId) { - return Ok(await _unitOfWork.SeriesRepository.GetFilesizeForSeriesAsync(seriesId)); + return Ok(await unitOfWork.SeriesRepository.GetFilesizeForSeriesAsync(seriesId)); } /// @@ -115,7 +103,7 @@ public class DownloadController : BaseApiController [HttpPost("bulk-series-size")] public async Task>> GetBulkSeriesSize([FromBody] IList seriesIds) { - return Ok(await _unitOfWork.SeriesRepository.GetFilesizeForMultipleSeriesAsync(seriesIds)); + return Ok(await unitOfWork.SeriesRepository.GetFilesizeForMultipleSeriesAsync(seriesIds)); } @@ -124,15 +112,17 @@ public class DownloadController : BaseApiController /// /// /// - [Authorize(PolicyGroups.DownloadPolicy)] + [VolumeAccess] [HttpGet("volume")] + [Authorize(PolicyGroups.DownloadPolicy)] public async Task DownloadVolume(int volumeId, [FromQuery] string? correlationId = null) { - if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(UserId, "permission-denied")); - var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId); - if (volume == null) return BadRequest(await _localizationService.Translate(UserId, "volume-doesnt-exist")); - var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId); + var volume = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId); + if (volume == null) return BadRequest(await localizationService.Translate(UserId, "volume-doesnt-exist")); + + var files = await unitOfWork.VolumeRepository.GetFilesForVolume(volumeId); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId); + try { return await DownloadFiles(files, $"download_{Username!}_v{volumeId}", $"{series!.Name} - Volume {volume.Name}.zip", correlationId); @@ -143,16 +133,9 @@ public class DownloadController : BaseApiController } } - private async Task HasDownloadPermission() - { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); - if (user == null) return false; - return await _accountService.HasDownloadPermission(user); - } - private PhysicalFileResult GetFirstFileDownload(IEnumerable files) { - var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files); + var (zipFile, contentType, fileDownloadName) = downloadService.GetFirstFileDownload(files); return PhysicalFile(zipFile, contentType, fileDownloadName, true); } @@ -161,18 +144,23 @@ public class DownloadController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("chapter")] public async Task DownloadChapter(int chapterId, [FromQuery] string? correlationId = null) { - if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(UserId, "permission-denied")); - var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); - if (chapter == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); - var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId); + var files = await unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(chapterId); + if (chapter == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); + + var volume = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId); + try { - return await DownloadFiles(files, $"download_{Username!}_c{chapterId}", $"{series!.Name} - Chapter {chapter.GetNumberTitle()}.zip", correlationId); + return await DownloadFiles(files, + $"download_{Username!}_c{chapterId}", + $"{series!.Name} - Chapter {chapter.GetNumberTitle()}.zip", + correlationId); } catch (KavitaException ex) { @@ -186,7 +174,7 @@ public class DownloadController : BaseApiController var filename = Path.GetFileNameWithoutExtension(downloadName); try { - await _eventHub.SendMessageAsync(MessageFactory.DownloadProgress, + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, MessageFactory.DownloadProgressEvent(username, filename, $"Downloading {filename}", 0F, "started", correlationId)); @@ -195,47 +183,47 @@ public class DownloadController : BaseApiController // Emit "ended" after the response is fully sent to the client HttpContext.Response.OnCompleted(async () => { - await _eventHub.SendMessageAsync(MessageFactory.DownloadProgress, + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, MessageFactory.DownloadProgressEvent(username, filename, "Download Complete", 1F, "ended", correlationId)); }); return GetFirstFileDownload(files); } - var filePath = _archiveService.CreateZipFromFoldersForDownload(files.Select(c => c.FilePath).ToList(), tempFolder, ProgressCallback); + var filePath = archiveService.CreateZipFromFoldersForDownload(files.Select(c => c.FilePath).ToList(), tempFolder, ProgressCallback); - await _eventHub.SendMessageAsync(MessageFactory.DownloadProgress, + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, MessageFactory.DownloadProgressEvent(username, filename, "Download Complete", 1F, "ended", correlationId)); - return PhysicalFile(filePath, DefaultContentType, downloadName, true); + return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(downloadName), true); async Task ProgressCallback(Tuple progressInfo) { - await _eventHub.SendMessageAsync(MessageFactory.DownloadProgress, + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, MessageFactory.DownloadProgressEvent(username, filename, $"Processing {Path.GetFileNameWithoutExtension(progressInfo.Item1)}", - Math.Clamp(progressInfo.Item2, 0F, 1F), "updated", correlationId)); + Math.Clamp(progressInfo.Item2, 0F, 1F), correlationId)); } } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when trying to download files"); - await _eventHub.SendMessageAsync(MessageFactory.DownloadProgress, + logger.LogError(ex, "There was an exception when trying to download files"); + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, MessageFactory.DownloadProgressEvent(Username!, filename, "Download Complete", 1F, "ended", correlationId)); throw; } } + [SeriesAccess] [HttpGet("series")] + [Authorize(PolicyGroups.DownloadPolicy)] public async Task DownloadSeries(int seriesId, [FromQuery] string? correlationId = null) { - if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(UserId, "permission-denied")); - - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); if (series == null) return BadRequest("Invalid Series"); - var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId); + var files = await unitOfWork.SeriesRepository.GetFilesForSeries(seriesId); try { return await DownloadFiles(files, $"download_{Username!}_s{seriesId}", $"{series.Name}.zip", correlationId); @@ -252,25 +240,32 @@ public class DownloadController : BaseApiController /// /// [HttpPost("bookmarks")] + [Authorize(PolicyGroups.DownloadPolicy)] public async Task DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto) { - if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(UserId, "permission-denied")); - if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest(await _localizationService.Translate(UserId, "bookmarks-empty")); + if (downloadBookmarkDto.Bookmarks.DistinctBy(b => b.SeriesId).Count() > 1) + return BadRequest(); + + var seriesId = downloadBookmarkDto.Bookmarks.First().SeriesId; + if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, seriesId, HttpContext.RequestAborted)) + return NotFound(); + + if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest(await localizationService.Translate(UserId, "bookmarks-empty")); - // We know that all bookmarks will be for one single seriesId var userId = UserId; var username = Username!; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); - var files = await _bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id)); + var files = await bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id)); var filename = $"{series!.Name} - Bookmarks.zip"; - await _eventHub.SendMessageAsync(MessageFactory.DownloadProgress, + + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), $"Downloading {filename}",0F)); - var seriesIds = string.Join("_", downloadBookmarkDto.Bookmarks.Select(b => b.SeriesId).Distinct()); - var filePath = _archiveService.CreateZipForDownload(files, - $"download_{userId}_{seriesIds}_bookmarks"); - await _eventHub.SendMessageAsync(MessageFactory.DownloadProgress, + + var filePath = archiveService.CreateZipForDownload(files,$"download_{userId}_{seriesId}_bookmarks"); + + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), $"Downloading {filename}", 1F)); diff --git a/Kavita.Server/Controllers/EmailController.cs b/Kavita.Server/Controllers/EmailController.cs new file mode 100644 index 000000000..d075961a0 --- /dev/null +++ b/Kavita.Server/Controllers/EmailController.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Email; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Kavita.Server.Controllers; + +[Authorize(Policy = PolicyGroups.AdminPolicy)] +public class EmailController(IUnitOfWork unitOfWork) : BaseApiController +{ + [HttpGet("all")] + public async Task>> GetEmails() + { + return Ok(await unitOfWork.EmailHistoryRepository.GetEmailDtos(UserParams.Default)); + } +} diff --git a/Kavita.Server/Controllers/FallbackController.cs b/Kavita.Server/Controllers/FallbackController.cs new file mode 100644 index 000000000..29012ba99 --- /dev/null +++ b/Kavita.Server/Controllers/FallbackController.cs @@ -0,0 +1,24 @@ +using System.IO; +using Kavita.API.Attributes; +using Kavita.API.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Kavita.Server.Controllers; + +[AllowAnonymous] +public class FallbackController : Controller +{ + + [SkipDeviceTracking] + public IActionResult Index() + { + if (HttpContext.Request.Path.StartsWithSegments("/api")) + { + return NotFound(); + } + + return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML"); + } +} + diff --git a/API/Controllers/FilterController.cs b/Kavita.Server/Controllers/FilterController.cs similarity index 55% rename from API/Controllers/FilterController.cs rename to Kavita.Server/Controllers/FilterController.cs index cdd4bc2a5..ad2c9e236 100644 --- a/API/Controllers/FilterController.cs +++ b/Kavita.Server/Controllers/FilterController.cs @@ -2,37 +2,30 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Dashboard; -using API.DTOs.Filtering.v2; -using API.Entities; -using API.Helpers; -using API.Middleware; -using API.Services; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Models; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.User; +using Kavita.Server.Attributes; +using Kavita.Services; +using Kavita.Services.Helpers; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; -#nullable enable +namespace Kavita.Server.Controllers; -public class FilterController : BaseApiController +public class FilterController( + IUnitOfWork unitOfWork, + ILocalizationService localizationService, + IStreamService streamService, + ILogger logger) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IStreamService _streamService; - private readonly ILogger _logger; - - public FilterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IStreamService streamService, - ILogger logger) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _streamService = streamService; - _logger = logger; - } - /// /// Creates or Updates the filter /// @@ -42,11 +35,11 @@ public class FilterController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task CreateOrUpdateSmartFilter(FilterV2Dto dto) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.SmartFilters); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.SmartFilters); if (user == null) return Unauthorized(); if (string.IsNullOrWhiteSpace(dto.Name)) return BadRequest("Name must be set"); - if (Seed.DefaultStreams.Any(s => s.Name.Equals(dto.Name, StringComparison.InvariantCultureIgnoreCase))) + if (Defaults.DefaultStreams.Any(s => s.Name.Equals(dto.Name, StringComparison.InvariantCultureIgnoreCase))) { return BadRequest("You cannot use the name of a system provided stream"); } @@ -57,7 +50,7 @@ public class FilterController : BaseApiController { // Update the filter existingFilter.Filter = SmartFilterHelper.Encode(dto); - _unitOfWork.AppUserSmartFilterRepository.Update(existingFilter); + unitOfWork.AppUserSmartFilterRepository.Update(existingFilter); } else { @@ -67,11 +60,11 @@ public class FilterController : BaseApiController Filter = SmartFilterHelper.Encode(dto) }; user.SmartFilters.Add(existingFilter); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); } - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return Ok(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -81,9 +74,9 @@ public class FilterController : BaseApiController /// /// [HttpGet] - public ActionResult> GetFilters() + public async Task>> GetFilters() { - return Ok(_unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(UserId)); + return Ok(await unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(UserId)); } /// @@ -96,17 +89,17 @@ public class FilterController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteFilter(int filterId) { - var filter = await _unitOfWork.AppUserSmartFilterRepository.GetById(filterId); + var filter = await unitOfWork.AppUserSmartFilterRepository.GetById(filterId); if (filter == null) return Ok(); // This needs to delete any dashboard filters that have it too - var streams = await _unitOfWork.UserRepository.GetDashboardStreamWithFilter(filter.Id); - _unitOfWork.UserRepository.Delete(streams); + var streams = await unitOfWork.UserRepository.GetDashboardStreamWithFilter(filter.Id); + unitOfWork.UserRepository.Delete(streams); - var streams2 = await _unitOfWork.UserRepository.GetSideNavStreamWithFilter(filter.Id); - _unitOfWork.UserRepository.Delete(streams2); + var streams2 = await unitOfWork.UserRepository.GetSideNavStreamWithFilter(filter.Id); + unitOfWork.UserRepository.Delete(streams2); - _unitOfWork.AppUserSmartFilterRepository.Delete(filter); - await _unitOfWork.CommitAsync(); + unitOfWork.AppUserSmartFilterRepository.Delete(filter); + await unitOfWork.CommitAsync(); return Ok(); } @@ -144,7 +137,7 @@ public class FilterController : BaseApiController { try { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.SmartFilters); if (user == null) return Unauthorized(); @@ -152,31 +145,31 @@ public class FilterController : BaseApiController if (string.IsNullOrWhiteSpace(name)) { - return BadRequest(await _localizationService.Translate(user.Id, "smart-filter-name-required")); + return BadRequest(await localizationService.Translate(user.Id, "smart-filter-name-required")); } - if (Seed.DefaultStreams.Any(s => s.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))) + if (Defaults.DefaultStreams.Any(s => s.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))) { - return BadRequest(await _localizationService.Translate(user.Id, "smart-filter-system-name")); + return BadRequest(await localizationService.Translate(user.Id, "smart-filter-system-name")); } var filter = user.SmartFilters.FirstOrDefault(f => f.Id == filterId); if (filter == null) { - return BadRequest(await _localizationService.Translate(user.Id, "filter-not-found")); + return BadRequest(await localizationService.Translate(user.Id, "filter-not-found")); } filter.Name = name; - _unitOfWork.AppUserSmartFilterRepository.Update(filter); - await _unitOfWork.CommitAsync(); + unitOfWork.AppUserSmartFilterRepository.Update(filter); + await unitOfWork.CommitAsync(); - await _streamService.RenameSmartFilterStreams(filter); + await streamService.RenameSmartFilterStreams(filter); return Ok(); } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when renaming smart filter: {FilterId}", filterId); - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + logger.LogError(ex, "There was an exception when renaming smart filter: {FilterId}", filterId); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } } diff --git a/API/Controllers/FontController.cs b/Kavita.Server/Controllers/FontController.cs similarity index 59% rename from API/Controllers/FontController.cs rename to Kavita.Server/Controllers/FontController.cs index 7402d2608..624225a46 100644 --- a/API/Controllers/FontController.cs +++ b/Kavita.Server/Controllers/FontController.cs @@ -2,44 +2,33 @@ using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Font; -using API.Entities.Enums.Font; -using API.Middleware; -using API.Services; -using API.Services.Tasks; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Font; +using Kavita.Models.Entities.Enums.Font; +using Kavita.Server.Attributes; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using MimeTypes; -namespace API.Controllers; +namespace Kavita.Server.Controllers; [Authorize] -public class FontController : BaseApiController +public class FontController( + IUnitOfWork unitOfWork, + IDirectoryService directoryService, + IFontService fontService, + IMapper mapper, + ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly IFontService _fontService; - private readonly IMapper _mapper; - private readonly ILocalizationService _localizationService; - private readonly Regex _fontFileExtensionRegex = new(Parser.FontFileExtensions, RegexOptions.IgnoreCase, Parser.RegexTimeout); - public FontController(IUnitOfWork unitOfWork, IDirectoryService directoryService, - IFontService fontService, IMapper mapper, ILocalizationService localizationService) - { - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _fontService = fontService; - _mapper = mapper; - _localizationService = localizationService; - } - /// /// List out the fonts /// @@ -47,29 +36,24 @@ public class FontController : BaseApiController [HttpGet("all")] public async Task>> GetFonts() { - return Ok(await _unitOfWork.EpubFontRepository.GetFontDtosAsync()); + return Ok(await unitOfWork.EpubFontRepository.GetFontDtosAsync()); } /// /// Returns a font file /// /// - /// /// [HttpGet] - [AllowAnonymous] [SkipDeviceTracking] - public async Task GetFont(int fontId, string apiKey) + public async Task GetFont(int fontId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByAuthKeyAsync(apiKey); - if (userId == 0) return BadRequest(); - - var font = await _unitOfWork.EpubFontRepository.GetFontAsync(fontId); + var font = await unitOfWork.EpubFontRepository.GetFontAsync(fontId); if (font == null) return NotFound(); if (font.Provider == FontProvider.System) return BadRequest("System provided fonts are not loaded by API"); - var path = Path.Join(_directoryService.EpubFontDirectory, font.FileName); + var path = Path.Join(directoryService.EpubFontDirectory, font.FileName); return CachedFile(path); } @@ -85,10 +69,10 @@ public class FontController : BaseApiController public async Task DeleteFont(int fontId, bool force = false) { var forceDelete = User.IsInRole(PolicyConstants.AdminRole) && force; - var fontInUse = await _fontService.IsFontInUse(fontId); + var fontInUse = await fontService.IsFontInUse(fontId); if (!fontInUse || forceDelete) { - await _fontService.Delete(fontId); + await fontService.Delete(fontId); } return Ok(); @@ -102,7 +86,7 @@ public class FontController : BaseApiController [HttpGet("in-use")] public async Task> IsFontInUse(int fontId) { - return Ok(await _fontService.IsFontInUse(fontId)); + return Ok(await fontService.IsFontInUse(fontId)); } /// @@ -120,8 +104,8 @@ public class FontController : BaseApiController var tempFile = await UploadToTemp(formFile); - var font = await _fontService.CreateFontFromFileAsync(tempFile); - return Ok(_mapper.Map(font)); + var font = await fontService.CreateFontFromFileAsync(tempFile); + return Ok(mapper.Map(font)); } [HttpPost("upload-by-url")] @@ -131,18 +115,18 @@ public class FontController : BaseApiController // Validate url try { - var font = await _fontService.CreateFontFromUrl(url); - return Ok(_mapper.Map(font)); + var font = await fontService.CreateFontFromUrl(url); + return Ok(mapper.Map(font)); } catch (KavitaException ex) { - return BadRequest(_localizationService.Translate(UserId, ex.Message)); + return BadRequest(localizationService.Translate(UserId, ex.Message)); } } private async Task UploadToTemp(IFormFile file) { - var outputFile = Path.Join(_directoryService.TempDirectory, file.FileName); + var outputFile = Path.Join(directoryService.TempDirectory, file.FileName); await using var stream = System.IO.File.Create(outputFile); await file.CopyToAsync(stream); diff --git a/API/Controllers/HealthController.cs b/Kavita.Server/Controllers/HealthController.cs similarity index 86% rename from API/Controllers/HealthController.cs rename to Kavita.Server/Controllers/HealthController.cs index 63a5f27d8..cb7ec5e0d 100644 --- a/API/Controllers/HealthController.cs +++ b/Kavita.Server/Controllers/HealthController.cs @@ -1,13 +1,11 @@ -using API.Middleware; +using Kavita.API.Attributes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -[SkipDeviceTracking] [AllowAnonymous] +[SkipDeviceTracking] public class HealthController : BaseApiController { /// diff --git a/Kavita.Server/Controllers/ImageController.cs b/Kavita.Server/Controllers/ImageController.cs new file mode 100644 index 000000000..d19fca007 --- /dev/null +++ b/Kavita.Server/Controllers/ImageController.cs @@ -0,0 +1,263 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Reading; +using Kavita.Models.Constants; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; +using Kavita.Server.Attributes; +using Kavita.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Kavita.Server.Controllers; + +/// +/// Responsible for servicing up images stored in Kavita for entities +/// +/// +[SkipDeviceTracking] +public class ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService, + ILocalizationService localizationService, IReadingListService readingListService, + ICoverDbService coverDbService, ICollectionTagService collectionTagService) : BaseApiController +{ + + /// + /// Returns cover image for Chapter + /// + /// + /// + /// + [ChapterAccess] + [HttpGet("chapter-cover")] + public async Task GetChapterCoverImage(int chapterId, string apiKey) + { + var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId)); + return PhysicalFile(path); + } + + /// + /// Returns cover image for Library + /// + /// + /// + /// + [LibraryAccess] + [HttpGet("library-cover")] + public async Task GetLibraryCoverImage(int libraryId, string apiKey) + { + var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId)); + return PhysicalFile(path); + } + + /// + /// Returns cover image for Volume + /// + /// + /// + /// + [VolumeAccess] + [HttpGet("volume-cover")] + public async Task GetVolumeCoverImage(int volumeId, string apiKey) + { + var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId)); + return PhysicalFile(path); + } + + /// + /// Returns cover image for Series + /// + /// Id of Series + /// + /// + [SeriesAccess] + [HttpGet("series-cover")] + public async Task GetSeriesCoverImage(int seriesId, string apiKey) + { + var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId)); + return PhysicalFile(path); + } + + /// + /// Returns cover image for Collection + /// + /// + /// + /// + [HttpGet("collection-cover")] + public async Task GetCollectionCoverImage(int collectionTagId, string apiKey) + { + var collectionTag = await unitOfWork.CollectionTagRepository.GetCollectionAsync(collectionTagId, ct: HttpContext.RequestAborted); + if (collectionTag == null || (collectionTag.AppUserId != UserId && !collectionTag.Promoted)) return NotFound(); + + var path = Path.Join(directoryService.CoverImageDirectory, collectionTag.CoverImage); + if (string.IsNullOrEmpty(path) || !directoryService.FileSystem.File.Exists(path)) + { + path = await collectionTagService.GenerateCollectionCoverImage(collectionTagId); + } + + return PhysicalFile(path); + } + + /// + /// Returns cover image for a Reading List + /// + /// + /// + /// + [HttpGet("readinglist-cover")] + public async Task GetReadingListCoverImage(int readingListId, string apiKey) + { + var readingList = await unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId, ct: HttpContext.RequestAborted); + if (readingList == null || (readingList.AppUserId != UserId && !readingList.Promoted)) return NotFound(); + + var path = Path.Join(directoryService.CoverImageDirectory, readingList.CoverImage); + if (string.IsNullOrEmpty(path) || !directoryService.FileSystem.File.Exists(path)) + { + path = await readingListService.GenerateReadingListCoverImage(readingListId); + } + + return PhysicalFile(path); + } + + /// + /// Returns image for a given bookmark page + /// + /// This request is served unauthenticated, but user must be passed via api key to validate + /// + /// Starts at 0 + /// API Key for user. Needed to authenticate request + /// Only applicable for Epubs - handles multiple images on one page + /// + [ChapterAccess] + [HttpGet("bookmark")] + public async Task GetBookmarkImage(int chapterId, int pageNum, string apiKey, int imageOffset = 0) + { + var bookmark = await unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, imageOffset, UserId); + if (bookmark == null) return BadRequest(await localizationService.Translate(UserId, "bookmark-doesnt-exist")); + + var bookmarkDirectory = + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + var path = Path.Join(bookmarkDirectory, bookmark.FileName); + + return PhysicalFile(path); + } + + /// + /// Returns the image associated with a web-link + /// + /// + /// + /// + [HttpGet("web-link")] + public async Task GetWebLinkImage(string url, string apiKey) + { + if (string.IsNullOrEmpty(url)) return BadRequest(await localizationService.Translate(UserId, "must-be-defined", "Url")); + + var encodeFormat = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; + + // Check if the domain exists + var domainFilePath = directoryService.FileSystem.Path.Join(directoryService.FaviconDirectory, ImageService.GetWebLinkFormat(url, encodeFormat)); + if (!directoryService.FileSystem.File.Exists(domainFilePath)) + { + // We need to request the favicon and save it + try + { + domainFilePath = directoryService.FileSystem.Path.Join(directoryService.FaviconDirectory, + await coverDbService.DownloadFaviconAsync(url, encodeFormat)); + } + catch (Exception) + { + return BadRequest(await localizationService.Translate(UserId, "generic-favicon")); + } + } + + return PhysicalFile(domainFilePath); + } + + + /// + /// Returns the image associated with a publisher + /// + /// + /// + /// + [HttpGet("publisher")] + public async Task GetPublisherImage(string publisherName, string apiKey) + { + if (string.IsNullOrEmpty(publisherName)) return BadRequest(await localizationService.Translate(UserId, "must-be-defined", "publisherName")); + if (publisherName.Contains("..")) return BadRequest(); + + var encodeFormat = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; + + // Check if the domain exists + var domainFilePath = directoryService.FileSystem.Path.Join(directoryService.PublisherDirectory, ImageService.GetPublisherFormat(publisherName, encodeFormat)); + if (!directoryService.FileSystem.File.Exists(domainFilePath)) + { + // We need to request the favicon and save it + try + { + domainFilePath = directoryService.FileSystem.Path.Join(directoryService.PublisherDirectory, + await coverDbService.DownloadPublisherImageAsync(publisherName, encodeFormat)); + } + catch (Exception) + { + return BadRequest(await localizationService.Translate(UserId, "generic-favicon")); + } + } + + return CachedFile(domainFilePath); + } + + /// + /// Returns cover image for Person + /// + /// + /// + /// + [PersonAccess] + [HttpGet("person-cover")] + public async Task GetPersonCoverImage(int personId, string apiKey) + { + var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.UserRepository.GetPersonCoverImageAsync(personId)); + return PhysicalFile(path); + } + + /// + /// Returns cover image for User + /// + /// + /// + /// + [HttpGet("user-cover")] + public async Task GetUserCoverImage(int userId, string apiKey) + { + var filename = await unitOfWork.UserRepository.GetCoverImageAsync(userId); + if (filename == null) return NotFound(); + + var path = Path.Join(directoryService.CoverImageDirectory, filename); + return CachedFile(path); + } + + /// + /// Returns a temp coverupload image + /// + /// Requires Admin Role to perform upload + /// Filename of file. This is used with upload/upload-by-url + /// + /// + [HttpGet("cover-upload")] + [Authorize(PolicyConstants.AdminRole)] + public async Task GetCoverUploadImage(string filename, string apiKey) + { + if (filename.Contains("..")) return BadRequest(await localizationService.Translate(UserId, "invalid-filename")); + + var path = Path.Join(directoryService.TempDirectory, filename); + return PhysicalFile(path); + } +} diff --git a/API/Controllers/KoreaderController.cs b/Kavita.Server/Controllers/KoreaderController.cs similarity index 73% rename from API/Controllers/KoreaderController.cs rename to Kavita.Server/Controllers/KoreaderController.cs index 42b46ad6d..60982970a 100644 --- a/API/Controllers/KoreaderController.cs +++ b/Kavita.Server/Controllers/KoreaderController.cs @@ -1,16 +1,13 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; using System; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Koreader; -using API.Extensions; -using API.Services; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Koreader; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; -#nullable enable +namespace Kavita.Server.Controllers; /// /// The endpoint to interface with Koreader's Progress Sync plugin. @@ -19,17 +16,9 @@ namespace API.Controllers; /// Koreader uses a different form of authentication. It stores the username and password in headers. /// https://github.com/koreader/koreader/blob/master/plugins/kosync.koplugin/KOSyncClient.lua /// -public class KoreaderController : BaseApiController +public class KoreaderController(IKoreaderService koreaderService, ILogger logger) + : BaseApiController { - private readonly IKoreaderService _koreaderService; - private readonly ILogger _logger; - - public KoreaderController(IKoreaderService koreaderService, ILogger logger) - { - _koreaderService = koreaderService; - _logger = logger; - } - [HttpGet("{apiKey}/users/auth")] public IActionResult Authenticate(string apiKey) { @@ -47,7 +36,7 @@ public class KoreaderController : BaseApiController { try { - await _koreaderService.SaveProgress(request, UserId); + await koreaderService.SaveProgress(request, UserId); return Ok(new KoreaderProgressUpdateDto{ Document = request.document, Timestamp = DateTime.UtcNow }); } @@ -68,8 +57,8 @@ public class KoreaderController : BaseApiController { try { - var response = await _koreaderService.GetProgress(ebookHash, UserId); - _logger.LogDebug("Koreader response progress for User ({UserName}): {Progress}", Username, response.progress.Sanitize()); + var response = await koreaderService.GetProgress(ebookHash, UserId); + logger.LogDebug("Koreader response progress for User ({UserName}): {Progress}", Username, response.progress.Sanitize()); // We must pack this manually for Koreader due to a bug in their code: https://github.com/koreader/koreader/issues/13629 diff --git a/API/Controllers/LibraryController.cs b/Kavita.Server/Controllers/LibraryController.cs similarity index 64% rename from API/Controllers/LibraryController.cs rename to Kavita.Server/Controllers/LibraryController.cs index c9e864de1..b7a98fa98 100644 --- a/API/Controllers/LibraryController.cs +++ b/Kavita.Server/Controllers/LibraryController.cs @@ -3,80 +3,67 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Account; -using API.DTOs.JumpBar; -using API.DTOs.System; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using AutoMapper; using EasyCaching.Core; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.JumpBar; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.DTOs.System; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Server.Attributes; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using TaskScheduler = API.Services.TaskScheduler; +using TaskScheduler = Kavita.Services.TaskScheduler; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; [Authorize] -public class LibraryController : BaseApiController +public class LibraryController( + IDirectoryService directoryService, + ILogger logger, + IMapper mapper, + ITaskScheduler taskScheduler, + IUnitOfWork unitOfWork, + IEventHub eventHub, + ILibraryWatcher libraryWatcher, + IEasyCachingProviderFactory cachingProviderFactory, + ILocalizationService localizationService) + : BaseApiController { - private readonly IDirectoryService _directoryService; - private readonly ILogger _logger; - private readonly IMapper _mapper; - private readonly ITaskScheduler _taskScheduler; - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - private readonly ILibraryWatcher _libraryWatcher; - private readonly ILocalizationService _localizationService; - private readonly IEasyCachingProvider _libraryCacheProvider; + private readonly IEasyCachingProvider _libraryCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.Library); private const string CacheKey = "library_"; - public LibraryController(IDirectoryService directoryService, - ILogger logger, IMapper mapper, ITaskScheduler taskScheduler, - IUnitOfWork unitOfWork, IEventHub eventHub, ILibraryWatcher libraryWatcher, - IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService) - { - _directoryService = directoryService; - _logger = logger; - _mapper = mapper; - _taskScheduler = taskScheduler; - _unitOfWork = unitOfWork; - _eventHub = eventHub; - _libraryWatcher = libraryWatcher; - _localizationService = localizationService; - - _libraryCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.Library); - } - /// /// Creates a new Library. Upon library creation, adds new library to all Admin accounts. /// /// /// Created Library - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("create")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> AddLibrary(UpdateLibraryDto dto) { - if (await _unitOfWork.LibraryRepository.LibraryExists(dto.Name)) + if (await unitOfWork.LibraryRepository.LibraryExists(dto.Name)) { - return BadRequest(await _localizationService.Translate(UserId, "library-name-exists")); + return BadRequest(await localizationService.Translate(UserId, "library-name-exists")); } var library = new LibraryBuilder(dto.Name, dto.Type) @@ -105,64 +92,65 @@ public class LibraryController : BaseApiController // Override Scrobbling for Comic libraries since there are no providers to scrobble to if (library.Type == LibraryType.Comic) { - _logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name); + logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name.Sanitize()); library.AllowScrobbling = false; } - _unitOfWork.LibraryRepository.Add(library); + unitOfWork.LibraryRepository.Add(library); - var admins = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).ToList(); + var admins = (await unitOfWork.UserRepository.GetAdminUsersAsync()).ToList(); foreach (var admin in admins) { admin.Libraries ??= new List(); admin.Libraries.Add(library); } - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-library")); - _logger.LogInformation("Created a new library: {LibraryName}", library.Name); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-library")); + + logger.LogInformation("Created a new library: {LibraryName}", library.Name.Sanitize()); // Restart Folder watching if on - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (settings.EnableFolderWatching) { - await _libraryWatcher.RestartWatching(); + await libraryWatcher.RestartWatching(); } // Assign all the necessary users with this library side nav var userIds = admins.Select(u => u.Id).Append(UserId).ToList(); - var userNeedingNewLibrary = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams)) + var userNeedingNewLibrary = (await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams)) .Where(u => userIds.Contains(u.Id)) .ToList(); foreach (var user in userNeedingNewLibrary) { user.CreateSideNavFromLibrary(library); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); } - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-library")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-library")); // I added this twice as some users were having issues where their new library wasn't added to the side nav. // I wasn't able to reproduce but could validate it didn't happen with this extra commit. (https://github.com/Kareadita/Kavita/issues/4248) - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); } await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); if (library.FolderWatching) { - await _libraryWatcher.RestartWatching(); + await libraryWatcher.RestartWatching(); } - BackgroundJob.Enqueue(() => _taskScheduler.ScanLibrary(library.Id, false)); - await _eventHub.SendMessageAsync(MessageFactory.LibraryModified, + BackgroundJob.Enqueue(() => taskScheduler.ScanLibrary(library.Id, false)); + await eventHub.SendMessageAsync(MessageFactory.LibraryModified, MessageFactory.LibraryModifiedEvent(library.Id, "create"), false); - await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, + await eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(UserId), false); - return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(library.Id)); + return Ok(await unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(library.Id)); } /// @@ -170,8 +158,8 @@ public class LibraryController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("list")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult> GetDirectories(string? path) { if (string.IsNullOrEmpty(path)) @@ -183,21 +171,23 @@ public class LibraryController : BaseApiController })); } - if (!Directory.Exists(path)) return Ok(_directoryService.ListDirectory(Path.GetDirectoryName(path)!)); + if (path.Contains("..")) return BadRequest(); - return Ok(_directoryService.ListDirectory(path)); + if (!Directory.Exists(path)) return Ok(directoryService.ListDirectory(Path.GetDirectoryName(path)!)); + + return Ok(directoryService.ListDirectory(path)); } /// /// For each root, checks if there are any supported files at root to warn the user during library creation about an invalid setup /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("has-files-at-root")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult> AnyFilesAtRoot(CheckForFilesInFolderRootsDto dto) { var foldersWithFilesAtRoot = dto.Roots - .Where(root => _directoryService + .Where(root => directoryService .GetFilesWithCertainExtensions(root, Parser.SupportedExtensions, SearchOption.TopDirectoryOnly) .Any()) .ToList(); @@ -210,16 +200,17 @@ public class LibraryController : BaseApiController /// /// If the user is not an admin, only id, type, and name will be returned /// + [HttpGet] + [LibraryAccess] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)] - [HttpGet] public async Task> GetLibrary(int libraryId) { if (User.IsInRole(PolicyConstants.AdminRole)) { - return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(libraryId)); + return Ok(await unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(libraryId)); } - return Ok(await _unitOfWork.LibraryRepository.GetLiteLibraryDtoByIdAsync(libraryId)); + return Ok(await unitOfWork.LibraryRepository.GetLiteLibraryDtoByIdAsync(libraryId)); } /// @@ -240,7 +231,7 @@ public class LibraryController : BaseApiController [HttpGet("user-libraries")] public async Task>> GetLibrariesForUser(int userId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); if (user == null || string.IsNullOrEmpty(user.UserName)) return BadRequest(); var ownLibraries = await GetLibrariesForUser(Username!); @@ -262,7 +253,7 @@ public class LibraryController : BaseApiController var result = await _libraryCacheProvider.GetAsync>(cacheKey); if (result.HasValue) return result.Value; - var ret = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username); + var ret = await unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username); await _libraryCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromHours(24)); return ret; } @@ -272,13 +263,11 @@ public class LibraryController : BaseApiController /// /// /// + [LibraryAccess] [HttpGet("jump-bar")] - public async Task>> GetJumpBar(int libraryId) + public ActionResult> GetJumpBar(int libraryId) { - if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, UserId)) - return BadRequest(await _localizationService.Translate(UserId, "no-library-access")); - - return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId)); + return Ok(unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId)); } /// @@ -286,25 +275,26 @@ public class LibraryController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("grant-access")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username, AppUserIncludes.SideNavStreams); - if (user == null) return BadRequest(await _localizationService.Translate(UserId, "user-doesnt-exist")); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username, AppUserIncludes.SideNavStreams); + if (user == null) return BadRequest(await localizationService.Translate(UserId, "user-doesnt-exist")); var libraryString = string.Join(',', updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name)); - _logger.LogInformation("Granting user {UserName} access to: {Libraries}", updateLibraryForUserDto.Username, libraryString); + logger.LogInformation("Granting user {UserId} access to: {Libraries}", user.Id, libraryString.Sanitize()); - var allLibraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync(); + var allLibraries = await unitOfWork.LibraryRepository.GetLibrariesAsync(); foreach (var library in allLibraries) { - library.AppUsers ??= new List(); + library.AppUsers ??= []; + var libraryContainsUser = library.AppUsers.Any(u => u.UserName == user.UserName); var libraryIsSelected = updateLibraryForUserDto.SelectedLibraries.Any(l => l.Id == library.Id); + if (libraryContainsUser && !libraryIsSelected) { - // Remove library.AppUsers.Remove(user); user.RemoveSideNavFromLibrary(library); } @@ -315,25 +305,25 @@ public class LibraryController : BaseApiController } } - if (!_unitOfWork.HasChanges()) + if (!unitOfWork.HasChanges()) { - _logger.LogInformation("No changes for update library access"); - return Ok(_mapper.Map(user)); + logger.LogInformation("No changes for update library access"); + return Ok(mapper.Map(user)); } - if (await _unitOfWork.CommitAsync()) + if (await unitOfWork.CommitAsync()) { - _logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username); + logger.LogInformation("Added: {SelectedLibraries} to {UserId}", libraryString.Sanitize(), user.Id); // Bust cache await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - return Ok(_mapper.Map(user)); + return Ok(mapper.Map(user)); } - return BadRequest(await _localizationService.Translate(UserId, "generic-library")); + return BadRequest(await localizationService.Translate(UserId, "generic-library")); } /// @@ -342,12 +332,12 @@ public class LibraryController : BaseApiController /// /// If true, will ignore any optimizations to avoid file I/O and will treat similar to a first scan /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("scan")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task Scan(int libraryId, bool force = false) { - if (libraryId <= 0) return BadRequest(await _localizationService.Translate(UserId, "greater-0", "libraryId")); - await _taskScheduler.ScanLibrary(libraryId, force); + if (libraryId <= 0) return BadRequest(await localizationService.Translate(UserId, "greater-0", "libraryId")); + await taskScheduler.ScanLibrary(libraryId, force); return Ok(); } @@ -355,13 +345,13 @@ public class LibraryController : BaseApiController /// Enqueues a bunch of library scans /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("scan-multiple")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task ScanMultiple(BulkActionDto dto) { foreach (var libraryId in dto.Ids) { - await _taskScheduler.ScanLibrary(libraryId, dto.Force ?? false); + await taskScheduler.ScanLibrary(libraryId, dto.Force ?? false); } return Ok(); @@ -372,19 +362,19 @@ public class LibraryController : BaseApiController /// /// If true, will ignore any optimizations to avoid file I/O and will treat similar to a first scan /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("scan-all")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult ScanAll(bool force = false) { - _taskScheduler.ScanLibraries(force); + taskScheduler.ScanLibraries(force); return Ok(); } - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("refresh-metadata")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult RefreshMetadata(int libraryId, bool force = true, bool forceColorscape = true) { - _taskScheduler.RefreshMetadata(libraryId, force, forceColorscape); + taskScheduler.RefreshMetadata(libraryId, force, forceColorscape); return Ok(); } @@ -394,7 +384,7 @@ public class LibraryController : BaseApiController { foreach (var libraryId in dto.Ids) { - _taskScheduler.RefreshMetadata(libraryId, dto.Force ?? false, forceColorscape); + taskScheduler.RefreshMetadata(libraryId, dto.Force ?? false, forceColorscape); } return Ok(); @@ -405,14 +395,14 @@ public class LibraryController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("copy-settings-from")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task CopySettingsFromLibraryToLibraries(CopySettingsFromLibraryDto dto) { - var sourceLibrary = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.SourceLibraryId, LibraryIncludes.ExcludePatterns | LibraryIncludes.FileTypes); + var sourceLibrary = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.SourceLibraryId, LibraryIncludes.ExcludePatterns | LibraryIncludes.FileTypes); if (sourceLibrary == null) return BadRequest("SourceLibraryId must exist"); - var libraries = await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.TargetLibraryIds, LibraryIncludes.ExcludePatterns | LibraryIncludes.FileTypes | LibraryIncludes.Folders); + var libraries = await unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.TargetLibraryIds, LibraryIncludes.ExcludePatterns | LibraryIncludes.FileTypes | LibraryIncludes.Folders); foreach (var targetLibrary in libraries) { UpdateLibrarySettings(new UpdateLibraryDto() @@ -432,11 +422,11 @@ public class LibraryController : BaseApiController }, targetLibrary, dto.IncludeType); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); if (sourceLibrary.FolderWatching) { - BackgroundJob.Enqueue(() => _libraryWatcher.RestartWatching()); + BackgroundJob.Enqueue(() => libraryWatcher.RestartWatching()); } return Ok(); @@ -451,28 +441,28 @@ public class LibraryController : BaseApiController [HttpPost("scan-folder")] public async Task ScanFolder(ScanFolderDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByAuthKey(dto.ApiKey); + var user = await unitOfWork.UserRepository.GetUserByAuthKey(dto.ApiKey); if (user == null) return Unauthorized(); // Validate user has Admin privileges - var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); + var isAdmin = await unitOfWork.UserRepository.IsUserAdminAsync(user); if (!isAdmin) return BadRequest("API key must belong to an admin"); if (dto.FolderPath.Contains("..")) { - return BadRequest(await _localizationService.Translate(UserId, "invalid-path")); + return BadRequest(await localizationService.Translate(UserId, "invalid-path")); } dto.FolderPath = Parser.NormalizePath(dto.FolderPath); - var libraryFolder = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync()) + var libraryFolder = (await unitOfWork.LibraryRepository.GetLibraryDtosAsync()) .SelectMany(l => l.Folders) .Distinct() .Select(Parser.NormalizePath); - var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder, [dto.FolderPath]); + var seriesFolder = directoryService.FindHighestDirectoriesFromFiles(libraryFolder, [dto.FolderPath]); - _taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath, dto.AbortOnNoSeriesMatch); + taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath, dto.AbortOnNoSeriesMatch); return Ok(); } @@ -483,11 +473,11 @@ public class LibraryController : BaseApiController /// This does not touch any files /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpDelete("delete")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> DeleteLibrary(int libraryId) { - _logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, Username!); + logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, Username!); try { @@ -495,7 +485,7 @@ public class LibraryController : BaseApiController if (result) { // Inform the user's side nav to remove it if needed - await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, + await eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(UserId), false); } return Ok(result); @@ -512,14 +502,18 @@ public class LibraryController : BaseApiController /// This does not touch any files /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpDelete("delete-multiple")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> DeleteMultipleLibraries([FromQuery] List libraryIds) { var username = Username!; - _logger.LogInformation("Libraries {LibraryIds} are being deleted by {UserName}", libraryIds, username); - foreach (var libraryId in libraryIds) + var allLibraries = await unitOfWork.LibraryRepository.GetLibrariesAsync(); + var toDelete = allLibraries.Where(l => libraryIds.Contains(l.Id)).Select(l => l.Id).ToList(); + + logger.LogInformation("Libraries {LibraryIds} are being deleted by {UserName}", toDelete, username); + + foreach (var libraryId in toDelete) { try { @@ -536,79 +530,79 @@ public class LibraryController : BaseApiController private async Task DeleteLibrary(int libraryId, int userId) { - var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId); + var series = await unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId); var seriesIds = series.Select(x => x.Id).ToArray(); var chapterIds = - await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(seriesIds); + await unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(seriesIds); try { if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId)) { - _logger.LogInformation("User is attempting to delete a library while a scan is in progress"); - throw new KavitaException(await _localizationService.Translate(userId, "delete-library-while-scan")); + logger.LogInformation("User is attempting to delete a library while a scan is in progress"); + throw new KavitaException(await localizationService.Translate(userId, "delete-library-while-scan")); } - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); if (library == null) { - throw new KavitaException(await _localizationService.Translate(userId, "library-doesnt-exist")); + throw new KavitaException(await localizationService.Translate(userId, "library-doesnt-exist")); } // Due to a bad schema that I can't figure out how to fix, we need to erase all RelatedSeries before we delete the library // Aka SeriesRelation has an invalid foreign key - foreach (var s in await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(library.Id, SeriesIncludes.Related)) + foreach (var s in await unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(library.Id, SeriesIncludes.Related)) { s.Relations = new List(); - _unitOfWork.SeriesRepository.Update(s); + unitOfWork.SeriesRepository.Update(s); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); - _unitOfWork.LibraryRepository.Delete(library); + unitOfWork.LibraryRepository.Delete(library); - var streams = await _unitOfWork.UserRepository.GetSideNavStreamsByLibraryId(library.Id); - _unitOfWork.UserRepository.Delete(streams); + var streams = await unitOfWork.UserRepository.GetSideNavStreamsByLibraryId(library.Id); + unitOfWork.UserRepository.Delete(streams); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); - await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, + await eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), false); if (chapterIds.Any()) { - await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); - await _unitOfWork.CommitAsync(); - _taskScheduler.CleanupChapters(chapterIds); + await unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); + await unitOfWork.CommitAsync(); + taskScheduler.CleanupChapters(chapterIds); } - BackgroundJob.Enqueue(() => _libraryWatcher.RestartWatching()); + BackgroundJob.Enqueue(() => libraryWatcher.RestartWatching()); foreach (var seriesId in seriesIds) { - await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, + await eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, MessageFactory.SeriesRemovedEvent(seriesId, string.Empty, libraryId), false); } - await _eventHub.SendMessageAsync(MessageFactory.LibraryModified, + await eventHub.SendMessageAsync(MessageFactory.LibraryModified, MessageFactory.LibraryModifiedEvent(libraryId, "delete"), false); - var userPreferences = await _unitOfWork.DataContext.AppUserPreferences.ToListAsync(); + var userPreferences = await unitOfWork.DataContext.AppUserPreferences.ToListAsync(); foreach (var userPreference in userPreferences) { userPreference.SocialPreferences.SocialLibraries = userPreference.SocialPreferences.SocialLibraries .Where(l => l != libraryId).ToList(); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); return true; } catch (Exception ex) { - _logger.LogError(ex, "There was a critical issue. Please try again"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was a critical issue. Please try again"); + await unitOfWork.RollbackAsync(); return false; } } @@ -618,12 +612,12 @@ public class LibraryController : BaseApiController /// /// If empty or null, will return true as that is invalid /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("name-exists")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> IsLibraryNameValid(string name) { if (string.IsNullOrWhiteSpace(name)) return Ok(true); - return Ok(await _unitOfWork.LibraryRepository.LibraryExists(name.Trim())); + return Ok(await unitOfWork.LibraryRepository.LibraryExists(name.Trim())); } /// @@ -632,17 +626,17 @@ public class LibraryController : BaseApiController /// Any folder or type change will invoke a scan. /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("update")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task UpdateLibrary(UpdateLibraryDto dto) { var userId = UserId; - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); - if (library == null) return BadRequest(await _localizationService.Translate(userId, "library-doesnt-exist")); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); + if (library == null) return BadRequest(await localizationService.Translate(userId, "library-doesnt-exist")); var newName = dto.Name.Trim(); - if (await _unitOfWork.LibraryRepository.LibraryExists(newName) && !library.Name.Equals(newName)) - return BadRequest(await _localizationService.Translate(userId, "library-name-exists")); + if (await unitOfWork.LibraryRepository.LibraryExists(newName) && !library.Name.Equals(newName)) + return BadRequest(await localizationService.Translate(userId, "library-name-exists")); var originalFoldersCount = library.Folders.Count; @@ -653,27 +647,27 @@ public class LibraryController : BaseApiController var folderWatchingUpdate = library.FolderWatching != dto.FolderWatching; UpdateLibrarySettings(dto, library); - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(userId, "generic-library-update")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(userId, "generic-library-update")); if (folderWatchingUpdate || originalFoldersCount != dto.Folders.Count() || typeUpdate) { - BackgroundJob.Enqueue(() => _libraryWatcher.RestartWatching()); + BackgroundJob.Enqueue(() => libraryWatcher.RestartWatching()); } if (originalFoldersCount != dto.Folders.Count() || typeUpdate) { - await _taskScheduler.ScanLibrary(library.Id); + await taskScheduler.ScanLibrary(library.Id); } - await _eventHub.SendMessageAsync(MessageFactory.LibraryModified, + await eventHub.SendMessageAsync(MessageFactory.LibraryModified, MessageFactory.LibraryModifiedEvent(library.Id, "update"), false); - await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, + await eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), false); await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); - return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(library.Id)); + return Ok(await unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(library.Id)); } @@ -711,12 +705,12 @@ public class LibraryController : BaseApiController // Override Scrobbling for Comic libraries since there are no providers to scrobble to if (library.Type is LibraryType.Comic or LibraryType.ComicVine) { - _logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name.Replace(Environment.NewLine, string.Empty)); + logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name.Replace(Environment.NewLine, string.Empty)); library.AllowScrobbling = false; } - _unitOfWork.LibraryRepository.Update(library); + unitOfWork.LibraryRepository.Update(library); } /// @@ -724,9 +718,10 @@ public class LibraryController : BaseApiController /// /// /// + [LibraryAccess] [HttpGet("type")] public async Task> GetLibraryType(int libraryId) { - return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId)); + return Ok(await unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId)); } } diff --git a/API/Controllers/LicenseController.cs b/Kavita.Server/Controllers/LicenseController.cs similarity index 92% rename from API/Controllers/LicenseController.cs rename to Kavita.Server/Controllers/LicenseController.cs index 9e946b222..8e30330e7 100644 --- a/API/Controllers/LicenseController.cs +++ b/Kavita.Server/Controllers/LicenseController.cs @@ -1,20 +1,19 @@ using System; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.KavitaPlus.License; -using API.Entities.Enums; -using API.Services; -using API.Services.Plus; using EasyCaching.Core; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.KavitaPlus.License; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Plus; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using TaskScheduler = API.Services.TaskScheduler; +using TaskScheduler = Kavita.Services.TaskScheduler; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; public class LicenseController( IUnitOfWork unitOfWork, @@ -50,8 +49,8 @@ public class LicenseController( /// Has any license registered with the instance. Does not validate against Kavita+ API /// /// - [Authorize(PolicyGroups.AdminPolicy)] [HttpGet("has-license")] + [Authorize(PolicyGroups.AdminPolicy)] public async Task> HasLicense() { return Ok(!string.IsNullOrEmpty( @@ -63,8 +62,8 @@ public class LicenseController( /// /// Force checking the API and skip the 8-hour cache /// - [Authorize(PolicyGroups.AdminPolicy)] [HttpGet("info")] + [Authorize(PolicyGroups.AdminPolicy)] public async Task> GetLicenseInfo(bool forceCheck = false) { try @@ -81,8 +80,8 @@ public class LicenseController( /// Remove the Kavita+ License on the Server /// /// - [Authorize(PolicyGroups.AdminPolicy)] [HttpDelete] + [Authorize(PolicyGroups.AdminPolicy)] public async Task RemoveLicense() { logger.LogInformation("Removing license on file for Server"); @@ -97,8 +96,8 @@ public class LicenseController( } - [Authorize(PolicyGroups.AdminPolicy)] [HttpPost("reset")] + [Authorize(PolicyGroups.AdminPolicy)] public async Task ResetLicense(UpdateLicenseDto dto) { logger.LogInformation("Resetting license on file for Server"); @@ -126,8 +125,8 @@ public class LicenseController( /// /// Caches the result /// - [Authorize(PolicyGroups.AdminPolicy)] [HttpPost] + [Authorize(PolicyGroups.AdminPolicy)] public async Task UpdateLicense(UpdateLicenseDto dto) { try diff --git a/API/Controllers/LocaleController.cs b/Kavita.Server/Controllers/LocaleController.cs similarity index 58% rename from API/Controllers/LocaleController.cs rename to Kavita.Server/Controllers/LocaleController.cs index 44713e35d..d716d1681 100644 --- a/API/Controllers/LocaleController.cs +++ b/Kavita.Server/Controllers/LocaleController.cs @@ -2,38 +2,32 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.DTOs; -using API.Services; using EasyCaching.Core; +using Kavita.API.Services; using Kavita.Common.EnvironmentInfo; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class LocaleController : BaseApiController +public class LocaleController( + ILocalizationService localizationService, + IEasyCachingProviderFactory cachingProviderFactory) + : BaseApiController { - private readonly ILocalizationService _localizationService; - private readonly IEasyCachingProvider _localeCacheProvider; + private readonly IEasyCachingProvider _localeCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.LocaleOptions); private static readonly string CacheKey = "locales_" + BuildInfo.Version; - public LocaleController(ILocalizationService localizationService, IEasyCachingProviderFactory cachingProviderFactory) - { - _localizationService = localizationService; - _localeCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.LocaleOptions); - } - /// /// Returns all applicable locales on the server /// /// This can be cached as it will not change per version. /// - [AllowAnonymous] [HttpGet] + [AllowAnonymous] public async Task>> GetAllLocales() { var result = await _localeCacheProvider.GetAsync>(CacheKey); @@ -42,7 +36,7 @@ public class LocaleController : BaseApiController return Ok(result.Value); } - var ret = _localizationService.GetLocales().Where(l => l.TranslationCompletion > 0f); + var ret = localizationService.GetLocales().Where(l => l.TranslationCompletion > 0f); await _localeCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromDays(1)); return Ok(ret); diff --git a/API/Controllers/ManageController.cs b/Kavita.Server/Controllers/ManageController.cs similarity index 50% rename from API/Controllers/ManageController.cs rename to Kavita.Server/Controllers/ManageController.cs index 0469f5a32..596d97478 100644 --- a/API/Controllers/ManageController.cs +++ b/Kavita.Server/Controllers/ManageController.cs @@ -1,47 +1,34 @@ -#nullable enable -using System; -using System.Collections.Generic; +using System; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs; -using API.DTOs.KavitaPlus.Manage; -using API.Extensions; -using API.Helpers; -using API.Services.Plus; +using Kavita.API.Database; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.KavitaPlus.Manage; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; /// /// All things centered around Managing the Kavita instance, that isn't aligned with an entity /// [Authorize(PolicyGroups.AdminPolicy)] -public class ManageController : BaseApiController +public class ManageController(IUnitOfWork unitOfWork) : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILicenseService _licenseService; - - public ManageController(IUnitOfWork unitOfWork, ILicenseService licenseService) - { - _unitOfWork = unitOfWork; - _licenseService = licenseService; - } - /// /// Returns a list of all Series that is Kavita+ applicable to metadata match and the status of it /// /// + [KPlus] [Authorize(PolicyGroups.AdminPolicy)] [HttpPost("series-metadata")] public async Task>> SeriesMetadata(ManageMatchFilterDto filter, [FromQuery] UserParams? userParams) { - //if (!await _licenseService.HasActiveLicense()) return Ok(Array.Empty()); - userParams ??= UserParams.Default; - var res = await _unitOfWork.ExternalSeriesMetadataRepository.GetAllSeries(filter, userParams); + var res = await unitOfWork.ExternalSeriesMetadataRepository.GetAllSeries(filter, userParams); Response.AddPaginationHeader(res); return Ok(res); diff --git a/API/Controllers/MetadataController.cs b/Kavita.Server/Controllers/MetadataController.cs similarity index 93% rename from API/Controllers/MetadataController.cs rename to Kavita.Server/Controllers/MetadataController.cs index 795a9a02e..d5246584a 100644 --- a/API/Controllers/MetadataController.cs +++ b/Kavita.Server/Controllers/MetadataController.cs @@ -3,24 +3,23 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Filtering; -using API.DTOs.Metadata; -using API.DTOs.Metadata.Browse; -using API.DTOs.Person; -using API.DTOs.SeriesDetail; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Services; -using API.Services.Plus; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services.Plus; using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Extensions; +using Kavita.Services.Helpers; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; -#nullable enable +namespace Kavita.Server.Controllers; public class MetadataController(IUnitOfWork unitOfWork, IExternalMetadataService metadataService) : BaseApiController { @@ -126,8 +125,8 @@ public class MetadataController(IUnitOfWork unitOfWork, IExternalMetadataService /// String separated libraryIds or null for all ratings /// This API is cached for 1 hour, varying by libraryIds /// - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] [HttpGet("age-ratings")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] public async Task>> GetAllAgeRatings(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); @@ -149,8 +148,8 @@ public class MetadataController(IUnitOfWork unitOfWork, IExternalMetadataService /// String separated libraryIds or null for all publication status /// This API is cached for 1 hour, varying by libraryIds /// - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] [HttpGet("publication-status")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] public ActionResult> GetAllPublicationStatus(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); @@ -236,7 +235,6 @@ public class MetadataController(IUnitOfWork unitOfWork, IExternalMetadataService private async Task PrepareSeriesDetail(List userReviews, SeriesDetailPlusDto? ret) { - var isAdmin = User.IsInRole(PolicyConstants.AdminRole); var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId)!; if (ret != null) @@ -245,13 +243,17 @@ public class MetadataController(IUnitOfWork unitOfWork, IExternalMetadataService ret.Reviews = userReviews; } - if (!isAdmin && ret?.Recommendations != null && user != null) + if (ret?.Recommendations != null && user != null) { - // Re-obtain owned series and take into account age restriction + // Re-obtain owned series and take into account age restriction and include series progress var seriesIds = ret.Recommendations.OwnedSeries.Select(s => s.Id); ret.Recommendations.OwnedSeries = await unitOfWork.SeriesRepository.GetSeriesDtoByIdsAsync(seriesIds, user); - ret.Recommendations.ExternalSeries = []; + + if (!User.IsInRole(PolicyConstants.AdminRole)) + { + ret.Recommendations.ExternalSeries = []; + } } if (ret?.Recommendations != null && user != null) diff --git a/API/Controllers/OPDSController.cs b/Kavita.Server/Controllers/OPDSController.cs similarity index 71% rename from API/Controllers/OPDSController.cs rename to Kavita.Server/Controllers/OPDSController.cs index 1e2b8bba2..ed69c1472 100644 --- a/API/Controllers/OPDSController.cs +++ b/Kavita.Server/Controllers/OPDSController.cs @@ -2,52 +2,36 @@ using System; using System.IO; using System.Threading.Tasks; using System.Xml.Serialization; -using API.Constants; -using API.Data; -using API.DTOs.OPDS; -using API.DTOs.OPDS.Requests; -using API.DTOs.Progress; -using API.Entities.Enums; -using API.Exceptions; -using API.Services; -using API.Services.Reading; +using Kavita.API.Database; +using Kavita.API.Errors; +using Kavita.API.Services; +using Kavita.API.Services.Reading; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.OPDS; +using Kavita.Models.DTOs.OPDS.Requests; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; +using Kavita.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MimeTypes; -namespace API.Controllers; -#nullable enable +namespace Kavita.Server.Controllers; [Authorize] -public class OpdsController : BaseApiController +public class OpdsController( + IUnitOfWork unitOfWork, + IDownloadService downloadService, + IDirectoryService directoryService, + ICacheService cacheService, + IReaderService readerService, + ILocalizationService localizationService, + IOpdsService opdsService) + : BaseApiController { - private readonly IOpdsService _opdsService; - private readonly IUnitOfWork _unitOfWork; - private readonly IDownloadService _downloadService; - private readonly IDirectoryService _directoryService; - private readonly ICacheService _cacheService; - private readonly IReaderService _readerService; - private readonly IAccountService _accountService; - private readonly ILocalizationService _localizationService; - private readonly XmlSerializer _xmlOpenSearchSerializer; - - public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService, - IDirectoryService directoryService, ICacheService cacheService, - IReaderService readerService, IAccountService accountService, - ILocalizationService localizationService, IOpdsService opdsService) - { - _unitOfWork = unitOfWork; - _downloadService = downloadService; - _directoryService = directoryService; - _cacheService = cacheService; - _readerService = readerService; - _accountService = accountService; - _localizationService = localizationService; - _opdsService = opdsService; - - _xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription)); - } + private readonly XmlSerializer _xmlOpenSearchSerializer = new(typeof(OpenSearchDescription)); /// @@ -62,22 +46,22 @@ public class OpdsController : BaseApiController { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetCatalogue(new OpdsCatalogueRequest + var feed = await opdsService.GetCatalogue(new OpdsCatalogueRequest { ApiKey = apiKey, Prefix = prefix, BaseUrl = baseUrl, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), UserId = UserId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } private async Task> GetPrefix() { - var baseUrl = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl)).Value; + var baseUrl = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl)).Value; var prefix = OpdsService.DefaultApiPrefix; if (!Configuration.DefaultBaseUrl.Equals(baseUrl, StringComparison.InvariantCultureIgnoreCase)) { @@ -92,26 +76,26 @@ public class OpdsController : BaseApiController /// Get the User's Smart Filter series - Supports Pagination /// /// - [HttpGet("{apiKey}/smart-filters/{filterId}")] [Produces("application/xml")] + [HttpGet("{apiKey}/smart-filters/{filterId}")] public async Task GetSmartFilter(string apiKey, int filterId, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { var userId = UserId; var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetSeriesFromSmartFilter(new OpdsItemsFromEntityIdRequest() + var feed = await opdsService.GetSeriesFromSmartFilter(new OpdsItemsFromEntityIdRequest() { ApiKey = apiKey, Prefix = prefix, BaseUrl = baseUrl, EntityId = filterId, UserId = userId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), PageNumber = pageNumber }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } /// @@ -120,8 +104,8 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/smart-filters")] [Produces("application/xml")] + [HttpGet("{apiKey}/smart-filters")] public async Task GetSmartFilters(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try @@ -129,17 +113,17 @@ public class OpdsController : BaseApiController var userId = UserId; var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetSmartFilters(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetSmartFilters(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = userId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -161,17 +145,17 @@ public class OpdsController : BaseApiController { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetLibraries(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetLibraries(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -185,25 +169,25 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/want-to-read")] [Produces("application/xml")] + [HttpGet("{apiKey}/want-to-read")] public async Task GetWantToRead(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetWantToRead(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetWantToRead(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -217,25 +201,25 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/collections")] [Produces("application/xml")] + [HttpGet("{apiKey}/collections")] public async Task GetCollections(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetCollections(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetCollections(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -250,26 +234,26 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/collections/{collectionId}")] [Produces("application/xml")] + [HttpGet("{apiKey}/collections/{collectionId}")] public async Task GetCollection(int collectionId, string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetSeriesFromCollection(new OpdsItemsFromEntityIdRequest() + var feed = await opdsService.GetSeriesFromCollection(new OpdsItemsFromEntityIdRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, EntityId = collectionId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -283,25 +267,25 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/reading-list")] [Produces("application/xml")] + [HttpGet("{apiKey}/reading-list")] public async Task GetReadingLists(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetReadingLists(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetReadingLists(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -316,26 +300,26 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/reading-list/{readingListId}")] [Produces("application/xml")] + [HttpGet("{apiKey}/reading-list/{readingListId}")] public async Task GetReadingListItems(int readingListId, string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetReadingListItems(new OpdsItemsFromEntityIdRequest() + var feed = await opdsService.GetReadingListItems(new OpdsItemsFromEntityIdRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, EntityId = readingListId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -351,26 +335,26 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/libraries/{libraryId}")] [Produces("application/xml")] + [HttpGet("{apiKey}/libraries/{libraryId}")] public async Task GetSeriesForLibrary(int libraryId, string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetSeriesFromLibrary(new OpdsItemsFromEntityIdRequest() + var feed = await opdsService.GetSeriesFromLibrary(new OpdsItemsFromEntityIdRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, EntityId = libraryId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -384,24 +368,24 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/recently-added")] [Produces("application/xml")] + [HttpGet("{apiKey}/recently-added")] public async Task GetRecentlyAdded(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetRecentlyAdded(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetRecentlyAdded(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -416,25 +400,25 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/more-in-genre")] [Produces("application/xml")] + [HttpGet("{apiKey}/more-in-genre")] public async Task GetMoreInGenre(string apiKey, [FromQuery] int genreId, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetMoreInGenre(new OpdsItemsFromEntityIdRequest() + var feed = await opdsService.GetMoreInGenre(new OpdsItemsFromEntityIdRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, EntityId = genreId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -448,24 +432,24 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/recently-updated")] [Produces("application/xml")] + [HttpGet("{apiKey}/recently-updated")] public async Task GetRecentlyUpdated(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetRecentlyUpdated(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetRecentlyUpdated(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -486,17 +470,17 @@ public class OpdsController : BaseApiController try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetOnDeck(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetOnDeck(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -517,17 +501,17 @@ public class OpdsController : BaseApiController try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.Search(new OpdsSearchRequest() + var feed = await opdsService.Search(new OpdsSearchRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, Query = query, }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -544,8 +528,8 @@ public class OpdsController : BaseApiController var feed = new OpenSearchDescription() { - ShortName = await _localizationService.Translate(userId, "search"), - Description = await _localizationService.Translate(userId, "search-description"), + ShortName = await localizationService.Translate(userId, "search"), + Description = await localizationService.Translate(userId, "search-description"), Url = new SearchLink() { Type = FeedLinkType.AtomAcquisition, @@ -565,6 +549,7 @@ public class OpdsController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("{apiKey}/series/{seriesId}")] [Produces("application/xml")] public async Task GetSeriesDetail(string apiKey, int seriesId) @@ -573,17 +558,17 @@ public class OpdsController : BaseApiController { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest() + var feed = await opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, EntityId = seriesId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -598,26 +583,27 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}")] + [VolumeAccess] [Produces("application/xml")] + [HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}")] public async Task GetVolume(string apiKey, int seriesId, int volumeId) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetItemsFromVolume(new OpdsItemsFromCompoundEntityIdsRequest() + var feed = await opdsService.GetItemsFromVolume(new OpdsItemsFromCompoundEntityIdsRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, SeriesId = seriesId, VolumeId = volumeId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -633,27 +619,28 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}")] + [ChapterAccess] [Produces("application/xml")] + [HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}")] public async Task GetChapter(string apiKey, int seriesId, int volumeId, int chapterId) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetItemsFromChapter(new OpdsItemsFromCompoundEntityIdsRequest() + var feed = await opdsService.GetItemsFromChapter(new OpdsItemsFromCompoundEntityIdsRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, SeriesId = seriesId, VolumeId = volumeId, ChapterId = chapterId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -670,18 +657,13 @@ public class OpdsController : BaseApiController /// /// Not used. Only for Chunky to allow download links /// + [ChapterAccess] + [Authorize(PolicyConstants.DownloadRole)] [HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}/download/{filename}")] public async Task DownloadFile(string apiKey, int seriesId, int volumeId, int chapterId, string filename) { - var userId = UserId; - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (!await _accountService.HasDownloadPermission(user)) - { - return Forbid(await _localizationService.Translate(userId, "download-not-allowed")); - } - - var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); - var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files); + var files = await unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); + var (zipFile, contentType, fileDownloadName) = downloadService.GetFirstFileDownload(files); return PhysicalFile(zipFile, contentType, fileDownloadName, true); } @@ -707,22 +689,23 @@ public class OpdsController : BaseApiController /// /// Optional parameter. Can pass false and progress saving will be suppressed /// + [ChapterAccess] [HttpGet("{apiKey}/image")] public async Task GetPageStreamedImage(string apiKey, [FromQuery] int libraryId, [FromQuery] int seriesId, [FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber, [FromQuery] bool saveProgress = true) { var userId = UserId; - if (pageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "Page")); - var chapter = await _cacheService.Ensure(chapterId, true); - if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "cache-file-find")); + if (pageNumber < 0) return BadRequest(await localizationService.Translate(userId, "greater-0", "Page")); + var chapter = await cacheService.Ensure(chapterId, true); + if (chapter == null) return BadRequest(await localizationService.Translate(userId, "cache-file-find")); try { - var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber); + var path = cacheService.GetCachedPagePath(chapter.Id, pageNumber); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) - return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", pageNumber)); + return BadRequest(await localizationService.Translate(userId, "no-image-for-page", pageNumber)); - var content = await _directoryService.ReadFileAsync(path); + var content = await directoryService.ReadFileAsync(path); var format = Path.GetExtension(path); // Save progress for the user (except Panels, they will use a direct connection) @@ -735,14 +718,14 @@ public class OpdsController : BaseApiController var koreaderOffset = 0; if (userAgent.StartsWith("Koreader", StringComparison.InvariantCultureIgnoreCase)) { - var totalPages = await _unitOfWork.ChapterRepository.GetChapterTotalPagesAsync(chapterId); + var totalPages = await unitOfWork.ChapterRepository.GetChapterTotalPagesAsync(chapterId); if (totalPages - pageNumber < 2) { koreaderOffset = 1; } } - await _readerService.SaveReadingProgress(new ProgressDto() + await readerService.SaveReadingProgress(new ProgressDto() { ChapterId = chapterId, PageNum = pageNumber + koreaderOffset, @@ -756,7 +739,7 @@ public class OpdsController : BaseApiController } catch (Exception) { - _cacheService.CleanupChapters([chapterId]); + cacheService.CleanupChapters([chapterId]); throw; } } @@ -765,11 +748,11 @@ public class OpdsController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Month)] public async Task GetFavicon(string apiKey) { - var files = _directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico"); - if (files.Length == 0) return BadRequest(await _localizationService.Translate(UserId, "favicon-doesnt-exist")); + var files = directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico"); + if (files.Length == 0) return BadRequest(await localizationService.Translate(UserId, "favicon-doesnt-exist")); var path = files[0]; - var content = await _directoryService.ReadFileAsync(path); + var content = await directoryService.ReadFileAsync(path); var format = Path.GetExtension(path); return File(content, MimeTypeMap.GetMimeType(format)); diff --git a/API/Controllers/OidcController.cs b/Kavita.Server/Controllers/OidcController.cs similarity index 83% rename from API/Controllers/OidcController.cs rename to Kavita.Server/Controllers/OidcController.cs index 87c3637bb..2f15746bd 100644 --- a/API/Controllers/OidcController.cs +++ b/Kavita.Server/Controllers/OidcController.cs @@ -1,28 +1,26 @@ -#nullable enable -using System.Threading.Tasks; -using API.Extensions; -using API.Middleware; -using API.Services; +using System.Threading.Tasks; +using Kavita.API.Attributes; using Kavita.Common; +using Kavita.Server.Extensions; +using Kavita.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; -namespace API.Controllers; +namespace Kavita.Server.Controllers; [Route("[controller]")] -public class OidcController(ILogger logger, [FromServices] ConfigurationManager? configurationManager = null): ControllerBase +public class OidcController([FromServices] ConfigurationManager? configurationManager = null): ControllerBase { - [SkipDeviceTracking] [AllowAnonymous] + [SkipDeviceTracking] [HttpGet("login")] public IActionResult Login(string returnUrl = "/") { - if (returnUrl == "/") + if (returnUrl == "/" || !Url.IsLocalUrl(returnUrl)) { returnUrl = Configuration.BaseUrl; } diff --git a/API/Controllers/PanelsController.cs b/Kavita.Server/Controllers/PanelsController.cs similarity index 54% rename from API/Controllers/PanelsController.cs rename to Kavita.Server/Controllers/PanelsController.cs index eb039c1bd..4ad7ac2d9 100644 --- a/API/Controllers/PanelsController.cs +++ b/Kavita.Server/Controllers/PanelsController.cs @@ -1,29 +1,17 @@ using System.Threading.Tasks; -using API.Data; -using API.DTOs.Progress; -using API.Services.Reading; +using Kavita.API.Database; +using Kavita.API.Services.Reading; +using Kavita.Models.DTOs.Progress; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// For the Panels app explicitly /// -[AllowAnonymous] -public class PanelsController : BaseApiController +public class PanelsController(IReaderService readerService, IUnitOfWork unitOfWork) : BaseApiController { - private readonly IReaderService _readerService; - private readonly IUnitOfWork _unitOfWork; - - public PanelsController(IReaderService readerService, IUnitOfWork unitOfWork) - { - _readerService = readerService; - _unitOfWork = unitOfWork; - } - /// /// Saves the progress of a given chapter. /// @@ -33,9 +21,7 @@ public class PanelsController : BaseApiController [HttpPost("save-progress")] public async Task SaveProgress(ProgressDto dto, [FromQuery] string apiKey) { - if (string.IsNullOrEmpty(apiKey)) return Unauthorized("ApiKey is required"); - var userId = await _unitOfWork.UserRepository.GetUserIdByAuthKeyAsync(apiKey); - await _readerService.SaveReadingProgress(dto, userId); + await readerService.SaveReadingProgress(dto, UserId); return Ok(); } @@ -48,10 +34,7 @@ public class PanelsController : BaseApiController [HttpGet("get-progress")] public async Task> GetProgress(int chapterId, [FromQuery] string apiKey) { - if (string.IsNullOrEmpty(apiKey)) return Unauthorized("ApiKey is required"); - var userId = await _unitOfWork.UserRepository.GetUserIdByAuthKeyAsync(apiKey); - - var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, userId); + var progress = await unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, UserId); if (progress == null) return Ok(new ProgressDto() { PageNum = 0, diff --git a/API/Controllers/PersonController.cs b/Kavita.Server/Controllers/PersonController.cs similarity index 59% rename from API/Controllers/PersonController.cs rename to Kavita.Server/Controllers/PersonController.cs index 9f4638a7c..3281e6f96 100644 --- a/API/Controllers/PersonController.cs +++ b/Kavita.Server/Controllers/PersonController.cs @@ -1,61 +1,46 @@ using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Metadata; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Metadata.Browse; -using API.DTOs.Metadata.Browse.Requests; -using API.DTOs.Person; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Services; -using API.Services.Plus; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; +using Kavita.Services.Plus; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Nager.ArticleNumber; -namespace API.Controllers; -#nullable enable - -public class PersonController : BaseApiController +namespace Kavita.Server.Controllers; +public class PersonController( + IUnitOfWork unitOfWork, + ILocalizationService localizationService, + IMapper mapper, + ICoverDbService coverDbService, + IImageService imageService, + IEventHub eventHub, + IPersonService personService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IMapper _mapper; - private readonly ICoverDbService _coverDbService; - private readonly IImageService _imageService; - private readonly IEventHub _eventHub; - private readonly IPersonService _personService; - - public PersonController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper, - ICoverDbService coverDbService, IImageService imageService, IEventHub eventHub, IPersonService personService) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _mapper = mapper; - _coverDbService = coverDbService; - _imageService = imageService; - _eventHub = eventHub; - _personService = personService; - } - - [HttpGet] public async Task> GetPersonByName(string name) { - var person = await _unitOfWork.PersonRepository.GetPersonDtoByName(name, UserId); + var person = await unitOfWork.PersonRepository.GetPersonDtoByName(name, UserId); if (person == null) return NotFound(); - person.Roles = (await _unitOfWork.PersonRepository.GetRolesForPersonByName(person.Id, UserId)).ToList(); + person.Roles = (await unitOfWork.PersonRepository.GetRolesForPersonByName(person.Id, UserId)).ToList(); EnrichWithWebLinks(person); @@ -101,7 +86,7 @@ public class PersonController : BaseApiController [HttpGet("search")] public async Task>> SearchPeople([FromQuery] string queryString) { - return Ok(await _unitOfWork.PersonRepository.SearchPeople(queryString)); + return Ok(await unitOfWork.PersonRepository.SearchPeople(queryString)); } /// @@ -112,13 +97,14 @@ public class PersonController : BaseApiController [HttpGet("roles")] public async Task>> GetRolesForPersonByName(int personId) { - return Ok(await _unitOfWork.PersonRepository.GetRolesForPersonByName(personId, UserId)); + return Ok(await unitOfWork.PersonRepository.GetRolesForPersonByName(personId, UserId)); } /// /// Returns a list of authors and artists for browsing /// + /// /// /// [HttpPost("all")] @@ -126,7 +112,7 @@ public class PersonController : BaseApiController { userParams ??= UserParams.Default; - var list = await _unitOfWork.PersonRepository.GetBrowsePersonDtos(UserId, filter, userParams); + var list = await unitOfWork.PersonRepository.GetBrowsePersonDtos(UserId, filter, userParams); Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); return Ok(list); @@ -137,29 +123,29 @@ public class PersonController : BaseApiController /// /// /// - [Authorize(PolicyGroups.AdminPolicy)] [HttpPost("update")] + [Authorize(PolicyGroups.AdminPolicy)] public async Task> UpdatePerson(UpdatePersonDto dto) { // This needs to get all people and update them equally - var person = await _unitOfWork.PersonRepository.GetPersonById(dto.Id, PersonIncludes.Aliases); - if (person == null) return BadRequest(_localizationService.Translate(UserId, "person-doesnt-exist")); + var person = await unitOfWork.PersonRepository.GetPersonById(dto.Id, PersonIncludes.Aliases); + if (person == null) return BadRequest(localizationService.Translate(UserId, "person-doesnt-exist")); - if (string.IsNullOrEmpty(dto.Name)) return BadRequest(await _localizationService.Translate(UserId, "person-name-required")); + if (string.IsNullOrEmpty(dto.Name)) return BadRequest(await localizationService.Translate(UserId, "person-name-required")); // Validate the name is unique - if (dto.Name != person.Name && !(await _unitOfWork.PersonRepository.IsNameUnique(dto.Name))) + if (dto.Name != person.Name && !(await unitOfWork.PersonRepository.IsNameUnique(dto.Name))) { - return BadRequest(await _localizationService.Translate(UserId, "person-name-unique")); + return BadRequest(await localizationService.Translate(UserId, "person-name-unique")); } // Update name first, in case it got moved to aliases person.Name = dto.Name.Trim(); person.NormalizedName = person.Name.ToNormalized(); - var success = await _personService.UpdatePersonAliasesAsync(person, dto.Aliases); - if (!success) return BadRequest(await _localizationService.Translate(UserId, "aliases-have-overlap")); + var success = await personService.UpdatePersonAliasesAsync(person, dto.Aliases); + if (!success) return BadRequest(await localizationService.Translate(UserId, "aliases-have-overlap")); person.Description = dto.Description ?? string.Empty; @@ -185,10 +171,10 @@ public class PersonController : BaseApiController person.Asin = asin; } - _unitOfWork.PersonRepository.Update(person); - await _unitOfWork.CommitAsync(); + unitOfWork.PersonRepository.Update(person); + await unitOfWork.CommitAsync(); - return Ok(_mapper.Map(person)); + return Ok(mapper.Map(person)); } /// @@ -196,26 +182,28 @@ public class PersonController : BaseApiController /// /// /// + [PersonAccess] [HttpPost("fetch-cover")] public async Task> DownloadCoverImage([FromQuery] int personId) { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var person = await _unitOfWork.PersonRepository.GetPersonById(personId); - if (person == null) return BadRequest(_localizationService.Translate(UserId, "person-doesnt-exist")); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var person = await unitOfWork.PersonRepository.GetPersonById(personId); + if (person == null) return BadRequest(localizationService.Translate(UserId, "person-doesnt-exist")); - var personImage = await _coverDbService.DownloadPersonImageAsync(person, settings.EncodeMediaAs); + var personImage = await coverDbService.DownloadPersonImageAsync(person, settings.EncodeMediaAs); if (string.IsNullOrEmpty(personImage)) { - return BadRequest(await _localizationService.Translate(UserId, "person-image-doesnt-exist")); + return BadRequest(await localizationService.Translate(UserId, "person-image-doesnt-exist")); } person.CoverImage = personImage; - _imageService.UpdateColorScape(person); - _unitOfWork.PersonRepository.Update(person); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(person.Id, "person"), false); + imageService.UpdateColorScape(person); + unitOfWork.PersonRepository.Update(person); + + await unitOfWork.CommitAsync(); + await eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(person.Id, "person"), false); return Ok(personImage); } @@ -225,10 +213,11 @@ public class PersonController : BaseApiController /// /// /// + [PersonAccess] [HttpGet("series-known-for")] public async Task>> GetKnownSeries(int personId) { - return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId, UserId)); + return Ok(await unitOfWork.PersonRepository.GetSeriesKnownFor(personId, UserId)); } /// @@ -237,10 +226,11 @@ public class PersonController : BaseApiController /// /// /// + [PersonAccess] [HttpGet("chapters-by-role")] public async Task>> GetChaptersByRole(int personId, PersonRole role) { - return Ok(await _unitOfWork.PersonRepository.GetChaptersForPersonByRole(personId, UserId, role)); + return Ok(await unitOfWork.PersonRepository.GetChaptersForPersonByRole(personId, UserId, role)); } /// @@ -252,16 +242,16 @@ public class PersonController : BaseApiController [Authorize(PolicyGroups.AdminPolicy)] public async Task> MergePeople(PersonMergeDto dto) { - var dst = await _unitOfWork.PersonRepository.GetPersonById(dto.DestId, PersonIncludes.All); + var dst = await unitOfWork.PersonRepository.GetPersonById(dto.DestId, PersonIncludes.All); if (dst == null) return BadRequest(); - var src = await _unitOfWork.PersonRepository.GetPersonById(dto.SrcId, PersonIncludes.All); + var src = await unitOfWork.PersonRepository.GetPersonById(dto.SrcId, PersonIncludes.All); if (src == null) return BadRequest(); - await _personService.MergePeopleAsync(src, dst); - await _eventHub.SendMessageAsync(MessageFactory.PersonMerged, MessageFactory.PersonMergedMessage(dst, src)); + await personService.MergePeopleAsync(src, dst); + await eventHub.SendMessageAsync(MessageFactory.PersonMerged, MessageFactory.PersonMergedMessage(dst, src)); - return Ok(_mapper.Map(dst)); + return Ok(mapper.Map(dst)); } /// @@ -272,11 +262,11 @@ public class PersonController : BaseApiController [HttpPost("valid-alias")] public async Task> IsValidAlias(PersonAliasCheckDto dto) { - var person = await _unitOfWork.PersonRepository.GetPersonById(dto.PersonId, PersonIncludes.Aliases); + var person = await unitOfWork.PersonRepository.GetPersonById(dto.PersonId, PersonIncludes.Aliases); if (person == null) return NotFound(); var aliasIsName = dto.Name.ToNormalized() == dto.Alias.ToNormalized(); - var existingAlias = await _unitOfWork.PersonRepository.AnyAliasExist(dto.Alias); + var existingAlias = await unitOfWork.PersonRepository.AnyAliasExist(dto.Alias); return Ok(!existingAlias && !aliasIsName); } diff --git a/API/Controllers/PluginController.cs b/Kavita.Server/Controllers/PluginController.cs similarity index 93% rename from API/Controllers/PluginController.cs rename to Kavita.Server/Controllers/PluginController.cs index 3a06fb06c..e9a7f74f1 100644 --- a/API/Controllers/PluginController.cs +++ b/Kavita.Server/Controllers/PluginController.cs @@ -1,24 +1,22 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs; -using API.DTOs.Misc; -using API.Entities.Enums; -using API.Middleware; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Misc; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; [SkipDeviceTracking] public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger logger) @@ -86,7 +84,7 @@ public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService /// Will always return null if the Auth Key does not belong to this account /// [HttpGet("authkey-expires")] - public async Task> GetAuthKeyExpiration() + public async Task> GetAuthKeyExpiration() { var authKey = AuthKey; if (string.IsNullOrEmpty(authKey)) @@ -94,7 +92,7 @@ public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService var exp = await unitOfWork.UserRepository.GetAuthKeyExpiration(authKey, UserId); - return Ok(new { ExpiresAt = exp?.ToUniversalTime() }); + return Ok(new AuthKeyExpiresAtDto { ExpiresAt = exp?.ToUniversalTime() }); } diff --git a/API/Controllers/RatingController.cs b/Kavita.Server/Controllers/RatingController.cs similarity index 57% rename from API/Controllers/RatingController.cs rename to Kavita.Server/Controllers/RatingController.cs index d377a33c8..a04493056 100644 --- a/API/Controllers/RatingController.cs +++ b/Kavita.Server/Controllers/RatingController.cs @@ -1,32 +1,24 @@ using System; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Services; -using API.Services.Plus; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// Responsible for providing external ratings for Series /// -public class RatingController : BaseApiController +public class RatingController( + IUnitOfWork unitOfWork, + IRatingService ratingService, + ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IRatingService _ratingService; - private readonly ILocalizationService _localizationService; - - public RatingController(IUnitOfWork unitOfWork, IRatingService ratingService, ILocalizationService localizationService) - { - _unitOfWork = unitOfWork; - _ratingService = ratingService; - _localizationService = localizationService; - } - /// /// Update the users' rating of the given series /// @@ -36,15 +28,18 @@ public class RatingController : BaseApiController [HttpPost("series")] public async Task UpdateSeriesRating(UpdateRatingDto updateRating) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings); if (user == null) throw new UnauthorizedAccessException(); - if (await _ratingService.UpdateSeriesRating(user, updateRating)) + if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, updateRating.SeriesId)) + return NotFound(); + + if (await ratingService.UpdateSeriesRating(user, updateRating)) { return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } /// @@ -56,15 +51,18 @@ public class RatingController : BaseApiController [HttpPost("chapter")] public async Task UpdateChapterRating(UpdateRatingDto updateRating) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings); if (user == null) throw new UnauthorizedAccessException(); - if (await _ratingService.UpdateChapterRating(user, updateRating)) + if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, updateRating.SeriesId)) + return NotFound(); + + if (await ratingService.UpdateChapterRating(user, updateRating)) { return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } /// @@ -72,13 +70,14 @@ public class RatingController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("overall-series")] public async Task> GetOverallSeriesRating(int seriesId) { return Ok(new RatingDto() { Provider = ScrobbleProvider.Kavita, - AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, UserId), + AverageScore = await unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, UserId), FavoriteCount = 0, }); } @@ -88,13 +87,14 @@ public class RatingController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("overall-chapter")] public async Task> GetOverallChapterRating(int chapterId) { return Ok(new RatingDto() { Provider = ScrobbleProvider.Kavita, - AverageScore = await _unitOfWork.ChapterRepository.GetAverageUserRating(chapterId, UserId), + AverageScore = await unitOfWork.ChapterRepository.GetAverageUserRating(chapterId, UserId), FavoriteCount = 0, }); } diff --git a/API/Controllers/ReaderController.cs b/Kavita.Server/Controllers/ReaderController.cs similarity index 61% rename from API/Controllers/ReaderController.cs rename to Kavita.Server/Controllers/ReaderController.cs index a7672e58d..765af4767 100644 --- a/API/Controllers/ReaderController.cs +++ b/Kavita.Server/Controllers/ReaderController.cs @@ -3,70 +3,46 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Filtering.v2; -using API.DTOs.Progress; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Middleware; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using Hangfire; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Server.Attributes; +using Kavita.Services; +using Kavita.Services.Metadata; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using MimeTypes; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// For all things regarding reading, mainly focusing on non-Book related entities /// -public class ReaderController : BaseApiController +/// +public class ReaderController(ICacheService cacheService, + IUnitOfWork unitOfWork, ILogger logger, + IReaderService readerService, IBookmarkService bookmarkService, IEventHub eventHub, + IScrobblingService scrobblingService, + ILocalizationService localizationService, + IBookService bookService) : BaseApiController { - private readonly ICacheService _cacheService; - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IReaderService _readerService; - private readonly IBookmarkService _bookmarkService; - private readonly IAccountService _accountService; - private readonly IEventHub _eventHub; - private readonly IScrobblingService _scrobblingService; - private readonly ILocalizationService _localizationService; - private readonly IBookService _bookService; - - /// - public ReaderController(ICacheService cacheService, - IUnitOfWork unitOfWork, ILogger logger, - IReaderService readerService, IBookmarkService bookmarkService, - IAccountService accountService, IEventHub eventHub, - IScrobblingService scrobblingService, - ILocalizationService localizationService, - IBookService bookService) - { - _cacheService = cacheService; - _unitOfWork = unitOfWork; - _logger = logger; - _readerService = readerService; - _bookmarkService = bookmarkService; - _accountService = accountService; - _eventHub = eventHub; - _scrobblingService = scrobblingService; - _localizationService = localizationService; - _bookService = bookService; - } /// /// Returns the PDF for the chapterId. @@ -75,28 +51,24 @@ public class ReaderController : BaseApiController /// Auth Key for authentication /// Converts PDF into images per-page - Used for Mihon mainly /// - [HttpGet("pdf")] + [ChapterAccess] [SkipDeviceTracking] + [HttpGet("pdf")] public async Task GetPdf(int chapterId, string apiKey, bool extractPdf = false) { if (!UserContext.IsAuthenticated) return Unauthorized(); - var chapter = await _cacheService.Ensure(chapterId, extractPdf); + var chapter = await cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); - // Validate the user has access to the PDF - var series = await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapter.Id, - await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(Username!)); - if (series == null) return BadRequest(await _localizationService.Translate(UserId, "invalid-access")); - try { - var path = _cacheService.GetCachedFile(chapter); + var path = cacheService.GetCachedFile(chapter); return CachedFile(path, maxAge: TimeSpan.FromHours(1).Seconds); } catch (Exception) { - _cacheService.CleanupChapters([chapterId]); + cacheService.CleanupChapters([chapterId]); throw; } } @@ -110,24 +82,24 @@ public class ReaderController : BaseApiController /// User's API Key for authentication /// Should Kavita extract pdf into images. Defaults to false. /// - [HttpGet("image")] + [ChapterAccess] [SkipDeviceTracking] - [AllowAnonymous] + [HttpGet("image")] public async Task GetImage(int chapterId, int page, string apiKey, bool extractPdf = false) { if (page < 0) page = 0; try { - var chapter = await _cacheService.Ensure(chapterId, extractPdf); + var chapter = await cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); - var path = _cacheService.GetCachedPagePath(chapter.Id, page); + var path = cacheService.GetCachedPagePath(chapter.Id, page); return CachedFile(path, maxAge: TimeSpan.FromHours(1).Seconds); } catch (Exception) { - _cacheService.CleanupChapters([chapterId]); + cacheService.CleanupChapters([chapterId]); throw; } } @@ -139,16 +111,17 @@ public class ReaderController : BaseApiController /// /// /// - [HttpGet("thumbnail")] + [ChapterAccess] [SkipDeviceTracking] - [AllowAnonymous] + [HttpGet("thumbnail")] public async Task GetThumbnail(int chapterId, int pageNum, string apiKey) { - var chapter = await _cacheService.Ensure(chapterId, true); + var chapter = await cacheService.Ensure(chapterId, true); if (chapter == null) return NoContent(); - var images = _cacheService.GetCachedPages(chapterId); - var path = await _readerService.GetThumbnail(chapter, pageNum, images); + var images = cacheService.GetCachedPages(chapterId); + + var path = await readerService.GetThumbnail(chapter, pageNum, images); return CachedFile(path, maxAge: TimeSpan.FromHours(1).Seconds); } @@ -160,13 +133,13 @@ public class ReaderController : BaseApiController /// /// We must use api key as bookmarks could be leaked to other users via the API /// - [HttpGet("bookmark-image")] + [SeriesAccess] [SkipDeviceTracking] - [AllowAnonymous] + [HttpGet("bookmark-image")] public async Task GetBookmarkImage(int seriesId, string apiKey, int page) { if (page < 0) page = 0; - var totalPages = await _cacheService.CacheBookmarkForSeries(UserId, seriesId); + var totalPages = await cacheService.CacheBookmarkForSeries(UserId, seriesId); if (page > totalPages) { page = totalPages; @@ -174,12 +147,12 @@ public class ReaderController : BaseApiController try { - var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page); + var path = cacheService.GetCachedBookmarkPagePath(seriesId, page); return CachedFile(path, maxAge: TimeSpan.FromHours(1).Seconds); } catch (Exception) { - _cacheService.CleanupBookmarks([seriesId]); + cacheService.CleanupBookmarks([seriesId]); throw; } } @@ -192,16 +165,17 @@ public class ReaderController : BaseApiController /// /// /// - [HttpGet("file-dimensions")] + [ChapterAccess] [SkipDeviceTracking] + [HttpGet("file-dimensions")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf"])] public async Task>> GetFileDimensions(int chapterId, bool extractPdf = false) { if (chapterId <= 0) return ArraySegment.Empty; - var chapter = await _cacheService.Ensure(chapterId, extractPdf); + var chapter = await cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); - return Ok(_cacheService.GetCachedFileDimensions(_cacheService.GetCachePath(chapterId))); + return Ok(cacheService.GetCachedFileDimensions(cacheService.GetCachePath(chapterId))); } /// @@ -212,19 +186,20 @@ public class ReaderController : BaseApiController /// Should Kavita extract pdf into images. Defaults to false. /// Include file dimensions. Only useful for image-based reading /// + [ChapterAccess] [HttpGet("chapter-info")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf", "includeDimensions"])] public async Task> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false) { if (chapterId <= 0) return Ok(null); // This can happen occasionally from UI, we should just ignore - var chapter = await _cacheService.Ensure(chapterId, extractPdf); + var chapter = await cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); - var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId); - if (dto == null) return BadRequest(await _localizationService.Translate(UserId, "perform-scan")); + var dto = await unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId); + if (dto == null) return BadRequest(await localizationService.Translate(UserId, "perform-scan")); var mangaFile = chapter.Files.First(); - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(dto.SeriesId, UserId); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(dto.SeriesId, UserId); if (series == null) return Unauthorized(); var info = new ChapterInfoDto() @@ -248,8 +223,8 @@ public class ReaderController : BaseApiController if (includeDimensions) { - info.PageDimensions = _cacheService.GetCachedFileDimensions(_cacheService.GetCachePath(chapterId)); - info.DoublePairs = _readerService.GetPairs(info.PageDimensions); + info.PageDimensions = cacheService.GetCachedFileDimensions(cacheService.GetCachePath(chapterId)); + info.DoublePairs = readerService.GetPairs(info.PageDimensions); } if (info.ChapterTitle is {Length: > 0}) { @@ -266,7 +241,7 @@ public class ReaderController : BaseApiController } else { - info.Subtitle = await _localizationService.Translate(UserId, "volume-num", info.VolumeNumber); + info.Subtitle = await localizationService.Translate(UserId, "volume-num", info.VolumeNumber); if (!Parser.IsDefaultChapter(info.ChapterNumber)) { info.Subtitle += " " + ReaderService.FormatChapterName(info.LibraryType, true, true) + @@ -284,12 +259,13 @@ public class ReaderController : BaseApiController /// Series Id for all bookmarks /// Include file dimensions (extra I/O). Defaults to true. /// + [SeriesAccess] [HttpGet("bookmark-info")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "includeDimensions"])] public async Task> GetBookmarkInfo(int seriesId, bool includeDimensions = true) { - var totalPages = await _cacheService.CacheBookmarkForSeries(UserId, seriesId); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.None); + var totalPages = await cacheService.CacheBookmarkForSeries(UserId, seriesId); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.None); var info = new BookmarkInfoDto() { @@ -302,8 +278,8 @@ public class ReaderController : BaseApiController if (includeDimensions) { - info.PageDimensions = _cacheService.GetCachedFileDimensions(_cacheService.GetBookmarkCachePath(seriesId)); - info.DoublePairs = _readerService.GetPairs(info.PageDimensions); + info.PageDimensions = cacheService.GetCachedFileDimensions(cacheService.GetBookmarkCachePath(seriesId)); + info.DoublePairs = readerService.GetPairs(info.PageDimensions); } return Ok(info); @@ -318,21 +294,21 @@ public class ReaderController : BaseApiController [HttpPost("mark-read")] public async Task MarkRead(MarkReadDto markReadDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); try { - await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId); + await readerService.MarkSeriesAsRead(user, markReadDto.SeriesId); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId)); - BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markReadDto.SeriesId, user.Id)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId)); + BackgroundJob.Enqueue(() => unitOfWork.SeriesRepository.ClearOnDeckRemoval(markReadDto.SeriesId, user.Id)); return Ok(); } @@ -345,13 +321,13 @@ public class ReaderController : BaseApiController [HttpPost("mark-unread")] public async Task MarkUnread(MarkReadDto markReadDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); - await _readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId); + await readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId); - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId)); return Ok(); } @@ -363,15 +339,15 @@ public class ReaderController : BaseApiController [HttpPost("mark-volume-unread")] public async Task MarkVolumeAsUnread(MarkVolumeReadDto markVolumeReadDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); - var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); - await _readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters); + var chapters = await unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); + await readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters); - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId)); return Ok(); } @@ -383,29 +359,29 @@ public class ReaderController : BaseApiController [HttpPost("mark-volume-read")] public async Task MarkVolumeAsRead(MarkVolumeReadDto markVolumeReadDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); - var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); + var chapters = await unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); if (user == null) return Unauthorized(); try { - await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters); + await readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); - await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, + await eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, markVolumeReadDto.SeriesId, markVolumeReadDto.VolumeId, 0, chapters.Sum(c => c.Pages))); - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId)); - BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markVolumeReadDto.SeriesId, user.Id)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId)); + BackgroundJob.Enqueue(() => unitOfWork.SeriesRepository.ClearOnDeckRemoval(markVolumeReadDto.SeriesId, user.Id)); return Ok(); } @@ -418,23 +394,23 @@ public class ReaderController : BaseApiController [HttpPost("mark-multiple-read")] public async Task MarkMultipleAsRead(MarkVolumesReadDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); user.Progresses ??= []; - var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); + var chapterIds = await unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); foreach (var chapterId in dto.ChapterIds) { chapterIds.Add(chapterId); } - var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds); - await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters.ToList()); + var chapters = await unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds); + await readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters.ToList()); - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId)); - BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(dto.SeriesId, user.Id)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId)); + BackgroundJob.Enqueue(() => unitOfWork.SeriesRepository.ClearOnDeckRemoval(dto.SeriesId, user.Id)); return Ok(); } @@ -447,25 +423,25 @@ public class ReaderController : BaseApiController [HttpPost("mark-multiple-unread")] public async Task MarkMultipleAsUnread(MarkVolumesReadDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); user.Progresses ??= new List(); - var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); + var chapterIds = await unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); foreach (var chapterId in dto.ChapterIds) { chapterIds.Add(chapterId); } - var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds); - await _readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters.ToList()); + var chapters = await unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds); + await readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters.ToList()); - if (await _unitOfWork.CommitAsync()) + if (await unitOfWork.CommitAsync()) { - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId)); return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); } /// @@ -476,22 +452,22 @@ public class ReaderController : BaseApiController [HttpPost("mark-multiple-series-read")] public async Task MarkMultipleSeriesAsRead(MarkMultipleSeriesAsReadDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); user.Progresses ??= new List(); - var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true); + var volumes = await unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true); foreach (var volume in volumes) { - await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters); + await readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters); } - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); foreach (var sId in dto.SeriesIds) { - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, sId)); - BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(sId, user.Id)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, sId)); + BackgroundJob.Enqueue(() => unitOfWork.SeriesRepository.ClearOnDeckRemoval(sId, user.Id)); } return Ok(); } @@ -504,26 +480,26 @@ public class ReaderController : BaseApiController [HttpPost("mark-multiple-series-unread")] public async Task MarkMultipleSeriesAsUnread(MarkMultipleSeriesAsReadDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); user.Progresses ??= []; - var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true); + var volumes = await unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true); foreach (var volume in volumes) { - await _readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters); + await readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters); } - if (await _unitOfWork.CommitAsync()) + if (await unitOfWork.CommitAsync()) { foreach (var sId in dto.SeriesIds) { - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, sId)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, sId)); } return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); } /// @@ -531,11 +507,12 @@ public class ReaderController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("get-progress")] public async Task> GetProgress(int chapterId) { - var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, UserId); - _logger.LogDebug("Get Progress for {ChapterId} is {Pages}", chapterId, progress?.PageNum ?? 0); + var progress = await unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, UserId); + logger.LogDebug("Get Progress for {ChapterId} is {Pages}", chapterId, progress?.PageNum ?? 0); if (progress == null) return Ok(new ProgressDto() { @@ -558,9 +535,9 @@ public class ReaderController : BaseApiController { var userId = UserId; - if (!await _readerService.SaveReadingProgress(progressDto, userId)) + if (!await readerService.SaveReadingProgress(progressDto, userId)) { - return BadRequest(await _localizationService.Translate(userId, "generic-read-progress")); + return BadRequest(await localizationService.Translate(userId, "generic-read-progress")); } return Ok(); @@ -571,10 +548,11 @@ public class ReaderController : BaseApiController /// Otherwise, loop through the chapters and volumes in order to find the next chapter which has progress. /// /// + [SeriesAccess] [HttpGet("continue-point")] public async Task> GetContinuePoint(int seriesId) { - return Ok(await _readerService.GetContinuePoint(seriesId, UserId)); + return Ok(await readerService.GetContinuePoint(seriesId, UserId)); } /// @@ -582,10 +560,11 @@ public class ReaderController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("has-progress")] public async Task> HasProgress(int seriesId) { - return Ok(await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, UserId)); + return Ok(await unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, UserId)); } /// @@ -593,10 +572,11 @@ public class ReaderController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("chapter-bookmarks")] public async Task>> GetBookmarks(int chapterId) { - return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(UserId, chapterId)); + return Ok(await unitOfWork.UserRepository.GetBookmarkDtosForChapter(UserId, chapterId)); } /// @@ -607,7 +587,7 @@ public class ReaderController : BaseApiController [HttpPost("all-bookmarks")] public async Task>> GetAllBookmarks(FilterV2Dto filterDto) { - return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(UserId, filterDto)); + return Ok(await unitOfWork.UserRepository.GetAllBookmarkDtos(UserId, filterDto)); } /// @@ -618,36 +598,36 @@ public class ReaderController : BaseApiController [HttpPost("remove-bookmarks")] public async Task RemoveBookmarks(RemoveBookmarkForSeriesDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); if (user == null) return Unauthorized(); - if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok(await localizationService.Translate(UserId, "nothing-to-do")); try { var bookmarksToRemove = user.Bookmarks.Where(bmk => bmk.SeriesId == dto.SeriesId).ToList(); user.Bookmarks = user.Bookmarks.Where(bmk => bmk.SeriesId != dto.SeriesId).ToList(); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) + if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync()) { try { - await _bookmarkService.DeleteBookmarkFiles(bookmarksToRemove); + await bookmarkService.DeleteBookmarkFiles(bookmarksToRemove); } catch (Exception ex) { - _logger.LogError(ex, "There was an issue cleaning up old bookmarks"); + logger.LogError(ex, "There was an issue cleaning up old bookmarks"); } return Ok(); } } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when trying to clear bookmarks"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was an exception when trying to clear bookmarks"); + await unitOfWork.RollbackAsync(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-clear-bookmarks")); + return BadRequest(await localizationService.Translate(UserId, "generic-clear-bookmarks")); } /// @@ -658,9 +638,9 @@ public class ReaderController : BaseApiController [HttpPost("bulk-remove-bookmarks")] public async Task BulkRemoveBookmarks(BulkRemoveBookmarkForSeriesDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); if (user == null) return Unauthorized(); - if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok(await localizationService.Translate(UserId, "nothing-to-do")); try { @@ -668,23 +648,23 @@ public class ReaderController : BaseApiController { var bookmarksToRemove = user.Bookmarks.Where(bmk => bmk.SeriesId == seriesId).ToList(); user.Bookmarks = user.Bookmarks.Where(bmk => bmk.SeriesId != seriesId).ToList(); - _unitOfWork.UserRepository.Update(user); - await _bookmarkService.DeleteBookmarkFiles(bookmarksToRemove); + unitOfWork.UserRepository.Update(user); + await bookmarkService.DeleteBookmarkFiles(bookmarksToRemove); } - if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) + if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync()) { return Ok(); } } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when trying to clear bookmarks"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was an exception when trying to clear bookmarks"); + await unitOfWork.RollbackAsync(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-clear-bookmarks")); + return BadRequest(await localizationService.Translate(UserId, "generic-clear-bookmarks")); } /// @@ -692,10 +672,11 @@ public class ReaderController : BaseApiController /// /// /// + [VolumeAccess] [HttpGet("volume-bookmarks")] public async Task>> GetBookmarksForVolume(int volumeId) { - return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(UserId, volumeId)); + return Ok(await unitOfWork.UserRepository.GetBookmarkDtosForVolume(UserId, volumeId)); } /// @@ -703,10 +684,11 @@ public class ReaderController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("series-bookmarks")] public async Task>> GetBookmarksForSeries(int seriesId) { - return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(UserId, seriesId)); + return Ok(await unitOfWork.UserRepository.GetBookmarkDtosForSeries(UserId, seriesId)); } /// @@ -716,41 +698,38 @@ public class ReaderController : BaseApiController /// /// [HttpPost("bookmark")] + [Authorize(PolicyGroups.BookmarkPolicy)] public async Task BookmarkPage(BookmarkDto bookmarkDto) { try { // Don't let user save past total pages. - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, - AppUserIncludes.Bookmarks); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); if (user == null) return new UnauthorizedResult(); - if (!await _accountService.HasBookmarkPermission(user)) - return BadRequest(await _localizationService.Translate(UserId, "bookmark-permission")); - - var chapter = await _cacheService.Ensure(bookmarkDto.ChapterId); + var chapter = await cacheService.Ensure(bookmarkDto.ChapterId); if (chapter == null || chapter.Files.Count == 0) - return BadRequest(await _localizationService.Translate(UserId, "cache-file-find")); + return BadRequest(await localizationService.Translate(UserId, "cache-file-find")); - bookmarkDto.Page = _readerService.CapPageToChapter(chapter, bookmarkDto.Page); + bookmarkDto.Page = readerService.CapPageToChapter(chapter, bookmarkDto.Page); string path; string? chapterTitle; if (Parser.IsEpub(chapter.Files.First().Extension!)) { - var cachedFilePath = _cacheService.GetCachedFile(chapter); - path = await _bookService.CopyImageToTempFromBook(chapter.Id, bookmarkDto, cachedFilePath); + var cachedFilePath = cacheService.GetCachedFile(chapter); + path = await bookService.CopyImageToTempFromBook(chapter.Id, bookmarkDto, cachedFilePath); - var chapterEntity = await _unitOfWork.ChapterRepository.GetChapterAsync(bookmarkDto.ChapterId); - if (chapterEntity == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); - var toc = await _bookService.GenerateTableOfContents(chapterEntity); + var chapterEntity = await unitOfWork.ChapterRepository.GetChapterAsync(bookmarkDto.ChapterId); + if (chapterEntity == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); + var toc = await bookService.GenerateTableOfContents(chapterEntity); chapterTitle = BookService.GetChapterTitleFromToC(toc, bookmarkDto.Page); } else { - path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page); + path = cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page); chapterTitle = chapter.TitleName; } @@ -758,20 +737,20 @@ public class ReaderController : BaseApiController - if (string.IsNullOrEmpty(path) || !await _bookmarkService.BookmarkPage(user, bookmarkDto, path)) + if (string.IsNullOrEmpty(path) || !await bookmarkService.BookmarkPage(user, bookmarkDto, path)) { - return BadRequest(await _localizationService.Translate(UserId, "bookmark-save")); + return BadRequest(await localizationService.Translate(UserId, "bookmark-save")); } - BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId)); + BackgroundJob.Enqueue(() => cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId)); return Ok(); } catch (KavitaException ex) { - _logger.LogError(ex, "There was an exception when trying to create a bookmark"); - return BadRequest(await _localizationService.Translate(UserId, "bookmark-save")); + logger.LogError(ex, "There was an exception when trying to create a bookmark"); + return BadRequest(await localizationService.Translate(UserId, "bookmark-save")); } } @@ -782,24 +761,21 @@ public class ReaderController : BaseApiController /// /// [HttpPost("unbookmark")] + [Authorize(PolicyGroups.BookmarkPolicy)] public async Task UnBookmarkPage(BookmarkDto bookmarkDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); if (user == null) return new UnauthorizedResult(); + if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok(); - if (!await _accountService.HasBookmarkPermission(user)) + if (!await bookmarkService.RemoveBookmarkPage(user, bookmarkDto)) { - return BadRequest(await _localizationService.Translate(UserId, "bookmark-permission")); - } - - if (!await _bookmarkService.RemoveBookmarkPage(user, bookmarkDto)) - { - return BadRequest(await _localizationService.Translate(UserId, "bookmark-save")); + return BadRequest(await localizationService.Translate(UserId, "bookmark-save")); } - BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId)); + BackgroundJob.Enqueue(() => cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId)); return Ok(); } @@ -814,11 +790,12 @@ public class ReaderController : BaseApiController /// /// /// chapter id for next manga - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "volumeId", "currentChapterId"])] + [SeriesAccess] [HttpGet("next-chapter")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "volumeId", "currentChapterId"])] public async Task> GetNextChapter(int seriesId, int volumeId, int currentChapterId) { - return Ok(await _readerService.GetNextChapterIdAsync(seriesId, volumeId, currentChapterId, UserId)); + return Ok(await readerService.GetNextChapterIdAsync(seriesId, volumeId, currentChapterId, UserId)); } @@ -832,11 +809,12 @@ public class ReaderController : BaseApiController /// /// /// chapter id for next manga - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "volumeId", "currentChapterId"])] + [SeriesAccess] [HttpGet("prev-chapter")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "volumeId", "currentChapterId"])] public async Task> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId) { - return Ok(await _readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, UserId)); + return Ok(await readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, UserId)); } /// @@ -845,20 +823,21 @@ public class ReaderController : BaseApiController /// For Epubs, this does not check words inside a chapter due to overhead so may not work in all cases. /// /// + [SeriesAccess] [HttpGet("time-left")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId"])] public async Task> GetEstimateToCompletion(int seriesId) { var userId = UserId; - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); - if (series == null) return BadRequest(await _localizationService.Translate(UserId, "series-doesnt-exist")); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + if (series == null) return BadRequest(await localizationService.Translate(UserId, "series-doesnt-exist")); // Get all sum of all chapters with progress that is complete then subtract from series. Multiply by modifiers - var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(seriesId, userId); + var progress = await unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(seriesId, userId); if (series.Format == MangaFormat.Epub) { var chapters = - await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(progress.Select(p => p.ChapterId).ToList()); + await unitOfWork.ChapterRepository.GetChaptersByIdsAsync(progress.Select(p => p.ChapterId).ToList()); // Word count var progressCount = chapters.Sum(c => c.WordCount); var wordsLeft = series.WordCount - progressCount; @@ -879,19 +858,20 @@ public class ReaderController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("time-left-for-chapter")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "chapterId"])] public async Task> GetEstimateToCompletionForChapter(int seriesId, int chapterId) { var userId = UserId; - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); - var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, userId); - if (series == null || chapter == null) return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + var chapter = await unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, userId); + if (series == null || chapter == null) return BadRequest(await localizationService.Translate(UserId, "generic-error")); if (series.Format == MangaFormat.Epub) { // Get the word counts for all the pages - var pageCounts = await _bookService.GetWordCountsPerPage(chapter.Files.First().FilePath); // TODO: Cache + var pageCounts = await bookService.GetWordCountsPerPage(chapter.Files.First().FilePath); // TODO: Cache if (pageCounts == null) return ReaderService.GetTimeEstimate(series.WordCount, 0, true); // Sum character counts only for pages that have been read @@ -916,10 +896,11 @@ public class ReaderController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("ptoc")] public ActionResult> GetPersonalToC(int chapterId) { - return Ok(_unitOfWork.UserTableOfContentRepository.GetPersonalToC(UserId, chapterId)); + return Ok(unitOfWork.UserTableOfContentRepository.GetPersonalToC(UserId, chapterId)); } /// @@ -929,18 +910,19 @@ public class ReaderController : BaseApiController /// /// /// + [ChapterAccess] [HttpDelete("ptoc")] public async Task DeletePersonalToc([FromQuery] int chapterId, [FromQuery] int pageNum, [FromQuery] string title) { var userId = UserId; - if (string.IsNullOrWhiteSpace(title)) return BadRequest(await _localizationService.Translate(userId, "name-required")); - if (pageNum < 0) return BadRequest(await _localizationService.Translate(userId, "valid-number")); + if (string.IsNullOrWhiteSpace(title)) return BadRequest(await localizationService.Translate(userId, "name-required")); + if (pageNum < 0) return BadRequest(await localizationService.Translate(userId, "valid-number")); - var toc = await _unitOfWork.UserTableOfContentRepository.Get(userId, chapterId, pageNum, title); + var toc = await unitOfWork.UserTableOfContentRepository.Get(userId, chapterId, pageNum, title); if (toc == null) return Ok(); - _unitOfWork.UserTableOfContentRepository.Remove(toc); - await _unitOfWork.CommitAsync(); + unitOfWork.UserTableOfContentRepository.Remove(toc); + await unitOfWork.CommitAsync(); return Ok(); } @@ -956,21 +938,24 @@ public class ReaderController : BaseApiController { // Validate there isn't already an existing page title combo? var userId = UserId; - if (string.IsNullOrWhiteSpace(dto.Title)) return BadRequest(await _localizationService.Translate(userId, "name-required")); - if (dto.PageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "valid-number")); - if (await _unitOfWork.UserTableOfContentRepository.IsUnique(userId, dto.ChapterId, dto.PageNumber, + if (string.IsNullOrWhiteSpace(dto.Title)) return BadRequest(await localizationService.Translate(userId, "name-required")); + + if (!await unitOfWork.UserRepository.HasAccessToChapter(UserId, dto.ChapterId)) return NotFound(); + + if (dto.PageNumber < 0) return BadRequest(await localizationService.Translate(userId, "valid-number")); + if (await unitOfWork.UserTableOfContentRepository.IsUnique(userId, dto.ChapterId, dto.PageNumber, dto.Title.Trim())) { - return BadRequest(await _localizationService.Translate(userId, "duplicate-bookmark")); + return BadRequest(await localizationService.Translate(userId, "duplicate-bookmark")); } // Look up the chapter this PTOC is associated with to get the chapter title (if there is one) - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId); - if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "chapter-doesnt-exist")); - var toc = await _bookService.GenerateTableOfContents(chapter); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId); + if (chapter == null) return BadRequest(await localizationService.Translate(userId, "chapter-doesnt-exist")); + var toc = await bookService.GenerateTableOfContents(chapter); var chapterTitle = BookService.GetChapterTitleFromToC(toc, dto.PageNumber); - _unitOfWork.UserTableOfContentRepository.Attach(new AppUserTableOfContent() + unitOfWork.UserTableOfContentRepository.Attach(new AppUserTableOfContent() { Title = dto.Title.Trim(), ChapterId = dto.ChapterId, @@ -982,7 +967,7 @@ public class ReaderController : BaseApiController ChapterTitle = chapterTitle, AppUserId = userId }); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -991,11 +976,13 @@ public class ReaderController : BaseApiController /// Check if we should prompt the user for rereads for the given series /// /// + /// /// + [SeriesAccess] [HttpGet("prompt-reread/series")] public async Task> ShouldPromptForSeriesReRead(int seriesId, int libraryId) { - return Ok(await _readerService.CheckSeriesForReRead(UserId, seriesId, libraryId)); + return Ok(await readerService.CheckSeriesForReRead(UserId, seriesId, libraryId)); } /// @@ -1005,10 +992,11 @@ public class ReaderController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("prompt-reread/volume")] public async Task> ShouldPromptForVolumeReRead(int libraryId, int seriesId, int volumeId) { - return Ok(await _readerService.CheckVolumeForReRead(UserId, volumeId, seriesId, libraryId)); + return Ok(await readerService.CheckVolumeForReRead(UserId, volumeId, seriesId, libraryId)); } /// @@ -1018,16 +1006,17 @@ public class ReaderController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("prompt-reread/chapter")] public async Task> ShouldPromptForChapterReRead(int libraryId, int seriesId, int chapterId) { - return Ok(await _readerService.CheckChapterForReRead(UserId, chapterId, seriesId, libraryId)); + return Ok(await readerService.CheckChapterForReRead(UserId, chapterId, seriesId, libraryId)); } [HttpGet("first-progress-date")] public async Task> GetFirstReadingDate(int userId) { - return Ok(await _unitOfWork.AppUserProgressRepository.GetFirstProgressForUser(userId)); + return Ok(await unitOfWork.AppUserProgressRepository.GetFirstProgressForUser(userId)); } } diff --git a/API/Controllers/ReadingListController.cs b/Kavita.Server/Controllers/ReadingListController.cs similarity index 55% rename from API/Controllers/ReadingListController.cs rename to Kavita.Server/Controllers/ReadingListController.cs index a9d98f03e..e3b2134eb 100644 --- a/API/Controllers/ReadingListController.cs +++ b/Kavita.Server/Controllers/ReadingListController.cs @@ -1,42 +1,32 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Person; -using API.DTOs.ReadingLists; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Middleware; -using API.Services; -using API.Services.Reading; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Reading; using Kavita.Common; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; +using Kavita.Services.Reading; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; [Authorize] -public class ReadingListController : BaseApiController +public class ReadingListController( + IUnitOfWork unitOfWork, + IReadingListService readingListService, + ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IReadingListService _readingListService; - private readonly ILocalizationService _localizationService; - private readonly IReaderService _readerService; - - public ReadingListController(IUnitOfWork unitOfWork, IReadingListService readingListService, - ILocalizationService localizationService, IReaderService readerService) - { - _unitOfWork = unitOfWork; - _readingListService = readingListService; - _localizationService = localizationService; - _readerService = readerService; - } - /// /// Fetches a single Reading List /// @@ -45,10 +35,10 @@ public class ReadingListController : BaseApiController [HttpGet] public async Task> GetList(int readingListId) { - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, UserId); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, UserId); if (readingList == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-restricted")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-restricted")); } return Ok(readingList); @@ -65,7 +55,7 @@ public class ReadingListController : BaseApiController public async Task>> GetListsForUser([FromQuery] UserParams userParams, bool includePromoted = true, bool sortByLastModified = false) { - var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(UserId, includePromoted, + var items = await unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(UserId, includePromoted, userParams, sortByLastModified); Response.AddPaginationHeader(items.CurrentPage, items.PageSize, items.TotalCount, items.TotalPages); @@ -80,7 +70,7 @@ public class ReadingListController : BaseApiController [HttpGet("lists-for-series")] public async Task>> GetListsForSeries(int seriesId) { - return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(UserId, + return Ok(await unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(UserId, seriesId, true)); } @@ -92,7 +82,7 @@ public class ReadingListController : BaseApiController [HttpGet("lists-for-chapter")] public async Task>> GetListsForChapter(int chapterId) { - return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtosForChapterAndUserAsync(UserId, + return Ok(await unitOfWork.ReadingListRepository.GetReadingListDtosForChapterAndUserAsync(UserId, chapterId, true)); } @@ -105,7 +95,7 @@ public class ReadingListController : BaseApiController [HttpGet("items")] public async Task>> GetListForUser(int readingListId) { - return Ok(await _readingListService.GetReadingListItems(readingListId, UserId)); + return Ok(await readingListService.GetReadingListItems(readingListId, UserId)); } @@ -119,16 +109,16 @@ public class ReadingListController : BaseApiController public async Task UpdateListItemPosition(UpdateReadingListPosition dto) { // Make sure UI buffers events - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } - if (await _readingListService.UpdateReadingListItemPosition(dto)) return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + if (await readingListService.UpdateReadingListItemPosition(dto)) return Ok(await localizationService.Translate(UserId, "reading-list-updated")); - return BadRequest(await _localizationService.Translate(UserId, "reading-list-position")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-position")); } /// @@ -140,18 +130,18 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteListItem(UpdateReadingListPosition dto) { - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } - if (await _readingListService.DeleteReadingListItem(dto)) + if (await readingListService.DeleteReadingListItem(dto)) { - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } - return BadRequest(await _localizationService.Translate(UserId, "reading-list-item-delete")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-item-delete")); } /// @@ -163,18 +153,18 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteReadFromList([FromQuery] int readingListId) { - var user = await _readingListService.UserHasReadingListAccess(readingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(readingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } - if (await _readingListService.RemoveFullyReadItems(readingListId, user)) + if (await readingListService.RemoveFullyReadItems(readingListId, user)) { - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } - return BadRequest(await _localizationService.Translate(UserId, "reading-list-item-delete")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-item-delete")); } /// @@ -186,16 +176,16 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteList([FromQuery] int readingListId) { - var user = await _readingListService.UserHasReadingListAccess(readingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(readingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } - if (await _readingListService.DeleteReadingList(readingListId, user)) - return Ok(await _localizationService.Translate(UserId, "reading-list-deleted")); + if (await readingListService.DeleteReadingList(readingListId, user)) + return Ok(await localizationService.Translate(UserId, "reading-list-deleted")); - return BadRequest(await _localizationService.Translate(UserId, "generic-reading-list-delete")); + return BadRequest(await localizationService.Translate(UserId, "generic-reading-list-delete")); } /// @@ -207,19 +197,19 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> CreateList(CreateReadingListDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.ReadingLists); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.ReadingLists); if (user == null) return Unauthorized(); try { - await _readingListService.CreateReadingListForUser(user, dto.Title); + await readingListService.CreateReadingListForUser(user, dto.Title); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } - return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(user.Id, dto.Title)); + return Ok(await unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(user.Id, dto.Title)); } /// @@ -231,25 +221,25 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateList(UpdateReadingListDto dto) { - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId); - if (readingList == null) return BadRequest(await _localizationService.Translate(UserId, "reading-list-doesnt-exist")); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId); + if (readingList == null) return BadRequest(await localizationService.Translate(UserId, "reading-list-doesnt-exist")); - var user = await _readingListService.UserHasReadingListAccess(readingList.Id, Username!); + var user = await readingListService.UserHasReadingListAccess(readingList.Id, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } try { - await _readingListService.UpdateReadingList(readingList, dto); + await readingListService.UpdateReadingList(readingList, dto); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } /// @@ -261,37 +251,37 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateListBySeries(UpdateReadingListBySeriesDto dto) { - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); - if (readingList == null) return BadRequest(await _localizationService.Translate(UserId, "reading-list-doesnt-exist")); + if (readingList == null) return BadRequest(await localizationService.Translate(UserId, "reading-list-doesnt-exist")); var chapterIdsForSeries = - await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync([dto.SeriesId]); + await unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync([dto.SeriesId]); // If there are adds, tell tracking this has been modified - if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList)) + if (await readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList)) { - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } try { - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + await unitOfWork.CommitAsync(); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } } catch { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + return Ok(await localizationService.Translate(UserId, "nothing-to-do")); } @@ -304,40 +294,40 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateListByMultiple(UpdateReadingListByMultipleDto dto) { - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); - if (readingList == null) return BadRequest(await _localizationService.Translate(UserId, "reading-list-doesnt-exist")); + if (readingList == null) return BadRequest(await localizationService.Translate(UserId, "reading-list-doesnt-exist")); - var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); + var chapterIds = await unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); foreach (var chapterId in dto.ChapterIds) { chapterIds.Add(chapterId); } // If there are adds, tell tracking this has been modified - if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIds, readingList)) + if (await readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIds, readingList)) { - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } try { - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + await unitOfWork.CommitAsync(); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } } catch { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + return Ok(await localizationService.Translate(UserId, "nothing-to-do")); } /// @@ -349,110 +339,110 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateListByMultipleSeries(UpdateReadingListByMultipleSeriesDto dto) { - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); - if (readingList == null) return BadRequest(await _localizationService.Translate(UserId, "reading-list-doesnt-exist")); + if (readingList == null) return BadRequest(await localizationService.Translate(UserId, "reading-list-doesnt-exist")); - var ids = await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray()); + var ids = await unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray()); foreach (var seriesId in ids.Keys) { // If there are adds, tell tracking this has been modified - if (await _readingListService.AddChaptersToReadingList(seriesId, ids[seriesId], readingList)) + if (await readingListService.AddChaptersToReadingList(seriesId, ids[seriesId], readingList)) { - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } } try { - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + await unitOfWork.CommitAsync(); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } } catch { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + return Ok(await localizationService.Translate(UserId, "nothing-to-do")); } [HttpPost("update-by-volume")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateListByVolume(UpdateReadingListByVolumeDto dto) { - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); - if (readingList == null) return BadRequest(await _localizationService.Translate(UserId, "reading-list-doesnt-exist")); + if (readingList == null) return BadRequest(await localizationService.Translate(UserId, "reading-list-doesnt-exist")); var chapterIdsForVolume = - (await _unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList(); + (await unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList(); // If there are adds, tell tracking this has been modified - if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForVolume, readingList)) + if (await readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForVolume, readingList)) { - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } try { - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + await unitOfWork.CommitAsync(); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } } catch { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + return Ok(await localizationService.Translate(UserId, "nothing-to-do")); } [HttpPost("update-by-chapter")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateListByChapter(UpdateReadingListByChapterDto dto) { - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); - if (readingList == null) return BadRequest(await _localizationService.Translate(UserId, "reading-list-doesnt-exist")); + if (readingList == null) return BadRequest(await localizationService.Translate(UserId, "reading-list-doesnt-exist")); // If there are adds, tell tracking this has been modified - if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, new List() { dto.ChapterId }, readingList)) + if (await readingListService.AddChaptersToReadingList(dto.SeriesId, new List() { dto.ChapterId }, readingList)) { - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } try { - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + await unitOfWork.CommitAsync(); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } } catch { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + return Ok(await localizationService.Translate(UserId, "nothing-to-do")); } @@ -462,11 +452,12 @@ public class ReadingListController : BaseApiController /// /// PersonRole /// + [ReadingListAccess] [HttpGet("people")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute, VaryByQueryKeys = ["readingListId", "role"])] public ActionResult> GetPeopleByRoleForList(int readingListId, PersonRole role) { - return Ok(_unitOfWork.ReadingListRepository.GetReadingListPeopleAsync(readingListId, role)); + return Ok(unitOfWork.ReadingListRepository.GetReadingListPeopleAsync(readingListId, role)); } /// @@ -474,11 +465,12 @@ public class ReadingListController : BaseApiController /// /// /// + [ReadingListAccess] [HttpGet("all-people")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute, VaryByQueryKeys = ["readingListId"])] public async Task>> GetAllPeopleForList(int readingListId) { - return Ok(await _unitOfWork.ReadingListRepository.GetReadingListAllPeopleAsync(readingListId)); + return Ok(await unitOfWork.ReadingListRepository.GetReadingListAllPeopleAsync(readingListId)); } /// @@ -487,12 +479,15 @@ public class ReadingListController : BaseApiController /// /// /// Chapter ID for next item, -1 if nothing exists + [ReadingListAccess] [HttpGet("next-chapter")] public async Task> GetNextChapter(int currentChapterId, int readingListId) { - var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList(); + var items = (await unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList(); + var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId); - if (readingListItem == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); + if (readingListItem == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); + var index = items.IndexOf(readingListItem) + 1; if (items.Count > index) { @@ -508,12 +503,15 @@ public class ReadingListController : BaseApiController /// /// /// ChapterId for next item, -1 if nothing exists + [ReadingListAccess] [HttpGet("prev-chapter")] public async Task> GetPrevChapter(int currentChapterId, int readingListId) { - var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList(); + var items = (await unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList(); + var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId); - if (readingListItem == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); + if (readingListItem == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); + var index = items.IndexOf(readingListItem) - 1; if (0 <= index) { @@ -528,12 +526,12 @@ public class ReadingListController : BaseApiController /// /// If empty or null, will return true as that is invalid /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("name-exists")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> DoesNameExists(string name) { if (string.IsNullOrEmpty(name)) return true; - return Ok(await _unitOfWork.ReadingListRepository.ReadingListExists(name)); + return Ok(await unitOfWork.ReadingListRepository.ReadingListExists(name)); } @@ -551,20 +549,20 @@ public class ReadingListController : BaseApiController var userId = UserId; if (!User.IsInRole(PolicyConstants.PromoteRole) && !User.IsInRole(PolicyConstants.AdminRole)) { - return BadRequest(await _localizationService.Translate(userId, "permission-denied")); + return BadRequest(await localizationService.Translate(userId, "permission-denied")); } - var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListsByIds(dto.ReadingListIds); + var readingLists = await unitOfWork.ReadingListRepository.GetReadingListsByIds(dto.ReadingListIds); foreach (var readingList in readingLists) { if (readingList.AppUserId != userId) continue; readingList.Promoted = dto.Promoted; - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return Ok(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -579,15 +577,15 @@ public class ReadingListController : BaseApiController public async Task DeleteMultipleReadingLists(DeleteReadingListsDto dto) { // This needs to take into account owner as I can select other users cards - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ReadingLists); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ReadingLists); if (user == null) return Unauthorized(); user.ReadingLists = user.ReadingLists.Where(uc => !dto.ReadingListIds.Contains(uc.Id)).ToList(); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return Ok(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -601,7 +599,7 @@ public class ReadingListController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["readingListId"])] public async Task> GetReadingListInfo(int readingListId) { - var result = await _unitOfWork.ReadingListRepository.GetReadingListInfoAsync(readingListId); + var result = await unitOfWork.ReadingListRepository.GetReadingListInfoAsync(readingListId); if (result == null) return Ok(null); diff --git a/API/Controllers/ReadingProfileController.cs b/Kavita.Server/Controllers/ReadingProfileController.cs similarity index 88% rename from API/Controllers/ReadingProfileController.cs rename to Kavita.Server/Controllers/ReadingProfileController.cs index f2eef16f9..e7a792312 100644 --- a/API/Controllers/ReadingProfileController.cs +++ b/Kavita.Server/Controllers/ReadingProfileController.cs @@ -1,16 +1,17 @@ -#nullable enable using System; using System.Collections.Generic; using System.Threading.Tasks; -using API.Data; -using API.DTOs; -using API.Services; -using API.Services.Reading; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Reading; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; +namespace Kavita.Server.Controllers; public record BulkSetSeriesProfiles(List ProfileIds, List SeriesIds); @@ -38,6 +39,7 @@ public class ReadingProfileController(ILogger logger, /// /// Defaults to currently active device /// + [SeriesAccess] [HttpGet("{libraryId:int}/{seriesId:int}")] public async Task> GetProfileForSeries(int libraryId, int seriesId, [FromQuery] bool skipImplicit, [FromQuery] int? deviceId = null) { @@ -51,6 +53,7 @@ public class ReadingProfileController(ILogger logger, /// /// /// + [SeriesAccess] [HttpGet("series")] public async Task>> GetProfilesForSeries(int seriesId) { @@ -62,6 +65,7 @@ public class ReadingProfileController(ILogger logger, /// /// /// + [LibraryAccess] [HttpGet("library")] public async Task>> GetProfilesForLibrary(int libraryId) { @@ -74,6 +78,7 @@ public class ReadingProfileController(ILogger logger, /// /// [HttpPost("create")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> CreateReadingProfile([FromBody] UserReadingProfileDto dto) { return Ok(await readingProfileService.CreateReadingProfile(UserId, dto)); @@ -83,9 +88,10 @@ public class ReadingProfileController(ILogger logger, /// Promotes the implicit profile to a user profile. Removes the series from other profiles /// /// - /// Defaults to currently active device + /// Defaults to the currently active device /// [HttpPost("promote")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> PromoteImplicitReadingProfile([FromQuery] int profileId, [FromQuery] int? deviceId = null) { deviceId ??= clientInfoAccessor.CurrentDeviceId; @@ -100,9 +106,11 @@ public class ReadingProfileController(ILogger logger, /// /// /// - /// Defaults to currently active device + /// Defaults to the currently active device /// + [SeriesAccess] [HttpPost("series")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateReadingProfileForSeries( [FromBody] UserReadingProfileDto dto, [FromQuery] int libraryId, [FromQuery] int seriesId, [FromQuery] int? deviceId = null) { @@ -121,6 +129,7 @@ public class ReadingProfileController(ILogger logger, /// Defaults to currently active device /// [HttpPost("update-parent")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateParentProfileForSeries( [FromBody] UserReadingProfileDto dto, [FromQuery] int libraryId, [FromQuery] int seriesId, [FromQuery] int? deviceId = null) { @@ -139,6 +148,7 @@ public class ReadingProfileController(ILogger logger, /// This does not update connected series and libraries. /// [HttpPost] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateReadingProfile(UserReadingProfileDto dto) { return Ok(await readingProfileService.UpdateReadingProfile(UserId, dto)); @@ -152,6 +162,7 @@ public class ReadingProfileController(ILogger logger, /// /// [HttpDelete] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteReadingProfile([FromQuery] int profileId) { await readingProfileService.DeleteReadingProfile(UserId, profileId); @@ -164,7 +175,9 @@ public class ReadingProfileController(ILogger logger, /// /// /// + [SeriesAccess] [HttpPost("series/{seriesId:int}")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task SetSeriesProfiles(int seriesId, List profileIds) { await readingProfileService.SetSeriesProfiles(UserId, profileIds, seriesId); @@ -176,7 +189,9 @@ public class ReadingProfileController(ILogger logger, /// /// /// + [SeriesAccess] [HttpDelete("series/{seriesId:int}")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task ClearSeriesProfile(int seriesId) { await readingProfileService.ClearSeriesProfile(UserId, seriesId); @@ -189,7 +204,9 @@ public class ReadingProfileController(ILogger logger, /// /// /// + [LibraryAccess] [HttpPost("library/{libraryId:int}")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task SetLibraryProfiles(int libraryId, List profileIds) { await readingProfileService.SetLibraryProfiles(UserId, profileIds, libraryId); @@ -201,7 +218,9 @@ public class ReadingProfileController(ILogger logger, /// /// /// + [LibraryAccess] [HttpDelete("library/{libraryId:int}")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task ClearLibraryProfile(int libraryId) { await readingProfileService.ClearLibraryProfile(UserId, libraryId); @@ -214,6 +233,7 @@ public class ReadingProfileController(ILogger logger, /// /// [HttpPost("bulk")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task BulkAddReadingProfile(BulkSetSeriesProfiles body) { await readingProfileService.BulkSetSeriesProfiles(UserId, body.ProfileIds, body.SeriesIds); @@ -227,6 +247,7 @@ public class ReadingProfileController(ILogger logger, /// /// [HttpPost("set-devices")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task SetProfileDevices([FromQuery] int profileId, [FromBody] List deviceIds) { await readingProfileService.SetProfileDevices(UserId, profileId, deviceIds); diff --git a/API/Controllers/ReviewController.cs b/Kavita.Server/Controllers/ReviewController.cs similarity index 56% rename from API/Controllers/ReviewController.cs rename to Kavita.Server/Controllers/ReviewController.cs index 276005486..3773423a4 100644 --- a/API/Controllers/ReviewController.cs +++ b/Kavita.Server/Controllers/ReviewController.cs @@ -1,46 +1,41 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.SeriesDetail; -using API.Helpers.Builders; -using API.Services.Plus; using AutoMapper; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services.Plus; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class ReviewController : BaseApiController +public class ReviewController( + IUnitOfWork unitOfWork, + IMapper mapper, + IScrobblingService scrobblingService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IMapper _mapper; - private readonly IScrobblingService _scrobblingService; - - public ReviewController(IUnitOfWork unitOfWork, - IMapper mapper, IScrobblingService scrobblingService) - { - _unitOfWork = unitOfWork; - _mapper = mapper; - _scrobblingService = scrobblingService; - } - - /// /// Updates the user's review for a given series /// /// /// [HttpPost("series")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateSeriesReview(UpdateUserReviewDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings); if (user == null) return Unauthorized(); - var ratingBuilder = new RatingBuilder(await _unitOfWork.UserRepository.GetUserRatingAsync(dto.SeriesId, user.Id)); + if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, dto.SeriesId)) + return NotFound(); + + var ratingBuilder = new RatingBuilder(await unitOfWork.UserRepository.GetUserRatingAsync(dto.SeriesId, user.Id)); var rating = ratingBuilder .WithBody(dto.Body) @@ -52,13 +47,13 @@ public class ReviewController : BaseApiController user.Ratings.Add(rating); } - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); BackgroundJob.Enqueue(() => - _scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body)); - return Ok(_mapper.Map(rating)); + scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body)); + return Ok(mapper.Map(rating)); } /// @@ -67,16 +62,20 @@ public class ReviewController : BaseApiController /// chapterId must be set /// [HttpPost("chapter")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateChapterReview(UpdateUserReviewDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ChapterRatings); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ChapterRatings); if (user == null) return Unauthorized(); if (dto.ChapterId == null) return BadRequest(); - int chapterId = dto.ChapterId.Value; + if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, dto.SeriesId)) + return NotFound(); - var ratingBuilder = new ChapterRatingBuilder(await _unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, chapterId)); + var chapterId = dto.ChapterId.Value; + + var ratingBuilder = new ChapterRatingBuilder(await unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, chapterId)); var rating = ratingBuilder .WithBody(dto.Body) @@ -89,11 +88,11 @@ public class ReviewController : BaseApiController user.ChapterRatings.Add(rating); } - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); - return Ok(_mapper.Map(rating)); + return Ok(mapper.Map(rating)); } @@ -102,16 +101,17 @@ public class ReviewController : BaseApiController /// /// [HttpDelete("series")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteSeriesReview([FromQuery] int seriesId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings); if (user == null) return Unauthorized(); user.Ratings = user.Ratings.Where(r => r.SeriesId != seriesId).ToList(); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -121,16 +121,17 @@ public class ReviewController : BaseApiController /// /// [HttpDelete("chapter")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteChapterReview([FromQuery] int chapterId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ChapterRatings); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ChapterRatings); if (user == null) return Unauthorized(); user.ChapterRatings = user.ChapterRatings.Where(r => r.ChapterId != chapterId).ToList(); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -145,6 +146,6 @@ public class ReviewController : BaseApiController [HttpGet("all")] public async Task>> GetAllReviewsForUser(int userId, float? rating = null, string? filterQuery = null) { - return Ok(await _unitOfWork.UserRepository.GetAllReviewsForUser(userId, UserId, filterQuery, rating)); + return Ok(await unitOfWork.UserRepository.GetAllReviewsForUser(userId, UserId, filterQuery, rating)); } } diff --git a/API/Controllers/ScrobblingController.cs b/Kavita.Server/Controllers/ScrobblingController.cs similarity index 67% rename from API/Controllers/ScrobblingController.cs rename to Kavita.Server/Controllers/ScrobblingController.cs index fc5512ed3..78a759816 100644 --- a/API/Controllers/ScrobblingController.cs +++ b/Kavita.Server/Controllers/ScrobblingController.cs @@ -2,44 +2,35 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.Account; -using API.DTOs.Scrobbling; -using API.Entities.Scrobble; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.KavitaPlus.Account; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Scrobble; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class ScrobblingController : BaseApiController +public class ScrobblingController( + IUnitOfWork unitOfWork, + IScrobblingService scrobblingService, + ILogger logger, + ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IScrobblingService _scrobblingService; - private readonly ILogger _logger; - private readonly ILocalizationService _localizationService; - - public ScrobblingController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, - ILogger logger, ILocalizationService localizationService) - { - _unitOfWork = unitOfWork; - _scrobblingService = scrobblingService; - _logger = logger; - _localizationService = localizationService; - } - /// /// Get the current user's AniList token /// @@ -47,7 +38,7 @@ public class ScrobblingController : BaseApiController [HttpGet("anilist-token")] public async Task> GetAniListToken() { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (user == null) return Unauthorized(); return Ok(user.AniListAccessToken); @@ -60,7 +51,7 @@ public class ScrobblingController : BaseApiController [HttpGet("mal-token")] public async Task> GetMalToken() { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (user == null) return Unauthorized(); return Ok(new MalUserInfoDto() @@ -76,15 +67,16 @@ public class ScrobblingController : BaseApiController /// /// True if the token was new or not [HttpPost("update-anilist-token")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateAniListToken(AniListUpdateDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (user == null) return Unauthorized(); var isNewToken = string.IsNullOrEmpty(user.AniListAccessToken); user.AniListAccessToken = dto.Token; - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); return Ok(isNewToken); } @@ -95,17 +87,18 @@ public class ScrobblingController : BaseApiController /// /// True if the token was new or not [HttpPost("update-mal-token")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateMalToken(MalUserInfoDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (user == null) return Unauthorized(); var isNewToken = string.IsNullOrEmpty(user.MalAccessToken); user.MalAccessToken = dto.AccessToken; user.MalUserName = dto.Username; - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); return Ok(isNewToken); } @@ -115,9 +108,10 @@ public class ScrobblingController : BaseApiController /// /// [HttpPost("generate-scrobble-events")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public ActionResult GenerateScrobbleEvents() { - BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistory(UserId)); + BackgroundJob.Enqueue(() => scrobblingService.CreateEventsFromExistingHistory(UserId)); return Ok(); } @@ -130,7 +124,7 @@ public class ScrobblingController : BaseApiController [HttpGet("token-expired")] public async Task> HasTokenExpired(ScrobbleProvider provider) { - return Ok(await _scrobblingService.HasTokenExpired(UserId, provider)); + return Ok(await scrobblingService.HasTokenExpired(UserId, provider)); } /// @@ -138,22 +132,22 @@ public class ScrobblingController : BaseApiController /// /// Requires admin /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("scrobble-errors")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task>> GetScrobbleErrors() { - return Ok(await _unitOfWork.ScrobbleRepository.GetScrobbleErrors()); + return Ok(await unitOfWork.ScrobbleRepository.GetScrobbleErrors()); } /// /// Clears the scrobbling errors table /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("clear-errors")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task ClearScrobbleErrors() { - await _unitOfWork.ScrobbleRepository.ClearScrobbleErrors(); + await unitOfWork.ScrobbleRepository.ClearScrobbleErrors(); return Ok(); } @@ -166,7 +160,7 @@ public class ScrobblingController : BaseApiController public async Task>> GetScrobblingEvents([FromQuery] UserParams pagination, [FromBody] ScrobbleEventFilter filter) { pagination ??= UserParams.Default; - var events = await _unitOfWork.ScrobbleRepository.GetUserEvents(UserId, filter, pagination); + var events = await unitOfWork.ScrobbleRepository.GetUserEvents(UserId, filter, pagination); Response.AddPaginationHeader(events.CurrentPage, events.PageSize, events.TotalCount, events.TotalPages); return Ok(events); @@ -179,7 +173,7 @@ public class ScrobblingController : BaseApiController [HttpGet("holds")] public async Task>> GetScrobbleHolds() { - return Ok(await _unitOfWork.UserRepository.GetHolds(UserId)); + return Ok(await unitOfWork.UserRepository.GetHolds(UserId)); } /// @@ -190,7 +184,7 @@ public class ScrobblingController : BaseApiController [HttpGet("has-hold")] public async Task> HasHold(int seriesId) { - return Ok(await _unitOfWork.UserRepository.HasHoldOnSeries(UserId, seriesId)); + return Ok(await unitOfWork.UserRepository.HasHoldOnSeries(UserId, seriesId)); } /// @@ -201,7 +195,7 @@ public class ScrobblingController : BaseApiController [HttpGet("library-allows-scrobbling")] public async Task> LibraryAllowsScrobbling(int seriesId) { - return Ok(await _unitOfWork.LibraryRepository.GetAllowsScrobblingBySeriesId(seriesId)); + return Ok(await unitOfWork.LibraryRepository.GetAllowsScrobblingBySeriesId(seriesId)); } /// @@ -210,25 +204,26 @@ public class ScrobblingController : BaseApiController /// /// [HttpPost("add-hold")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task AddHold(int seriesId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ScrobbleHolds); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ScrobbleHolds); if (user == null) return Unauthorized(); if (user.ScrobbleHolds.Any(s => s.SeriesId == seriesId)) - return Ok(await _localizationService.Translate(user.Id, "nothing-to-do")); + return Ok(await localizationService.Translate(user.Id, "nothing-to-do")); var seriesHold = new ScrobbleHoldBuilder() .WithSeriesId(seriesId) .Build(); user.ScrobbleHolds.Add(seriesHold); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); try { - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); // When a hold is placed on a series, clear any pre-existing Scrobble Events - await _scrobblingService.ClearEventsForSeries(user.Id, seriesId); + await scrobblingService.ClearEventsForSeries(user.Id, seriesId); return Ok(); } catch (DbUpdateConcurrencyException ex) @@ -240,16 +235,16 @@ public class ScrobblingController : BaseApiController } // Retry the update - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); return Ok(); } catch (Exception ex) { // Handle other exceptions or log the error - _logger.LogError(ex, "An error occurred while adding the hold"); + logger.LogError(ex, "An error occurred while adding the hold"); return StatusCode(StatusCodes.Status500InternalServerError, - await _localizationService.Translate(UserId, "nothing-to-do")); + await localizationService.Translate(UserId, "nothing-to-do")); } } @@ -259,15 +254,16 @@ public class ScrobblingController : BaseApiController /// /// [HttpDelete("remove-hold")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task RemoveHold(int seriesId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ScrobbleHolds); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ScrobbleHolds); if (user == null) return Unauthorized(); user.ScrobbleHolds = user.ScrobbleHolds.Where(h => h.SeriesId != seriesId).ToList(); - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); return Ok(); } @@ -278,7 +274,7 @@ public class ScrobblingController : BaseApiController [HttpGet("has-ran-scrobble-gen")] public async Task> HasRanScrobbleGen() { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId); return Ok(user is {HasRunScrobbleEventGeneration: true}); } @@ -288,11 +284,12 @@ public class ScrobblingController : BaseApiController /// /// [HttpPost("bulk-remove-events")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task BulkRemoveScrobbleEvents(IList eventIds) { - var events = await _unitOfWork.ScrobbleRepository.GetUserEvents(UserId, eventIds); - _unitOfWork.ScrobbleRepository.Remove(events); - await _unitOfWork.CommitAsync(); + var events = await unitOfWork.ScrobbleRepository.GetUserEvents(UserId, eventIds); + unitOfWork.ScrobbleRepository.Remove(events); + await unitOfWork.CommitAsync(); return Ok(); } } diff --git a/API/Controllers/SearchController.cs b/Kavita.Server/Controllers/SearchController.cs similarity index 56% rename from API/Controllers/SearchController.cs rename to Kavita.Server/Controllers/SearchController.cs index ef1555909..670f1a756 100644 --- a/API/Controllers/SearchController.cs +++ b/Kavita.Server/Controllers/SearchController.cs @@ -1,31 +1,22 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Search; -using API.Services; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Search; +using Kavita.Server.Attributes; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// Responsible for the Search interface from the UI /// -public class SearchController : BaseApiController +public class SearchController(IUnitOfWork unitOfWork, ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - - public SearchController(IUnitOfWork unitOfWork, ILocalizationService localizationService) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - } - /// /// Returns the series for the MangaFile id. If the user does not have access (shouldn't happen by the UI), /// then null is returned @@ -35,7 +26,13 @@ public class SearchController : BaseApiController [HttpGet("series-for-mangafile")] public async Task> GetSeriesForMangaFile(int mangaFileId) { - return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, UserId)); + var series = await unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, UserId); + if (series == null) return NotFound(); + + if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, series.Id)) + return NotFound(); + + return Ok(series); } /// @@ -44,10 +41,11 @@ public class SearchController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("series-for-chapter")] public async Task> GetSeriesForChapter(int chapterId) { - return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, UserId)); + return Ok(await unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, UserId)); } /// @@ -59,14 +57,14 @@ public class SearchController : BaseApiController [HttpGet("search")] public async Task> Search(string queryString, [FromQuery] bool includeChapterAndFiles = true) { - queryString = Services.Tasks.Scanner.Parser.Parser.CleanQuery(queryString); + queryString = Parser.CleanQuery(queryString); - var libraries = await _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(UserId, QueryContext.Search); - if (libraries.Count == 0) return BadRequest(await _localizationService.Translate(UserId, "libraries-restricted")); + var libraries = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(UserId, QueryContext.Search); + if (libraries.Count == 0) return BadRequest(await localizationService.Translate(UserId, "libraries-restricted")); var isAdmin = UserContext.HasRole(PolicyConstants.AdminRole); - var series = await _unitOfWork.SeriesRepository.SearchSeries(UserId, isAdmin, + var series = await unitOfWork.SeriesRepository.SearchSeries(UserId, isAdmin, libraries, queryString, includeChapterAndFiles); return Ok(series); diff --git a/API/Controllers/SeriesController.cs b/Kavita.Server/Controllers/SeriesController.cs similarity index 69% rename from API/Controllers/SeriesController.cs rename to Kavita.Server/Controllers/SeriesController.cs index e9e1e5fcd..69e2747e2 100644 --- a/API/Controllers/SeriesController.cs +++ b/Kavita.Server/Controllers/SeriesController.cs @@ -1,73 +1,52 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Dashboard; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; -using API.DTOs.Metadata; -using API.DTOs.Metadata.Matching; -using API.DTOs.Recommendation; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Entities.MetadataMatching; -using API.Extensions; -using API.Helpers; -using API.Middleware; -using API.Services; -using API.Services.Plus; using EasyCaching.Core; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; using Kavita.Common; using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Metadata.Matching; +using Kavita.Models.DTOs.Recommendation; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class SeriesController : BaseApiController +public class SeriesController( + ILogger logger, + ITaskScheduler taskScheduler, + IUnitOfWork unitOfWork, + ISeriesService seriesService, + ILicenseService licenseService, + IEasyCachingProviderFactory cachingProviderFactory, + ILocalizationService localizationService, + IExternalMetadataService externalMetadataService, + IHostEnvironment environment) + : BaseApiController { - private readonly ILogger _logger; - private readonly ITaskScheduler _taskScheduler; - private readonly IUnitOfWork _unitOfWork; - private readonly ISeriesService _seriesService; - private readonly ILicenseService _licenseService; - private readonly ILocalizationService _localizationService; - private readonly IExternalMetadataService _externalMetadataService; - private readonly IHostEnvironment _environment; - private readonly IEasyCachingProvider _externalSeriesCacheProvider; - private readonly IEasyCachingProvider _matchSeriesCacheProvider; + private readonly IEasyCachingProvider _externalSeriesCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries); + private readonly IEasyCachingProvider _matchSeriesCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusMatchSeries); private const string CacheKey = "externalSeriesData_"; private const string MatchSeriesCacheKey = "matchSeries_"; - public SeriesController(ILogger logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, - ISeriesService seriesService, ILicenseService licenseService, - IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService, - IExternalMetadataService externalMetadataService, IHostEnvironment environment) - { - _logger = logger; - _taskScheduler = taskScheduler; - _unitOfWork = unitOfWork; - _seriesService = seriesService; - _licenseService = licenseService; - _localizationService = localizationService; - _externalMetadataService = externalMetadataService; - _environment = environment; - - _externalSeriesCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries); - _matchSeriesCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusMatchSeries); - } - /// /// Gets series with the applied Filter /// @@ -79,7 +58,7 @@ public class SeriesController : BaseApiController { var userId = UserId; var series = - await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto); + await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -92,10 +71,11 @@ public class SeriesController : BaseApiController /// Series Id to fetch details for /// /// Throws an exception if the series Id does exist + [SeriesAccess] [HttpGet("{seriesId:int}")] public async Task> GetSeries(int seriesId) { - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, UserId); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, UserId); if (series == null) return NoContent(); return Ok(series); } @@ -105,26 +85,31 @@ public class SeriesController : BaseApiController /// /// /// If the series was deleted or not - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpDelete("{seriesId}")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> DeleteSeries(int seriesId) { var username = Username!; - _logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username); + logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username); - return Ok(await _seriesService.DeleteMultipleSeries([seriesId])); + return Ok(await seriesService.DeleteMultipleSeries([seriesId])); } - [Authorize(Policy = PolicyGroups.AdminPolicy)] + /// + /// Deletes multiple series from Kavita at once + /// + /// + /// [HttpPost("delete-multiple")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task DeleteMultipleSeries(DeleteSeriesDto dto) { var username = Username!; - _logger.LogInformation("Series {@SeriesId} is being deleted by {UserName}", dto.SeriesIds, username); + logger.LogInformation("Series {@SeriesId} is being deleted by {UserName}", dto.SeriesIds, username); - if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok(true); + if (await seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok(true); - return BadRequest(await _localizationService.Translate(UserId, "generic-series-delete")); + return BadRequest(await localizationService.Translate(UserId, "generic-series-delete")); } /// @@ -132,24 +117,37 @@ public class SeriesController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("volumes")] public async Task>> GetVolumes(int seriesId) { - return Ok(await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, UserId)); + return Ok(await unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, UserId)); } + /// + /// Returns a single Volume with progress information and Chapters + /// + /// + /// + [VolumeAccess] [HttpGet("volume")] public async Task> GetVolume(int volumeId) { - var vol = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, UserId); + var vol = await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, UserId); if (vol == null) return NoContent(); return Ok(vol); } + /// + /// Returns a single Chapter with progress information + /// + /// + /// + [ChapterAccess] [HttpGet("chapter")] public async Task> GetChapter(int chapterId) { - var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, UserId); + var chapter = await unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, UserId); if (chapter == null) return NoContent(); return Ok(chapter); @@ -161,11 +159,12 @@ public class SeriesController : BaseApiController /// /// Updated Series [HttpPost("update")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> UpdateSeries(UpdateSeriesDto updateSeries) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(updateSeries.Id); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(updateSeries.Id); if (series == null) - return BadRequest(await _localizationService.Translate(UserId, "series-doesnt-exist")); + return BadRequest(await localizationService.Translate(UserId, "series-doesnt-exist")); series.NormalizedName = series.Name.ToNormalized(); if (!string.IsNullOrEmpty(updateSeries.SortName?.Trim())) @@ -189,24 +188,24 @@ public class SeriesController : BaseApiController series.CoverImage = null; series.CoverImageLocked = false; series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Covers); - _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); + logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); series.ResetColorScape(); } - _unitOfWork.SeriesRepository.Update(series); + unitOfWork.SeriesRepository.Update(series); - if (!await _unitOfWork.CommitAsync()) + if (!await unitOfWork.CommitAsync()) { - return BadRequest(await _localizationService.Translate(UserId, "generic-series-update")); + return BadRequest(await localizationService.Translate(UserId, "generic-series-update")); } if (needsRefreshMetadata) { - await _taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id); + await taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id); } - return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(series.Id, UserId)); + return Ok(await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(series.Id, UserId)); } /// @@ -220,7 +219,7 @@ public class SeriesController : BaseApiController { var userId = UserId; var series = - await _unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, userParams, filterDto); + await unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, userParams, filterDto); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -236,7 +235,7 @@ public class SeriesController : BaseApiController public async Task>> GetRecentlyAddedChapters([FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; - return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(UserId, userParams)); + return Ok(await unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(UserId, userParams)); } /// @@ -255,10 +254,10 @@ public class SeriesController : BaseApiController { var seriesForUser = userId ?? UserId; - filterDto.Statements.AddRange(await _seriesService.GetProfilePrivacyStatements(seriesForUser, UserId)); + filterDto.Statements.AddRange(await seriesService.GetProfilePrivacyStatements(seriesForUser, UserId)); var series = - await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(seriesForUser, userParams, filterDto, context); + await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(seriesForUser, userParams, filterDto, context); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -275,7 +274,7 @@ public class SeriesController : BaseApiController [HttpPost("on-deck")] public async Task>> GetOnDeck([FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) { - var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(UserId, libraryId, userParams, null); + var pagedList = await unitOfWork.SeriesRepository.GetOnDeck(UserId, libraryId, userParams, null); Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages); @@ -291,7 +290,7 @@ public class SeriesController : BaseApiController [HttpPost("remove-from-on-deck")] public async Task RemoveFromOnDeck([FromQuery] int seriesId) { - await _unitOfWork.SeriesRepository.RemoveFromOnDeck(seriesId, UserId); + await unitOfWork.SeriesRepository.RemoveFromOnDeck(seriesId, UserId); return Ok(); } @@ -305,7 +304,7 @@ public class SeriesController : BaseApiController [HttpGet("currently-reading")] public async Task>> GetCurrentlyReadingForUser([FromQuery] UserParams userParams, [FromQuery] int userId) { - var pagedList = await _seriesService.GetCurrentlyReading(userId, UserId, userParams); + var pagedList = await seriesService.GetCurrentlyReading(userId, UserId, userParams); Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages); @@ -318,11 +317,11 @@ public class SeriesController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("refresh-metadata")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task RefreshSeriesMetadata(RefreshSeriesDto refreshSeriesDto) { - await _taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate, refreshSeriesDto.ForceColorscape); + await taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate, refreshSeriesDto.ForceColorscape); return Ok(); } @@ -331,11 +330,11 @@ public class SeriesController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("scan")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult ScanSeries(RefreshSeriesDto refreshSeriesDto) { - _taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, true); + taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, true); return Ok(); } @@ -344,11 +343,11 @@ public class SeriesController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("analyze")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult AnalyzeSeries(RefreshSeriesDto refreshSeriesDto) { - _taskScheduler.AnalyzeFilesForSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate); + taskScheduler.AnalyzeFilesForSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate); return Ok(); } @@ -357,10 +356,11 @@ public class SeriesController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("metadata")] public async Task> GetSeriesMetadata(int seriesId) { - return Ok(await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId)); + return Ok(await unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId)); } /// @@ -369,12 +369,13 @@ public class SeriesController : BaseApiController /// /// [HttpPost("metadata")] + [Authorize(PolicyGroups.AdminPolicy)] public async Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto) { - if (!await _seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto)) - return BadRequest(await _localizationService.Translate(UserId, "update-metadata-fail")); + if (!await seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto)) + return BadRequest(await localizationService.Translate(UserId, "update-metadata-fail")); - return Ok(await _localizationService.Translate(UserId, "series-updated")); + return Ok(await localizationService.Translate(UserId, "series-updated")); } @@ -389,7 +390,7 @@ public class SeriesController : BaseApiController { var userId = UserId; var series = - await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams); + await unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -404,8 +405,8 @@ public class SeriesController : BaseApiController [HttpPost("series-by-ids")] public async Task>> GetAllSeriesById(SeriesByIdsDto dto) { - if (dto.SeriesIds == null) return BadRequest(await _localizationService.Translate(UserId, "invalid-payload")); - return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, UserId)); + if (dto.SeriesIds == null) return BadRequest(await localizationService.Translate(UserId, "invalid-payload")); + return Ok(await unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, UserId)); } /// @@ -413,13 +414,13 @@ public class SeriesController : BaseApiController /// /// /// - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = ["ageRating"])] [HttpGet("age-rating")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = ["ageRating"])] public async Task> GetAgeRating(int ageRating) { var val = (AgeRating) ageRating; if (val == AgeRating.NotApplicable) - return await _localizationService.Translate(UserId, "age-restriction-not-applicable"); + return await localizationService.Translate(UserId, "age-restriction-not-applicable"); return Ok(val.ToDescription()); } @@ -430,16 +431,17 @@ public class SeriesController : BaseApiController /// /// /// Do not rely on this API externally. May change without hesitation. + [SeriesAccess] [HttpGet("series-detail")] public async Task> GetSeriesDetailBreakdown(int seriesId) { try { - return await _seriesService.GetSeriesDetail(seriesId, UserId); + return await seriesService.GetSeriesDetail(seriesId, UserId); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } } @@ -451,10 +453,11 @@ public class SeriesController : BaseApiController /// /// Type of Relationship to pull back /// + [SeriesAccess] [HttpGet("related")] public async Task>> GetRelatedSeries(int seriesId, RelationKind relation) { - return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(UserId, seriesId, relation)); + return Ok(await unitOfWork.SeriesRepository.GetSeriesForRelationKind(UserId, seriesId, relation)); } /// @@ -462,10 +465,11 @@ public class SeriesController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("all-related")] public async Task> GetAllRelatedSeries(int seriesId) { - return Ok(await _seriesService.GetRelatedSeries(UserId, seriesId)); + return Ok(await seriesService.GetRelatedSeries(UserId, seriesId)); } @@ -474,27 +478,23 @@ public class SeriesController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("update-related")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto) { - if (await _seriesService.UpdateRelatedSeries(dto)) + if (await seriesService.UpdateRelatedSeries(dto)) { return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-relationship")); + return BadRequest(await localizationService.Translate(UserId, "generic-relationship")); } - [Authorize(Policy = PolicyGroups.AdminPolicy)] + [KPlus] [HttpGet("external-series-detail")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> GetExternalSeriesInfo(int? aniListId, long? malId, int? seriesId) { - if (!await _licenseService.HasActiveLicense()) - { - return BadRequest(); - } - var cacheKey = $"{CacheKey}-{aniListId ?? 0}-{malId ?? 0}-{seriesId ?? 0}"; var results = await _externalSeriesCacheProvider.GetAsync(cacheKey); if (results.HasValue) @@ -504,7 +504,7 @@ public class SeriesController : BaseApiController try { - var ret = await _externalMetadataService.GetExternalSeriesDetail(aniListId, malId, seriesId); + var ret = await externalMetadataService.GetExternalSeriesDetail(aniListId, malId, seriesId); await _externalSeriesCacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromMinutes(15)); return Ok(ret); } @@ -520,12 +520,13 @@ public class SeriesController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("next-expected")] public async Task> GetNextExpectedChapter(int seriesId) { var userId = UserId; - return Ok(await _seriesService.GetEstimatedChapterCreationDate(seriesId, userId)); + return Ok(await seriesService.GetEstimatedChapterCreationDate(seriesId, userId)); } /// @@ -534,16 +535,17 @@ public class SeriesController : BaseApiController /// /// [HttpPost("match")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task>> MatchSeries(MatchSeriesDto dto) { var cacheKey = $"{MatchSeriesCacheKey}-{dto.SeriesId}-{dto.Query}"; var results = await _matchSeriesCacheProvider.GetAsync>(cacheKey); - if (results.HasValue && !_environment.IsDevelopment()) + if (results.HasValue && !environment.IsDevelopment()) { return Ok(results.Value); } - var ret = await _externalMetadataService.MatchSeries(dto); + var ret = await externalMetadataService.MatchSeries(dto); await _matchSeriesCacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromMinutes(1)); return Ok(ret); @@ -556,9 +558,10 @@ public class SeriesController : BaseApiController /// /// [HttpPost("update-match")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult UpdateSeriesMatch([FromQuery] int seriesId, [FromQuery] int? aniListId, [FromQuery] long? malId, [FromQuery] int? cbrId) { - BackgroundJob.Enqueue(() => _externalMetadataService.FixSeriesMatch(seriesId, aniListId, malId, cbrId)); + BackgroundJob.Enqueue(() => externalMetadataService.FixSeriesMatch(seriesId, aniListId, malId, cbrId)); return Ok(); } @@ -570,9 +573,10 @@ public class SeriesController : BaseApiController /// /// [HttpPost("dont-match")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task UpdateDontMatch([FromQuery] int seriesId, [FromQuery] bool dontMatch) { - await _externalMetadataService.UpdateSeriesDontMatch(seriesId, dontMatch); + await externalMetadataService.UpdateSeriesDontMatch(seriesId, dontMatch); return Ok(); } @@ -583,7 +587,7 @@ public class SeriesController : BaseApiController [HttpGet("series-with-annotations")] public async Task>> GetSeriesWithAnnotations() { - var data = await _unitOfWork.AnnotationRepository.GetSeriesWithAnnotations(UserId); + var data = await unitOfWork.AnnotationRepository.GetSeriesWithAnnotations(UserId); return Ok(data); } diff --git a/API/Controllers/ServerController.cs b/Kavita.Server/Controllers/ServerController.cs similarity index 61% rename from API/Controllers/ServerController.cs rename to Kavita.Server/Controllers/ServerController.cs index 9bd3bbba9..5dc8e033f 100644 --- a/API/Controllers/ServerController.cs +++ b/Kavita.Server/Controllers/ServerController.cs @@ -3,64 +3,45 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Jobs; -using API.DTOs.MediaErrors; -using API.DTOs.Stats; -using API.DTOs.Update; -using API.Entities.Enums; -using API.Helpers; -using API.Services; -using API.Services.Tasks; using EasyCaching.Core; using Hangfire; using Hangfire.Storage; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; using Kavita.Common; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Jobs; +using Kavita.Models.DTOs.MediaErrors; +using Kavita.Models.DTOs.Stats; +using Kavita.Models.DTOs.Update; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using MimeTypes; -using TaskScheduler = API.Services.TaskScheduler; +using TaskScheduler = Kavita.Services.TaskScheduler; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; [Authorize(PolicyGroups.AdminPolicy)] -public class ServerController : BaseApiController +public class ServerController( + ILogger logger, + IBackupService backupService, + IArchiveService archiveService, + IVersionUpdaterService versionUpdaterService, + IStatsService statsService, + ICleanupService cleanupService, + IScannerService scannerService, + ITaskScheduler taskScheduler, + IUnitOfWork unitOfWork, + IEasyCachingProviderFactory cachingProviderFactory, + IThemeService themeService, + ILocalizationService localizationService) + : BaseApiController { - private readonly ILogger _logger; - private readonly IBackupService _backupService; - private readonly IArchiveService _archiveService; - private readonly IVersionUpdaterService _versionUpdaterService; - private readonly IStatsService _statsService; - private readonly ICleanupService _cleanupService; - private readonly IScannerService _scannerService; - private readonly ITaskScheduler _taskScheduler; - private readonly IUnitOfWork _unitOfWork; - private readonly IEasyCachingProviderFactory _cachingProviderFactory; - private readonly ILocalizationService _localizationService; - - public ServerController(ILogger logger, - IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, - IStatsService statsService, ICleanupService cleanupService, IScannerService scannerService, - ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory, - ILocalizationService localizationService) - { - _logger = logger; - _backupService = backupService; - _archiveService = archiveService; - _versionUpdaterService = versionUpdaterService; - _statsService = statsService; - _cleanupService = cleanupService; - _scannerService = scannerService; - _taskScheduler = taskScheduler; - _unitOfWork = unitOfWork; - _cachingProviderFactory = cachingProviderFactory; - _localizationService = localizationService; - } - /// /// Performs an ad-hoc cleanup of Cache /// @@ -68,8 +49,8 @@ public class ServerController : BaseApiController [HttpPost("clear-cache")] public ActionResult ClearCache() { - _logger.LogInformation("{UserName} is clearing cache of server from admin dashboard", Username!); - _cleanupService.CleanupCacheAndTempDirectories(); + logger.LogInformation("{UserName} is clearing cache of server from admin dashboard", Username!); + cleanupService.CleanupCacheAndTempDirectories(); return Ok(); } @@ -81,7 +62,7 @@ public class ServerController : BaseApiController [HttpPost("cleanup-want-to-read")] public ActionResult CleanupWantToRead() { - _logger.LogInformation("{UserName} is clearing running want to read cleanup from admin dashboard", Username!); + logger.LogInformation("{UserName} is clearing running want to read cleanup from admin dashboard", Username!); RecurringJob.TriggerJob(TaskScheduler.RemoveFromWantToReadTaskId); return Ok(); @@ -94,7 +75,7 @@ public class ServerController : BaseApiController [HttpPost("cleanup")] public ActionResult Cleanup() { - _logger.LogInformation("{UserName} is clearing running general cleanup from admin dashboard", Username!); + logger.LogInformation("{UserName} is clearing running general cleanup from admin dashboard", Username!); RecurringJob.TriggerJob(TaskScheduler.CleanupTaskId); return Ok(); @@ -107,7 +88,7 @@ public class ServerController : BaseApiController [HttpPost("backup-db")] public ActionResult BackupDatabase() { - _logger.LogInformation("{UserName} is backing up database of server from admin dashboard", Username!); + logger.LogInformation("{UserName} is backing up database of server from admin dashboard", Username!); RecurringJob.TriggerJob(TaskScheduler.BackupTaskId); return Ok(); } @@ -119,12 +100,12 @@ public class ServerController : BaseApiController [HttpPost("analyze-files")] public async Task AnalyzeFiles() { - _logger.LogInformation("{UserName} is performing file analysis from admin dashboard", Username!); + logger.LogInformation("{UserName} is performing file analysis from admin dashboard", Username!); if (TaskScheduler.HasAlreadyEnqueuedTask(ScannerService.Name, "AnalyzeFiles", [], TaskScheduler.DefaultQueue, true)) - return Ok(await _localizationService.Translate(UserId, "job-already-running")); + return Ok(await localizationService.Translate(UserId, "job-already-running")); - BackgroundJob.Enqueue(() => _scannerService.AnalyzeFiles()); + BackgroundJob.Enqueue(() => scannerService.AnalyzeFiles()); return Ok(); } @@ -137,7 +118,7 @@ public class ServerController : BaseApiController [HttpGet("server-info-slim")] public async Task> GetSlimVersion() { - return Ok(await _statsService.GetServerInfoSlim()); + return Ok(await statsService.GetServerInfoSlim()); } @@ -148,13 +129,13 @@ public class ServerController : BaseApiController [HttpPost("convert-media")] public async Task ScheduleConvertCovers() { - var encoding = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; + var encoding = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; if (encoding == EncodeFormat.PNG) { - return BadRequest(await _localizationService.Translate(UserId, "encode-as-warning")); + return BadRequest(await localizationService.Translate(UserId, "encode-as-warning")); } - _taskScheduler.CovertAllCoversToEncoding(); + taskScheduler.ConvertAllCoversToEncoding(); return Ok(); } @@ -166,16 +147,16 @@ public class ServerController : BaseApiController [HttpGet("logs")] public async Task GetLogs() { - var files = _backupService.GetLogFiles(); + var files = backupService.GetLogFiles(); try { - var zipPath = _archiveService.CreateZipForDownload(files, "logs"); + var zipPath = archiveService.CreateZipForDownload(files, "logs"); return PhysicalFile(zipPath, MimeTypeMap.GetMimeType(Path.GetExtension(zipPath)), System.Web.HttpUtility.UrlEncode(Path.GetFileName(zipPath)), true); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } } @@ -186,7 +167,7 @@ public class ServerController : BaseApiController [HttpGet("check-for-updates")] public async Task CheckForAnnouncements() { - await _taskScheduler.CheckForUpdate(); + await taskScheduler.CheckForUpdate(); return Ok(); } @@ -196,7 +177,7 @@ public class ServerController : BaseApiController [HttpGet("check-update")] public async Task> CheckForUpdates() { - return Ok(await _versionUpdaterService.CheckForUpdate()); + return Ok(await versionUpdaterService.CheckForUpdate()); } /// @@ -206,7 +187,7 @@ public class ServerController : BaseApiController [HttpGet("check-out-of-date")] public async Task> CheckHowOutOfDate(bool stableOnly = true) { - return Ok(await _versionUpdaterService.GetNumberOfReleasesBehind(stableOnly)); + return Ok(await versionUpdaterService.GetNumberOfReleasesBehind(stableOnly)); } @@ -215,14 +196,10 @@ public class ServerController : BaseApiController /// /// How many releases from the latest to return /// - [AllowAnonymous] [HttpGet("changelog")] public async Task>> GetChangelog(int count = 0) { - // Strange bug where [Authorize] doesn't work - if (UserId == 0) return Unauthorized(); - - return Ok(await _versionUpdaterService.GetAllReleases(count)); + return Ok(await versionUpdaterService.GetAllReleases(count)); } /// @@ -236,7 +213,7 @@ public class ServerController : BaseApiController new JobDto() { Id = dto.Id, - Title = await _localizationService.Translate(UserId, dto.Id), + Title = await localizationService.Translate(UserId, dto.Id), Cron = dto.Cron, LastExecutionUtc = dto.LastExecution.HasValue ? new DateTime(dto.LastExecution.Value.Ticks, DateTimeKind.Utc) : null }); @@ -252,7 +229,7 @@ public class ServerController : BaseApiController [HttpGet("media-errors")] public ActionResult> GetMediaErrors() { - return Ok(_unitOfWork.MediaErrorRepository.GetAllErrorDtosAsync()); + return Ok(unitOfWork.MediaErrorRepository.GetAllErrorDtosAsync()); } /// @@ -263,7 +240,7 @@ public class ServerController : BaseApiController [HttpPost("clear-media-alerts")] public async Task ClearMediaErrors() { - await _unitOfWork.MediaErrorRepository.DeleteAll(); + await unitOfWork.MediaErrorRepository.DeleteAll(); return Ok(); } @@ -276,8 +253,8 @@ public class ServerController : BaseApiController [HttpPost("bust-kavitaplus-cache")] public async Task BustReviewAndRecCache() { - _logger.LogInformation("Busting Kavita+ Cache"); - var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries); + logger.LogInformation("Busting Kavita+ Cache"); + var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries); await provider.FlushAsync(); return Ok(); } @@ -290,7 +267,7 @@ public class ServerController : BaseApiController [HttpPost("sync-themes")] public async Task SyncThemes() { - await _taskScheduler.SyncThemes(); + await themeService.SyncThemes(); return Ok(); } diff --git a/API/Controllers/SettingsController.cs b/Kavita.Server/Controllers/SettingsController.cs similarity index 62% rename from API/Controllers/SettingsController.cs rename to Kavita.Server/Controllers/SettingsController.cs index 50f718d55..8f88345c5 100644 --- a/API/Controllers/SettingsController.cs +++ b/Kavita.Server/Controllers/SettingsController.cs @@ -2,54 +2,39 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs; -using API.DTOs.Email; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Settings; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Converters; -using API.Services; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Models; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Email; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Extensions; +using Kavita.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class SettingsController : BaseApiController +public class SettingsController( + ILogger logger, + IUnitOfWork unitOfWork, + IMapper mapper, + IEmailService emailService, + ILocalizationService localizationService, + ISettingsService settingsService, + IAuthenticationSchemeProvider authenticationSchemeProvider, + IOidcService oidcService) + : BaseApiController { - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IMapper _mapper; - private readonly IEmailService _emailService; - private readonly ILocalizationService _localizationService; - private readonly ISettingsService _settingsService; - private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; - private readonly IOidcService _oidcService; - - public SettingsController(ILogger logger, IUnitOfWork unitOfWork, IMapper mapper, - IEmailService emailService, ILocalizationService localizationService, ISettingsService settingsService, - IAuthenticationSchemeProvider authenticationSchemeProvider, IOidcService oidcService) - { - _logger = logger; - _unitOfWork = unitOfWork; - _mapper = mapper; - _emailService = emailService; - _localizationService = localizationService; - _settingsService = settingsService; - _authenticationSchemeProvider = authenticationSchemeProvider; - _oidcService = oidcService; - } - /// /// Returns the base url for this instance (if set) /// @@ -57,7 +42,7 @@ public class SettingsController : BaseApiController [HttpGet("base-url")] public async Task> GetBaseUrl() { - var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settingsDto = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); return Ok(settingsDto.BaseUrl); } @@ -65,67 +50,67 @@ public class SettingsController : BaseApiController /// Returns the server settings /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> GetSettings() { - var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settingsDto = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); // Do not send OIDC secret to user settingsDto.OidcConfig.Secret = "*".Repeat(settingsDto.OidcConfig.Secret.Length); return Ok(settingsDto); } - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("reset")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> ResetSettings() { - _logger.LogInformation("{UserName} is resetting Server Settings", Username!); + logger.LogInformation("{UserName} is resetting Server Settings", Username!); - return await UpdateSettings(_mapper.Map(Seed.DefaultSettings)); + return await UpdateSettings(mapper.Map(Defaults.DefaultSettings)); } /// /// Resets the IP Addresses /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("reset-ip-addresses")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> ResetIpAddressesSettings() { - _logger.LogInformation("{UserName} is resetting IP Addresses Setting", Username!); - var ipAddresses = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.IpAddresses); + logger.LogInformation("{UserName} is resetting IP Addresses Setting", Username!); + var ipAddresses = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.IpAddresses); ipAddresses.Value = Configuration.DefaultIpAddresses; - _unitOfWork.SettingsRepository.Update(ipAddresses); + unitOfWork.SettingsRepository.Update(ipAddresses); - if (!await _unitOfWork.CommitAsync()) + if (!await unitOfWork.CommitAsync()) { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()); + return Ok(await unitOfWork.SettingsRepository.GetSettingsDtoAsync()); } /// /// Resets the Base url /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("reset-base-url")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> ResetBaseUrlSettings() { - _logger.LogInformation("{UserName} is resetting Base Url Setting", Username!); - var baseUrl = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl); + logger.LogInformation("{UserName} is resetting Base Url Setting", Username!); + var baseUrl = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl); baseUrl.Value = Configuration.DefaultBaseUrl; - _unitOfWork.SettingsRepository.Update(baseUrl); + unitOfWork.SettingsRepository.Update(baseUrl); - if (!await _unitOfWork.CommitAsync()) + if (!await unitOfWork.CommitAsync()) { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } Configuration.BaseUrl = baseUrl.Value; - return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()); + return Ok(await unitOfWork.SettingsRepository.GetSettingsDtoAsync()); } /// @@ -135,7 +120,7 @@ public class SettingsController : BaseApiController [HttpGet("is-email-setup")] public async Task> IsEmailSetup() { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); return Ok(settings.IsEmailSetup()); } @@ -145,25 +130,25 @@ public class SettingsController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> UpdateSettings(ServerSettingDto updateSettingsDto) { - _logger.LogInformation("{UserName} is updating Server Settings", Username!); + logger.LogInformation("{UserName} is updating Server Settings", Username!); try { - var d = await _settingsService.UpdateSettings(updateSettingsDto); + var d = await settingsService.UpdateSettings(updateSettingsDto); return Ok(d); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when updating server settings"); - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + logger.LogError(ex, "There was an exception when updating server settings"); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } } @@ -171,22 +156,22 @@ public class SettingsController : BaseApiController /// All values allowed for Task Scheduling APIs. A custom cron job is not included. Disabled is not applicable for Cleanup. /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("task-frequencies")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult> GetTaskFrequencies() { return Ok(CronConverter.Options); } - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("library-types")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult> GetLibraryTypes() { return Ok(Enum.GetValues().Select(t => t.ToDescription())); } - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("log-levels")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult> GetLogLevels() { return Ok(new[] {"Trace", "Debug", "Information", "Warning", "Critical"}); @@ -195,7 +180,7 @@ public class SettingsController : BaseApiController [HttpGet("opds-enabled")] public async Task> GetOpdsEnabled() { - var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settingsDto = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); return Ok(settingsDto.EnableOpds); } @@ -215,24 +200,24 @@ public class SettingsController : BaseApiController /// Sends a test email to see if email settings are hooked up correctly /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("test-email-url")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> TestEmailServiceUrl() { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId); if (string.IsNullOrEmpty(user?.Email)) return BadRequest("Your account has no email on record. Cannot email."); - return Ok(await _emailService.SendTestEmail(user!.Email)); + return Ok(await emailService.SendTestEmail(user!.Email)); } /// /// Get the metadata settings for Kavita+ users. /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("metadata-settings")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> GetMetadataSettings() { - return Ok(await _unitOfWork.SettingsRepository.GetMetadataSettingDto()); + return Ok(await unitOfWork.SettingsRepository.GetMetadataSettingDto()); } @@ -241,17 +226,17 @@ public class SettingsController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("metadata-settings")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> UpdateMetadataSettings(MetadataSettingsDto dto) { try { - return Ok(await _settingsService.UpdateMetadataSettings(dto)); + return Ok(await settingsService.UpdateMetadataSettings(dto)); } catch (Exception ex) { - _logger.LogError(ex, "There was an issue when updating metadata settings"); + logger.LogError(ex, "There was an issue when updating metadata settings"); return BadRequest(ex.Message); } } @@ -260,17 +245,17 @@ public class SettingsController : BaseApiController /// Import field mappings /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("import-field-mappings")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> ImportFieldMappings([FromBody] ImportFieldMappingsDto dto) { try { - return Ok(await _settingsService.ImportFieldMappings(dto.Data, dto.Settings)); + return Ok(await settingsService.ImportFieldMappings(dto.Data, dto.Settings)); } catch (Exception ex) { - _logger.LogError(ex, "There was an issue importing field mappings"); + logger.LogError(ex, "There was an issue importing field mappings"); return BadRequest(ex.Message); } } @@ -284,10 +269,10 @@ public class SettingsController : BaseApiController [HttpGet("oidc")] public async Task> GetOidcConfig() { - var oidcScheme = await _authenticationSchemeProvider.GetSchemeAsync(IdentityServiceExtensions.OpenIdConnect); + var oidcScheme = await authenticationSchemeProvider.GetSchemeAsync(IdentityServiceExtensions.OpenIdConnect); - var settings = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; - var publicConfig = _mapper.Map(settings); + var settings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var publicConfig = mapper.Map(settings); publicConfig.Enabled = oidcScheme != null && !string.IsNullOrEmpty(settings.Authority) && !string.IsNullOrEmpty(settings.ClientId) && @@ -300,7 +285,7 @@ public class SettingsController : BaseApiController [HttpPost("reset-external-ids")] public async Task ResetExternalIds() { - await _oidcService.ClearOidcIds(); + await oidcService.ClearOidcIds(); return Ok(); } @@ -314,7 +299,7 @@ public class SettingsController : BaseApiController [HttpPost("is-valid-authority")] public async Task> IsValidAuthority([FromBody] AuthorityValidationDto authority) { - return Ok(await _settingsService.IsValidAuthority(authority.Authority)); + return Ok(await settingsService.IsValidAuthority(authority.Authority)); } diff --git a/API/Controllers/StatsController.cs b/Kavita.Server/Controllers/StatsController.cs similarity index 93% rename from API/Controllers/StatsController.cs rename to Kavita.Server/Controllers/StatsController.cs index c996e741c..69f4e79fe 100644 --- a/API/Controllers/StatsController.cs +++ b/Kavita.Server/Controllers/StatsController.cs @@ -5,31 +5,27 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.Statistics; -using API.DTOs.Stats.V3; -using API.DTOs.Stats.V3.ClientDevice; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Middleware; -using API.Services; -using API.Services.Tasks.Scanner.Parser; using CsvHelper; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.Statistics; +using Kavita.Models.DTOs.Stats.V3.ClientDevice; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using MimeTypes; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; public class StatsController( IStatisticService statService, @@ -308,13 +304,13 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetReadingPaceForUser(filter, userId, year, booksOnly, UserId)); + return Ok(await statService.GetReadingPaceForUser(filter, userId, year, booksOnly, UserId, HttpContext.RequestAborted)); } /// - /// Returns top 10 genres that user likes reading + /// Returns the top 10 genres that the user likes reading /// /// /// @@ -326,7 +322,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetGenreBreakdownForUser(filter, userId, UserId)); + return Ok(await statService.GetGenreBreakdownForUser(filter, userId, UserId, HttpContext.RequestAborted)); } /// @@ -342,7 +338,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetTagBreakdownForUser(filter, userId, UserId)); + return Ok(await statService.GetTagBreakdownForUser(filter, userId, UserId, HttpContext.RequestAborted)); } @@ -353,7 +349,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetPageSpreadForUser(filter, userId, UserId)); + return Ok(await statService.GetPageSpreadForUser(filter, userId, UserId, HttpContext.RequestAborted)); } [ProfilePrivacy] @@ -363,7 +359,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetWordSpreadForUser(filter, userId, UserId)); + return Ok(await statService.GetWordSpreadForUser(filter, userId, UserId, HttpContext.RequestAborted)); } [ProfilePrivacy] @@ -373,7 +369,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetMostReadAuthors(filter, userId, UserId)); + return Ok(await statService.GetMostReadAuthors(filter, userId, UserId, HttpContext.RequestAborted)); } /// @@ -389,7 +385,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - var dto = await statService.GetTimeReadingByHour(filter, userId, UserId); + var dto = await statService.GetTimeReadingByHour(filter, userId, UserId, HttpContext.RequestAborted); if (dto == null) return BadRequest(); return Ok(dto); @@ -408,7 +404,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetReadsPerMonth(filter, userId, UserId)); + return Ok(await statService.GetReadsPerMonth(filter, userId, UserId, HttpContext.RequestAborted)); } /// @@ -421,7 +417,7 @@ public class StatsController( [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)] public async Task> GetTotalReads(int userId) { - return Ok(await statService.GetTotalReads(userId, UserId)); + return Ok(await statService.GetTotalReads(userId, UserId, HttpContext.RequestAborted)); } [ProfilePrivacy] @@ -429,7 +425,7 @@ public class StatsController( public async Task> GetStatsForUserBar([FromQuery] StatsFilterDto filter, int userId) { await CleanStatsFilter(filter, userId); - return Ok(await statService.GetUserStatBar(filter, userId, UserId)); + return Ok(await statService.GetUserStatBar(filter, userId, UserId, HttpContext.RequestAborted)); } [ProfilePrivacy] @@ -437,7 +433,7 @@ public class StatsController( [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)] public async Task> GetUserReadStatistics(int userId) { - return Ok(await statService.GetUserReadStatistics(userId, [])); + return Ok(await statService.GetUserReadStatistics(userId, [], HttpContext.RequestAborted)); } @@ -451,7 +447,7 @@ public class StatsController( [HttpGet("reading-history")] public async Task>> GetReadingHistoryItems([FromQuery] StatsFilterDto filter, [FromQuery] UserParams userParams) { - var result = await statService.GetReadingHistoryItems(filter, userParams, UserId, UserId); + var result = await statService.GetReadingHistoryItems(filter, userParams, UserId, UserId, HttpContext.RequestAborted); Response.AddPaginationHeader(result.CurrentPage, result.PageSize, result.TotalCount, result.TotalPages); diff --git a/API/Controllers/StreamController.cs b/Kavita.Server/Controllers/StreamController.cs similarity index 75% rename from API/Controllers/StreamController.cs rename to Kavita.Server/Controllers/StreamController.cs index 35dab90c8..fdb6834d9 100644 --- a/API/Controllers/StreamController.cs +++ b/Kavita.Server/Controllers/StreamController.cs @@ -1,33 +1,23 @@ using System.Collections.Generic; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Dashboard; -using API.DTOs.SideNav; -using API.Middleware; -using API.Services; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.SideNav; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable - +namespace Kavita.Server.Controllers; /// /// Responsible for anything that deals with Streams (SmartFilters, ExternalSource, DashboardStream, SideNavStream) /// -public class StreamController : BaseApiController +public class StreamController( + IStreamService streamService, + IUnitOfWork unitOfWork) + : BaseApiController { - private readonly IStreamService _streamService; - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - - public StreamController(IStreamService streamService, IUnitOfWork unitOfWork, ILocalizationService localizationService) - { - _streamService = streamService; - _unitOfWork = unitOfWork; - _localizationService = localizationService; - } - /// /// Returns the layout of the user's dashboard /// @@ -35,7 +25,7 @@ public class StreamController : BaseApiController [HttpGet("dashboard")] public async Task>> GetDashboardLayout(bool visibleOnly = true) { - return Ok(await _streamService.GetDashboardStreams(UserId, visibleOnly)); + return Ok(await streamService.GetDashboardStreams(UserId, visibleOnly)); } /// @@ -44,7 +34,7 @@ public class StreamController : BaseApiController [HttpGet("sidenav")] public async Task>> GetSideNav(bool visibleOnly = true) { - return Ok(await _streamService.GetSidenavStreams(UserId, visibleOnly)); + return Ok(await streamService.GetSidenavStreams(UserId, visibleOnly)); } /// @@ -53,7 +43,7 @@ public class StreamController : BaseApiController [HttpGet("external-sources")] public async Task>> GetExternalSources() { - return Ok(await _streamService.GetExternalSources(UserId)); + return Ok(await streamService.GetExternalSources(UserId)); } /// @@ -65,7 +55,7 @@ public class StreamController : BaseApiController public async Task> CreateExternalSource(ExternalSourceDto dto) { // Check if a host and api key exists for the current user - return Ok(await _streamService.CreateExternalSource(UserId, dto)); + return Ok(await streamService.CreateExternalSource(UserId, dto)); } /// @@ -78,7 +68,7 @@ public class StreamController : BaseApiController public async Task> UpdateExternalSource(ExternalSourceDto dto) { // Check if a host and api key exists for the current user - return Ok(await _streamService.UpdateExternalSource(UserId, dto)); + return Ok(await streamService.UpdateExternalSource(UserId, dto)); } /// @@ -90,7 +80,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> ExternalSourceExists(ExternalSourceDto dto) { - return Ok(await _unitOfWork.AppUserExternalSourceRepository.ExternalSourceExists(UserId, dto.Name, dto.Host, dto.ApiKey)); + return Ok(await unitOfWork.AppUserExternalSourceRepository.ExternalSourceExists(UserId, dto.Name, dto.Host, dto.ApiKey)); } /// @@ -102,7 +92,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task ExternalSourceExists(int externalSourceId) { - await _streamService.DeleteExternalSource(UserId, externalSourceId); + await streamService.DeleteExternalSource(UserId, externalSourceId); return Ok(); } @@ -116,7 +106,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> AddDashboard([FromQuery] int smartFilterId) { - return Ok(await _streamService.CreateDashboardStreamFromSmartFilter(UserId, smartFilterId)); + return Ok(await streamService.CreateDashboardStreamFromSmartFilter(UserId, smartFilterId)); } /// @@ -128,7 +118,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateDashboardStream(DashboardStreamDto dto) { - await _streamService.UpdateDashboardStream(UserId, dto); + await streamService.UpdateDashboardStream(UserId, dto); return Ok(); } @@ -141,7 +131,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateDashboardStreamPosition(UpdateStreamPositionDto dto) { - await _streamService.UpdateDashboardStreamPosition(UserId, dto); + await streamService.UpdateDashboardStreamPosition(UserId, dto); return Ok(); } @@ -155,7 +145,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> AddSideNav([FromQuery] int smartFilterId) { - return Ok(await _streamService.CreateSideNavStreamFromSmartFilter(UserId, smartFilterId)); + return Ok(await streamService.CreateSideNavStreamFromSmartFilter(UserId, smartFilterId)); } /// @@ -167,7 +157,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> AddSideNavFromExternalSource([FromQuery] int externalSourceId) { - return Ok(await _streamService.CreateSideNavStreamFromExternalSource(UserId, externalSourceId)); + return Ok(await streamService.CreateSideNavStreamFromExternalSource(UserId, externalSourceId)); } /// @@ -179,7 +169,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateSideNavStream(SideNavStreamDto dto) { - await _streamService.UpdateSideNavStream(UserId, dto); + await streamService.UpdateSideNavStream(UserId, dto); return Ok(); } @@ -192,7 +182,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateSideNavStreamPosition(UpdateStreamPositionDto dto) { - await _streamService.UpdateSideNavStreamPosition(UserId, dto); + await streamService.UpdateSideNavStreamPosition(UserId, dto); return Ok(); } @@ -200,7 +190,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task BulkUpdateSideNavStream(BulkUpdateSideNavStreamVisibilityDto dto) { - await _streamService.UpdateSideNavStreamBulk(UserId, dto); + await streamService.UpdateSideNavStreamBulk(UserId, dto); return Ok(); } @@ -213,7 +203,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteSmartFilterSideNavStream([FromQuery] int sideNavStreamId) { - await _streamService.DeleteSideNavSmartFilterStream(UserId, sideNavStreamId); + await streamService.DeleteSideNavSmartFilterStream(UserId, sideNavStreamId); return Ok(); } @@ -226,7 +216,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteSmartFilterDashboardStream([FromQuery] int dashboardStreamId) { - await _streamService.DeleteDashboardSmartFilterStream(UserId, dashboardStreamId); + await streamService.DeleteDashboardSmartFilterStream(UserId, dashboardStreamId); return Ok(); } } diff --git a/API/Controllers/TachiyomiController.cs b/Kavita.Server/Controllers/TachiyomiController.cs similarity index 53% rename from API/Controllers/TachiyomiController.cs rename to Kavita.Server/Controllers/TachiyomiController.cs index c370f6169..eac5d2dec 100644 --- a/API/Controllers/TachiyomiController.cs +++ b/Kavita.Server/Controllers/TachiyomiController.cs @@ -1,32 +1,22 @@ using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Services; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Models.DTOs; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// All APIs are for Tachiyomi extension and app. They have hacks for our implementation and should not be used for any /// other purposes. /// -public class TachiyomiController : BaseApiController +public class TachiyomiController( + IUnitOfWork unitOfWork, + ITachiyomiService tachiyomiService, + ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ITachiyomiService _tachiyomiService; - private readonly ILocalizationService _localizationService; - - public TachiyomiController(IUnitOfWork unitOfWork, ITachiyomiService tachiyomiService, - ILocalizationService localizationService) - { - _unitOfWork = unitOfWork; - _tachiyomiService = tachiyomiService; - _localizationService = localizationService; - } - /// /// Given the series Id, this should return the latest chapter that has been fully read. /// @@ -35,8 +25,8 @@ public class TachiyomiController : BaseApiController [HttpGet("latest-chapter")] public async Task> GetLatestChapter(int seriesId) { - if (seriesId < 1) return BadRequest(await _localizationService.Translate(UserId, "greater-0", "SeriesId")); - return Ok(await _tachiyomiService.GetLatestChapter(seriesId, UserId)); + if (seriesId < 1) return BadRequest(await localizationService.Translate(UserId, "greater-0", "SeriesId")); + return Ok(await tachiyomiService.GetLatestChapter(seriesId, UserId)); } /// @@ -47,8 +37,8 @@ public class TachiyomiController : BaseApiController [HttpPost("mark-chapter-until-as-read")] public async Task> MarkChaptersUntilAsRead(int seriesId, float chapterNumber) { - var user = (await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, + var user = (await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress))!; - return Ok(await _tachiyomiService.MarkChaptersUntilAsRead(user, seriesId, chapterNumber)); + return Ok(await tachiyomiService.MarkChaptersUntilAsRead(user, seriesId, chapterNumber)); } } diff --git a/API/Controllers/ThemeController.cs b/Kavita.Server/Controllers/ThemeController.cs similarity index 63% rename from API/Controllers/ThemeController.cs rename to Kavita.Server/Controllers/ThemeController.cs index 1efc969a4..a970fafe5 100644 --- a/API/Controllers/ThemeController.cs +++ b/Kavita.Server/Controllers/ThemeController.cs @@ -2,45 +2,32 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Theme; -using API.Middleware; -using API.Services; -using API.Services.Tasks; using AutoMapper; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Theme; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class ThemeController : BaseApiController +public class ThemeController( + IUnitOfWork unitOfWork, + IThemeService themeService, + ILocalizationService localizationService, + IDirectoryService directoryService, + IMapper mapper) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IThemeService _themeService; - private readonly ILocalizationService _localizationService; - private readonly IDirectoryService _directoryService; - private readonly IMapper _mapper; - - - public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, - ILocalizationService localizationService, IDirectoryService directoryService, IMapper mapper) - { - _unitOfWork = unitOfWork; - _themeService = themeService; - _localizationService = localizationService; - _directoryService = directoryService; - _mapper = mapper; - } - [HttpGet] public async Task>> GetThemes() { - return Ok(await _unitOfWork.SiteThemeRepository.GetThemeDtos()); + return Ok(await unitOfWork.SiteThemeRepository.GetThemeDtos()); } @@ -50,11 +37,11 @@ public class ThemeController : BaseApiController { try { - await _themeService.UpdateDefault(dto.ThemeId); + await themeService.UpdateDefault(dto.ThemeId); } catch (KavitaException) { - return BadRequest(await _localizationService.Translate(UserId, "theme-doesnt-exist")); + return BadRequest(await localizationService.Translate(UserId, "theme-doesnt-exist")); } return Ok(); @@ -70,11 +57,11 @@ public class ThemeController : BaseApiController { try { - return Ok(await _themeService.GetContent(themeId)); + return Ok(await themeService.GetContent(themeId)); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Get("en", ex.Message)); + return BadRequest(await localizationService.Get("en", ex.Message)); } } @@ -86,7 +73,7 @@ public class ThemeController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)] public async Task>> BrowseThemes() { - var themes = await _themeService.GetDownloadableThemes(); + var themes = await themeService.GetDownloadableThemes(); return Ok(themes.Where(t => !t.AlreadyDownloaded)); } @@ -99,7 +86,7 @@ public class ThemeController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task>> DeleteTheme(int themeId) { - await _themeService.DeleteTheme(themeId); + await themeService.DeleteTheme(themeId); return Ok(); } @@ -112,7 +99,7 @@ public class ThemeController : BaseApiController [HttpPost("download-theme")] public async Task> DownloadTheme(DownloadableSiteThemeDto dto) { - return Ok(_mapper.Map(await _themeService.DownloadRepoTheme(dto))); + return Ok(mapper.Map(await themeService.DownloadRepoTheme(dto))); } /// @@ -129,13 +116,13 @@ public class ThemeController : BaseApiController var tempFile = await UploadToTemp(formFile); // Set summary as "Uploaded by Username! on DATE" - var theme = await _themeService.CreateThemeFromFile(tempFile, Username!); - return Ok(_mapper.Map(theme)); + var theme = await themeService.CreateThemeFromFile(tempFile, Username!); + return Ok(mapper.Map(theme)); } private async Task UploadToTemp(IFormFile file) { - var outputFile = Path.Join(_directoryService.TempDirectory, file.FileName); + var outputFile = Path.Join(directoryService.TempDirectory, file.FileName); await using var stream = System.IO.File.Create(outputFile); await file.CopyToAsync(stream); stream.Close(); diff --git a/API/Controllers/UploadController.cs b/Kavita.Server/Controllers/UploadController.cs similarity index 96% rename from API/Controllers/UploadController.cs rename to Kavita.Server/Controllers/UploadController.cs index 7aabc5d8c..ea24e4502 100644 --- a/API/Controllers/UploadController.cs +++ b/Kavita.Server/Controllers/UploadController.cs @@ -1,24 +1,26 @@ using System; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Uploads; -using API.Entities.Enums; -using API.Entities.MetadataMatching; -using API.Extensions; -using API.Middleware; -using API.Services; -using API.Services.Tasks.Metadata; -using API.SignalR; using Flurl.Http; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.DTOs.Uploads; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Server.Attributes; +using Kavita.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; [SkipDeviceTracking] public class UploadController : BaseApiController @@ -151,9 +153,8 @@ public class UploadController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] - [RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)] [HttpPost("collection")] + [RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)] public async Task UploadCollectionCoverImageFromUrl(UploadFileDto uploadFileDto) { // Check if Url is non empty, request the image and place in temp, then ask image service to handle it. @@ -163,6 +164,9 @@ public class UploadController : BaseApiController var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(uploadFileDto.Id); if (tag == null) return BadRequest(await _localizationService.Translate(UserId, "collection-doesnt-exist")); + if (!User.IsInRole(PolicyConstants.AdminRole) && tag.AppUserId != UserId) + return Unauthorized(); + var filePath = string.Empty; var lockState = false; if (!string.IsNullOrEmpty(uploadFileDto.Url)) @@ -486,7 +490,7 @@ public class UploadController : BaseApiController { try { - if (uploadFileDto.Id != UserId) return Forbid(); + if (uploadFileDto.Id != UserId) return NotFound(); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(uploadFileDto.Id); if (user == null) return BadRequest(await _localizationService.Translate(UserId, "user-doesnt-exist")); diff --git a/API/Controllers/UsersController.cs b/Kavita.Server/Controllers/UsersController.cs similarity index 61% rename from API/Controllers/UsersController.cs rename to Kavita.Server/Controllers/UsersController.cs index 48d2a8e47..2142327e1 100644 --- a/API/Controllers/UsersController.cs +++ b/Kavita.Server/Controllers/UsersController.cs @@ -2,63 +2,55 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Account; -using API.DTOs.KavitaPlus.Account; -using API.Middleware; -using API.Services; -using API.Services.Plus; -using API.SignalR; using AutoMapper; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.SignalR; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.KavitaPlus.Account; +using Kavita.Models.DTOs.SignalR; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; #nullable enable [Authorize] -public class UsersController : BaseApiController +public class UsersController( + IUnitOfWork unitOfWork, + IMapper mapper, + IEventHub eventHub, + ILocalizationService localizationService, + ILicenseService licenseService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IMapper _mapper; - private readonly IEventHub _eventHub; - private readonly ILocalizationService _localizationService; - private readonly ILicenseService _licenseService; - - public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub, - ILocalizationService localizationService, ILicenseService licenseService) - { - _unitOfWork = unitOfWork; - _mapper = mapper; - _eventHub = eventHub; - _localizationService = localizationService; - _licenseService = licenseService; - } - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpDelete("delete-user")] public async Task DeleteUser(string username) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(username); if (user == null) return BadRequest(); // Remove all likes for the user, so like counts are correct - var annotations = await _unitOfWork.AnnotationRepository.GetAllAnnotations(); + var annotations = await unitOfWork.AnnotationRepository.GetAllAnnotations(); foreach (var annotation in annotations.Where(a => a.Likes.Contains(user.Id))) { annotation.Likes.Remove(user.Id); - _unitOfWork.AnnotationRepository.Update(annotation); + unitOfWork.AnnotationRepository.Update(annotation); } - _unitOfWork.UserRepository.Delete(user); + unitOfWork.UserRepository.Delete(user); - if (await _unitOfWork.CommitAsync()) return Ok(); + if (await unitOfWork.CommitAsync()) return Ok(); - return BadRequest(await _localizationService.Translate(UserId, "generic-user-delete")); + return BadRequest(await localizationService.Translate(UserId, "generic-user-delete")); } /// @@ -70,7 +62,7 @@ public class UsersController : BaseApiController [HttpGet] public async Task>> GetUsers(bool includePending = false) { - return Ok(await _unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync(!includePending)); + return Ok(await unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync(!includePending)); } /// @@ -83,10 +75,10 @@ public class UsersController : BaseApiController public async Task> GetProfileInfo(int userId) { // Validate that the user has sharing enabled - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); if (user == null) return BadRequest(); - return Ok(_mapper.Map(user)); + return Ok(mapper.Map(user)); } /// @@ -98,7 +90,7 @@ public class UsersController : BaseApiController [Authorize] public async Task> HasProfileShared(int userId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); return Ok(user?.UserPreferences?.SocialPreferences?.ShareProfile ?? false); } @@ -110,9 +102,9 @@ public class UsersController : BaseApiController [HttpGet("has-reading-progress")] public async Task> HasReadingProgress(int libraryId) { - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); - if (library == null) return BadRequest(await _localizationService.Translate(UserId, "library-doesnt-exist")); - return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, UserId)); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + if (library == null) return BadRequest(await localizationService.Translate(UserId, "library-doesnt-exist")); + return Ok(await unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, UserId)); } /// @@ -123,7 +115,7 @@ public class UsersController : BaseApiController [HttpGet("has-library-access")] public async Task< ActionResult> HasLibraryAccess(int libraryId) { - var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(Username!); + var libs = await unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(Username!); return Ok(libs.Any(x => x.Id == libraryId)); } @@ -137,7 +129,7 @@ public class UsersController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdatePreferences(UserPreferencesDto preferencesDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.UserPreferences); if (user == null) return Unauthorized(); @@ -154,7 +146,7 @@ public class UsersController : BaseApiController existingPreferences.PromptForRereadsAfter = Math.Max(preferencesDto.PromptForRereadsAfter, 0); existingPreferences.CustomKeyBinds = preferencesDto.CustomKeyBinds; - var allLibs = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)) + var allLibs = (await unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)) .Select(l => l.Id).ToList(); preferencesDto.SocialPreferences.SocialLibraries = preferencesDto.SocialPreferences.SocialLibraries @@ -163,7 +155,7 @@ public class UsersController : BaseApiController existingPreferences.OpdsPreferences = preferencesDto.OpdsPreferences; - if (await _licenseService.HasActiveLicense()) + if (await licenseService.HasActiveLicense(ct: HttpContext.RequestAborted)) { existingPreferences.AniListScrobblingEnabled = preferencesDto.AniListScrobblingEnabled; existingPreferences.WantToReadSync = preferencesDto.WantToReadSync; @@ -173,22 +165,22 @@ public class UsersController : BaseApiController if (preferencesDto.Theme != null && existingPreferences.Theme.Id != preferencesDto.Theme?.Id) { - var theme = await _unitOfWork.SiteThemeRepository.GetTheme(preferencesDto.Theme!.Id); - existingPreferences.Theme = theme ?? await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); + var theme = await unitOfWork.SiteThemeRepository.GetTheme(preferencesDto.Theme!.Id); + existingPreferences.Theme = theme ?? await unitOfWork.SiteThemeRepository.GetDefaultTheme(); } - if (_localizationService.GetLocales().Select(l => l.FileName).Contains(preferencesDto.Locale)) + if (localizationService.GetLocales().Select(l => l.FileName).Contains(preferencesDto.Locale)) { existingPreferences.Locale = preferencesDto.Locale; } - _unitOfWork.UserRepository.Update(existingPreferences); + unitOfWork.UserRepository.Update(existingPreferences); - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-user-pref")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-user-pref")); - await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); + await eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); return Ok(preferencesDto); } @@ -199,8 +191,8 @@ public class UsersController : BaseApiController [HttpGet("get-preferences")] public async Task> GetPreferences() { - return _mapper.Map( - await _unitOfWork.UserRepository.GetPreferencesAsync(Username!)); + return mapper.Map( + await unitOfWork.UserRepository.GetPreferencesAsync(Username!)); } @@ -212,7 +204,7 @@ public class UsersController : BaseApiController [HttpGet("names")] public async Task>> GetUserNames() { - return Ok((await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(u => u.UserName)); + return Ok((await unitOfWork.UserRepository.GetAllUsersAsync()).Select(u => u.UserName)); } /// @@ -220,12 +212,11 @@ public class UsersController : BaseApiController /// /// Kavita+ only /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] + [KPlus] [HttpGet("tokens")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task>> GetUserTokens() { - if (!await _licenseService.HasActiveLicense()) return BadRequest(_localizationService.Translate(UserId, "kavitaplus-restricted")); - - return Ok((await _unitOfWork.UserRepository.GetUserTokenInfo())); + return Ok(await unitOfWork.UserRepository.GetUserTokenInfo()); } } diff --git a/Kavita.Server/Controllers/VolumeController.cs b/Kavita.Server/Controllers/VolumeController.cs new file mode 100644 index 000000000..7b2a76804 --- /dev/null +++ b/Kavita.Server/Controllers/VolumeController.cs @@ -0,0 +1,74 @@ +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.SignalR; +using Kavita.Server.Attributes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Kavita.Server.Controllers; + +public class VolumeController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub) + : BaseApiController +{ + /// + /// Returns the appropriate Volume + /// + /// + /// + [VolumeAccess] + [HttpGet] + public async Task> GetVolume(int volumeId) + { + return Ok(await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, UserId)); + } + + [HttpDelete] + [Authorize(Policy = PolicyGroups.AdminPolicy)] + public async Task> DeleteVolume(int volumeId) + { + var volume = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId, + VolumeIncludes.Chapters | VolumeIncludes.People | VolumeIncludes.Tags); + if (volume == null) + return BadRequest(localizationService.Translate(UserId, "volume-doesnt-exist")); + + unitOfWork.VolumeRepository.Remove(volume); + + if (await unitOfWork.CommitAsync()) + { + await eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false); + return Ok(true); + } + + return Ok(false); + } + + [HttpPost("multiple")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] + public async Task> DeleteMultipleVolumes(int[] volumesIds) + { + var volumes = await unitOfWork.VolumeRepository.GetVolumesById(volumesIds); + if (volumes.Count != volumesIds.Length) + { + return BadRequest(localizationService.Translate(UserId, "volume-doesnt-exist")); + } + + unitOfWork.VolumeRepository.Remove(volumes); + + if (!await unitOfWork.CommitAsync()) + { + return Ok(false); + } + + foreach (var volume in volumes) + { + await eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false); + } + + return Ok(true); + } +} diff --git a/API/Controllers/WantToReadController.cs b/Kavita.Server/Controllers/WantToReadController.cs similarity index 56% rename from API/Controllers/WantToReadController.cs rename to Kavita.Server/Controllers/WantToReadController.cs index 33d8b0b15..e87a4d68d 100644 --- a/API/Controllers/WantToReadController.cs +++ b/Kavita.Server/Controllers/WantToReadController.cs @@ -1,45 +1,32 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; -using API.DTOs.WantToRead; -using API.Entities; -using API.Extensions; -using API.Helpers; -using API.Middleware; -using API.Services; -using API.Services.Plus; using Hangfire; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.WantToRead; +using Kavita.Models.Entities.User; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable - +namespace Kavita.Server.Controllers; /// /// Responsible for all things Want To Read /// [Route("api/want-to-read")] -public class WantToReadController : BaseApiController +public class WantToReadController( + IUnitOfWork unitOfWork, + IScrobblingService scrobblingService, + ILocalizationService localizationService, + ISeriesService seriesService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IScrobblingService _scrobblingService; - private readonly ILocalizationService _localizationService; - private readonly ISeriesService _seriesService; - - public WantToReadController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, - ILocalizationService localizationService, ISeriesService seriesService) - { - _unitOfWork = unitOfWork; - _scrobblingService = scrobblingService; - _localizationService = localizationService; - _seriesService = seriesService; - } - /// /// Return all Series that are in the current logged in user's Want to Read list, filtered /// @@ -55,18 +42,19 @@ public class WantToReadController : BaseApiController userParams ??= new UserParams(); // Add profile privacy filter - filterDto.Statements.AddRange(await _seriesService.GetProfilePrivacyStatements(wantToReadForUser, UserId)); + filterDto.Statements.AddRange(await seriesService.GetProfilePrivacyStatements(wantToReadForUser, UserId)); - var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(wantToReadForUser, userParams, filterDto); + var pagedList = await unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(wantToReadForUser, userParams, filterDto); Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages); return Ok(pagedList); } [HttpGet] + [SeriesAccess] public async Task> IsSeriesInWantToRead([FromQuery] int seriesId) { - return Ok(await _unitOfWork.SeriesRepository.IsSeriesInWantToRead(UserId, seriesId)); + return Ok(await unitOfWork.SeriesRepository.IsSeriesInWantToRead(UserId, seriesId)); } /// @@ -77,7 +65,7 @@ public class WantToReadController : BaseApiController [HttpPost("add-series")] public async Task AddSeries(UpdateWantToReadDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.WantToRead); if (user == null) return Unauthorized(); @@ -92,17 +80,17 @@ public class WantToReadController : BaseApiController }); } - if (!_unitOfWork.HasChanges()) return Ok(); - if (await _unitOfWork.CommitAsync()) + if (!unitOfWork.HasChanges()) return Ok(); + if (await unitOfWork.CommitAsync()) { foreach (var sId in dto.SeriesIds) { - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleWantToReadUpdate(user.Id, sId, true)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleWantToReadUpdate(user.Id, sId, true)); } return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-reading-list-update")); + return BadRequest(await localizationService.Translate(UserId, "generic-reading-list-update")); } /// @@ -113,7 +101,7 @@ public class WantToReadController : BaseApiController [HttpPost("remove-series")] public async Task RemoveSeries(UpdateWantToReadDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.WantToRead); if (user == null) return Unauthorized(); @@ -121,17 +109,17 @@ public class WantToReadController : BaseApiController .Where(s => !dto.SeriesIds.Contains(s.SeriesId)) .ToList(); - if (!_unitOfWork.HasChanges()) return Ok(); - if (await _unitOfWork.CommitAsync()) + if (!unitOfWork.HasChanges()) return Ok(); + if (await unitOfWork.CommitAsync()) { foreach (var sId in dto.SeriesIds) { - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleWantToReadUpdate(user.Id, sId, false)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleWantToReadUpdate(user.Id, sId, false)); } return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-reading-list-update")); + return BadRequest(await localizationService.Translate(UserId, "generic-reading-list-update")); } } diff --git a/API/EmailTemplates/AuthKeyExpired.html b/Kavita.Server/EmailTemplates/AuthKeyExpired.html similarity index 100% rename from API/EmailTemplates/AuthKeyExpired.html rename to Kavita.Server/EmailTemplates/AuthKeyExpired.html diff --git a/API/EmailTemplates/AuthKeyExpiredFragment.html b/Kavita.Server/EmailTemplates/AuthKeyExpiredFragment.html similarity index 100% rename from API/EmailTemplates/AuthKeyExpiredFragment.html rename to Kavita.Server/EmailTemplates/AuthKeyExpiredFragment.html diff --git a/API/EmailTemplates/AuthKeyExpiringFragment.html b/Kavita.Server/EmailTemplates/AuthKeyExpiringFragment.html similarity index 100% rename from API/EmailTemplates/AuthKeyExpiringFragment.html rename to Kavita.Server/EmailTemplates/AuthKeyExpiringFragment.html diff --git a/API/EmailTemplates/AuthKeyExpiringSoon.html b/Kavita.Server/EmailTemplates/AuthKeyExpiringSoon.html similarity index 100% rename from API/EmailTemplates/AuthKeyExpiringSoon.html rename to Kavita.Server/EmailTemplates/AuthKeyExpiringSoon.html diff --git a/API/EmailTemplates/EmailChange.html b/Kavita.Server/EmailTemplates/EmailChange.html similarity index 100% rename from API/EmailTemplates/EmailChange.html rename to Kavita.Server/EmailTemplates/EmailChange.html diff --git a/API/EmailTemplates/EmailConfirm.html b/Kavita.Server/EmailTemplates/EmailConfirm.html similarity index 100% rename from API/EmailTemplates/EmailConfirm.html rename to Kavita.Server/EmailTemplates/EmailConfirm.html diff --git a/API/EmailTemplates/EmailPasswordReset.html b/Kavita.Server/EmailTemplates/EmailPasswordReset.html similarity index 100% rename from API/EmailTemplates/EmailPasswordReset.html rename to Kavita.Server/EmailTemplates/EmailPasswordReset.html diff --git a/API/EmailTemplates/EmailTest.html b/Kavita.Server/EmailTemplates/EmailTest.html similarity index 100% rename from API/EmailTemplates/EmailTest.html rename to Kavita.Server/EmailTemplates/EmailTest.html diff --git a/API/EmailTemplates/KavitaPlusDebug.html b/Kavita.Server/EmailTemplates/KavitaPlusDebug.html similarity index 100% rename from API/EmailTemplates/KavitaPlusDebug.html rename to Kavita.Server/EmailTemplates/KavitaPlusDebug.html diff --git a/API/EmailTemplates/SendToDevice.html b/Kavita.Server/EmailTemplates/SendToDevice.html similarity index 100% rename from API/EmailTemplates/SendToDevice.html rename to Kavita.Server/EmailTemplates/SendToDevice.html diff --git a/API/EmailTemplates/TokenExpiration.html b/Kavita.Server/EmailTemplates/TokenExpiration.html similarity index 100% rename from API/EmailTemplates/TokenExpiration.html rename to Kavita.Server/EmailTemplates/TokenExpiration.html diff --git a/API/EmailTemplates/TokenExpiringSoon.html b/Kavita.Server/EmailTemplates/TokenExpiringSoon.html similarity index 100% rename from API/EmailTemplates/TokenExpiringSoon.html rename to Kavita.Server/EmailTemplates/TokenExpiringSoon.html diff --git a/API/EmailTemplates/base.html b/Kavita.Server/EmailTemplates/base.html similarity index 100% rename from API/EmailTemplates/base.html rename to Kavita.Server/EmailTemplates/base.html diff --git a/Kavita.Server/Extensions/ApplicationServiceExtensions.cs b/Kavita.Server/Extensions/ApplicationServiceExtensions.cs new file mode 100644 index 000000000..52309edec --- /dev/null +++ b/Kavita.Server/Extensions/ApplicationServiceExtensions.cs @@ -0,0 +1,62 @@ +using Kavita.API.Services; +using Kavita.API.Store; +using Kavita.Common; +using Kavita.Database.Extensions; +using Kavita.Models.Constants; +using Kavita.Server.Logging; +using Kavita.Server.Middleware; +using Kavita.Server.Store; +using Kavita.Services.Extensions; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Kavita.Server.Extensions; + + +public static class ApplicationServiceExtensions +{ + public static void AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env) + { + services.AddScoped(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); + + services.AddKavitaDatabases(); + services.AddKavitaServices(); + + services.AddSignalR(opt => opt.EnableDetailedErrors = true); + + services.AddEasyCaching(options => + { + options.UseInMemory(EasyCacheProfiles.Favicon); + options.UseInMemory(EasyCacheProfiles.Publisher); + options.UseInMemory(EasyCacheProfiles.Library); + options.UseInMemory(EasyCacheProfiles.RevokedJwt); + options.UseInMemory(EasyCacheProfiles.LocaleOptions); + + // KavitaPlus stuff + options.UseInMemory(EasyCacheProfiles.KavitaPlusExternalSeries); + options.UseInMemory(EasyCacheProfiles.License); + options.UseInMemory(EasyCacheProfiles.LicenseInfo); + options.UseInMemory(EasyCacheProfiles.KavitaPlusMatchSeries); + }); + + services.AddMemoryCache(options => + { + options.SizeLimit = Configuration.CacheSize * 1024 * 1024; // 75 MB + options.CompactionPercentage = 0.1; // LRU compaction, Evict 10% when limit reached + }); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSwaggerGen(g => + { + g.UseInlineDefinitionsForEnums(); + }); + } +} diff --git a/API/Extensions/HttpExtensions.cs b/Kavita.Server/Extensions/HttpExtensions.cs similarity index 93% rename from API/Extensions/HttpExtensions.cs rename to Kavita.Server/Extensions/HttpExtensions.cs index de8f59c36..fa30b9968 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/Kavita.Server/Extensions/HttpExtensions.cs @@ -1,9 +1,8 @@ -using System.Text.Json; -using API.Helpers; +using System.Text.Json; +using Kavita.Common.Helpers; using Microsoft.AspNetCore.Http; -namespace API.Extensions; -#nullable enable +namespace Kavita.Server.Extensions; public static class HttpExtensions { diff --git a/API/Extensions/IdentityServiceExtensions.cs b/Kavita.Server/Extensions/IdentityServiceExtensions.cs similarity index 96% rename from API/Extensions/IdentityServiceExtensions.cs rename to Kavita.Server/Extensions/IdentityServiceExtensions.cs index 3da1aed22..ce2bfc31c 100644 --- a/API/Extensions/IdentityServiceExtensions.cs +++ b/Kavita.Server/Extensions/IdentityServiceExtensions.cs @@ -5,14 +5,15 @@ using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Entities; -using API.Entities.Progress; -using API.Helpers; -using API.Middleware; -using API.Services; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Database; +using Kavita.Models.Constants; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Server.Helpers; +using Kavita.Server.Middleware; +using Kavita.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -28,8 +29,7 @@ using Microsoft.IdentityModel.Tokens; using Serilog; using MessageReceivedContext = Microsoft.AspNetCore.Authentication.JwtBearer.MessageReceivedContext; -namespace API.Extensions; -#nullable enable +namespace Kavita.Server.Extensions; public static class IdentityServiceExtensions { @@ -154,7 +154,9 @@ public static class IdentityServiceExtensions .AddPolicy(PolicyGroups.DownloadPolicy, policy => policy.RequireRole(PolicyConstants.DownloadRole, PolicyConstants.AdminRole)) .AddPolicy(PolicyGroups.ChangePasswordPolicy, - policy => policy.RequireRole(PolicyConstants.ChangePasswordRole, PolicyConstants.AdminRole)); + policy => policy.RequireRole(PolicyConstants.ChangePasswordRole, PolicyConstants.AdminRole)) + .AddPolicy(PolicyGroups.BookmarkPolicy, + policy => policy.RequireRole(PolicyConstants.BookmarkRole, PolicyConstants.AdminRole)); return services; } diff --git a/API/Helpers/BrowserHelper.cs b/Kavita.Server/Helpers/BrowserHelper.cs similarity index 97% rename from API/Helpers/BrowserHelper.cs rename to Kavita.Server/Helpers/BrowserHelper.cs index d11e2122f..c7bde8099 100644 --- a/API/Helpers/BrowserHelper.cs +++ b/Kavita.Server/Helpers/BrowserHelper.cs @@ -1,7 +1,6 @@ -using System; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Helpers; +namespace Kavita.Server.Helpers; /// /// Handles all things around Parsing Headers diff --git a/API/Helpers/OpenIdConnectEventsHelper.cs b/Kavita.Server/Helpers/OpenIdConnectEventsHelper.cs similarity index 98% rename from API/Helpers/OpenIdConnectEventsHelper.cs rename to Kavita.Server/Helpers/OpenIdConnectEventsHelper.cs index 3605251a2..1ef4143d1 100644 --- a/API/Helpers/OpenIdConnectEventsHelper.cs +++ b/Kavita.Server/Helpers/OpenIdConnectEventsHelper.cs @@ -2,17 +2,17 @@ using System; using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; -using API.Extensions; -using API.Services; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Server.Extensions; +using Kavita.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Serilog; -namespace API.Helpers; -#nullable enable +namespace Kavita.Server.Helpers; public class OpenIdConnectEventsHelper: OpenIdConnectEvents { diff --git a/API/I18N/ar.json b/Kavita.Server/I18N/ar.json similarity index 100% rename from API/I18N/ar.json rename to Kavita.Server/I18N/ar.json diff --git a/API/I18N/as.json b/Kavita.Server/I18N/as.json similarity index 100% rename from API/I18N/as.json rename to Kavita.Server/I18N/as.json diff --git a/API/I18N/ca.json b/Kavita.Server/I18N/ca.json similarity index 100% rename from API/I18N/ca.json rename to Kavita.Server/I18N/ca.json diff --git a/API/I18N/cs.json b/Kavita.Server/I18N/cs.json similarity index 100% rename from API/I18N/cs.json rename to Kavita.Server/I18N/cs.json diff --git a/API/I18N/da.json b/Kavita.Server/I18N/da.json similarity index 100% rename from API/I18N/da.json rename to Kavita.Server/I18N/da.json diff --git a/API/I18N/de.json b/Kavita.Server/I18N/de.json similarity index 100% rename from API/I18N/de.json rename to Kavita.Server/I18N/de.json diff --git a/API/I18N/el.json b/Kavita.Server/I18N/el.json similarity index 100% rename from API/I18N/el.json rename to Kavita.Server/I18N/el.json diff --git a/API/I18N/en.json b/Kavita.Server/I18N/en.json similarity index 100% rename from API/I18N/en.json rename to Kavita.Server/I18N/en.json diff --git a/API/I18N/es.json b/Kavita.Server/I18N/es.json similarity index 100% rename from API/I18N/es.json rename to Kavita.Server/I18N/es.json diff --git a/API/I18N/et.json b/Kavita.Server/I18N/et.json similarity index 100% rename from API/I18N/et.json rename to Kavita.Server/I18N/et.json diff --git a/API/I18N/fa.json b/Kavita.Server/I18N/fa.json similarity index 100% rename from API/I18N/fa.json rename to Kavita.Server/I18N/fa.json diff --git a/API/I18N/fi.json b/Kavita.Server/I18N/fi.json similarity index 100% rename from API/I18N/fi.json rename to Kavita.Server/I18N/fi.json diff --git a/API/I18N/fr.json b/Kavita.Server/I18N/fr.json similarity index 100% rename from API/I18N/fr.json rename to Kavita.Server/I18N/fr.json diff --git a/API/I18N/ga.json b/Kavita.Server/I18N/ga.json similarity index 100% rename from API/I18N/ga.json rename to Kavita.Server/I18N/ga.json diff --git a/API/I18N/he.json b/Kavita.Server/I18N/he.json similarity index 100% rename from API/I18N/he.json rename to Kavita.Server/I18N/he.json diff --git a/API/I18N/hi.json b/Kavita.Server/I18N/hi.json similarity index 100% rename from API/I18N/hi.json rename to Kavita.Server/I18N/hi.json diff --git a/API/I18N/hr.json b/Kavita.Server/I18N/hr.json similarity index 100% rename from API/I18N/hr.json rename to Kavita.Server/I18N/hr.json diff --git a/API/I18N/hu.json b/Kavita.Server/I18N/hu.json similarity index 100% rename from API/I18N/hu.json rename to Kavita.Server/I18N/hu.json diff --git a/API/I18N/id.json b/Kavita.Server/I18N/id.json similarity index 100% rename from API/I18N/id.json rename to Kavita.Server/I18N/id.json diff --git a/API/I18N/it.json b/Kavita.Server/I18N/it.json similarity index 100% rename from API/I18N/it.json rename to Kavita.Server/I18N/it.json diff --git a/API/I18N/ja.json b/Kavita.Server/I18N/ja.json similarity index 100% rename from API/I18N/ja.json rename to Kavita.Server/I18N/ja.json diff --git a/API/I18N/ko.json b/Kavita.Server/I18N/ko.json similarity index 100% rename from API/I18N/ko.json rename to Kavita.Server/I18N/ko.json diff --git a/API/I18N/lt.json b/Kavita.Server/I18N/lt.json similarity index 100% rename from API/I18N/lt.json rename to Kavita.Server/I18N/lt.json diff --git a/API/I18N/ms.json b/Kavita.Server/I18N/ms.json similarity index 100% rename from API/I18N/ms.json rename to Kavita.Server/I18N/ms.json diff --git a/API/I18N/nb_NO.json b/Kavita.Server/I18N/nb_NO.json similarity index 100% rename from API/I18N/nb_NO.json rename to Kavita.Server/I18N/nb_NO.json diff --git a/API/I18N/nl.json b/Kavita.Server/I18N/nl.json similarity index 100% rename from API/I18N/nl.json rename to Kavita.Server/I18N/nl.json diff --git a/API/I18N/pl.json b/Kavita.Server/I18N/pl.json similarity index 100% rename from API/I18N/pl.json rename to Kavita.Server/I18N/pl.json diff --git a/API/I18N/pt.json b/Kavita.Server/I18N/pt.json similarity index 100% rename from API/I18N/pt.json rename to Kavita.Server/I18N/pt.json diff --git a/API/I18N/pt_BR.json b/Kavita.Server/I18N/pt_BR.json similarity index 100% rename from API/I18N/pt_BR.json rename to Kavita.Server/I18N/pt_BR.json diff --git a/API/I18N/ru.json b/Kavita.Server/I18N/ru.json similarity index 100% rename from API/I18N/ru.json rename to Kavita.Server/I18N/ru.json diff --git a/API/I18N/sk.json b/Kavita.Server/I18N/sk.json similarity index 100% rename from API/I18N/sk.json rename to Kavita.Server/I18N/sk.json diff --git a/API/I18N/sl.json b/Kavita.Server/I18N/sl.json similarity index 100% rename from API/I18N/sl.json rename to Kavita.Server/I18N/sl.json diff --git a/API/I18N/sv.json b/Kavita.Server/I18N/sv.json similarity index 100% rename from API/I18N/sv.json rename to Kavita.Server/I18N/sv.json diff --git a/API/I18N/ta.json b/Kavita.Server/I18N/ta.json similarity index 100% rename from API/I18N/ta.json rename to Kavita.Server/I18N/ta.json diff --git a/API/I18N/te.json b/Kavita.Server/I18N/te.json similarity index 100% rename from API/I18N/te.json rename to Kavita.Server/I18N/te.json diff --git a/API/I18N/th.json b/Kavita.Server/I18N/th.json similarity index 100% rename from API/I18N/th.json rename to Kavita.Server/I18N/th.json diff --git a/API/I18N/tr.json b/Kavita.Server/I18N/tr.json similarity index 100% rename from API/I18N/tr.json rename to Kavita.Server/I18N/tr.json diff --git a/API/I18N/uk.json b/Kavita.Server/I18N/uk.json similarity index 100% rename from API/I18N/uk.json rename to Kavita.Server/I18N/uk.json diff --git a/API/I18N/vi.json b/Kavita.Server/I18N/vi.json similarity index 100% rename from API/I18N/vi.json rename to Kavita.Server/I18N/vi.json diff --git a/API/I18N/zh_Hans.json b/Kavita.Server/I18N/zh_Hans.json similarity index 100% rename from API/I18N/zh_Hans.json rename to Kavita.Server/I18N/zh_Hans.json diff --git a/API/I18N/zh_Hant.json b/Kavita.Server/I18N/zh_Hant.json similarity index 100% rename from API/I18N/zh_Hant.json rename to Kavita.Server/I18N/zh_Hant.json diff --git a/Kavita.Server/Kavita.Server.csproj b/Kavita.Server/Kavita.Server.csproj new file mode 100644 index 000000000..210701930 --- /dev/null +++ b/Kavita.Server/Kavita.Server.csproj @@ -0,0 +1,162 @@ + + + + Exe + Default + net10.0 + true + Linux + true + true + ../favicon.ico + enable + latestmajor + false + disable + + + + false + ../favicon.ico + bin\$(Configuration)\$(AssemblyName).xml + + + + bin\$(Configuration)\$(AssemblyName).xml + 1701;1702;1591 + + + + + True + $(NoWarn);1591 + $(NoWarn);CA1873 + + + + en + + + + + Kavita + kareadita.github.io + Copyright 2020-$([System.DateTime]::Now.ToString('yyyy')) kavitareader.com (GNU General Public v3) + + $(Configuration)-dev + + false + false + false + + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + Always + + + + + + + + + Always + + + Always + + + + Always + + + Always + + + + + <_DeploymentManifestIconFile Remove="favicon.ico" /> + + + diff --git a/API/Logging/LogEnricher.cs b/Kavita.Server/Logging/LogEnricher.cs similarity index 95% rename from API/Logging/LogEnricher.cs rename to Kavita.Server/Logging/LogEnricher.cs index e580f663e..b67b5e037 100644 --- a/API/Logging/LogEnricher.cs +++ b/Kavita.Server/Logging/LogEnricher.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Http; using Serilog; -namespace API.Logging; +namespace Kavita.Server.Logging; public static class LogEnricher { diff --git a/API/Logging/LogLevelOptions.cs b/Kavita.Server/Logging/LogLevelOptions.cs similarity index 97% rename from API/Logging/LogLevelOptions.cs rename to Kavita.Server/Logging/LogLevelOptions.cs index 47df3973d..3d254e042 100644 --- a/API/Logging/LogLevelOptions.cs +++ b/Kavita.Server/Logging/LogLevelOptions.cs @@ -4,7 +4,7 @@ using Serilog.Core; using Serilog.Events; using Serilog.Formatting.Display; -namespace API.Logging; +namespace Kavita.Server.Logging; /// /// This class represents information for configuring Logging in the Application. Only a high log level is exposed and Kavita @@ -49,7 +49,7 @@ public static class LogLevelOptions .MinimumLevel.Override("Microsoft.AspNetCore.ResponseCaching.ResponseCachingMiddleware", LogEventLevel.Error) .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Error) .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Error) - .MinimumLevel.Override("API.Middleware.AuthKeyAuthenticationHandler", LogEventLevel.Error) + .MinimumLevel.Override("Kavita.Server.Middleware.AuthKeyAuthenticationHandler", LogEventLevel.Error) .Enrich.FromLogContext() .Enrich.WithThreadId() .Enrich.With(new ApiKeyEnricher()) diff --git a/Kavita.Server/Logging/LoggingService.cs b/Kavita.Server/Logging/LoggingService.cs new file mode 100644 index 000000000..b2435ac72 --- /dev/null +++ b/Kavita.Server/Logging/LoggingService.cs @@ -0,0 +1,11 @@ +using Kavita.API.Services; + +namespace Kavita.Server.Logging; + +public class LoggingService: ILoggingService +{ + public void SwitchLogLevel(string level) + { + LogLevelOptions.SwitchLogLevel(level); + } +} diff --git a/API/Data/Misc/ManualMigration.cs b/Kavita.Server/ManualMigrations/ManualMigration.cs similarity index 92% rename from API/Data/Misc/ManualMigration.cs rename to Kavita.Server/ManualMigrations/ManualMigration.cs index 907cac648..0625ff67b 100644 --- a/API/Data/Misc/ManualMigration.cs +++ b/Kavita.Server/ManualMigrations/ManualMigration.cs @@ -1,9 +1,10 @@ using System.Threading.Tasks; -using API.Entities.History; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.Misc; +namespace Kavita.Server.ManualMigrations; public abstract class ManualMigration { diff --git a/API/Data/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs b/Kavita.Server/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs similarity index 92% rename from API/Data/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs rename to Kavita.Server/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs index f3197f44b..9c5c9e27b 100644 --- a/API/Data/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs +++ b/Kavita.Server/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs @@ -1,15 +1,12 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._11; /// /// Introduced in v0.7.11 with the removal of .Kavitaignore files diff --git a/API/Data/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs b/Kavita.Server/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs similarity index 95% rename from API/Data/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs rename to Kavita.Server/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs index daa62ea77..a9374944c 100644 --- a/API/Data/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs +++ b/Kavita.Server/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs @@ -2,14 +2,15 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.DTOs.Filtering.v2; -using API.Entities.History; -using API.Helpers; -using Kavita.Common.EnvironmentInfo; +using Kavita.API.Database; +using Kavita.Database; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.History; +using Kavita.Services.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._11; /// /// v0.7.10.2 introduced a bad encoding, this will migrate those bad smart filters diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs similarity index 90% rename from API/Data/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs index 92195c9d0..c4b639d7e 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs @@ -1,11 +1,10 @@ -using System; -using System.Threading.Tasks; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; +using System.Threading.Tasks; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; /// /// For the v0.7.14 release, one of the nightlies had bad data that would cause issues. This drops those records diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs similarity index 97% rename from API/Data/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs index ae0f17c16..ae232cb61 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs @@ -1,11 +1,11 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Services; using Flurl.Http; +using Kavita.API.Services; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; public static class MigrateEmailTemplates { diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateManualHistory.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateManualHistory.cs similarity index 95% rename from API/Data/ManualMigrations/v0.7.14/MigrateManualHistory.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateManualHistory.cs index 781fd3193..98bac2f03 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateManualHistory.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateManualHistory.cs @@ -1,10 +1,11 @@ using System; using System.Threading.Tasks; -using API.Entities.History; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; /// /// Introduced in v0.7.14, will store history so that going forward, migrations can just check against the history diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs similarity index 90% rename from API/Data/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs index 539afd972..3058212e7 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs @@ -1,12 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; +using Kavita.API.Database; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; public static class MigrateVolumeLookupName { diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs similarity index 92% rename from API/Data/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs index 73b2896fc..add1c0d2d 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs @@ -1,10 +1,11 @@ using System.Threading.Tasks; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Database; +using Kavita.Models.Entities.History; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; /// /// Introduced in v0.7.14, this migrates the existing Volume Name -> Volume Min/Max Number diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs similarity index 95% rename from API/Data/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs index 62d5bb076..3ca677a1f 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs @@ -2,13 +2,13 @@ using System.Globalization; using System.IO; using System.Threading.Tasks; -using API.Data.Misc; -using API.Services; using CsvHelper; +using Kavita.API.Services; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; /// /// v0.7.13.12/v0.7.14 - Want to read is extracted and saved in a csv diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs similarity index 92% rename from API/Data/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs index d336937c9..a028d7162 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs @@ -2,14 +2,15 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Data.Repositories; -using API.Entities; -using API.Services; using CsvHelper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Database; +using Kavita.Models.Entities.User; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; /// /// v0.7.13.12/v0.7.14 - Want to read is imported from a csv diff --git a/API/Data/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs b/Kavita.Server/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs similarity index 90% rename from API/Data/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs rename to Kavita.Server/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs index 9fea24199..d42ebd479 100644 --- a/API/Data/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs +++ b/Kavita.Server/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs @@ -1,14 +1,15 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -using API.Data.Repositories; -using API.Entities; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.Database; +using Kavita.Models.Entities; +using Kavita.Models.Entities.History; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._9; /// /// Introduced in v0.7.8.7 and v0.7.9, this adds SideNavStream's for all Libraries a User has access to diff --git a/API/Data/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs b/Kavita.Server/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs similarity index 94% rename from API/Data/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs rename to Kavita.Server/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs index fac184dc9..ba64f1f83 100644 --- a/API/Data/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs @@ -2,17 +2,21 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Constants; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Constants; +using Kavita.Models.Entities; +using Kavita.Models.Entities.History; +using Kavita.Models.Extensions; +using Kavita.Services; +using Kavita.Services.Builders; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; /// @@ -78,7 +82,7 @@ public static class MigrateLooseLeafChapters var chapters = await dataContext.Chapter .Where(c => c.VolumeId == distinctVolume.Volume.Id && !c.IsSpecial).ToListAsync(); - var newVolume = new VolumeBuilder(Parser.LooseLeafVolume) + var newVolume = new VolumeBuilder(ParserConstants.LooseLeafVolume) .WithSeriesId(seriesId) .WithCreated(distinctVolume.Volume.Created) .WithLastModified(distinctVolume.Volume.LastModified) diff --git a/API/Data/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs b/Kavita.Server/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs similarity index 95% rename from API/Data/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs rename to Kavita.Server/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs index cda83f05b..c93f5c8ec 100644 --- a/API/Data/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs @@ -2,17 +2,21 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Constants; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Constants; +using Kavita.Models.Entities; +using Kavita.Models.Entities.History; +using Kavita.Models.Extensions; +using Kavita.Services; +using Kavita.Services.Builders; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; public class UserProgressCsvRecord { @@ -95,7 +99,7 @@ public static class MigrateMixedSpecials var chapters = await dataContext.Chapter .Where(c => c.VolumeId == distinctVolume.Volume.Id && c.IsSpecial).ToListAsync(); - var newVolume = new VolumeBuilder(Parser.SpecialVolume) + var newVolume = new VolumeBuilder(ParserConstants.SpecialVolume) .WithSeriesId(seriesId) .WithCreated(distinctVolume.Volume.Created) .WithLastModified(distinctVolume.Volume.LastModified) diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateChapterFields.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterFields.cs similarity index 76% rename from API/Data/ManualMigrations/v0.8.0/MigrateChapterFields.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterFields.cs index 4187788ab..b4c35f37a 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateChapterFields.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterFields.cs @@ -1,13 +1,17 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; +using Kavita.Common.Constants; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Constants; +using Kavita.Models.Entities.History; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; @@ -35,9 +39,9 @@ public static class MigrateChapterFields "Running MigrateChapterFields migration - Updating {Count} volumes that only have specials in them", volumesWithJustSpecials.Count); foreach (var volume in volumesWithJustSpecials) { - volume.Name = $"{Parser.SpecialVolumeNumber}"; - volume.MinNumber = Parser.SpecialVolumeNumber; - volume.MaxNumber = Parser.SpecialVolumeNumber; + volume.Name = $"{ParserConstants.SpecialVolumeNumber}"; + volume.MinNumber = ParserConstants.SpecialVolumeNumber; + volume.MaxNumber = ParserConstants.SpecialVolumeNumber; } // Update all volumes that only have loose leafs in them @@ -49,9 +53,9 @@ public static class MigrateChapterFields "Running MigrateChapterFields migration - Updating {Count} volumes that only have loose leaf chapters in them", looseLeafVolumes.Count); foreach (var volume in looseLeafVolumes) { - volume.Name = $"{Parser.DefaultChapterNumber}"; - volume.MinNumber = Parser.DefaultChapterNumber; - volume.MaxNumber = Parser.DefaultChapterNumber; + volume.Name = $"{ParserConstants.DefaultChapterNumber}"; + volume.MinNumber = ParserConstants.DefaultChapterNumber; + volume.MaxNumber = ParserConstants.DefaultChapterNumber; } // Update all MangaFile @@ -67,9 +71,9 @@ public static class MigrateChapterFields "Running MigrateChapterFields migration - Updating {Count} loose leaf chapters", looseLeafChapters.Count); foreach (var chapter in looseLeafChapters) { - chapter.Number = Parser.DefaultChapter; - chapter.MinNumber = Parser.DefaultChapterNumber; - chapter.MaxNumber = Parser.DefaultChapterNumber; + chapter.Number = ParserConstants.DefaultChapter; + chapter.MinNumber = ParserConstants.DefaultChapterNumber; + chapter.MaxNumber = ParserConstants.DefaultChapterNumber; } dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateChapterNumber.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterNumber.cs similarity index 80% rename from API/Data/ManualMigrations/v0.8.0/MigrateChapterNumber.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterNumber.cs index 756055bb7..09c156bb3 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateChapterNumber.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterNumber.cs @@ -1,12 +1,15 @@ using System; using System.Threading.Tasks; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Common.Constants; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Constants; +using Kavita.Models.Entities.History; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; /// /// Introduced in v0.8.0, this migrates the existing Chapter Range -> Chapter Min/Max Number @@ -28,8 +31,8 @@ public static class MigrateChapterNumber { if (chapter.IsSpecial) { - chapter.MinNumber = Parser.DefaultChapterNumber; - chapter.MaxNumber = Parser.DefaultChapterNumber; + chapter.MinNumber = ParserConstants.DefaultChapterNumber; + chapter.MaxNumber = ParserConstants.DefaultChapterNumber; continue; } chapter.MinNumber = Parser.MinNumberFromRange(chapter.Range); diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateChapterRange.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterRange.cs similarity index 87% rename from API/Data/ManualMigrations/v0.8.0/MigrateChapterRange.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterRange.cs index 63e8b889d..352097da9 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateChapterRange.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterRange.cs @@ -1,13 +1,16 @@ using System; using System.Threading.Tasks; -using API.Entities.History; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; +using Kavita.Database; +using Kavita.Models.Entities.History; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; /// /// v0.8.0 changed the range to that it doesn't have filename by default diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs similarity index 91% rename from API/Data/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs index e29e706d0..784223ea4 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs @@ -1,16 +1,18 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using API.Extensions.QueryExtensions; +using Kavita.API.Database; +using Kavita.API.Repositories; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Database.Extensions; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.History; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; /// /// v0.8.0 refactored User Collections diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs similarity index 95% rename from API/Data/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs index 32b4d0fbf..af6743f92 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs @@ -1,12 +1,13 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; /// /// v0.8.0 ensured that MangaFile Path is normalized. This will normalize existing data to avoid churn. diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs similarity index 90% rename from API/Data/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs index 5cbb846af..8b3de40be 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs @@ -1,12 +1,13 @@ using System; using System.Threading.Tasks; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; /// /// v0.8.0 ensured that MangaFile Path is normalized. This will normalize existing data to avoid churn. diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateProgressExport.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateProgressExport.cs similarity index 97% rename from API/Data/ManualMigrations/v0.8.0/MigrateProgressExport.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateProgressExport.cs index 498dbb8cc..2d5145fa0 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateProgressExport.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateProgressExport.cs @@ -3,15 +3,16 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; -using API.Services; using CsvHelper; using CsvHelper.Configuration.Attributes; +using Kavita.API.Services; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; public class ProgressExport { diff --git a/API/Data/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs b/Kavita.Server/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs similarity index 92% rename from API/Data/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs rename to Kavita.Server/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs index 02c5b1b92..9aede70d1 100644 --- a/API/Data/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs +++ b/Kavita.Server/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs @@ -1,12 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; +using Kavita.API.Database; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._1; /// /// v0.8.0 released with a bug around LowestSeriesPath. This resets it for all users. diff --git a/API/Data/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs b/Kavita.Server/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs similarity index 93% rename from API/Data/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs rename to Kavita.Server/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs index 9d5e4c59f..3a971c317 100644 --- a/API/Data/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs +++ b/Kavita.Server/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs @@ -1,11 +1,12 @@ using System; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._2; /// /// v0.8.2 switches Default Kavita installs to WAL diff --git a/API/Data/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs b/Kavita.Server/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs similarity index 87% rename from API/Data/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs rename to Kavita.Server/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs index cdbe08287..eaf13ec30 100644 --- a/API/Data/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs +++ b/Kavita.Server/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs @@ -1,12 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._2; /// /// v0.8.2 introduced Theme repo viewer, this adds Description to existing SiteTheme defaults @@ -25,7 +27,7 @@ public static class ManualMigrateThemeDescription var theme = await context.SiteTheme.FirstOrDefaultAsync(t => t.Name == "Dark"); if (theme != null) { - theme.Description = Seed.DefaultThemes.First().Description; + theme.Description = Defaults.DefaultThemes.First().Description; } if (context.ChangeTracker.HasChanges()) diff --git a/API/Data/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs b/Kavita.Server/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs similarity index 91% rename from API/Data/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs rename to Kavita.Server/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs index f1ccea6cf..33504c577 100644 --- a/API/Data/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs +++ b/Kavita.Server/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs @@ -3,14 +3,15 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Entities.Enums; -using API.Entities.History; -using API.Services; +using Kavita.API.Services; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._2; /// /// v0.8.2 I started collecting information on when the user first installed Kavita as a nice to have info for the user diff --git a/API/Data/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs b/Kavita.Server/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs similarity index 92% rename from API/Data/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs rename to Kavita.Server/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs index 2db296444..c2116a740 100644 --- a/API/Data/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs +++ b/Kavita.Server/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs @@ -1,14 +1,15 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Services; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._2; #nullable enable /// diff --git a/API/Data/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs b/Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs similarity index 93% rename from API/Data/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs rename to Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs index f9e94836e..a3167c3bf 100644 --- a/API/Data/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs +++ b/Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using API.Entities.Enums; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._4; /// /// At some point, encoding settings wrote bad data to the backend, maybe in v0.8.0. This just fixes any bad data. diff --git a/API/Data/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs b/Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs similarity index 93% rename from API/Data/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs rename to Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs index 2ae22ff52..e0a38a484 100644 --- a/API/Data/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs +++ b/Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs @@ -1,11 +1,12 @@ using System; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._4; /// /// Due to a bug in the initial merge of People/Scanner rework, people got messed up bad. This migration will clear out the table only for nightly users: 0.8.3.15/0.8.3.16 diff --git a/API/Data/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs b/Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs similarity index 91% rename from API/Data/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs rename to Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs index 452ca5d09..072000a07 100644 --- a/API/Data/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs +++ b/Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs @@ -1,13 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.Enums; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._4; /// /// When I removed Scrobble support for Book libraries, I forgot to turn the setting off for said libraries. diff --git a/API/Data/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs b/Kavita.Server/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs similarity index 92% rename from API/Data/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs rename to Kavita.Server/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs index 16a1d7a1a..e7aa5c990 100644 --- a/API/Data/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs +++ b/Kavita.Server/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs @@ -1,12 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; +using Kavita.API.Database; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._4; /// /// v0.8.3 still had a bug around LowestSeriesPath. This resets it for all users. diff --git a/API/Data/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs similarity index 90% rename from API/Data/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs rename to Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs index 9398b43ab..56fd3567f 100644 --- a/API/Data/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs +++ b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs @@ -1,13 +1,15 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; -using API.Entities.Metadata; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Database.Migrations; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using ExternalSeriesMetadata = Kavita.Models.Entities.Metadata.ExternalSeriesMetadata; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._5; /// /// v0.8.5 - Migrating Kavita+ BlacklistedSeries table to Series entity to streamline implementation and generate a "Needs Manual Match" entry for the Series diff --git a/API/Data/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs similarity index 94% rename from API/Data/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs rename to Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs index 7869b4235..25f27abb2 100644 --- a/API/Data/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs +++ b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs @@ -1,12 +1,13 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._5; /// /// v0.8.5 - Migrating Kavita+ Series that are Blacklisted but have valid ExternalSeries row diff --git a/API/Data/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs similarity index 90% rename from API/Data/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs rename to Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs index bbc4dc593..7715cac08 100644 --- a/API/Data/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs +++ b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs @@ -1,13 +1,14 @@ using System; using System.Threading.Tasks; -using API.DTOs.KavitaPlus.Manage; -using API.Entities.History; -using API.Extensions.QueryExtensions; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.KavitaPlus.Manage; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._5; /// /// v0.8.5 - After user testing, the needs manual match has some edge cases from migrations and for best user experience, diff --git a/API/Data/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs similarity index 94% rename from API/Data/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs rename to Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs index b0d483de6..7d6facfa2 100644 --- a/API/Data/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs +++ b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs @@ -1,12 +1,13 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._5; /// /// v0.8.5 - There seems to be some scrobble events that are pre-scrobble error table that can be processed over and over. diff --git a/API/Data/ManualMigrations/v0.8.5/MigrateProgressExport.cs b/Kavita.Server/ManualMigrations/v0.8.5/MigrateProgressExport.cs similarity index 96% rename from API/Data/ManualMigrations/v0.8.5/MigrateProgressExport.cs rename to Kavita.Server/ManualMigrations/v0.8.5/MigrateProgressExport.cs index e0175fbf3..0e776a904 100644 --- a/API/Data/ManualMigrations/v0.8.5/MigrateProgressExport.cs +++ b/Kavita.Server/ManualMigrations/v0.8.5/MigrateProgressExport.cs @@ -3,14 +3,15 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; -using API.Services; using CsvHelper; +using Kavita.API.Services; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._5; /// diff --git a/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs b/Kavita.Server/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs similarity index 93% rename from API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs rename to Kavita.Server/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs index d0f9421ee..b76fc65a3 100644 --- a/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs +++ b/Kavita.Server/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs @@ -1,12 +1,13 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._6; /// /// v0.8.6 - Manually check when a user triggers scrobble event generation diff --git a/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs b/Kavita.Server/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs similarity index 92% rename from API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs rename to Kavita.Server/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs index 4749ff2ec..518ce8318 100644 --- a/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs +++ b/Kavita.Server/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs @@ -1,13 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._6; /// /// v0.8.6 - Change to not scrobble specials as they will never process, this migration removes all existing scrobble events diff --git a/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs b/Kavita.Server/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs similarity index 94% rename from API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs rename to Kavita.Server/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs index 514ba23ac..e837b6f23 100644 --- a/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs +++ b/Kavita.Server/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs @@ -1,14 +1,15 @@ using System; using System.Threading.Tasks; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using API.Extensions; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; +using Kavita.Database; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.History; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._7; public static class ManualMigrateReadingProfiles { diff --git a/API/Data/ManualMigrations/v0.8.8/ManualMigrateBookReadingProgress.cs b/Kavita.Server/ManualMigrations/v0.8.8/ManualMigrateBookReadingProgress.cs similarity index 96% rename from API/Data/ManualMigrations/v0.8.8/ManualMigrateBookReadingProgress.cs rename to Kavita.Server/ManualMigrations/v0.8.8/ManualMigrateBookReadingProgress.cs index e03d7de59..4b83067b8 100644 --- a/API/Data/ManualMigrations/v0.8.8/ManualMigrateBookReadingProgress.cs +++ b/Kavita.Server/ManualMigrations/v0.8.8/ManualMigrateBookReadingProgress.cs @@ -1,12 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; +using Kavita.API.Database; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._8; /// /// v0.8.8 - Switch existing xpaths saved to a descoped version diff --git a/API/Data/ManualMigrations/v0.8.8/ManualMigrateEnableMetadataMatchingDefault.cs b/Kavita.Server/ManualMigrations/v0.8.8/ManualMigrateEnableMetadataMatchingDefault.cs similarity index 93% rename from API/Data/ManualMigrations/v0.8.8/ManualMigrateEnableMetadataMatchingDefault.cs rename to Kavita.Server/ManualMigrations/v0.8.8/ManualMigrateEnableMetadataMatchingDefault.cs index 5bb8aeb94..1e5cb0139 100644 --- a/API/Data/ManualMigrations/v0.8.8/ManualMigrateEnableMetadataMatchingDefault.cs +++ b/Kavita.Server/ManualMigrations/v0.8.8/ManualMigrateEnableMetadataMatchingDefault.cs @@ -1,11 +1,13 @@ using System; using System.Threading.Tasks; -using API.Entities.History; +using Kavita.API.Database; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._8; /// /// v0.8.8 - If Kavita+ users had Metadata Matching settings already, ensure the new non-Kavita+ system is enabled to match diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateBadKoreaderProgress.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateBadKoreaderProgress.cs similarity index 94% rename from API/Data/ManualMigrations/v0.8.9/MigrateBadKoreaderProgress.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateBadKoreaderProgress.cs index 79e73d0ce..46af27039 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateBadKoreaderProgress.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateBadKoreaderProgress.cs @@ -1,10 +1,10 @@ using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// /// v0.8.8.28 - There was bad code in the nightlies where Progress events with LibraryId = 0 could be saved. This will fix up those events. diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateFormatToActivityData.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateFormatToActivityData.cs similarity index 95% rename from API/Data/ManualMigrations/v0.8.9/MigrateFormatToActivityData.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateFormatToActivityData.cs index 1ce4d9340..1e0cbd6b7 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateFormatToActivityData.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateFormatToActivityData.cs @@ -1,11 +1,11 @@ using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Entities.Enums; +using Kavita.Database; +using Kavita.Models.Entities.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// /// v0.8.8.16 - Needed to add Format to the ActivityData to optimize a query diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateIncorrectUtcMidnightRollovers.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateIncorrectUtcMidnightRollovers.cs similarity index 96% rename from API/Data/ManualMigrations/v0.8.9/MigrateIncorrectUtcMidnightRollovers.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateIncorrectUtcMidnightRollovers.cs index 5cb98846e..3719502b9 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateIncorrectUtcMidnightRollovers.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateIncorrectUtcMidnightRollovers.cs @@ -1,11 +1,11 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; public class MigrateIncorrectUtcTimes: ManualMigration { diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateMissingAppUserRatingDateColumns.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateMissingAppUserRatingDateColumns.cs similarity index 98% rename from API/Data/ManualMigrations/v0.8.9/MigrateMissingAppUserRatingDateColumns.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateMissingAppUserRatingDateColumns.cs index b3462560a..c8393e04b 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateMissingAppUserRatingDateColumns.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateMissingAppUserRatingDateColumns.cs @@ -2,11 +2,11 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// /// v0.8.9 - AppUserRating is missing Created/CreatedUtc/LastModified/LastModifiedUtc on old installs diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateMissingCreatedUtcDate.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateMissingCreatedUtcDate.cs similarity index 90% rename from API/Data/ManualMigrations/v0.8.9/MigrateMissingCreatedUtcDate.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateMissingCreatedUtcDate.cs index c8a664d5e..a61b54aab 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateMissingCreatedUtcDate.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateMissingCreatedUtcDate.cs @@ -1,13 +1,11 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// /// v0.8.9 - Some AppUser rows are missing CreatedUtc date diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateProgressToReadingSessions.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateProgressToReadingSessions.cs similarity index 96% rename from API/Data/ManualMigrations/v0.8.9/MigrateProgressToReadingSessions.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateProgressToReadingSessions.cs index 1a7e17468..fa89ff655 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateProgressToReadingSessions.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateProgressToReadingSessions.cs @@ -2,17 +2,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using API.Entities.Progress; -using API.Services.Reading; -using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Services.Reading; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateToAuthKeys.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateToAuthKeys.cs similarity index 87% rename from API/Data/ManualMigrations/v0.8.9/MigrateToAuthKeys.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateToAuthKeys.cs index 4009609a3..83f56f8c9 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateToAuthKeys.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateToAuthKeys.cs @@ -1,16 +1,13 @@ using System; -using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Entities.Enums.User; -using API.Entities.History; -using API.Entities.User; -using API.Helpers; -using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Helpers; +using Kavita.Database; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// /// v0.8.9 - Migrating from fixed api key to user-defined with configurable length @@ -29,7 +26,7 @@ public class MigrateToAuthKeys : ManualMigration foreach (var user in allUsers) { if (user.AuthKeys.Count != 0) continue; - + var key = new AppUserAuthKey() { Name = AuthKeyHelper.OpdsKeyName, diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateTotalReads.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateTotalReads.cs similarity index 89% rename from API/Data/ManualMigrations/v0.8.9/MigrateTotalReads.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateTotalReads.cs index 418de1f88..a2eeba5a5 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateTotalReads.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateTotalReads.cs @@ -1,15 +1,10 @@ -using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Entities.History; -using API.Entities.Progress; -using Kavita.Common.EnvironmentInfo; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// /// v0.8.9 - This migrates all records that diff --git a/API/Middleware/AuthKeyAuthenticationHandler.cs b/Kavita.Server/Middleware/AuthKeyAuthenticationHandler.cs similarity index 96% rename from API/Middleware/AuthKeyAuthenticationHandler.cs rename to Kavita.Server/Middleware/AuthKeyAuthenticationHandler.cs index bb0bc6c3a..913e0ca4e 100644 --- a/API/Middleware/AuthKeyAuthenticationHandler.cs +++ b/Kavita.Server/Middleware/AuthKeyAuthenticationHandler.cs @@ -5,11 +5,11 @@ using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Entities.Progress; -using API.Services; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Constants; +using Kavita.Models.Entities.Progress; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Hybrid; @@ -17,8 +17,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace API.Middleware; -#nullable enable +namespace Kavita.Server.Middleware; public class AuthKeyAuthenticationOptions : AuthenticationSchemeOptions { diff --git a/API/Middleware/RateLimit/AuthenticationRateLimiterPolicy.cs b/Kavita.Server/Middleware/AuthenticationRateLimiterPolicy.cs similarity index 95% rename from API/Middleware/RateLimit/AuthenticationRateLimiterPolicy.cs rename to Kavita.Server/Middleware/AuthenticationRateLimiterPolicy.cs index c2119bb13..dc5c0a4ac 100644 --- a/API/Middleware/RateLimit/AuthenticationRateLimiterPolicy.cs +++ b/Kavita.Server/Middleware/AuthenticationRateLimiterPolicy.cs @@ -6,8 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.RateLimiting; -namespace API.Middleware.RateLimit; -#nullable enable +namespace Kavita.Server.Middleware; public class AuthenticationRateLimiterPolicy : IRateLimiterPolicy { diff --git a/API/Services/Reading/ClientInfoAccessor.cs b/Kavita.Server/Middleware/ClientInfoAccessor.cs similarity index 66% rename from API/Services/Reading/ClientInfoAccessor.cs rename to Kavita.Server/Middleware/ClientInfoAccessor.cs index defaeb9c6..f3de2cf19 100644 --- a/API/Services/Reading/ClientInfoAccessor.cs +++ b/Kavita.Server/Middleware/ClientInfoAccessor.cs @@ -1,28 +1,9 @@ using System.Threading; -using API.Entities.Progress; -using API.Entities.User; +using Kavita.API.Services; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; -namespace API.Services.Reading; -#nullable enable - -/// -/// Provides access to client information for the current request. -/// This service captures details about the client making the request including -/// browser info, device type, authentication method, etc. -/// -public interface IClientInfoAccessor -{ - /// - /// Gets the client information for the current request. - /// Returns null if called outside an HTTP request context (e.g., background jobs). - /// - ClientInfoData? Current { get; } - string? CurrentUiFingerprint { get; } - /// - /// Client Device PK - /// - int? CurrentDeviceId { get; } -} +namespace Kavita.Server.Middleware; /// /// Thread-safe accessor for client information using AsyncLocal storage. diff --git a/API/Middleware/ClientInfoMiddleware.cs b/Kavita.Server/Middleware/ClientInfoMiddleware.cs similarity index 95% rename from API/Middleware/ClientInfoMiddleware.cs rename to Kavita.Server/Middleware/ClientInfoMiddleware.cs index ae34e0125..61015ec42 100644 --- a/API/Middleware/ClientInfoMiddleware.cs +++ b/Kavita.Server/Middleware/ClientInfoMiddleware.cs @@ -2,18 +2,16 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.Constants; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Extensions; -using API.Helpers; -using API.Services.Reading; -using API.Services.Store; +using Kavita.API.Store; +using Kavita.Common.Constants; +using Kavita.Common.Extensions; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Server.Helpers; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; -namespace API.Middleware; +namespace Kavita.Server.Middleware; /// diff --git a/API/Middleware/DeviceTrackingMiddleware.cs b/Kavita.Server/Middleware/DeviceTrackingMiddleware.cs similarity index 81% rename from API/Middleware/DeviceTrackingMiddleware.cs rename to Kavita.Server/Middleware/DeviceTrackingMiddleware.cs index c32d93048..a38b968de 100644 --- a/API/Middleware/DeviceTrackingMiddleware.cs +++ b/Kavita.Server/Middleware/DeviceTrackingMiddleware.cs @@ -1,14 +1,12 @@ using System; -using System.Diagnostics; using System.Threading.Tasks; -using API.Services; -using API.Services.Reading; -using API.Services.Store; +using Kavita.API.Attributes; +using Kavita.API.Services; +using Kavita.API.Store; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace API.Middleware; -#nullable enable +namespace Kavita.Server.Middleware; /// /// Middleware that identifies and tracks device activity for authenticated requests. @@ -63,10 +61,3 @@ public class DeviceTrackingMiddleware(RequestDelegate next, ILogger -/// Attribute to skip device tracking on specific endpoints. -/// Use for high-frequency endpoints where device tracking adds unnecessary overhead. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class SkipDeviceTrackingAttribute : Attribute; diff --git a/API/Middleware/ExceptionMiddleware.cs b/Kavita.Server/Middleware/ExceptionMiddleware.cs similarity index 89% rename from API/Middleware/ExceptionMiddleware.cs rename to Kavita.Server/Middleware/ExceptionMiddleware.cs index ec8418cf0..9d1e0cf77 100644 --- a/API/Middleware/ExceptionMiddleware.cs +++ b/Kavita.Server/Middleware/ExceptionMiddleware.cs @@ -2,12 +2,12 @@ using System.Net; using System.Text.Json; using System.Threading.Tasks; -using API.Errors; +using Kavita.API.Errors; using Kavita.Common; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace API.Middleware; +namespace Kavita.Server.Middleware; public class ExceptionMiddleware(RequestDelegate next, ILogger logger) { @@ -22,7 +22,7 @@ public class ExceptionMiddleware(RequestDelegate next, ILogger /// If the user is authenticated, will update the field. diff --git a/API/Middleware/UserContextMiddleware.cs b/Kavita.Server/Middleware/UserContextMiddleware.cs similarity index 91% rename from API/Middleware/UserContextMiddleware.cs rename to Kavita.Server/Middleware/UserContextMiddleware.cs index 1881c9fba..afba1939c 100644 --- a/API/Middleware/UserContextMiddleware.cs +++ b/Kavita.Server/Middleware/UserContextMiddleware.cs @@ -3,14 +3,12 @@ using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using API.Entities.Progress; -using API.Services; -using API.Services.Store; +using Kavita.Models.Entities.Progress; +using Kavita.Server.Store; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace API.Middleware; -#nullable enable +namespace Kavita.Server.Middleware; /// /// Middleware that resolves user identity from various authentication methods @@ -19,9 +17,7 @@ namespace API.Middleware; /// public class UserContextMiddleware(RequestDelegate next, ILogger logger) { - public async Task InvokeAsync( - HttpContext context, - UserContext userContext) + public async Task InvokeAsync(HttpContext context, UserContext userContext) { try { diff --git a/API/Program.cs b/Kavita.Server/Program.cs similarity index 95% rename from API/Program.cs rename to Kavita.Server/Program.cs index 15dad328e..98a61cf6d 100644 --- a/API/Program.cs +++ b/Kavita.Server/Program.cs @@ -4,15 +4,19 @@ using System.IO.Abstractions; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; -using API.Data; -using API.Data.ManualMigrations; -using API.Entities; -using API.Entities.Enums; -using API.Logging; -using API.Services; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Server.Logging; +using Kavita.Server.ManualMigrations.v0._7._14; +using Kavita.Server.ManualMigrations.v0._8._2; +using Kavita.Server.ManualMigrations.v0._8._4; +using Kavita.Services; +using Kavita.Services.SignalR; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -24,12 +28,11 @@ using Microsoft.Extensions.Logging; using NetVips; using Serilog; using Serilog.Events; +using Serilog.Formatting.Display; using Serilog.Sinks.AspNetCore.SignalR.Extensions; using Log = Serilog.Log; -using MessageTemplateTextFormatter = Serilog.Formatting.Display.MessageTemplateTextFormatter; -namespace API; -#nullable enable +namespace Kavita.Server; public class Program { diff --git a/Kavita.Server/Properties/launchSettings.json b/Kavita.Server/Properties/launchSettings.json new file mode 100644 index 000000000..66da4a630 --- /dev/null +++ b/Kavita.Server/Properties/launchSettings.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14778", + "sslPort": 44368 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Server": { + "workingDirectory": ".", + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/API/Startup.cs b/Kavita.Server/Startup.cs similarity index 94% rename from API/Startup.cs rename to Kavita.Server/Startup.cs index 368a86816..1fafdeea8 100644 --- a/API/Startup.cs +++ b/Kavita.Server/Startup.cs @@ -7,23 +7,34 @@ using System.Net; using System.Net.Sockets; using System.Reflection; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.ManualMigrations; -using API.DTOs.Internal; -using API.Entities.Enums; -using API.Extensions; -using API.Logging; -using API.Middleware; -using API.Middleware.RateLimit; -using API.Services; -using API.Services.HostedServices; -using API.Services.Tasks; -using API.SignalR; using Hangfire; using HtmlAgilityPack; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Constants; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Internal; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; +using Kavita.Server.Extensions; +using Kavita.Server.Logging; +using Kavita.Server.ManualMigrations.v0._7._11; +using Kavita.Server.ManualMigrations.v0._7._14; +using Kavita.Server.ManualMigrations.v0._7._9; +using Kavita.Server.ManualMigrations.v0._8._0; +using Kavita.Server.ManualMigrations.v0._8._1; +using Kavita.Server.ManualMigrations.v0._8._2; +using Kavita.Server.ManualMigrations.v0._8._4; +using Kavita.Server.ManualMigrations.v0._8._5; +using Kavita.Server.ManualMigrations.v0._8._6; +using Kavita.Server.ManualMigrations.v0._8._7; +using Kavita.Server.ManualMigrations.v0._8._8; +using Kavita.Server.ManualMigrations.v0._8._9; +using Kavita.Server.Middleware; +using Kavita.Services.SignalR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; @@ -40,9 +51,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi; using Serilog; -using TaskScheduler = API.Services.TaskScheduler; +using TaskScheduler = Kavita.Services.TaskScheduler; -namespace API; +namespace Kavita.Server; public class Startup { @@ -62,13 +73,9 @@ public class Startup public void ConfigureServices(IServiceCollection services) { services.Configure(_config); + services.AddMappings(); services.AddApplicationServices(_config, _env); - // Store keys inside database, such that cookies can be decrypted between container restarts - services.AddDataProtection() - .PersistKeysToDbContext() - .SetApplicationName(BuildInfo.AppName); - services.AddControllers(options => { options.CacheProfiles.Add(ResponseCacheProfiles.Minute, @@ -193,13 +200,8 @@ public class Startup // Add the processing server as IHostedService services.AddHangfireServer(options => { - options.Queues = [TaskScheduler.ScanQueue, TaskScheduler.DefaultQueue]; + options.Queues = [TaskSchedulerConstants.ScanQueue, TaskSchedulerConstants.DefaultQueue]; }); - - // Add IHostedService for startup tasks - // Any services that should be bootstrapped go here - services.AddHostedService(); - services.AddHostedService(); } private static void AddCompressionAndCaching(IServiceCollection services) @@ -244,7 +246,6 @@ public class Startup app.UseMiddleware(); app.UseMiddleware(); - if (env.IsDevelopment()) { app.UseSwagger(); diff --git a/API/Services/Store/CustomTicketStore.cs b/Kavita.Server/Store/CustomTicketStore.cs similarity index 98% rename from API/Services/Store/CustomTicketStore.cs rename to Kavita.Server/Store/CustomTicketStore.cs index 13a57af78..3b73dfed0 100644 --- a/API/Services/Store/CustomTicketStore.cs +++ b/Kavita.Server/Store/CustomTicketStore.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.Caching.Distributed; -namespace API.Services.Store; +namespace Kavita.Server.Store; /// /// The is used as for the OIDC implementation diff --git a/Kavita.Server/Store/UserContext.cs b/Kavita.Server/Store/UserContext.cs new file mode 100644 index 000000000..e527286ab --- /dev/null +++ b/Kavita.Server/Store/UserContext.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Kavita.API.Store; +using Kavita.Common; +using Kavita.Models.Entities.Progress; + +namespace Kavita.Server.Store; + +public class UserContext : IUserContext +{ + private int? _userId; + private string? _username; + private AuthenticationType _authType; + private List _roles = new(); + + public int? GetUserId() => _userId; + + public int GetUserIdOrThrow() + { + return _userId ?? throw new UnauthorizedAccessException(); + } + + public string? GetUsername() => _username; + + public AuthenticationType GetAuthenticationType() => _authType; + + public bool IsAuthenticated { get; private set; } + public IReadOnlyList Roles => _roles.AsReadOnly(); + + // Internal method used by middleware to set context + internal void SetUserContext(int userId, string username, AuthenticationType authType, IEnumerable roles) + { + _userId = userId; + _username = username; + _authType = authType; + IsAuthenticated = true; + _roles = roles?.ToList() ?? []; + } + + internal void Clear() + { + _userId = null; + _username = null; + _authType = AuthenticationType.Unknown; + IsAuthenticated = false; + _roles.Clear(); + } + + public bool HasRole(string role) + { + return _roles.Any(r => r.Equals(role, StringComparison.OrdinalIgnoreCase)); + } + + public bool HasAnyRole(params string[] roles) + { + return roles.Any(HasRole); + } + + public bool HasAllRoles(params string[] roles) + { + return roles.All(HasRole); + } +} diff --git a/API/config/appsettings.Development.json b/Kavita.Server/config/appsettings.Development.json similarity index 100% rename from API/config/appsettings.Development.json rename to Kavita.Server/config/appsettings.Development.json diff --git a/API/config/appsettings.json b/Kavita.Server/config/appsettings.json similarity index 100% rename from API/config/appsettings.json rename to Kavita.Server/config/appsettings.json diff --git a/API/config/templates/EmailChange.html b/Kavita.Server/config/templates/EmailChange.html similarity index 100% rename from API/config/templates/EmailChange.html rename to Kavita.Server/config/templates/EmailChange.html diff --git a/API/config/templates/EmailConfirm.html b/Kavita.Server/config/templates/EmailConfirm.html similarity index 100% rename from API/config/templates/EmailConfirm.html rename to Kavita.Server/config/templates/EmailConfirm.html diff --git a/API/config/templates/EmailMigration.html b/Kavita.Server/config/templates/EmailMigration.html similarity index 100% rename from API/config/templates/EmailMigration.html rename to Kavita.Server/config/templates/EmailMigration.html diff --git a/API/config/templates/EmailPasswordReset.html b/Kavita.Server/config/templates/EmailPasswordReset.html similarity index 100% rename from API/config/templates/EmailPasswordReset.html rename to Kavita.Server/config/templates/EmailPasswordReset.html diff --git a/API/config/templates/EmailTest.html b/Kavita.Server/config/templates/EmailTest.html similarity index 100% rename from API/config/templates/EmailTest.html rename to Kavita.Server/config/templates/EmailTest.html diff --git a/API/config/templates/SendToDevice.html b/Kavita.Server/config/templates/SendToDevice.html similarity index 100% rename from API/config/templates/SendToDevice.html rename to Kavita.Server/config/templates/SendToDevice.html diff --git a/API/config/templates/TokenExpiration.html b/Kavita.Server/config/templates/TokenExpiration.html similarity index 100% rename from API/config/templates/TokenExpiration.html rename to Kavita.Server/config/templates/TokenExpiration.html diff --git a/API/config/templates/TokenExpiringSoon.html b/Kavita.Server/config/templates/TokenExpiringSoon.html similarity index 100% rename from API/config/templates/TokenExpiringSoon.html rename to Kavita.Server/config/templates/TokenExpiringSoon.html diff --git a/API.Tests/Services/AccountServiceTests.cs b/Kavita.Services.Tests/AccountServiceTests.cs similarity index 95% rename from API.Tests/Services/AccountServiceTests.cs rename to Kavita.Services.Tests/AccountServiceTests.cs index 4989f4e78..5942ac032 100644 --- a/API.Tests/Services/AccountServiceTests.cs +++ b/Kavita.Services.Tests/AccountServiceTests.cs @@ -1,25 +1,23 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; using Kavita.Common; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class AccountServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -287,9 +285,9 @@ public class AccountServiceTests(ITestOutputHelper outputHelper): AbstractDbTest await userManager.CreateAsync(defaultAdmin); var accountService = new AccountService(userManager, Substitute.For>(), unitOfWork, mapper, Substitute.For()); - var settingsService = new SettingsService(unitOfWork, Substitute.For(), Substitute.For(), Substitute.For(), Substitute.For> (), Substitute.For()); + var settingsService = new SettingsService(unitOfWork, Substitute.For(), Substitute.For(), Substitute.For(), Substitute.For> (), Substitute.For(), Substitute.For()); user = await unitOfWork.UserRepository.GetUserByIdAsync(user.Id, AppUserIncludes.SideNavStreams); - return (user, accountService, userManager, settingsService); + return (user!, accountService, userManager, settingsService); } } diff --git a/API.Tests/Services/AnnotationServiceTests.cs b/Kavita.Services.Tests/AnnotationServiceTests.cs similarity index 94% rename from API.Tests/Services/AnnotationServiceTests.cs rename to Kavita.Services.Tests/AnnotationServiceTests.cs index 3929ab99d..02ebc52e4 100644 --- a/API.Tests/Services/AnnotationServiceTests.cs +++ b/Kavita.Services.Tests/AnnotationServiceTests.cs @@ -1,23 +1,24 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Reader; -using API.DTOs.Settings; -using API.Entities; -using API.Helpers.Builders; -using API.Services; -using API.SignalR; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class AnnotationServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/Kavita.Services.Tests/ArchiveServiceTests.cs similarity index 92% rename from API.Tests/Services/ArchiveServiceTests.cs rename to Kavita.Services.Tests/ArchiveServiceTests.cs index 489dd27ec..538b5be8f 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/Kavita.Services.Tests/ArchiveServiceTests.cs @@ -1,20 +1,18 @@ using System.Diagnostics; -using System.IO; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using System.IO.Compression; -using System.Linq; -using API.DTOs.Archive; -using API.Entities.Enums; -using API.Services; +using Kavita.API.Services; +using Kavita.Models.DTOs.Archive; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NetVips; using NSubstitute; using NSubstitute.Extensions; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ArchiveServiceTests { @@ -39,7 +37,7 @@ public class ArchiveServiceTests [InlineData("file in folder_alt.zip", true)] public void ArchiveNeedsFlatteningTest(string archivePath, bool expected) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives"); var file = Path.Join(testDirectory, archivePath); using var archive = ZipFile.OpenRead(file); Assert.Equal(expected, _archiveService.ArchiveNeedsFlattening(archive)); @@ -55,7 +53,7 @@ public class ArchiveServiceTests [InlineData("file in folder_alt.zip", true)] public void IsValidArchiveTest(string archivePath, bool expected) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives"); Assert.Equal(expected, _archiveService.IsValidArchive(Path.Join(testDirectory, archivePath))); } @@ -73,7 +71,7 @@ public class ArchiveServiceTests [InlineData("macos_withdotunder_one.zip", 1)] public void GetNumberOfPagesFromArchiveTest(string archivePath, int expected) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives"); var sw = Stopwatch.StartNew(); Assert.Equal(expected, _archiveService.GetNumberOfPagesFromArchive(Path.Join(testDirectory, archivePath))); _testOutputHelper.WriteLine($"Processed Original in {sw.ElapsedMilliseconds} ms"); @@ -92,7 +90,7 @@ public class ArchiveServiceTests public void CanOpenArchive(string archivePath, ArchiveLibrary expected) { var sw = Stopwatch.StartNew(); - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives"); Assert.Equal(expected, _archiveService.CanOpen(Path.Join(testDirectory, archivePath))); _testOutputHelper.WriteLine($"Processed Original in {sw.ElapsedMilliseconds} ms"); @@ -110,8 +108,8 @@ public class ArchiveServiceTests public void CanExtractArchive(string archivePath, int expectedFileCount) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives"); - var extractDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives/Extraction"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives"); + var extractDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives/Extraction"); _directoryService.ClearAndDeleteDirectory(extractDirectory); @@ -169,7 +167,7 @@ public class ArchiveServiceTests var imageService = new ImageService(Substitute.For>(), ds); var archiveService = Substitute.For(_logger, ds, imageService, Substitute.For()); - var testDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages")); + var testDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/CoverImages")); var expectedBytes = Image.Thumbnail(Path.Join(testDirectory, expectedOutputFile), 320).WriteToBuffer(".png"); archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.Default); @@ -201,7 +199,7 @@ public class ArchiveServiceTests var archiveService = Substitute.For(_logger, new DirectoryService(_directoryServiceLogger, new FileSystem()), imageService, Substitute.For()); - var testDirectory = API.Services.Tasks.Scanner.Parser.Parser.NormalizePath(Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages"))); + var testDirectory = Parser.NormalizePath(Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/CoverImages"))); var outputDir = Path.Join(testDirectory, "output"); _directoryService.ClearDirectory(outputDir); @@ -226,7 +224,7 @@ public class ArchiveServiceTests imageService.WriteCoverThumbnail(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(x => "cover.jpg"); var archiveService = new ArchiveService(_logger, _directoryService, imageService, Substitute.For()); - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/"); var inputPath = Path.GetFullPath(Path.Join(testDirectory, inputFile)); var outputPath = Path.Join(testDirectory, Path.GetFileNameWithoutExtension(inputFile) + "_output"); new DirectoryInfo(outputPath).Create(); @@ -240,7 +238,7 @@ public class ArchiveServiceTests [Fact] public void ShouldHaveComicInfo() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "ComicInfo.zip"); const string summaryInfo = "By all counts, Ryouta Sakamoto is a loser when he's not holed up in his room, bombing things into oblivion in his favorite online action RPG. But his very own uneventful life is blown to pieces when he's abducted and taken to an uninhabited island, where he soon learns the hard way that he's being pitted against others just like him in a explosives-riddled death match! How could this be happening? Who's putting them up to this? And why!? The name, not to mention the objective, of this very real survival game is eerily familiar to Ryouta, who has mastered its virtual counterpart-BTOOOM! Can Ryouta still come out on top when he's playing for his life!?"; @@ -252,7 +250,7 @@ public class ArchiveServiceTests [Fact] public void ShouldHaveComicInfo_CanParseUmlaut() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "Umlaut.zip"); var comicInfo = _archiveService.GetComicInfo(archive); @@ -263,7 +261,7 @@ public class ArchiveServiceTests [Fact] public void ShouldHaveComicInfo_WithAuthors() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "ComicInfo_authors.zip"); var comicInfo = _archiveService.GetComicInfo(archive); @@ -277,7 +275,7 @@ public class ArchiveServiceTests [InlineData("ComicInfo_duplicateInfos.rar")] public void ShouldHaveComicInfo_TopLevelFileOnly(string filename) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, filename); var comicInfo = _archiveService.GetComicInfo(archive); @@ -288,7 +286,7 @@ public class ArchiveServiceTests [Fact] public void ShouldHaveComicInfo_OutsideRoot() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "ComicInfo_outside_root.zip"); var comicInfo = _archiveService.GetComicInfo(archive); @@ -299,7 +297,7 @@ public class ArchiveServiceTests [Fact] public void ShouldHaveComicInfo_OutsideRoot_SharpCompress() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "ComicInfo_outside_root_SharpCompress.cb7"); var comicInfo = _archiveService.GetComicInfo(archive); @@ -314,7 +312,7 @@ public class ArchiveServiceTests [Fact] public void CanParseComicInfo() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "ComicInfo.zip"); var comicInfo = _archiveService.GetComicInfo(archive); @@ -338,7 +336,7 @@ public class ArchiveServiceTests [Fact] public void CanParseComicInfo_DefaultNumberIsBlank() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "ComicInfo2.zip"); var comicInfo = _archiveService.GetComicInfo(archive); diff --git a/API.Tests/Services/BackupServiceTests.cs b/Kavita.Services.Tests/BackupServiceTests.cs similarity index 89% rename from API.Tests/Services/BackupServiceTests.cs rename to Kavita.Services.Tests/BackupServiceTests.cs index 284bbc058..423b07fbb 100644 --- a/API.Tests/Services/BackupServiceTests.cs +++ b/Kavita.Services.Tests/BackupServiceTests.cs @@ -1,30 +1,19 @@ -using System; -using System.Data.Common; -using System.IO; +using System.Collections; using System.IO.Abstractions.TestingHelpers; -using System.Linq; using System.Reflection; -using System.Threading.Tasks; -using API.Data; -using API.Data.AutoMapper; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks; -using API.SignalR; using AutoMapper; using Hangfire; -using Microsoft.Data.Sqlite; +using Kavita.API.Services.SignalR; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.AutoMapper; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class BackupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -47,8 +36,8 @@ public class BackupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest( var backupService = new BackupService(_logger, unitOfWork, ds, _messageHub); var backupLogFiles = backupService.GetLogFiles(false).ToList(); - Assert.Single(backupLogFiles); - Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath($"{LogDirectory}kavita.log"), API.Services.Tasks.Scanner.Parser.Parser.NormalizePath(backupLogFiles.First())); + Assert.Single((IEnumerable)backupLogFiles); + Assert.Equal(Parser.NormalizePath($"{LogDirectory}kavita.log"), Parser.NormalizePath(backupLogFiles.First())); } [Fact] @@ -63,8 +52,8 @@ public class BackupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest( var ds = new DirectoryService(Substitute.For>(), filesystem); var backupService = new BackupService(_logger, unitOfWork, ds, _messageHub); - var backupLogFiles = backupService.GetLogFiles().Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList(); - Assert.Contains(backupLogFiles, file => file.Equals(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath($"{LogDirectory}kavita.log")) || file.Equals(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath($"{LogDirectory}kavita1.log"))); + var backupLogFiles = backupService.GetLogFiles().Select(Parser.NormalizePath).ToList(); + Assert.Contains(backupLogFiles, file => file.Equals(Parser.NormalizePath($"{LogDirectory}kavita.log")) || file.Equals(Parser.NormalizePath($"{LogDirectory}kavita1.log"))); } diff --git a/API.Tests/Services/BookServiceTests.cs b/Kavita.Services.Tests/BookServiceTests.cs similarity index 87% rename from API.Tests/Services/BookServiceTests.cs rename to Kavita.Services.Tests/BookServiceTests.cs index 1fb4c175d..edf94f639 100644 --- a/API.Tests/Services/BookServiceTests.cs +++ b/Kavita.Services.Tests/BookServiceTests.cs @@ -1,15 +1,12 @@ -using System.IO; -using System.IO.Abstractions; -using System.Threading.Tasks; -using API.Data; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using System.IO.Abstractions; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class BookServiceTests { @@ -32,14 +29,14 @@ public class BookServiceTests [InlineData("test.pdf", 1)] public void GetNumberOfPagesTest(string filePath, int expectedPages) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); Assert.Equal(expectedPages, _bookService.GetNumberOfPages(Path.Join(testDirectory, filePath))); } [Fact] public void ShouldHaveComicInfo() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var archive = Path.Join(testDirectory, "The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub"); const string summaryInfo = "Book Description"; @@ -52,7 +49,7 @@ public class BookServiceTests [Fact] public void ShouldHaveComicInfo_WithAuthors() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var archive = Path.Join(testDirectory, "The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub"); var comicInfo = _bookService.GetComicInfo(archive); @@ -63,7 +60,7 @@ public class BookServiceTests [Fact] public void ShouldParseAsVolumeGroup_WithoutSeriesIndex() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var archive = Path.Join(testDirectory, "TitleWithVolume_NoSeriesOrSeriesIndex.epub"); var comicInfo = _bookService.GetComicInfo(archive); @@ -75,7 +72,7 @@ public class BookServiceTests [Fact] public void ShouldParseAsVolumeGroup_WithSeriesIndex() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var archive = Path.Join(testDirectory, "TitleWithVolume.epub"); var comicInfo = _bookService.GetComicInfo(archive); @@ -87,7 +84,7 @@ public class BookServiceTests [Fact] public void ShouldHaveComicInfoForPdf() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var document = Path.Join(testDirectory, "test.pdf"); var comicInfo = _bookService.GetComicInfo(document); Assert.NotNull(comicInfo); @@ -98,7 +95,7 @@ public class BookServiceTests //[Fact] public void ShouldUsePdfInfoDict() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Books/PDFs"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/Library/Books/PDFs"); var document = Path.Join(testDirectory, "Rollo at Work SP01.pdf"); var comicInfo = _bookService.GetComicInfo(document); Assert.NotNull(comicInfo); @@ -110,7 +107,7 @@ public class BookServiceTests [Fact] public void ShouldHandleIndirectPdfObjects() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var document = Path.Join(testDirectory, "indirect.pdf"); var comicInfo = _bookService.GetComicInfo(document); Assert.NotNull(comicInfo); @@ -121,7 +118,7 @@ public class BookServiceTests [Fact] public void FailGracefullyWithEncryptedPdf() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var document = Path.Join(testDirectory, "encrypted.pdf"); var comicInfo = _bookService.GetComicInfo(document); Assert.Null(comicInfo); @@ -133,7 +130,7 @@ public class BookServiceTests var ds = new DirectoryService(Substitute.For>(), new FileSystem()); var pdfParser = new PdfParser(ds); - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var filePath = Path.Join(testDirectory, "Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf"); var comicInfo = _bookService.GetComicInfo(filePath); @@ -151,7 +148,7 @@ public class BookServiceTests [Fact] public async Task ShouldBeAbleToLookUpImage() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var filePath = Path.Join(testDirectory, "Relative Key Test File.epub"); var result = await _bookService.GetResourceAsync(filePath, "./images/titlepage800.png"); diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/Kavita.Services.Tests/BookmarkServiceTests.cs similarity index 91% rename from API.Tests/Services/BookmarkServiceTests.cs rename to Kavita.Services.Tests/BookmarkServiceTests.cs index 0e486d844..669d25e7f 100644 --- a/API.Tests/Services/BookmarkServiceTests.cs +++ b/Kavita.Services.Tests/BookmarkServiceTests.cs @@ -1,21 +1,20 @@ -using System.Collections.Generic; -using System.IO; +using System.Collections; using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class BookmarkServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -38,7 +37,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) @@ -69,7 +68,7 @@ Substitute.For()); Assert.True(result); - Assert.Single(ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories)); + Assert.Single((IEnumerable)ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories)); Assert.NotNull(await unitOfWork.UserRepository.GetBookmarkAsync(1)); } @@ -86,7 +85,7 @@ Substitute.For()); .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") .WithMinNumber(1) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .Build()) .Build()) .Build(); @@ -262,7 +261,7 @@ Substitute.For()); var files = await bookmarkService.GetBookmarkFilesById(new[] {1}); var actualFiles = ds.GetFiles(BookmarkDirectory, searchOption: SearchOption.AllDirectories); - Assert.Equal(files.Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList(), actualFiles.Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList()); + Assert.Equal(files.Select(Parser.NormalizePath).ToList(), actualFiles.Select(Parser.NormalizePath).ToList()); } diff --git a/API.Tests/FakeHybridCache.cs b/Kavita.Services.Tests/Cache/FakeHybridCache.cs similarity index 96% rename from API.Tests/FakeHybridCache.cs rename to Kavita.Services.Tests/Cache/FakeHybridCache.cs index 65629a541..b6f2ada54 100644 --- a/API.Tests/FakeHybridCache.cs +++ b/Kavita.Services.Tests/Cache/FakeHybridCache.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Caching.Hybrid; -namespace API.Tests; +namespace Kavita.Services.Tests.Cache; public class FakeHybridCache : HybridCache { diff --git a/API.Tests/FakeHybridCacheWithTracking.cs b/Kavita.Services.Tests/Cache/FakeHybridCacheWithTracking.cs similarity index 87% rename from API.Tests/FakeHybridCacheWithTracking.cs rename to Kavita.Services.Tests/Cache/FakeHybridCacheWithTracking.cs index 21c8dd691..d2ae17b90 100644 --- a/API.Tests/FakeHybridCacheWithTracking.cs +++ b/Kavita.Services.Tests/Cache/FakeHybridCacheWithTracking.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Caching.Hybrid; -namespace API.Tests; +namespace Kavita.Services.Tests.Cache; public class FakeHybridCacheWithTracking : FakeHybridCache { diff --git a/API.Tests/Services/CacheServiceTests.cs b/Kavita.Services.Tests/CacheServiceTests.cs similarity index 97% rename from API.Tests/Services/CacheServiceTests.cs rename to Kavita.Services.Tests/CacheServiceTests.cs index a1464ba83..b09198f03 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/Kavita.Services.Tests/CacheServiceTests.cs @@ -1,18 +1,17 @@ -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data.Metadata; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using System.IO.Abstractions.TestingHelpers; +using Kavita.API.Services; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using Kavita.Services.Builders; +using Kavita.Services.Reading; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; internal class MockReadingItemServiceForCacheService : IReadingItemService { diff --git a/API.Tests/Services/CleanupServiceTests.cs b/Kavita.Services.Tests/CleanupServiceTests.cs similarity index 95% rename from API.Tests/Services/CleanupServiceTests.cs rename to Kavita.Services.Tests/CleanupServiceTests.cs index 3b7cff2a6..6123f3cc4 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/Kavita.Services.Tests/CleanupServiceTests.cs @@ -1,29 +1,29 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.Collections; using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Filtering; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.Services.Tasks; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -136,7 +136,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest // Add 2 series with cover images context.Series.Add(new SeriesBuilder("Test 1") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithCoverImage("v01_c01.jpg").Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithCoverImage("v01_c01.jpg").Build()) .WithCoverImage("v01_c01.jpg") .Build()) .WithCoverImage("series_01.jpg") @@ -145,7 +145,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest context.Series.Add(new SeriesBuilder("Test 2") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithCoverImage("v01_c03.jpg").Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithCoverImage("v01_c03.jpg").Build()) .WithCoverImage("v01_c03.jpg") .Build()) .WithCoverImage("series_03.jpg") @@ -308,7 +308,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(logger, unitOfWork, messageHub, ds); await cleanupService.CleanupBackups(); - Assert.Single(ds.GetFiles(BackupDirectory, searchOption: SearchOption.AllDirectories)); + Assert.Single((IEnumerable)ds.GetFiles(BackupDirectory, searchOption: SearchOption.AllDirectories)); } [Fact] @@ -345,7 +345,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest var filesystem = CreateFileSystem(); foreach (var i in Enumerable.Range(1, 10)) { - var day = API.Services.Tasks.Scanner.Parser.Parser.PadZeros($"{i}"); + var day = Parser.PadZeros($"{i}"); filesystem.AddFile($"{LogDirectory}kavita202009{day}.log", new MockFileData("") { CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31)) @@ -358,7 +358,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(logger, unitOfWork, messageHub, ds); await cleanupService.CleanupLogs(); - Assert.Single(ds.GetFiles(LogDirectory, searchOption: SearchOption.AllDirectories)); + Assert.Single((IEnumerable)ds.GetFiles(LogDirectory, searchOption: SearchOption.AllDirectories)); } [Fact] @@ -367,7 +367,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest var filesystem = CreateFileSystem(); foreach (var i in Enumerable.Range(1, 9)) { - var day = API.Services.Tasks.Scanner.Parser.Parser.PadZeros($"{i}"); + var day = Parser.PadZeros($"{i}"); filesystem.AddFile($"{LogDirectory}kavita202009{day}.log", new MockFileData("") { CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31 - i)) @@ -402,12 +402,12 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest var (unitOfWork, context, _) = await CreateDatabase(); var (logger, messageHub, readerService) = await Setup(unitOfWork, context); - var c = new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + var c = new ChapterBuilder(Parser.DefaultChapter) .WithPages(1) .Build(); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(c) .Build()) .Build(); @@ -622,7 +622,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest c.UserProgress = new List(); s.Volumes = new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume).WithChapter(c).Build() + new VolumeBuilder(Parser.LooseLeafVolume).WithChapter(c).Build() }; context.Series.Add(s); diff --git a/API.Tests/Services/ClientDeviceServiceTests.cs b/Kavita.Services.Tests/ClientDeviceServiceTests.cs similarity index 85% rename from API.Tests/Services/ClientDeviceServiceTests.cs rename to Kavita.Services.Tests/ClientDeviceServiceTests.cs index 97783f8ca..13384eccb 100644 --- a/API.Tests/Services/ClientDeviceServiceTests.cs +++ b/Kavita.Services.Tests/ClientDeviceServiceTests.cs @@ -1,22 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Entities.User; -using API.Helpers.Builders; -using API.Services; +using System.Collections; using Kavita.Common; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; #nullable enable public class ClientDeviceServiceTests : AbstractDbTest @@ -34,8 +28,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_RegistersNewDevice_WhenNoExistingMatch() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -54,15 +48,15 @@ public class ClientDeviceServiceTests : AbstractDbTest Assert.Equal("Chrome on Windows", device.FriendlyName); Assert.True(device.IsActive); Assert.NotNull(device.CurrentClientInfo); - Assert.Single(device.History); + Assert.Single((IEnumerable)device.History); } [Fact] public async Task IdentifyOrRegisterDeviceAsync_MatchesExistingDevice_ByClientDeviceId() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -99,8 +93,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_MatchesExistingDevice_ByFingerprint_WhenClientDeviceIdNull() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -127,8 +121,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_UpdatesClientDeviceId_WhenFingerprintMatches() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -152,8 +146,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_UsesFuzzyMatching_ForBrowserVersionUpgrade() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -174,8 +168,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_CreatesNewDevice_WhenPlatformChanges() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -196,8 +190,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_IgnoresInactiveDevices() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -236,8 +230,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateDeviceFingerprint_GeneratesConsistentHash_ForSameInput() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -259,8 +253,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateDeviceFingerprint_Fallbacks_WhenBrowserChangesOneMajorVersion() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -281,8 +275,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateDeviceFingerprint_GeneratesDifferentHash_WhenBrowserChangesTwoMajorVersions() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -302,8 +296,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateDeviceFingerprint_IsCaseInsensitive() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -325,8 +319,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateDeviceFingerprint_UsesMajorVersionOnly_ForFingerprinting() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -348,8 +342,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_MatchesSameDevice_ForMinorBrowserVersionChanges() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -375,8 +369,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task FuzzyMatching_Matches_WithHighSimilarity() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -398,8 +392,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task FuzzyMatching_DoesNotMatch_WithLowSimilarity() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -421,8 +415,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task FuzzyMatching_OnlyConsidersRecentDevices_Within30Days() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -460,8 +454,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task UpdateDeviceActivity_UpdatesLastSeenUtc() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -484,8 +478,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task UpdateDeviceActivity_AddsHistoryRecord_WhenMeaningfulChanges() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -510,8 +504,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task UpdateDeviceActivity_DoesNotAddHistory_ForNonMeaningfulChanges() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -542,8 +536,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateFriendlyName_IncludesBrowserAndPlatform() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -562,8 +556,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateFriendlyName_UsesClientType_WhenNotWebBrowser() { // TODO: Remove these tests - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -582,8 +576,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateFriendlyName_HandlesNoPlatform() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -606,8 +600,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GetUserDevicesAsync_ReturnsOnlyActiveDevices_ByDefault() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -619,7 +613,7 @@ public class ClientDeviceServiceTests : AbstractDbTest await context.SaveChangesAsync(); // Act - var devices = (await service.GetUserDevicesAsync(user.Id, includeInactive: false)).ToList(); + var devices = (await unitOfWork.ClientDeviceRepository.GetUserDevicesAsync(user.Id, includeInactive: false)).ToList(); // Assert Assert.Single(devices); @@ -630,8 +624,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GetUserDevicesAsync_ReturnsAllDevices_WhenIncludeInactiveTrue() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -643,7 +637,7 @@ public class ClientDeviceServiceTests : AbstractDbTest await context.SaveChangesAsync(); // Act - var devices = await service.GetUserDevicesAsync(user.Id, includeInactive: true); + var devices = await unitOfWork.ClientDeviceRepository.GetUserDevicesAsync(user.Id, includeInactive: true); // Assert Assert.Equal(2, devices.Count()); @@ -653,8 +647,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task RenameDeviceAsync_UpdatesDeviceName() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -677,8 +671,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task RenameDeviceAsync_ReturnsFalse_WhenDeviceNotFound() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -695,8 +689,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task RemoveDeviceAsync_MarksDeviceAsInactive() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -719,8 +713,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task RemoveDeviceAsync_ReturnsFalse_WhenDeviceNotFound() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); diff --git a/API.Tests/Services/CollectionTagServiceTests.cs b/Kavita.Services.Tests/CollectionTagServiceTests.cs similarity index 96% rename from API.Tests/Services/CollectionTagServiceTests.cs rename to Kavita.Services.Tests/CollectionTagServiceTests.cs index e6b0b5000..178ec9c10 100644 --- a/API.Tests/Services/CollectionTagServiceTests.cs +++ b/Kavita.Services.Tests/CollectionTagServiceTests.cs @@ -1,25 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Collection; -using API.Entities; -using API.Entities.Enums; -using API.Extensions.QueryExtensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common; -using Microsoft.EntityFrameworkCore; +using Kavita.Database; +using Kavita.Database.Extensions; +using Kavita.Database.Tests; +using Kavita.Models; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class CollectionTagServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -29,7 +25,7 @@ public class CollectionTagServiceTests(ITestOutputHelper outputHelper): Abstract if (context.AppUserCollection.Any()) { - return new CollectionTagService(unitOfWork, Substitute.For()); + return new CollectionTagService(unitOfWork, Substitute.For(), Substitute.For()); } var s1 = new SeriesBuilder("Series 1").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Mature).Build()).Build(); @@ -39,7 +35,7 @@ public class CollectionTagServiceTests(ITestOutputHelper outputHelper): Abstract .WithSeries(s2) .Build()); - var user = new AppUserBuilder("majora2007", "majora2007", Seed.DefaultThemes.First()).Build(); + var user = new AppUserBuilder("majora2007", "majora2007", Defaults.DefaultThemes.First()).Build(); user.Collections = new List() { new AppUserCollectionBuilder("Tag 1").WithItems(new []{s1}).Build(), @@ -49,7 +45,7 @@ public class CollectionTagServiceTests(ITestOutputHelper outputHelper): Abstract await unitOfWork.CommitAsync(); - return new CollectionTagService(unitOfWork, Substitute.For()); + return new CollectionTagService(unitOfWork, Substitute.For(), Substitute.For()); } #region DeleteTag @@ -199,7 +195,7 @@ public class CollectionTagServiceTests(ITestOutputHelper outputHelper): Abstract var service = await Setup(unitOfWork, context); // Create a second user - var user2 = new AppUserBuilder("user2", "user2", Seed.DefaultThemes.First()).Build(); + var user2 = new AppUserBuilder("user2", "user2", Defaults.DefaultThemes.First()).Build(); unitOfWork.UserRepository.Add(user2); await unitOfWork.CommitAsync(); diff --git a/Kavita.Services.Tests/Comparers/ChapterSortComparerTest.cs b/Kavita.Services.Tests/Comparers/ChapterSortComparerTest.cs new file mode 100644 index 000000000..4bea7d3a6 --- /dev/null +++ b/Kavita.Services.Tests/Comparers/ChapterSortComparerTest.cs @@ -0,0 +1,18 @@ +using Kavita.Services.Comparators; +using Kavita.Services.Scanner; + +namespace Kavita.Services.Tests.Comparers; + +public class ChapterSortComparerDefaultLastTest +{ + [Theory] + [InlineData(new[] {1, 2, Parser.DefaultChapterNumber}, new[] {1, 2, Parser.DefaultChapterNumber})] + [InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})] + [InlineData(new[] {1, Parser.DefaultChapterNumber, Parser.DefaultChapterNumber}, new[] {1, Parser.DefaultChapterNumber, Parser.DefaultChapterNumber})] + [InlineData(new[] {Parser.DefaultChapterNumber, 1}, new[] {1, Parser.DefaultChapterNumber})] + public void ChapterSortTest(int[] input, int[] expected) + { + Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerDefaultLast()).ToArray()); + } + +} diff --git a/API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs b/Kavita.Services.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs similarity index 88% rename from API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs rename to Kavita.Services.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs index fbae46b59..ecacd4864 100644 --- a/API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs +++ b/Kavita.Services.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs @@ -1,8 +1,6 @@ -using System.Linq; -using API.Comparators; -using Xunit; +using Kavita.Services.Comparators; -namespace API.Tests.Comparers; +namespace Kavita.Services.Tests.Comparers; public class ChapterSortComparerDefaultFirstTests { diff --git a/Kavita.Services.Tests/Comparers/SortComparerZeroLastTests.cs b/Kavita.Services.Tests/Comparers/SortComparerZeroLastTests.cs new file mode 100644 index 000000000..d432087f0 --- /dev/null +++ b/Kavita.Services.Tests/Comparers/SortComparerZeroLastTests.cs @@ -0,0 +1,16 @@ +using Kavita.Services.Comparators; +using Kavita.Services.Scanner; + +namespace Kavita.Services.Tests.Comparers; + +public class SortComparerZeroLastTests +{ + [Theory] + [InlineData(new[] {Parser.DefaultChapterNumber, 1, 2,}, new[] {1, 2, Parser.DefaultChapterNumber})] + [InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})] + [InlineData(new[] {Parser.DefaultChapterNumber, Parser.DefaultChapterNumber, 1}, new[] {1, Parser.DefaultChapterNumber, Parser.DefaultChapterNumber})] + public void SortComparerZeroLastTest(int[] input, int[] expected) + { + Assert.Equal(expected, input.OrderBy(f => f, ChapterSortComparerDefaultLast.Default).ToArray()); + } +} diff --git a/API.Tests/Services/CoverDbServiceTests.cs b/Kavita.Services.Tests/CoverDbServiceTests.cs similarity index 90% rename from API.Tests/Services/CoverDbServiceTests.cs rename to Kavita.Services.Tests/CoverDbServiceTests.cs index d4886e0d9..2f7238df7 100644 --- a/API.Tests/Services/CoverDbServiceTests.cs +++ b/Kavita.Services.Tests/CoverDbServiceTests.cs @@ -1,34 +1,33 @@ -using System.IO; -using System.Reflection; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Entities.Enums; -using API.Extensions; -using API.Services; -using API.Services.Tasks.Metadata; -using API.SignalR; +using System.Reflection; using EasyCaching.Core; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Database.Tests; +using Kavita.Models.Constants; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Metadata; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class CoverDbServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { private static readonly IEasyCachingProviderFactory CacheFactory = Substitute.For(); private static readonly string FaviconPath = Path.Join(Directory.GetCurrentDirectory(), - "../../../Services/Test Data/CoverDbService/Favicons"); + "../../../Test Data/CoverDbService/Favicons"); /// /// Path to download files temp to. Should be empty after each test. /// private static readonly string TempPath = Path.Join(Directory.GetCurrentDirectory(), - "../../../Services/Test Data/CoverDbService/Temp"); + "../../../Test Data/CoverDbService/Temp"); private static Task<(IDirectoryService, ICoverDbService)> Setup(IUnitOfWork unitOfWork) diff --git a/API.Tests/Data/AesopsFables.epub b/Kavita.Services.Tests/Data/AesopsFables.epub similarity index 100% rename from API.Tests/Data/AesopsFables.epub rename to Kavita.Services.Tests/Data/AesopsFables.epub diff --git a/API.Tests/Services/DeviceServiceTests.cs b/Kavita.Services.Tests/DeviceServiceTests.cs similarity index 85% rename from API.Tests/Services/DeviceServiceTests.cs rename to Kavita.Services.Tests/DeviceServiceTests.cs index 12a79a03a..643610f4a 100644 --- a/API.Tests/Services/DeviceServiceTests.cs +++ b/Kavita.Services.Tests/DeviceServiceTests.cs @@ -1,17 +1,16 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using API.Data; -using API.DTOs.Device; -using API.DTOs.Device.EmailDevice; -using API.Entities; -using API.Entities.Enums.Device; -using API.Services; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.Database.Tests; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.Device; +using Kavita.Models.Entities.User; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class DeviceServiceDbTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Services/DeviceTrackingServiceTests.cs b/Kavita.Services.Tests/DeviceTrackingServiceTests.cs similarity index 98% rename from API.Tests/Services/DeviceTrackingServiceTests.cs rename to Kavita.Services.Tests/DeviceTrackingServiceTests.cs index f62f5d474..2e8f065a7 100644 --- a/API.Tests/Services/DeviceTrackingServiceTests.cs +++ b/Kavita.Services.Tests/DeviceTrackingServiceTests.cs @@ -1,21 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Entities.User; -using API.Helpers.Builders; -using API.Services; +using Kavita.API.Services; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Tests.Cache; using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -using System.Linq; -using API.Entities; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; #nullable enable diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/Kavita.Services.Tests/DirectoryServiceTests.cs similarity index 95% rename from API.Tests/Services/DirectoryServiceTests.cs rename to Kavita.Services.Tests/DirectoryServiceTests.cs index abc8254de..7839b9326 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/Kavita.Services.Tests/DirectoryServiceTests.cs @@ -1,20 +1,16 @@ -using System; -using System.Collections.Generic; +using System.Collections; using System.Globalization; -using System.IO; using System.IO.Abstractions.TestingHelpers; -using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; -using API.Services; using Kavita.Common.Helpers; +using Kavita.Database.Tests; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class DirectoryServiceTests: AbstractFsTest { @@ -43,7 +39,7 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); var files = new List(); var fileCount = ds.TraverseTreeParallelForEach(testDirectory, s => files.Add(s), - API.Services.Tasks.Scanner.Parser.Parser.ArchiveFileExtensions, _logger); + Parser.ArchiveFileExtensions, _logger); Assert.Equal(28, fileCount); Assert.Equal(28, files.Count); @@ -68,7 +64,7 @@ public class DirectoryServiceTests: AbstractFsTest try { var fileCount = ds.TraverseTreeParallelForEach("/manga/", s => files.Add(s), - API.Services.Tasks.Scanner.Parser.Parser.ImageFileExtensions, _logger); + Parser.ImageFileExtensions, _logger); Assert.Equal(1, fileCount); } catch @@ -100,7 +96,7 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); var files = new List(); var fileCount = ds.TraverseTreeParallelForEach(testDirectory, s => files.Add(s), - API.Services.Tasks.Scanner.Parser.Parser.ArchiveFileExtensions, _logger); + Parser.ArchiveFileExtensions, _logger); Assert.Equal(28, fileCount); Assert.Equal(28, files.Count); @@ -121,7 +117,7 @@ public class DirectoryServiceTests: AbstractFsTest fileSystem.AddFile($"{testDirectory}file_{29}.jpg", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), fileSystem); - var files = ds.GetFilesWithExtension(testDirectory, API.Services.Tasks.Scanner.Parser.Parser.ArchiveFileExtensions); + var files = ds.GetFilesWithExtension(testDirectory, Parser.ArchiveFileExtensions); Assert.Equal(10, files.Length); Assert.All(files, s => fileSystem.Path.GetExtension(s).Equals(".zip")); @@ -160,7 +156,7 @@ public class DirectoryServiceTests: AbstractFsTest fileSystem.AddFile($"{testDirectory}file_{29}.jpg", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), fileSystem); - var files = ds.GetFiles(testDirectory, API.Services.Tasks.Scanner.Parser.Parser.ArchiveFileExtensions).ToList(); + var files = ds.GetFiles(testDirectory, Parser.ArchiveFileExtensions).ToList(); Assert.Equal(10, files.Count); Assert.All(files, s => fileSystem.Path.GetExtension(s).Equals(".zip")); @@ -403,7 +399,8 @@ public class DirectoryServiceTests: AbstractFsTest fileSystem.AddFile($"{testDirectory}data-0.txt", new MockFileData("abc")); var ds = new DirectoryService(Substitute.For>(), fileSystem); - Assert.False(ds.IsDriveMounted("d:/manga/")); + // Windows GA runners mount on D drive, so we use E to ensure this test passes + Assert.False(ds.IsDriveMounted("e:/manga/")); } [Fact] @@ -615,12 +612,12 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); ds.CopyFilesToDirectory(new []{MockUnixSupport.Path($"{testDirectory}file.zip")}, "/manga/output/"); ds.CopyFilesToDirectory(new []{MockUnixSupport.Path($"{testDirectory}file.zip")}, "/manga/output/"); - var outputFiles = ds.GetFiles("/manga/output/").Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList(); + var outputFiles = ds.GetFiles("/manga/output/").Select(Parser.NormalizePath).ToList(); Assert.Equal(4, outputFiles.Count); // we have 2 already there and 2 copies // For some reason, this has C:/ on directory even though everything is emulated (System.IO.Abstractions issue, not changing) // https://github.com/TestableIO/System.IO.Abstractions/issues/831 - Assert.True(outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("/manga/output/file (3).zip")) - || outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("C:/manga/output/file (3).zip"))); + Assert.True(outputFiles.Contains(Parser.NormalizePath("/manga/output/file (3).zip")) + || outputFiles.Contains(Parser.NormalizePath("C:/manga/output/file (3).zip"))); } [Fact] @@ -632,12 +629,12 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); ds.CopyFilesToDirectory(new []{MockUnixSupport.Path($"{testDirectory}file.zip")}, "/manga/output/", new [] {"01"}); - var outputFiles = ds.GetFiles("/manga/output/").Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList(); - Assert.Single(outputFiles); + var outputFiles = ds.GetFiles("/manga/output/").Select(Parser.NormalizePath).ToList(); + Assert.Single((IEnumerable)outputFiles); // For some reason, this has C:/ on directory even though everything is emulated (System.IO.Abstractions issue, not changing) // https://github.com/TestableIO/System.IO.Abstractions/issues/831 - Assert.True(outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("/manga/output/01.zip")) - || outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("C:/manga/output/01.zip"))); + Assert.True(outputFiles.Contains(Parser.NormalizePath("/manga/output/01.zip")) + || outputFiles.Contains(Parser.NormalizePath("C:/manga/output/01.zip"))); } #endregion @@ -683,7 +680,7 @@ public class DirectoryServiceTests: AbstractFsTest fileSystem.AddFile($"{testDirectory}file_0.zip", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), fileSystem); - Assert.Single(ds.ListDirectory(testDirectory)); + Assert.Single((IEnumerable)ds.ListDirectory(testDirectory)); } #endregion @@ -965,7 +962,7 @@ public class DirectoryServiceTests: AbstractFsTest var globMatcher = new GlobMatcher(); globMatcher.AddExclude("*.*"); - var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions, globMatcher); + var allFiles = ds.ScanFiles("C:/Data/", Parser.SupportedExtensions, globMatcher); Assert.Empty(allFiles); @@ -991,9 +988,9 @@ public class DirectoryServiceTests: AbstractFsTest var globMatcher = new GlobMatcher(); globMatcher.AddExclude("**/Accel World/*"); - var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions, globMatcher); + var allFiles = ds.ScanFiles("C:/Data/", Parser.SupportedExtensions, globMatcher); - Assert.Single(allFiles); // Ignore files are not counted in files, only valid extensions + Assert.Single((IEnumerable)allFiles); // Ignore files are not counted in files, only valid extensions return Task.CompletedTask; } @@ -1023,7 +1020,7 @@ public class DirectoryServiceTests: AbstractFsTest var globMatcher = new GlobMatcher(); globMatcher.AddExclude("**/Accel World/*"); globMatcher.AddExclude("**/ArtBooks/*"); - var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions, globMatcher); + var allFiles = ds.ScanFiles("C:/Data/", Parser.SupportedExtensions, globMatcher); Assert.Equal(2, allFiles.Count); // Ignore files are not counted in files, only valid extensions @@ -1047,7 +1044,7 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); - var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions); + var allFiles = ds.ScanFiles("C:/Data/", Parser.SupportedExtensions); Assert.Equal(5, allFiles.Count); diff --git a/API.Tests/Entities/ComicInfoTests.cs b/Kavita.Services.Tests/Entities/ComicInfoTests.cs similarity index 90% rename from API.Tests/Entities/ComicInfoTests.cs rename to Kavita.Services.Tests/Entities/ComicInfoTests.cs index e43f4ee77..a832166a8 100644 --- a/API.Tests/Entities/ComicInfoTests.cs +++ b/Kavita.Services.Tests/Entities/ComicInfoTests.cs @@ -1,8 +1,8 @@ -using API.Data.Metadata; -using API.Entities.Enums; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Services.Extensions; -namespace API.Tests.Entities; +namespace Kavita.Services.Tests.Entities; public class ComicInfoTests { @@ -103,7 +103,7 @@ public class ComicInfoTests public void IsValid(string code) { // Note: ASIN's starting with "B0" are not able to be converted to ISBN - Assert.Equal(code, ComicInfo.ParseGtin(code)); + Assert.Equal(code, ComicInfoExtensions.ParseGtin(code)); } [Theory] @@ -111,7 +111,7 @@ public class ComicInfoTests [InlineData("9504000059437 ")] public void IsInvalid(string code) { - Assert.Equal(string.Empty, ComicInfo.ParseGtin(code)); + Assert.Equal(string.Empty, ComicInfoExtensions.ParseGtin(code)); } #endregion } diff --git a/API.Tests/Services/EntityNamingServiceTests.cs b/Kavita.Services.Tests/EntityNamingServiceTests.cs similarity index 99% rename from API.Tests/Services/EntityNamingServiceTests.cs rename to Kavita.Services.Tests/EntityNamingServiceTests.cs index c6b6cac95..a1d5a1146 100644 --- a/API.Tests/Services/EntityNamingServiceTests.cs +++ b/Kavita.Services.Tests/EntityNamingServiceTests.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using API.DTOs; -using API.DTOs.ReadingLists; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Xunit; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; #nullable enable public class EntityNamingServiceTests diff --git a/API.Tests/Extensions/ChapterListExtensionsTests.cs b/Kavita.Services.Tests/Extensions/ChapterListExtensionsTests.cs similarity index 67% rename from API.Tests/Extensions/ChapterListExtensionsTests.cs rename to Kavita.Services.Tests/Extensions/ChapterListExtensionsTests.cs index 7ef546e4f..14c881010 100644 --- a/API.Tests/Extensions/ChapterListExtensionsTests.cs +++ b/Kavita.Services.Tests/Extensions/ChapterListExtensionsTests.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner.Parser; -using Xunit; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Builders; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; -namespace API.Tests.Extensions; +namespace Kavita.Services.Tests.Extensions; public class ChapterListExtensionsTests { @@ -29,7 +27,7 @@ public class ChapterListExtensionsTests { var info = new ParserInfo() { - Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, + Chapters = Parser.DefaultChapter, Edition = "", Format = MangaFormat.Archive, FullFilePath = "/manga/darker than black.cbz", @@ -37,12 +35,12 @@ public class ChapterListExtensionsTests IsSpecial = false, Series = "darker than black", Title = "darker than black", - Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume + Volumes = Parser.LooseLeafVolume }; var chapterList = new List() { - CreateChapter("darker than black - Some special", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/darker than black - special.cbz", MangaFormat.Archive), true) + CreateChapter("darker than black - Some special", Parser.DefaultChapter, CreateFile("/manga/darker than black - special.cbz", MangaFormat.Archive), true) }; var actualChapter = chapterList.GetChapterByRange(info); @@ -56,7 +54,7 @@ public class ChapterListExtensionsTests { var info = new ParserInfo() { - Chapters = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, + Chapters = Parser.LooseLeafVolume, Edition = "", Format = MangaFormat.Archive, FullFilePath = "/manga/darker than black.cbz", @@ -64,12 +62,12 @@ public class ChapterListExtensionsTests IsSpecial = true, Series = "darker than black", Title = "darker than black", - Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume + Volumes = Parser.LooseLeafVolume }; var chapterList = new List() { - CreateChapter("darker than black", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true) + CreateChapter("darker than black", Parser.DefaultChapter, CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true) }; var actualChapter = chapterList.GetChapterByRange(info); @@ -82,7 +80,7 @@ public class ChapterListExtensionsTests { var info = new ParserInfo() { - Chapters = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, + Chapters = Parser.LooseLeafVolume, Edition = "", Format = MangaFormat.Archive, FullFilePath = "/manga/detective comics #001.cbz", @@ -90,13 +88,13 @@ public class ChapterListExtensionsTests IsSpecial = true, Series = "detective comics", Title = "detective comics", - Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume + Volumes = Parser.LooseLeafVolume }; var chapterList = new List() { - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) + CreateChapter("detective comics", Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), + CreateChapter("detective comics", Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) }; var actualChapter = chapterList.GetChapterByRange(info); @@ -117,7 +115,7 @@ public class ChapterListExtensionsTests IsSpecial = false, Series = "detective comics", Title = "detective comics", - Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume + Volumes = Parser.LooseLeafVolume }; var chapterList = new List() @@ -137,7 +135,7 @@ public class ChapterListExtensionsTests { var chapterList = new List() { - CreateChapter("darker than black", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true), + CreateChapter("darker than black", Parser.DefaultChapter, CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true), CreateChapter("darker than black", "1", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), false), }; @@ -176,8 +174,8 @@ public class ChapterListExtensionsTests { var chapterList = new List() { - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) + CreateChapter("detective comics", Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), + CreateChapter("detective comics", Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) }; chapterList[0].ReleaseDate = new DateTime(10, 1, 1, 0, 0, 0, DateTimeKind.Utc); @@ -191,8 +189,8 @@ public class ChapterListExtensionsTests { var chapterList = new List() { - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) + CreateChapter("detective comics", Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), + CreateChapter("detective comics", Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) }; chapterList[0].ReleaseDate = new DateTime(2002, 1, 1, 0, 0, 0, DateTimeKind.Utc); diff --git a/API.Tests/Extensions/FilterDtoExtensionsTests.cs b/Kavita.Services.Tests/Extensions/FilterDtoExtensionsTests.cs similarity index 83% rename from API.Tests/Extensions/FilterDtoExtensionsTests.cs rename to Kavita.Services.Tests/Extensions/FilterDtoExtensionsTests.cs index c9985f509..bfb558269 100644 --- a/API.Tests/Extensions/FilterDtoExtensionsTests.cs +++ b/Kavita.Services.Tests/Extensions/FilterDtoExtensionsTests.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using API.DTOs.Filtering; -using API.Entities.Enums; -using API.Extensions; -using Xunit; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; -namespace API.Tests.Extensions; +namespace Kavita.Services.Tests.Extensions; public class FilterDtoExtensionsTests { diff --git a/API.Tests/Extensions/ParserInfoListExtensionsTests.cs b/Kavita.Services.Tests/Extensions/ParserInfoListExtensionsTests.cs similarity index 86% rename from API.Tests/Extensions/ParserInfoListExtensionsTests.cs rename to Kavita.Services.Tests/Extensions/ParserInfoListExtensionsTests.cs index 227dd2b32..04511182a 100644 --- a/API.Tests/Extensions/ParserInfoListExtensionsTests.cs +++ b/Kavita.Services.Tests/Extensions/ParserInfoListExtensionsTests.cs @@ -1,17 +1,13 @@ -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using System.IO.Abstractions.TestingHelpers; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Builders; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Extensions; +namespace Kavita.Services.Tests.Extensions; public class ParserInfoListExtensions { @@ -41,7 +37,7 @@ public class ParserInfoListExtensions { infos.Add(_defaultParser.Parse( Path.Join("E:/Manga/Cynthia the Mission/", filename), - "E:/Manga/", "E:/Manga/", LibraryType.Manga)); + "E:/Manga/", "E:/Manga/", LibraryType.Manga)!); } var files = inputChapters.Select(s => new MangaFileBuilder(s, MangaFormat.Archive, 199).Build()).ToList(); @@ -59,7 +55,7 @@ public class ParserInfoListExtensions { _defaultParser.Parse( "E:/Manga/Cynthia the Mission/Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip", - "E:/Manga/", "E:/Manga/", LibraryType.Manga) + "E:/Manga/", "E:/Manga/", LibraryType.Manga)! }; var files = new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip"} diff --git a/API.Tests/Extensions/SeriesExtensionsTests.cs b/Kavita.Services.Tests/Extensions/SeriesExtensionsTests.cs similarity index 98% rename from API.Tests/Extensions/SeriesExtensionsTests.cs rename to Kavita.Services.Tests/Extensions/SeriesExtensionsTests.cs index adaecfba5..d9c0ece19 100644 --- a/API.Tests/Extensions/SeriesExtensionsTests.cs +++ b/Kavita.Services.Tests/Extensions/SeriesExtensionsTests.cs @@ -1,12 +1,11 @@ -using System.Linq; -using API.Comparators; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner.Parser; -using Xunit; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Builders; +using Kavita.Services.Comparators; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; -namespace API.Tests.Extensions; +namespace Kavita.Services.Tests.Extensions; public class SeriesExtensionsTests { diff --git a/API.Tests/Extensions/SeriesFilterTests.cs b/Kavita.Services.Tests/Extensions/SeriesFilterTests.cs similarity index 98% rename from API.Tests/Extensions/SeriesFilterTests.cs rename to Kavita.Services.Tests/Extensions/SeriesFilterTests.cs index dedc43b05..64cdf1b6b 100644 --- a/API.Tests/Extensions/SeriesFilterTests.cs +++ b/Kavita.Services.Tests/Extensions/SeriesFilterTests.cs @@ -1,27 +1,26 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.DTOs; -using API.DTOs.Filtering.v2; -using API.DTOs.Progress; -using API.Entities; -using API.Entities.Enums; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Database; +using Kavita.Database.Extensions.Filters; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Extensions; +namespace Kavita.Services.Tests.Extensions; public class SeriesFilterTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Extensions/VolumeListExtensionsTests.cs b/Kavita.Services.Tests/Extensions/VolumeListExtensionsTests.cs similarity index 59% rename from API.Tests/Extensions/VolumeListExtensionsTests.cs rename to Kavita.Services.Tests/Extensions/VolumeListExtensionsTests.cs index bbb8f215c..583454a4a 100644 --- a/API.Tests/Extensions/VolumeListExtensionsTests.cs +++ b/Kavita.Services.Tests/Extensions/VolumeListExtensionsTests.cs @@ -1,11 +1,10 @@ -using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; -using Xunit; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Builders; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; -namespace API.Tests.Extensions; +namespace Kavita.Services.Tests.Extensions; public class VolumeListExtensionsTests { @@ -20,14 +19,14 @@ public class VolumeListExtensionsTests .WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("4").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build(), }; @@ -42,16 +41,16 @@ public class VolumeListExtensionsTests var volumes = new List() { new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("0.5").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build(), }; @@ -69,13 +68,13 @@ public class VolumeListExtensionsTests .WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("4").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build(), }; @@ -92,13 +91,13 @@ public class VolumeListExtensionsTests .WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("4").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build(), }; @@ -115,13 +114,13 @@ public class VolumeListExtensionsTests .WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("4").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build(), }; @@ -141,10 +140,10 @@ public class VolumeListExtensionsTests new VolumeBuilder("1") .WithChapter(new ChapterBuilder("1").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build(), }; diff --git a/API.Tests/Services/ExternalMetadataServiceTests.cs b/Kavita.Services.Tests/ExternalMetadataServiceTests.cs similarity index 99% rename from API.Tests/Services/ExternalMetadataServiceTests.cs rename to Kavita.Services.Tests/ExternalMetadataServiceTests.cs index 0a94b3d36..60f0e538d 100644 --- a/API.Tests/Services/ExternalMetadataServiceTests.cs +++ b/Kavita.Services.Tests/ExternalMetadataServiceTests.cs @@ -1,32 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Recommendation; -using API.DTOs.Scrobbling; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Helpers.Builders; -using API.Services.Plus; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; -using AutoMapper; +using AutoMapper; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Plus; +using Kavita.API.Services.SignalR; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Recommendation; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; +using Kavita.Services.Builders; +using Kavita.Services.Plus; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; /// /// Given these rely on Kavita+, this will not have any [Fact]/[Theory] on them and must be manually checked @@ -101,6 +100,9 @@ public class ExternalMetadataServiceTests: AbstractDbTest Substitute.For(), Substitute.For(), Substitute.For()); + // Clear tracker so test body starts with a clean slate + context.ChangeTracker.Clear(); + return (externalMetadataService, genreLookup, tagLookup, personLookup); } diff --git a/API.Tests/Services/FileSystemTests.cs b/Kavita.Services.Tests/FileSystemTests.cs similarity index 87% rename from API.Tests/Services/FileSystemTests.cs rename to Kavita.Services.Tests/FileSystemTests.cs index 97250ea45..6b306059d 100644 --- a/API.Tests/Services/FileSystemTests.cs +++ b/Kavita.Services.Tests/FileSystemTests.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.IO.Abstractions.TestingHelpers; -using API.Services; -using Xunit; +using System.IO.Abstractions.TestingHelpers; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class FileSystemTests { diff --git a/API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs b/Kavita.Services.Tests/Helpers/BookSortTitlePrefixHelperTests.cs similarity index 99% rename from API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs rename to Kavita.Services.Tests/Helpers/BookSortTitlePrefixHelperTests.cs index e1f585806..671a32951 100644 --- a/API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/BookSortTitlePrefixHelperTests.cs @@ -1,7 +1,6 @@ -using API.Helpers; -using Xunit; +using Kavita.Services.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public class BookSortTitlePrefixHelperTests { diff --git a/API.Tests/Helpers/CacheHelperTests.cs b/Kavita.Services.Tests/Helpers/CacheHelperTests.cs similarity index 97% rename from API.Tests/Helpers/CacheHelperTests.cs rename to Kavita.Services.Tests/Helpers/CacheHelperTests.cs index 3962ba2df..1afa25b92 100644 --- a/API.Tests/Helpers/CacheHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/CacheHelperTests.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using Xunit; +using System.IO.Abstractions.TestingHelpers; +using Kavita.API.Services.Helpers; +using Kavita.Database.Tests; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Builders; +using Kavita.Services.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public class CacheHelperTests: AbstractFsTest { diff --git a/API.Tests/Helpers/KoreaderHelperTests.cs b/Kavita.Services.Tests/Helpers/KoreaderHelperTests.cs similarity index 98% rename from API.Tests/Helpers/KoreaderHelperTests.cs rename to Kavita.Services.Tests/Helpers/KoreaderHelperTests.cs index 1d4710f2c..31ef27a56 100644 --- a/API.Tests/Helpers/KoreaderHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/KoreaderHelperTests.cs @@ -1,9 +1,7 @@ -using API.DTOs.Progress; -using API.Helpers; -using Xunit; +using Kavita.Models.DTOs.Progress; +using Kavita.Services.Helpers; -namespace API.Tests.Helpers; -#nullable enable +namespace Kavita.Services.Tests.Helpers; public class KoreaderHelperTests { diff --git a/API.Tests/Helpers/OrderableHelperTests.cs b/Kavita.Services.Tests/Helpers/OrderableHelperTests.cs similarity index 97% rename from API.Tests/Helpers/OrderableHelperTests.cs rename to Kavita.Services.Tests/Helpers/OrderableHelperTests.cs index e0f18a60d..5cbeb730e 100644 --- a/API.Tests/Helpers/OrderableHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/OrderableHelperTests.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.Entities; -using API.Helpers; -using Xunit; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; +using Kavita.Models.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public class OrderableHelperTests { diff --git a/API.Tests/Helpers/ParserInfoFactory.cs b/Kavita.Services.Tests/Helpers/ParserInfoFactory.cs similarity index 90% rename from API.Tests/Helpers/ParserInfoFactory.cs rename to Kavita.Services.Tests/Helpers/ParserInfoFactory.cs index 40d0ea4f4..5c1353974 100644 --- a/API.Tests/Helpers/ParserInfoFactory.cs +++ b/Kavita.Services.Tests/Helpers/ParserInfoFactory.cs @@ -1,13 +1,9 @@ using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Common.Extensions; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public static class ParserInfoFactory { diff --git a/API.Tests/Helpers/PersonHelperTests.cs b/Kavita.Services.Tests/Helpers/PersonHelperTests.cs similarity index 97% rename from API.Tests/Helpers/PersonHelperTests.cs rename to Kavita.Services.Tests/Helpers/PersonHelperTests.cs index cee7c47c8..333c7db5c 100644 --- a/API.Tests/Helpers/PersonHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/PersonHelperTests.cs @@ -1,13 +1,11 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using Xunit; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Builders; +using Kavita.Services.Helpers; using Xunit.Abstractions; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public class PersonHelperTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/Kavita.Services.Tests/Helpers/ReviewHelperTests.cs b/Kavita.Services.Tests/Helpers/ReviewHelperTests.cs new file mode 100644 index 000000000..61764aa84 --- /dev/null +++ b/Kavita.Services.Tests/Helpers/ReviewHelperTests.cs @@ -0,0 +1,127 @@ +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Services.Helpers; + +namespace Kavita.Services.Tests.Helpers; + +public class ReviewHelperTests +{ + #region SelectSpectrumOfReviews Tests + + [Fact] + public void SelectSpectrumOfReviews_WhenLessThan10Reviews_ReturnsAllReviews() + { + + var reviews = CreateReviewList(8); + + // Act + var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); + + // Assert + Assert.Equal(8, result.Count); + Assert.Equal(reviews, result.OrderByDescending(r => r.Score)); + } + + [Fact] + public void SelectSpectrumOfReviews_WhenMoreThan10Reviews_Returns10Reviews() + { + + var reviews = CreateReviewList(20); + + // Act + var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); + + // Assert + Assert.Equal(10, result.Count); + Assert.Equal(reviews[0], result.First()); + Assert.Equal(reviews[19], result.Last()); + } + + [Fact] + public void SelectSpectrumOfReviews_WithExactly10Reviews_ReturnsAllReviews() + { + + var reviews = CreateReviewList(10); + + // Act + var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); + + // Assert + Assert.Equal(10, result.Count); + } + + [Fact] + public void SelectSpectrumOfReviews_WithLargeNumberOfReviews_ReturnsCorrectSpectrum() + { + + var reviews = CreateReviewList(100); + + // Act + var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); + + // Assert + Assert.Equal(10, result.Count); + Assert.Contains(reviews[0], result); + Assert.Contains(reviews[1], result); + Assert.Contains(reviews[98], result); + Assert.Contains(reviews[99], result); + } + + [Fact] + public void SelectSpectrumOfReviews_WithEmptyList_ReturnsEmptyList() + { + + var reviews = new List(); + + // Act + var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); + + // Assert + Assert.Empty(result); + } + + [Fact] + public void SelectSpectrumOfReviews_ResultsOrderedByScoreDescending() + { + + var reviews = new List + { + new UserReviewDto { Tagline = "1", Score = 3 }, + new UserReviewDto { Tagline = "2", Score = 5 }, + new UserReviewDto { Tagline = "3", Score = 1 }, + new UserReviewDto { Tagline = "4", Score = 4 }, + new UserReviewDto { Tagline = "5", Score = 2 } + }; + + // Act + var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); + + // Assert + Assert.Equal(5, result.Count); + Assert.Equal(5, result[0].Score); + Assert.Equal(4, result[1].Score); + Assert.Equal(3, result[2].Score); + Assert.Equal(2, result[3].Score); + Assert.Equal(1, result[4].Score); + } + + #endregion + + #region Helper Methods + + private static List CreateReviewList(int count) + { + var reviews = new List(); + for (var i = 0; i < count; i++) + { + reviews.Add(new UserReviewDto + { + Tagline = $"{i + 1}", + Score = count - i // This makes them ordered by score descending initially + }); + } + return reviews; + } + + #endregion +} + diff --git a/API.Tests/Helpers/ScannerHelper.cs b/Kavita.Services.Tests/Helpers/ScannerHelper.cs similarity index 95% rename from API.Tests/Helpers/ScannerHelper.cs rename to Kavita.Services.Tests/Helpers/ScannerHelper.cs index 7d5263661..be8ebba65 100644 --- a/API.Tests/Helpers/ScannerHelper.cs +++ b/Kavita.Services.Tests/Helpers/ScannerHelper.cs @@ -1,41 +1,39 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; +using System.IO.Abstractions; using System.IO.Compression; -using System.Linq; using System.Text; using System.Text.Json; -using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; -using API.Data; -using API.Data.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Tasks; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner; -using API.SignalR; +using Kavita.API.Database; +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.Scanner; +using Kavita.API.Services.SignalR; +using Kavita.Database; +using Kavita.Models; +using Kavita.Models.Builders; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit.Abstractions; -namespace API.Tests.Helpers; -#nullable enable +namespace Kavita.Services.Tests.Helpers; public class ScannerHelper { private readonly IUnitOfWork _unitOfWork; private readonly ITestOutputHelper _testOutputHelper; - private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"); - private readonly string _testcasesDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/TestCases"); - private readonly string _imagePath = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/1x1.png"); + private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/ScanTests"); + private readonly string _testcasesDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/TestCases"); + private readonly string _imagePath = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/1x1.png"); private static readonly string[] ComicInfoExtensions = [".cbz", ".cbr", ".zip", ".rar"]; private static readonly string[] EpubExtensions = [".epub"]; @@ -55,7 +53,7 @@ public class ScannerHelper .WithFolders([new FolderPath() {Path = testDirectoryPath}]) .Build(); - var admin = new AppUserBuilder("admin", "admin@kavita.com", Seed.DefaultThemes[0]) + var admin = new AppUserBuilder("admin", "admin@kavita.com", Defaults.DefaultThemes[0]) .WithLibrary(library) .Build(); diff --git a/API.Tests/Helpers/SeriesHelperTests.cs b/Kavita.Services.Tests/Helpers/SeriesHelperTests.cs similarity index 96% rename from API.Tests/Helpers/SeriesHelperTests.cs rename to Kavita.Services.Tests/Helpers/SeriesHelperTests.cs index 22b4a3cd1..00f235b1e 100644 --- a/API.Tests/Helpers/SeriesHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/SeriesHelperTests.cs @@ -1,14 +1,11 @@ -using System.Collections.Generic; -using System.Linq; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner; -using Xunit; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public class SeriesHelperTests { diff --git a/API.Tests/Helpers/SmartFilterHelperTests.cs b/Kavita.Services.Tests/Helpers/SmartFilterHelperTests.cs similarity index 62% rename from API.Tests/Helpers/SmartFilterHelperTests.cs rename to Kavita.Services.Tests/Helpers/SmartFilterHelperTests.cs index 974cb0ba6..651c0b5d0 100644 --- a/API.Tests/Helpers/SmartFilterHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/SmartFilterHelperTests.cs @@ -1,29 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.Data.ManualMigrations; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; -using API.Helpers; -using Xunit; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public class SmartFilterHelperTests { - [Theory] - [InlineData("", false)] - [InlineData("name=DC%20-%20On%20Deck&stmts=comparison%3D1%26field%3D20%26value%3D0,comparison%3D9%26field%3D20%26value%3D100,comparison%3D0%26field%3D19%26value%3D274&sortOptions=sortField%3D1&isAscending=True&limitTo=0&combination=1", true)] - [InlineData("name=English%20In%20Progress&stmts=comparison%253D8%252Cfield%253D7%252Cvalue%253D4%25252C3,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D8%252Cfield%253D3%252Cvalue%253Dja,comparison%253D1%252Cfield%253D20%252Cvalue%253D0&sortOptions=sortField%3D7,isAscending%3DFalse&limitTo=0&combination=1", true)] - [InlineData("name=Unread%20Isekai%20Light%20Novels&stmts=comparison%253D0%25C2%25A6field%253D20%25C2%25A6value%253D0%EF%BF%BDcomparison%253D5%25C2%25A6field%253D6%25C2%25A6value%253D230%EF%BF%BDcomparison%253D8%25C2%25A6field%253D7%25C2%25A6value%253D4%EF%BF%BDcomparison%253D0%25C2%25A6field%253D19%25C2%25A6value%253D14&sortOptions=sortField%3D5%C2%A6isAscending%3DFalse&limitTo=0&combination=1", false)] - [InlineData("name=Zero&stmts=comparison%3d7%26field%3d1%26value%3d0&sortOptions=sortField=2&isAscending=False&limitTo=0&combination=1", true)] - public void Test_ShouldMigrateFilter(string filter, bool expected) - { - Assert.Equal(expected, MigrateSmartFilterEncoding.ShouldMigrateFilter(filter)); - } - [Fact] public void Test_Decode() { @@ -126,24 +110,6 @@ public class SmartFilterHelperTests Assert.False(decoded.SortOptions.IsAscending); } - [Theory] - [InlineData("name=DC%20-%20On%20Deck&stmts=comparison%3D1%26field%3D20%26value%3D0,comparison%3D9%26field%3D20%26value%3D100,comparison%3D0%26field%3D19%26value%3D274&sortOptions=sortField%3D1&isAscending=True&limitTo=0&combination=1")] - [InlineData("name=Manga%20-%20On%20Deck&stmts=comparison%253D1%252Cfield%253D20%252Cvalue%253D0,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D0%252Cfield%253D19%252Cvalue%253D2&sortOptions=sortField%3D1,isAscending%3DTrue&limitTo=0&combination=1")] - [InlineData("name=English%20In%20Progress&stmts=comparison%253D8%252Cfield%253D7%252Cvalue%253D4%25252C3,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D8%252Cfield%253D3%252Cvalue%253Dja,comparison%253D1%252Cfield%253D20%252Cvalue%253D0&sortOptions=sortField%3D7,isAscending%3DFalse&limitTo=0&combination=1")] - public void MigrationWorks(string filter) - { - try - { - var updatedFilter = MigrateSmartFilterEncoding.EncodeFix(filter); - Assert.NotNull(updatedFilter); - } - catch (Exception ex) - { - Assert.Fail("Exception thrown: " + ex.Message); - } - - } - private static void AssertStatementSame(FilterStatementDto statement, FilterStatementDto statement2) { Assert.Equal(statement.Field, statement2.Field); diff --git a/API.Tests/Helpers/TestCaseGenerator.cs b/Kavita.Services.Tests/Helpers/TestCaseGenerator.cs similarity index 97% rename from API.Tests/Helpers/TestCaseGenerator.cs rename to Kavita.Services.Tests/Helpers/TestCaseGenerator.cs index 833da0502..58e4c5322 100644 --- a/API.Tests/Helpers/TestCaseGenerator.cs +++ b/Kavita.Services.Tests/Helpers/TestCaseGenerator.cs @@ -1,6 +1,4 @@ -using System.IO; - -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; /// /// Given a -testcase.txt file, will generate a folder with fake archive or book files. These files are just renamed txt files. diff --git a/API.Tests/Services/ImageServiceTests.cs b/Kavita.Services.Tests/ImageServiceTests.cs similarity index 96% rename from API.Tests/Services/ImageServiceTests.cs rename to Kavita.Services.Tests/ImageServiceTests.cs index f2c87e1ad..bfa586da0 100644 --- a/API.Tests/Services/ImageServiceTests.cs +++ b/Kavita.Services.Tests/ImageServiceTests.cs @@ -1,18 +1,14 @@ -using System.IO; -using System.Linq; -using System.Text; -using API.Entities.Enums; -using API.Services; +using System.Text; +using Kavita.Models.Entities.Enums; using NetVips; -using Xunit; using Image = NetVips.Image; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ImageServiceTests { - private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ImageService/Covers"); - private readonly string _testDirectoryColorScapes = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ImageService/ColorScapes"); + private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ImageService/Covers"); + private readonly string _testDirectoryColorScapes = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ImageService/ColorScapes"); private const string OutputPattern = "_output"; private const string BaselinePattern = "_baseline"; diff --git a/Kavita.Services.Tests/Kavita.Services.Tests.csproj b/Kavita.Services.Tests/Kavita.Services.Tests.csproj new file mode 100644 index 000000000..15b334b9d --- /dev/null +++ b/Kavita.Services.Tests/Kavita.Services.Tests.csproj @@ -0,0 +1,42 @@ + + + + net10.0 + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + PreserveNewest + + + I18N\%(RecursiveDir)%(FileName)%(Extension) + PreserveNewest + + + + diff --git a/API.Tests/Services/MetadataServiceTests.cs b/Kavita.Services.Tests/MetadataServiceTests.cs similarity index 81% rename from API.Tests/Services/MetadataServiceTests.cs rename to Kavita.Services.Tests/MetadataServiceTests.cs index 01a084242..1a6737cb2 100644 --- a/API.Tests/Services/MetadataServiceTests.cs +++ b/Kavita.Services.Tests/MetadataServiceTests.cs @@ -1,18 +1,15 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using API.Helpers; -using API.Services; +using System.IO.Abstractions.TestingHelpers; +using Kavita.API.Services.Helpers; +using Kavita.Services.Helpers; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class MetadataServiceTests { - private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives"); + private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives"); private const string TestCoverImageFile = "thumbnail.jpg"; private const string TestCoverArchive = @"c:\file in folder.zip"; - private readonly string _testCoverImageDirectory = Path.Join(Directory.GetCurrentDirectory(), @"../../../Services/Test Data/ArchiveService/CoverImages"); + private readonly string _testCoverImageDirectory = Path.Join(Directory.GetCurrentDirectory(), @"../../../Test Data/ArchiveService/CoverImages"); //private readonly MetadataService _metadataService; // private readonly IUnitOfWork _unitOfWork = Substitute.For(); // private readonly IImageService _imageService = Substitute.For(); diff --git a/API.Tests/Services/OidcServiceTests.cs b/Kavita.Services.Tests/OidcServiceTests.cs similarity index 98% rename from API.Tests/Services/OidcServiceTests.cs rename to Kavita.Services.Tests/OidcServiceTests.cs index 13dafd6d7..9cd859bb3 100644 --- a/API.Tests/Services/OidcServiceTests.cs +++ b/Kavita.Services.Tests/OidcServiceTests.cs @@ -1,19 +1,18 @@ -using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; -using System.Linq; using System.Security.Claims; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Scanner; using Kavita.Common; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; @@ -21,10 +20,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class OidcServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -645,7 +643,8 @@ public class OidcServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(ou Substitute.For(), Substitute.For(), Substitute.For>(), - Substitute.For() + Substitute.For(), + Substitute.For() ); private static ClaimsPrincipal BuildPrincipal(IEnumerable claims) diff --git a/API.Tests/Services/OpdsServiceTests.cs b/Kavita.Services.Tests/OpdsServiceTests.cs similarity index 97% rename from API.Tests/Services/OpdsServiceTests.cs rename to Kavita.Services.Tests/OpdsServiceTests.cs index 8b1602369..88425af46 100644 --- a/API.Tests/Services/OpdsServiceTests.cs +++ b/Kavita.Services.Tests/OpdsServiceTests.cs @@ -1,37 +1,38 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.OPDS; -using API.DTOs.OPDS.Requests; -using API.DTOs.Progress; -using API.Constants; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.SignalR; +using System.IO.Abstractions; using AutoMapper; using Hangfire; using Hangfire.InMemory; +using Kavita.API.Database; +using Kavita.API.Errors; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Common.Helpers; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.OPDS; +using Kavita.Models.DTOs.OPDS.Requests; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class OpdsServiceTests(ITestOutputHelper testOutputHelper) : AbstractDbTest(testOutputHelper) { - private readonly string _testFilePath = Path.Join(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/OpdsService"), "test.zip"); + private readonly string _testFilePath = Path.Join(Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/OpdsService"), "test.zip"); #region Setup @@ -89,7 +90,7 @@ public class OpdsServiceTests(ITestOutputHelper testOutputHelper) : AbstractDbTe foreach (var i in Enumerable.Range(0, numberOfSeries)) { var series = new SeriesBuilder("Test " + (i + 1)) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithSortOrder(counter) .WithPages(10) @@ -411,7 +412,7 @@ public class OpdsServiceTests(ITestOutputHelper testOutputHelper) : AbstractDbTe var (opdsService, _) = SetupService(unitOfWork, mapper); var user = await SetupSeriesAndUser(context, unitOfWork); - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => { await opdsService.Search(new OpdsSearchRequest { diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/Kavita.Services.Tests/ParseScannedFilesTests.cs similarity index 98% rename from API.Tests/Services/ParseScannedFilesTests.cs rename to Kavita.Services.Tests/ParseScannedFilesTests.cs index 2c064eb75..20d8bab8b 100644 --- a/API.Tests/Services/ParseScannedFilesTests.cs +++ b/Kavita.Services.Tests/ParseScannedFilesTests.cs @@ -1,26 +1,21 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; +using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Metadata; -using API.Data.Repositories; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; -using API.Tests.Helpers; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Database.Tests; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using Kavita.Services.Scanner; +using Kavita.Services.Tests.Helpers; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class MockReadingItemService : IReadingItemService { diff --git a/API.Tests/Parsers/BasicParserTests.cs b/Kavita.Services.Tests/Parsers/BasicParserTests.cs similarity index 97% rename from API.Tests/Parsers/BasicParserTests.cs rename to Kavita.Services.Tests/Parsers/BasicParserTests.cs index 32673e0e6..dacf96ed8 100644 --- a/API.Tests/Parsers/BasicParserTests.cs +++ b/Kavita.Services.Tests/Parsers/BasicParserTests.cs @@ -1,13 +1,11 @@ -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using System.IO.Abstractions.TestingHelpers; +using Kavita.Database.Tests; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Parsers; +namespace Kavita.Services.Tests.Parsers; public class BasicParserTests : AbstractFsTest { diff --git a/API.Tests/Parsers/BookParserTests.cs b/Kavita.Services.Tests/Parsers/BookParserTests.cs similarity index 94% rename from API.Tests/Parsers/BookParserTests.cs rename to Kavita.Services.Tests/Parsers/BookParserTests.cs index 90147ac6b..8a0e13d75 100644 --- a/API.Tests/Parsers/BookParserTests.cs +++ b/Kavita.Services.Tests/Parsers/BookParserTests.cs @@ -1,12 +1,11 @@ using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Parsers; +namespace Kavita.Services.Tests.Parsers; public class BookParserTests { diff --git a/API.Tests/Parsers/ComicVineParserTests.cs b/Kavita.Services.Tests/Parsers/ComicVineParserTests.cs similarity index 96% rename from API.Tests/Parsers/ComicVineParserTests.cs rename to Kavita.Services.Tests/Parsers/ComicVineParserTests.cs index 2f4fd568e..202906a17 100644 --- a/API.Tests/Parsers/ComicVineParserTests.cs +++ b/Kavita.Services.Tests/Parsers/ComicVineParserTests.cs @@ -1,13 +1,11 @@ using System.IO.Abstractions.TestingHelpers; -using API.Data.Metadata; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Parsers; +namespace Kavita.Services.Tests.Parsers; public class ComicVineParserTests { diff --git a/API.Tests/Parsers/DefaultParserTests.cs b/Kavita.Services.Tests/Parsers/DefaultParserTests.cs similarity index 99% rename from API.Tests/Parsers/DefaultParserTests.cs rename to Kavita.Services.Tests/Parsers/DefaultParserTests.cs index ffe14a7c3..a26227772 100644 --- a/API.Tests/Parsers/DefaultParserTests.cs +++ b/Kavita.Services.Tests/Parsers/DefaultParserTests.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; -using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using System.IO.Abstractions.TestingHelpers; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Parsers; +namespace Kavita.Services.Tests.Parsers; public class DefaultParserTests { diff --git a/API.Tests/Parsers/ImageParserTests.cs b/Kavita.Services.Tests/Parsers/ImageParserTests.cs similarity index 96% rename from API.Tests/Parsers/ImageParserTests.cs rename to Kavita.Services.Tests/Parsers/ImageParserTests.cs index 63df1926e..21c7b8df0 100644 --- a/API.Tests/Parsers/ImageParserTests.cs +++ b/Kavita.Services.Tests/Parsers/ImageParserTests.cs @@ -1,12 +1,10 @@ using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Parsers; +namespace Kavita.Services.Tests.Parsers; public class ImageParserTests { diff --git a/API.Tests/Parsers/PdfParserTests.cs b/Kavita.Services.Tests/Parsers/PdfParserTests.cs similarity index 95% rename from API.Tests/Parsers/PdfParserTests.cs rename to Kavita.Services.Tests/Parsers/PdfParserTests.cs index 08bf9f25d..2d6e5e59b 100644 --- a/API.Tests/Parsers/PdfParserTests.cs +++ b/Kavita.Services.Tests/Parsers/PdfParserTests.cs @@ -1,12 +1,10 @@ using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Parsers; +namespace Kavita.Services.Tests.Parsers; public class PdfParserTests { diff --git a/API.Tests/Parsing/BookParsingTests.cs b/Kavita.Services.Tests/Parsing/BookParsingTests.cs similarity index 70% rename from API.Tests/Parsing/BookParsingTests.cs rename to Kavita.Services.Tests/Parsing/BookParsingTests.cs index 9b02eff63..136d0aae5 100644 --- a/API.Tests/Parsing/BookParsingTests.cs +++ b/Kavita.Services.Tests/Parsing/BookParsingTests.cs @@ -1,7 +1,7 @@ -using API.Entities.Enums; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; -namespace API.Tests.Parsing; +namespace Kavita.Services.Tests.Parsing; public class BookParsingTests { @@ -11,7 +11,7 @@ public class BookParsingTests [InlineData("Faust - Volume 01 [Del Rey][Scans_Compressed]", "Faust")] public void ParseSeriesTest(string filename, string expected) { - Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Book)); + Assert.Equal(expected, Parser.ParseSeries(filename, LibraryType.Book)); } [Theory] @@ -19,6 +19,6 @@ public class BookParsingTests [InlineData("Faust - Volume 01 [Del Rey][Scans_Compressed]", "1")] public void ParseVolumeTest(string filename, string expected) { - Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Book)); + Assert.Equal(expected, Parser.ParseVolume(filename, LibraryType.Book)); } } diff --git a/API.Tests/Parsing/ComicParsingTests.cs b/Kavita.Services.Tests/Parsing/ComicParsingTests.cs similarity index 99% rename from API.Tests/Parsing/ComicParsingTests.cs rename to Kavita.Services.Tests/Parsing/ComicParsingTests.cs index a0375a566..1f2900ea9 100644 --- a/API.Tests/Parsing/ComicParsingTests.cs +++ b/Kavita.Services.Tests/Parsing/ComicParsingTests.cs @@ -1,8 +1,7 @@ -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; -namespace API.Tests.Parsing; +namespace Kavita.Services.Tests.Parsing; public class ComicParsingTests { diff --git a/API.Tests/Parsing/ImageParsingTests.cs b/Kavita.Services.Tests/Parsing/ImageParsingTests.cs similarity index 95% rename from API.Tests/Parsing/ImageParsingTests.cs rename to Kavita.Services.Tests/Parsing/ImageParsingTests.cs index 362b4b08c..00165e9c4 100644 --- a/API.Tests/Parsing/ImageParsingTests.cs +++ b/Kavita.Services.Tests/Parsing/ImageParsingTests.cs @@ -1,13 +1,12 @@ using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Parsing; +namespace Kavita.Services.Tests.Parsing; public class ImageParsingTests { @@ -30,7 +29,7 @@ public class ImageParsingTests var filepath = @"E:\Manga\Monster #8\Ch. 001-016 [MangaPlus] [Digital] [amit34521]\Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]\13.jpg"; var expectedInfo2 = new ParserInfo { - Series = "Monster #8", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", + Series = "Monster #8", Volumes = Parser.LooseLeafVolume, Edition = "", Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image, FullFilePath = filepath, IsSpecial = false }; diff --git a/API.Tests/Parsing/MangaParsingTests.cs b/Kavita.Services.Tests/Parsing/MangaParsingTests.cs similarity index 99% rename from API.Tests/Parsing/MangaParsingTests.cs rename to Kavita.Services.Tests/Parsing/MangaParsingTests.cs index 8a983e899..bc575e9e0 100644 --- a/API.Tests/Parsing/MangaParsingTests.cs +++ b/Kavita.Services.Tests/Parsing/MangaParsingTests.cs @@ -1,8 +1,7 @@ -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; -namespace API.Tests.Parsing; +namespace Kavita.Services.Tests.Parsing; public class MangaParsingTests { diff --git a/API.Tests/Parsing/ParserInfoTests.cs b/Kavita.Services.Tests/Parsing/ParserInfoTests.cs similarity index 94% rename from API.Tests/Parsing/ParserInfoTests.cs rename to Kavita.Services.Tests/Parsing/ParserInfoTests.cs index cbb8ae99a..fd6c3d73d 100644 --- a/API.Tests/Parsing/ParserInfoTests.cs +++ b/Kavita.Services.Tests/Parsing/ParserInfoTests.cs @@ -1,8 +1,9 @@ -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; -namespace API.Tests.Parsing; +namespace Kavita.Services.Tests.Parsing; public class ParserInfoTests { diff --git a/API.Tests/Parsing/ParsingTests.cs b/Kavita.Services.Tests/Parsing/ParsingTests.cs similarity index 98% rename from API.Tests/Parsing/ParsingTests.cs rename to Kavita.Services.Tests/Parsing/ParsingTests.cs index 5a91ab281..bce33b68d 100644 --- a/API.Tests/Parsing/ParsingTests.cs +++ b/Kavita.Services.Tests/Parsing/ParsingTests.cs @@ -1,10 +1,7 @@ using System.Globalization; -using System.Linq; -using API.Services.Tasks.Scanner.Parser; -using Xunit; -using static API.Services.Tasks.Scanner.Parser.Parser; +using static Kavita.Services.Scanner.Parser; -namespace API.Tests.Parsing; +namespace Kavita.Services.Tests.Parsing; public class ParsingTests { diff --git a/API.Tests/Services/PersonServiceTests.cs b/Kavita.Services.Tests/PersonServiceTests.cs similarity index 97% rename from API.Tests/Services/PersonServiceTests.cs rename to Kavita.Services.Tests/PersonServiceTests.cs index 3a4a1e4f7..efec0f914 100644 --- a/API.Tests/Services/PersonServiceTests.cs +++ b/Kavita.Services.Tests/PersonServiceTests.cs @@ -1,15 +1,13 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Data.Repositories; -using API.Entities.Enums; -using API.Entities.Person; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using Xunit; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Services.Builders; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class PersonServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Services/ProcessSeriesTests.cs b/Kavita.Services.Tests/ProcessSeriesTests.cs similarity index 90% rename from API.Tests/Services/ProcessSeriesTests.cs rename to Kavita.Services.Tests/ProcessSeriesTests.cs index 119e1bc10..ede59e433 100644 --- a/API.Tests/Services/ProcessSeriesTests.cs +++ b/Kavita.Services.Tests/ProcessSeriesTests.cs @@ -1,4 +1,4 @@ -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ProcessSeriesTests { @@ -33,7 +33,7 @@ public class ProcessSeriesTests // public void UpdateChapterFromComicInfo_() // { // // TODO: Do this - // var file = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 1.cbz"); + // var file = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 1.cbz"); // // Chapter and ComicInfo // var chapter = new ChapterBuilder("1") // .WithId(0) diff --git a/API.Tests/Services/RatingServiceTests.cs b/Kavita.Services.Tests/RatingServiceTests.cs similarity index 95% rename from API.Tests/Services/RatingServiceTests.cs rename to Kavita.Services.Tests/RatingServiceTests.cs index e10056380..3c93225d8 100644 --- a/API.Tests/Services/RatingServiceTests.cs +++ b/Kavita.Services.Tests/RatingServiceTests.cs @@ -1,19 +1,17 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Data.Repositories; -using API.DTOs; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; using Hangfire; using Hangfire.InMemory; +using Kavita.API.Repositories; +using Kavita.API.Services.Plus; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Builders; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class RatingServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Services/ReaderServiceRereadTests.cs b/Kavita.Services.Tests/ReaderServiceRereadTests.cs similarity index 98% rename from API.Tests/Services/ReaderServiceRereadTests.cs rename to Kavita.Services.Tests/ReaderServiceRereadTests.cs index 99ac8d72c..9e9477b9e 100644 --- a/API.Tests/Services/ReaderServiceRereadTests.cs +++ b/Kavita.Services.Tests/ReaderServiceRereadTests.cs @@ -1,22 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs; -using API.Entities.Enums; -using API.Entities.User; -using API.Services.Reading; -using API.Data; -using API.Data.Repositories; -using API.Services; -using API.Services.Plus; -using API.SignalR; +using System.IO.Abstractions.TestingHelpers; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Reading; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -using System.IO.Abstractions.TestingHelpers; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ReaderServiceRereadTests { diff --git a/API.Tests/Services/ReaderServiceTests.cs b/Kavita.Services.Tests/ReaderServiceTests.cs similarity index 88% rename from API.Tests/Services/ReaderServiceTests.cs rename to Kavita.Services.Tests/ReaderServiceTests.cs index 306f7c78f..912555146 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/Kavita.Services.Tests/ReaderServiceTests.cs @@ -1,29 +1,29 @@ -using System.Collections.Generic; -using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Progress; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.SignalR; +using System.IO.Abstractions.TestingHelpers; using Hangfire; using Hangfire.InMemory; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -using YamlDotNet.Core; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDbTest(testOutputHelper) { @@ -65,8 +65,8 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(1) .Build()) .Build()) @@ -102,8 +102,8 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(1) .Build()) .Build()) @@ -147,8 +147,8 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(1) .Build()) .Build()) @@ -211,11 +211,11 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(1) .Build()) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(2) .Build()) .Build()) @@ -257,11 +257,11 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(1) .Build()) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(2) .Build()) .Build()) @@ -358,7 +358,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) .WithVolume(new VolumeBuilder("1-2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build()) .WithVolume(new VolumeBuilder("3-4") @@ -536,7 +536,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) @@ -576,7 +576,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("66").Build()) .WithChapter(new ChapterBuilder("67").Build()) .Build()) @@ -586,7 +586,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build()) .Build(); @@ -607,7 +607,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb Assert.NotEqual(-1, nextChapter); var actualChapter = await unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.NotNull(actualChapter); - Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, actualChapter.Range); + Assert.Equal(Parser.DefaultChapter, actualChapter.Range); } [Fact] @@ -627,9 +627,9 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber).Build()) - .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).Build()) .Build()) .Build(); @@ -689,7 +689,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -721,12 +721,12 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build()) .Build(); @@ -755,15 +755,15 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build()) @@ -804,14 +804,14 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("A.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .WithChapter(new ChapterBuilder("B.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2) + .WithSortOrder(Parser.SpecialVolumeNumber + 2) .Build()) .Build()) .Build(); @@ -846,15 +846,15 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("A.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .WithPages(1) .Build()) .Build()) @@ -890,19 +890,19 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("A.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .WithPages(1) .Build()) .Build()) @@ -939,14 +939,14 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("A.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .WithChapter(new ChapterBuilder("B.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2) + .WithSortOrder(Parser.SpecialVolumeNumber + 2) .Build()) .Build()) .Build(); @@ -1117,16 +1117,16 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("40").WithPages(1).Build()) .WithChapter(new ChapterBuilder("50").WithPages(1).Build()) .WithChapter(new ChapterBuilder("60").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("Some Special Title") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .WithPages(1) .Build()) .Build()) @@ -1226,9 +1226,9 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build()) - .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 2).Build()) .Build()) .Build(); @@ -1296,7 +1296,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build()) .WithLibraryId(library.Id) .Build(); @@ -1328,13 +1328,13 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb await context.SaveChangesAsync(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build()) .WithLibraryId(library.Id) .Build(); @@ -1363,7 +1363,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb await context.SaveChangesAsync(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("5").Build()) .WithChapter(new ChapterBuilder("6").Build()) .WithChapter(new ChapterBuilder("7").Build()) @@ -1413,7 +1413,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb await context.SaveChangesAsync(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -1451,14 +1451,14 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("A.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .WithChapter(new ChapterBuilder("B.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2) + .WithSortOrder(Parser.SpecialVolumeNumber + 2) .Build()) .Build()) .WithLibraryId(library.Id) @@ -1496,7 +1496,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb await context.SaveChangesAsync(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -1573,7 +1573,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb await context.SaveChangesAsync(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").Build()) .WithChapter(new ChapterBuilder("96").Build()) .Build()) @@ -1625,7 +1625,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("1").WithPages(3).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithPages(4) .WithLibraryId(library.Id) @@ -1671,7 +1671,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("1", "1-11").WithPages(3).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithPages(4) .WithLibraryId(library.Id) @@ -1790,21 +1790,21 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") // Loose chapters - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("45").WithPages(1).Build()) .WithChapter(new ChapterBuilder("46").WithPages(1).Build()) .WithChapter(new ChapterBuilder("47").WithPages(1).Build()) .WithChapter(new ChapterBuilder("48").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("Some Special Title") - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .WithIsSpecial(true).WithPages(1) .Build()) .Build()) // One file volume .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) // Read + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) // Read .Build()) // Chapter-based volume .WithVolume(new VolumeBuilder("2") @@ -1872,12 +1872,12 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") // Loose chapters - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Prologue").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Prologue").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) .Build()) .WithLibraryId(library.Id) .Build(); @@ -1910,23 +1910,23 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") // Loose chapters .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithSortOrder(Parser.DefaultChapterNumber).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithSortOrder(Parser.DefaultChapterNumber).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("12") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithSortOrder(Parser.DefaultChapterNumber).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("99.9") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithSortOrder(Parser.DefaultChapterNumber).WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, "Short Stories").WithIsSpecial(true) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter, "Short Stories").WithIsSpecial(true) .WithSortOrder(0).WithPages(1).Build()) .Build()) .WithLibraryId(library.Id) @@ -1966,7 +1966,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithVolume(new VolumeBuilder("2") .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) .Build()) @@ -2029,7 +2029,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb await context.SaveChangesAsync(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("230").WithPages(1).Build()) .WithChapter(new ChapterBuilder("231").WithPages(1).Build()) .Build()) @@ -2073,19 +2073,19 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("100").WithPages(1).Build()) .WithChapter(new ChapterBuilder("101").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Christmas Eve").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Christmas Eve").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .Build(); @@ -2135,7 +2135,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("100").WithPages(1).Build()) .WithChapter(new ChapterBuilder("101").WithPages(1).Build()) .WithChapter(new ChapterBuilder("102").WithPages(1).Build()) @@ -2270,7 +2270,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) @@ -2317,13 +2317,13 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) .Build()) .Build(); @@ -2385,7 +2385,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("230").WithPages(1).Build()) //.WithChapter(new ChapterBuilder("231").WithPages(1).Build()) (Added later) .Build()) @@ -2395,7 +2395,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) //.WithChapter(new ChapterBuilder("14.9").WithPages(1).Build()) (added later) .Build()) .Build(); @@ -2442,13 +2442,13 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb context.Library.Add(library); await context.SaveChangesAsync(); - var readChapter1 = new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build(); - var readChapter2 = new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build(); - var volume = new VolumeBuilder("3").WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()).Build(); + var readChapter1 = new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build(); + var readChapter2 = new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build(); + var volume = new VolumeBuilder("3").WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()).Build(); var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("51").WithPages(1).Build()) .WithChapter(new ChapterBuilder("52").WithPages(1).Build()) .WithChapter(new ChapterBuilder("53").WithPages(1).Build()) @@ -2462,7 +2462,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .Build()) // 3, 4, and all loose leafs are unread should be unread .WithVolume(new VolumeBuilder("3") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("4") .WithChapter(new ChapterBuilder("40").WithPages(1).Build()) @@ -2523,13 +2523,13 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) .WithChapter(new ChapterBuilder("22").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("51").WithPages(1).Build()) .WithChapter(new ChapterBuilder("52").WithPages(1).Build()) .WithChapter(new ChapterBuilder("91").WithPages(2).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Special").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Special").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) .Build()) .Build(); @@ -2718,13 +2718,13 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) .Build()) .Build(); @@ -2764,14 +2764,14 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2.5").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) .Build()) .Build(); @@ -2813,10 +2813,10 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithLibraryId(library.Id) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .Build(); @@ -2854,7 +2854,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("45").WithPages(5).Build()) .WithChapter(new ChapterBuilder("46").WithPages(46).Build()) .WithChapter(new ChapterBuilder("47").WithPages(47).Build()) @@ -2862,15 +2862,15 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("49").WithPages(49).Build()) .WithChapter(new ChapterBuilder("50").WithPages(50).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(10).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(10).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(6).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(6).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(7).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(7).Build()) .Build()) .WithVolume(new VolumeBuilder("3") .WithChapter(new ChapterBuilder("12").WithPages(5).Build()) @@ -2929,11 +2929,11 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(2).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(2).Build()) .Build()) .Build(); @@ -2974,8 +2974,8 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(2).Build()) .Build()) .Build(); @@ -3054,24 +3054,24 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("10").WithPages(1).Build()) .WithChapter(new ChapterBuilder("20").WithPages(1).Build()) .WithChapter(new ChapterBuilder("30").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1997") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2002") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2003") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .Build(); @@ -3117,13 +3117,13 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("10").WithPages(1).Build()) .WithChapter(new ChapterBuilder("20").WithPages(1).Build()) .WithChapter(new ChapterBuilder("30").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1997") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) diff --git a/API.Tests/Services/ReadingHistoryServiceTests.cs b/Kavita.Services.Tests/ReadingHistoryServiceTests.cs similarity index 90% rename from API.Tests/Services/ReadingHistoryServiceTests.cs rename to Kavita.Services.Tests/ReadingHistoryServiceTests.cs index c1c340429..3507d5ffe 100644 --- a/API.Tests/Services/ReadingHistoryServiceTests.cs +++ b/Kavita.Services.Tests/ReadingHistoryServiceTests.cs @@ -1,22 +1,20 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Progress; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Extensions.QueryExtensions; -using API.Helpers.Builders; -using API.Services.Reading; +using Kavita.API.Repositories; +using Kavita.Database; +using Kavita.Database.Extensions; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ReadingHistoryServiceTests(ITestOutputHelper testOutputHelper) : AbstractDbTest(testOutputHelper) { diff --git a/API.Tests/Services/ReadingListServiceTests.cs b/Kavita.Services.Tests/ReadingListServiceTests.cs similarity index 96% rename from API.Tests/Services/ReadingListServiceTests.cs rename to Kavita.Services.Tests/ReadingListServiceTests.cs index 1443d6fb4..d8ddb21d6 100644 --- a/API.Tests/Services/ReadingListServiceTests.cs +++ b/Kavita.Services.Tests/ReadingListServiceTests.cs @@ -1,27 +1,27 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.ReadingLists; -using API.DTOs.ReadingLists.CBL; -using API.Entities; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.SignalR; +using System.IO.Abstractions.TestingHelpers; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +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.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -54,7 +54,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -104,7 +104,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithSeries(new SeriesBuilder("Test") .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -164,7 +164,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -227,7 +227,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -307,7 +307,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -366,7 +366,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -433,7 +433,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .Build() ) @@ -475,7 +475,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .Build() ) @@ -620,7 +620,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .Build() ) @@ -674,7 +674,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithReleaseDate(new DateTime(2005, 03, 01)) .Build() @@ -769,8 +769,8 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb } private static ReadingListItemDto CreateListItemDto(MangaFormat seriesFormat, LibraryType libraryType, - string volumeNumber = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, - string chapterNumber =API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, + string volumeNumber = Parser.LooseLeafVolume, + string chapterNumber =Parser.DefaultChapter, string chapterTitleName = "") { return new ReadingListItemDto() @@ -1082,7 +1082,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb private static CblReadingList LoadCblFromPath(string path) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ReadingListService/"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ReadingListService/"); var reader = new System.Xml.Serialization.XmlSerializer(typeof(CblReadingList)); using var file = new StreamReader(Path.Join(testDirectory, path)); @@ -1368,7 +1368,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb var series2 = new SeriesBuilder("Series 2") .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) diff --git a/API.Tests/Services/ReadingProfileServiceTest.cs b/Kavita.Services.Tests/ReadingProfileServiceTest.cs similarity index 99% rename from API.Tests/Services/ReadingProfileServiceTest.cs rename to Kavita.Services.Tests/ReadingProfileServiceTest.cs index 44f034bd1..52ff9a468 100644 --- a/API.Tests/Services/ReadingProfileServiceTest.cs +++ b/Kavita.Services.Tests/ReadingProfileServiceTest.cs @@ -1,21 +1,22 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Tests.Helpers; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Tests.Helpers; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Reading; using Microsoft.EntityFrameworkCore; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ReadingProfileServiceTest(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Services/ScannerServiceTests.cs b/Kavita.Services.Tests/ScannerServiceTests.cs similarity index 98% rename from API.Tests/Services/ScannerServiceTests.cs rename to Kavita.Services.Tests/ScannerServiceTests.cs index 0930a0690..97c6ef4f3 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/Kavita.Services.Tests/ScannerServiceTests.cs @@ -1,26 +1,21 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Metadata; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; -using API.Tests.Helpers; -using Hangfire; -using Xunit; +using Hangfire; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Services.Scanner; +using Kavita.Services.Tests.Helpers; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ScannerServiceTests: AbstractDbTest { private readonly ITestOutputHelper _testOutputHelper; - private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"); + private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/ScanTests"); public ScannerServiceTests(ITestOutputHelper testOutputHelper): base(testOutputHelper) { @@ -589,7 +584,7 @@ public class ScannerServiceTests: AbstractDbTest var testDirectoryPath = Path.Join( - Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"), + Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/ScanTests"), testcase.Replace(".json", string.Empty)); library.Folders = [ @@ -645,7 +640,7 @@ public class ScannerServiceTests: AbstractDbTest var testDirectoryPath = Path.Join( - Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"), + Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/ScanTests"), testcase.Replace(".json", string.Empty)); library.Folders = [ @@ -701,7 +696,7 @@ public class ScannerServiceTests: AbstractDbTest var library = await scannerHelper.GenerateScannerData(testcase, infos); var testDirectoryPath = Path.Combine(Directory.GetCurrentDirectory(), - "../../../Services/Test Data/ScannerService/ScanTests", + "../../../Test Data/ScannerService/ScanTests", testcase.Replace(".json", string.Empty)); library.Folders = @@ -791,7 +786,7 @@ public class ScannerServiceTests: AbstractDbTest var library = await scannerHelper.GenerateScannerData(testcase, infos); var testDirectoryPath = Path.Combine(Directory.GetCurrentDirectory(), - "../../../Services/Test Data/ScannerService/ScanTests", + "../../../Test Data/ScannerService/ScanTests", testcase.Replace(".json", string.Empty)); library.Folders = diff --git a/API.Tests/Services/ScrobblingServiceTests.cs b/Kavita.Services.Tests/ScrobblingServiceTests.cs similarity index 96% rename from API.Tests/Services/ScrobblingServiceTests.cs rename to Kavita.Services.Tests/ScrobblingServiceTests.cs index 87aa1ad0f..aa33c5ed0 100644 --- a/API.Tests/Services/ScrobblingServiceTests.cs +++ b/Kavita.Services.Tests/ScrobblingServiceTests.cs @@ -1,24 +1,25 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Scrobbling; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Scrobble; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Scrobble; +using Kavita.Services.Builders; +using Kavita.Services.Plus; +using Kavita.Services.Reading; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; #nullable enable public class ScrobblingServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) @@ -305,7 +306,7 @@ public class ScrobblingServiceTests(ITestOutputHelper outputHelper): AbstractDbT Arg.Is(data => data.ChapterNumber == (int)chapter.MaxNumber && data.VolumeNumber == (int)volume.MaxNumber - ), + ), Arg.Any()); } @@ -629,13 +630,13 @@ public class ScrobblingServiceTests(ITestOutputHelper outputHelper): AbstractDbT [InlineData("https://anilist.co/manga/30105/Kekkaishi/", 30105)] public void CanParseWeblink_AniList(string link, int? expectedId) { - Assert.Equal(ScrobblingService.ExtractId(link, ScrobblingService.AniListWeblinkWebsite), 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(ScrobblingService.ExtractId(link, ScrobblingService.MangaDexWeblinkWebsite), expectedId); + Assert.Equal(ScrobblingHelper.ExtractId(link, ScrobblingService.MangaDexWeblinkWebsite), expectedId); } } diff --git a/API.Tests/Services/SeriesServiceTests.cs b/Kavita.Services.Tests/SeriesServiceTests.cs similarity index 99% rename from API.Tests/Services/SeriesServiceTests.cs rename to Kavita.Services.Tests/SeriesServiceTests.cs index 92af211d9..347208250 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/Kavita.Services.Tests/SeriesServiceTests.cs @@ -1,34 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Globalization; +using System.Globalization; using System.IO.Abstractions; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.Person; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Scanner; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; internal class MockHostingEnvironment : IHostEnvironment { public string ApplicationName { get => "API"; set => throw new NotImplementedException(); } @@ -53,7 +51,6 @@ public class SeriesServiceTests(ITestOutputHelper outputHelper): AbstractDbTest( { var ds = new DirectoryService(Substitute.For>(), new FileSystem()); - var locService = new LocalizationService(ds, new MockHostingEnvironment(), Substitute.For(), Substitute.For()); diff --git a/API.Tests/Services/SettingsServiceTests.cs b/Kavita.Services.Tests/SettingsServiceTests.cs similarity index 97% rename from API.Tests/Services/SettingsServiceTests.cs rename to Kavita.Services.Tests/SettingsServiceTests.cs index 6aac262e1..e6da9ea5a 100644 --- a/API.Tests/Services/SettingsServiceTests.cs +++ b/Kavita.Services.Tests/SettingsServiceTests.cs @@ -1,20 +1,17 @@ -using System.Collections.Generic; -using System.IO.Abstractions; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.KavitaPlus.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Entities.MetadataMatching; -using API.Services; -using API.Services.Tasks.Scanner; +using System.IO.Abstractions; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class SettingsServiceTests { @@ -33,7 +30,8 @@ public class SettingsServiceTests _mockUnitOfWork = Substitute.For(); _settingsService = new SettingsService(_mockUnitOfWork, ds, Substitute.For(), Substitute.For(), - Substitute.For>(), Substitute.For()); + Substitute.For>(), Substitute.For(), + Substitute.For()); } #region ImportMetadataSettings diff --git a/API.Tests/Services/SiteThemeServiceTests.cs b/Kavita.Services.Tests/SiteThemeServiceTests.cs similarity index 94% rename from API.Tests/Services/SiteThemeServiceTests.cs rename to Kavita.Services.Tests/SiteThemeServiceTests.cs index 327d82060..da83700b3 100644 --- a/API.Tests/Services/SiteThemeServiceTests.cs +++ b/Kavita.Services.Tests/SiteThemeServiceTests.cs @@ -1,21 +1,17 @@ using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Entities; -using API.Entities.Enums.Theme; -using API.Extensions; -using API.Services; -using API.Services.Tasks; -using API.SignalR; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.Theme; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class SiteThemeServiceTest(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) diff --git a/API.Tests/Services/TachiyomiServiceTests.cs b/Kavita.Services.Tests/TachiyomiServiceTests.cs similarity index 92% rename from API.Tests/Services/TachiyomiServiceTests.cs rename to Kavita.Services.Tests/TachiyomiServiceTests.cs index 768bcc012..13a26e4a9 100644 --- a/API.Tests/Services/TachiyomiServiceTests.cs +++ b/Kavita.Services.Tests/TachiyomiServiceTests.cs @@ -1,22 +1,24 @@ -using API.Helpers.Builders; -using API.Services.Plus; -using API.Services.Reading; -using Xunit.Abstractions; - -namespace API.Tests.Services; -using System.Collections.Generic; -using System.IO.Abstractions.TestingHelpers; -using System.Threading.Tasks; -using Data; -using Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Services; -using SignalR; +using System.IO.Abstractions.TestingHelpers; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; +using Xunit.Abstractions; + +namespace Kavita.Services.Tests; public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -45,7 +47,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -91,7 +93,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -143,7 +145,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -195,7 +197,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -248,15 +250,15 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(199).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(192).Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(255).Build()) .Build()) .WithPages(646) @@ -297,7 +299,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -351,7 +353,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -395,7 +397,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -446,7 +448,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -495,7 +497,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (unitOfWork, context, mapper) = await CreateDatabase(); var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/LICENSE.md b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/LICENSE.md similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/LICENSE.md rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/LICENSE.md diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/empty.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/empty.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/empty.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/empty.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/file in folder in folder.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/file in folder in folder.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/file in folder in folder.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/file in folder in folder.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/file in folder.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/file in folder.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/file in folder.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/file in folder.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/file in folder_alt.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/file in folder_alt.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/file in folder_alt.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/file in folder_alt.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/flat file.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/flat file.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/flat file.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/flat file.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/macos_native.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_native.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/macos_native.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_native.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/macos_none.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_none.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/macos_none.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_none.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/macos_one.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_one.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/macos_one.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_one.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/macos_withdotunder_one.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_withdotunder_one.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/macos_withdotunder_one.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_withdotunder_one.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/winrar.rar b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/winrar.rar similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/winrar.rar rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/winrar.rar diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.xml b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo.xml similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.xml rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo.xml diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo2.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo2.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo2.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo2.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_authors.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_authors.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_authors.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_authors.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.rar b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.rar similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.rar rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.rar diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos_reversed.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos_reversed.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos_reversed.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos_reversed.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root_SharpCompress.cb7 b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root_SharpCompress.cb7 similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root_SharpCompress.cb7 rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root_SharpCompress.cb7 diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/Umlaut.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/Umlaut.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/Umlaut.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/Umlaut.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/file in folder.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/file in folder.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/file in folder.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/file in folder.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/macos_native.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/macos_native.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/macos_native.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/macos_native.png diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/macos_native.zip b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/macos_native.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/macos_native.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/macos_native.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.expected.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/sorting.expected.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.expected.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/sorting.expected.png diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.zip b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/sorting.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/sorting.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/test.expected.jpg b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/test.expected.jpg similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/test.expected.jpg rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/test.expected.jpg diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/test.zip b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/test.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/test.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/test.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/thumbnail.expected.jpg b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/thumbnail.expected.jpg similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/thumbnail.expected.jpg rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/thumbnail.expected.jpg diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/thumbnail.jpg b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/thumbnail.jpg similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/thumbnail.jpg rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/thumbnail.jpg diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.cbz b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.cbz similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.cbz rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.cbz diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.expected.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.expected.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.expected.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.expected.png diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - nested folder.cbz b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - nested folder.cbz similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - nested folder.cbz rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - nested folder.cbz diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.old.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.old.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.old.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.old.png diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.png diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - with folder.cbz b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - with folder.cbz similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - with folder.cbz rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - with folder.cbz diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.jpg b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.jpg similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.jpg rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.jpg diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.png diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10.cbz b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10.cbz similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10.cbz rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10.cbz diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10.expected.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10.expected.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10.expected.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10.expected.png diff --git a/API.Tests/Services/Test Data/ArchiveService/Formats/One File with DB_Supported.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Formats/One File with DB_Supported.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Formats/One File with DB_Supported.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Formats/One File with DB_Supported.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Thumbnails/001.jpg b/Kavita.Services.Tests/Test Data/ArchiveService/Thumbnails/001.jpg similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Thumbnails/001.jpg rename to Kavita.Services.Tests/Test Data/ArchiveService/Thumbnails/001.jpg diff --git a/API.Tests/Services/Test Data/BookService/Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf b/Kavita.Services.Tests/Test Data/BookService/Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf similarity index 100% rename from API.Tests/Services/Test Data/BookService/Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf rename to Kavita.Services.Tests/Test Data/BookService/Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf diff --git a/API.Tests/Services/Test Data/BookService/Relative Key Test File.epub b/Kavita.Services.Tests/Test Data/BookService/Relative Key Test File.epub similarity index 100% rename from API.Tests/Services/Test Data/BookService/Relative Key Test File.epub rename to Kavita.Services.Tests/Test Data/BookService/Relative Key Test File.epub diff --git a/API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf b/Kavita.Services.Tests/Test Data/BookService/Rollo at Work SP01.pdf similarity index 100% rename from API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf rename to Kavita.Services.Tests/Test Data/BookService/Rollo at Work SP01.pdf diff --git a/API.Tests/Services/Test Data/BookService/The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub b/Kavita.Services.Tests/Test Data/BookService/The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub similarity index 100% rename from API.Tests/Services/Test Data/BookService/The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub rename to Kavita.Services.Tests/Test Data/BookService/The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub diff --git a/API.Tests/Services/Test Data/BookService/TitleWithVolume.epub b/Kavita.Services.Tests/Test Data/BookService/TitleWithVolume.epub similarity index 100% rename from API.Tests/Services/Test Data/BookService/TitleWithVolume.epub rename to Kavita.Services.Tests/Test Data/BookService/TitleWithVolume.epub diff --git a/API.Tests/Services/Test Data/BookService/TitleWithVolume_NoSeriesOrSeriesIndex.epub b/Kavita.Services.Tests/Test Data/BookService/TitleWithVolume_NoSeriesOrSeriesIndex.epub similarity index 100% rename from API.Tests/Services/Test Data/BookService/TitleWithVolume_NoSeriesOrSeriesIndex.epub rename to Kavita.Services.Tests/Test Data/BookService/TitleWithVolume_NoSeriesOrSeriesIndex.epub diff --git a/API.Tests/Services/Test Data/BookService/content.opf b/Kavita.Services.Tests/Test Data/BookService/content.opf similarity index 100% rename from API.Tests/Services/Test Data/BookService/content.opf rename to Kavita.Services.Tests/Test Data/BookService/content.opf diff --git a/API.Tests/Services/Test Data/BookService/encrypted.pdf b/Kavita.Services.Tests/Test Data/BookService/encrypted.pdf similarity index 100% rename from API.Tests/Services/Test Data/BookService/encrypted.pdf rename to Kavita.Services.Tests/Test Data/BookService/encrypted.pdf diff --git a/API.Tests/Services/Test Data/BookService/indirect.pdf b/Kavita.Services.Tests/Test Data/BookService/indirect.pdf similarity index 100% rename from API.Tests/Services/Test Data/BookService/indirect.pdf rename to Kavita.Services.Tests/Test Data/BookService/indirect.pdf diff --git a/API.Tests/Services/Test Data/BookService/test.pdf b/Kavita.Services.Tests/Test Data/BookService/test.pdf similarity index 100% rename from API.Tests/Services/Test Data/BookService/test.pdf rename to Kavita.Services.Tests/Test Data/BookService/test.pdf diff --git a/API.Tests/Services/Test Data/BookService/test_ſ.pdf b/Kavita.Services.Tests/Test Data/BookService/test_ſ.pdf similarity index 100% rename from API.Tests/Services/Test Data/BookService/test_ſ.pdf rename to Kavita.Services.Tests/Test Data/BookService/test_ſ.pdf diff --git a/API.Tests/Services/Test Data/CacheService/Archives/file in folder in folder.zip b/Kavita.Services.Tests/Test Data/CacheService/Archives/file in folder in folder.zip similarity index 100% rename from API.Tests/Services/Test Data/CacheService/Archives/file in folder in folder.zip rename to Kavita.Services.Tests/Test Data/CacheService/Archives/file in folder in folder.zip diff --git a/API.Tests/Services/Test Data/CoverDbService/Existing/01.webp b/Kavita.Services.Tests/Test Data/CoverDbService/Existing/01.webp similarity index 100% rename from API.Tests/Services/Test Data/CoverDbService/Existing/01.webp rename to Kavita.Services.Tests/Test Data/CoverDbService/Existing/01.webp diff --git a/API.Tests/Services/Test Data/CoverDbService/Favicons/anilist.co.webp b/Kavita.Services.Tests/Test Data/CoverDbService/Favicons/anilist.co.webp similarity index 100% rename from API.Tests/Services/Test Data/CoverDbService/Favicons/anilist.co.webp rename to Kavita.Services.Tests/Test Data/CoverDbService/Favicons/anilist.co.webp diff --git a/API.Tests/Services/Test Data/DirectoryService/TestCases/Manga-testcase.txt b/Kavita.Services.Tests/Test Data/DirectoryService/TestCases/Manga-testcase.txt similarity index 100% rename from API.Tests/Services/Test Data/DirectoryService/TestCases/Manga-testcase.txt rename to Kavita.Services.Tests/Test Data/DirectoryService/TestCases/Manga-testcase.txt diff --git a/API.Tests/Services/Test Data/DirectoryService/extension/file.cbz b/Kavita.Services.Tests/Test Data/DirectoryService/extension/file.cbz similarity index 100% rename from API.Tests/Services/Test Data/DirectoryService/extension/file.cbz rename to Kavita.Services.Tests/Test Data/DirectoryService/extension/file.cbz diff --git a/API.Tests/Services/Test Data/DirectoryService/extension/file.rar b/Kavita.Services.Tests/Test Data/DirectoryService/extension/file.rar similarity index 100% rename from API.Tests/Services/Test Data/DirectoryService/extension/file.rar rename to Kavita.Services.Tests/Test Data/DirectoryService/extension/file.rar diff --git a/API.Tests/Services/Test Data/DirectoryService/extension/file2.cbz b/Kavita.Services.Tests/Test Data/DirectoryService/extension/file2.cbz similarity index 100% rename from API.Tests/Services/Test Data/DirectoryService/extension/file2.cbz rename to Kavita.Services.Tests/Test Data/DirectoryService/extension/file2.cbz diff --git a/API.Tests/Services/Test Data/DirectoryService/regex/file.txt b/Kavita.Services.Tests/Test Data/DirectoryService/regex/file.txt similarity index 100% rename from API.Tests/Services/Test Data/DirectoryService/regex/file.txt rename to Kavita.Services.Tests/Test Data/DirectoryService/regex/file.txt diff --git a/API.Tests/Services/Test Data/DirectoryService/regex/file2.txt b/Kavita.Services.Tests/Test Data/DirectoryService/regex/file2.txt similarity index 100% rename from API.Tests/Services/Test Data/DirectoryService/regex/file2.txt rename to Kavita.Services.Tests/Test Data/DirectoryService/regex/file2.txt diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/blue-2.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/blue-2.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/blue-2.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/blue-2.png diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/blue.jpg b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/blue.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/blue.jpg rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/blue.jpg diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/green-red.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/green-red.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/green-red.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/green-red.png diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/green.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/green.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/green.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/green.png diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue-2.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/lightblue-2.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue-2.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/lightblue-2.png diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/lightblue.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/lightblue.png diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/pink.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/pink.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/pink.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/pink.png diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/yellow-blue.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/yellow-blue.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/yellow-blue.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/yellow-blue.png diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-normal-2.jpg b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-2.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/comic-normal-2.jpg rename to Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-2.jpg diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-2_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-2_baseline.png new file mode 100644 index 0000000000000000000000000000000000000000..f497577f602257ed613473eb77f31ee76189111f GIT binary patch literal 248370 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
  • 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

    82p%@h$BOP?-$=d%W^H1@i?V`R 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

    pOJU&o zq8oD9@Fm>2`2@p$B&@x)w0=1_JkK*Op5m2kHOp3#U2*jG25-W>|7s0@D-%?Df~J=j zfW!lh1^{6tYI8EtE85xFK8WE?%Y+pEltX9h;6qvbuatS@9hQL(C~U#r7sXuXUf*D52aj ziYvAzuLpdtz8a`;lTm_NfBdohfSiIJiEm=EJ3EbZ?|RyUW}b8L&dIljENCQ&XNv29 z2qQhU_W3&A31>$gpOXw0imW0~oL5$wo?qvqyL%9Zq5mXWz*bMZ#%!06gN55nNvYkf zkdl4Q-@JT>M$o}ftMv>x9Kccna{LzHXam}a-GR08&rd}Vr@90*W-u+|;@|+L=NtHn zzfNTRH&TNvfa~c$Z8bH3{+3E80*%y+E=Azk7aSZMAOCJAeBRuNV9D>oh&8<}8?~eJ zd5~6KOQ)tIKR%e!E}aE7c1W>C-06Ywz6bH1*B!G&A4fWK`;J#cx@bnsE0$SW7ki1L zEK+2st1y{?xh0BAhjYOylbzm>>g)C@+q0yp8Zi> z+9Cc)%rjKI5f7s8iyZ)9mg8;aN8i(~6L>>GU;6k@2ch^3-#4yT;QCLVY zj_=dp&T=G&CQ4*fJ)4`I>%s#gB-3DCg>uyK+p?mevN1!lAEuHfRxE0&65Kz}Kag4s zp7)m;A_m4`lb<+Dtj(y+_*!X#{-L3Qb_FSc)D1ByChd;rrAFNv$x)+H=*^SJj#M>C zgt%C2XAH_=RjG;H33v$IOun+Rl9vTkZ(F*&54o~J>oY@uaG*S57 zKirp3%H0;3ETCl`B%B5yj6SMc-)Iv=moO2CThHt4`q3ThL3n^B`dw`{n09nebwe_)eWVX+HX z)bsApq`rVtBwcT5)!*p=?Cg%`RP+rg(lKG4?VZL3{#HJ>=xBm^BZFPkqOg?VJT&-Q z1I|tgk*hgo{Bb8!8=9m9QTp#|c`0Jw&p`-vV2tPIpd$3U_?Rw1PdmR6BldJKw7#KV X5qz~tvY+;c03R6%1@S6Tqrm?Ic+}>6 literal 0 HcmV?d00001 diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-square.jpg b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-square.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/comic-square.jpg rename to Kavita.Services.Tests/Test Data/ImageService/Covers/comic-square.jpg diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-square_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-square_baseline.png new file mode 100644 index 0000000000000000000000000000000000000000..bc8658bdf916aa1f31b55c0087cb7d6a3f086b4d GIT binary patch literal 385306 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%0D}A!{0rf_| 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≀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 z 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 R;{yNy002ovPDHLkV1hU~RB-?R literal 0 HcmV?d00001 diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-wide.jpg b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-wide.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/comic-wide.jpg rename to Kavita.Services.Tests/Test Data/ImageService/Covers/comic-wide.jpg diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-wide_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-wide_baseline.png new file mode 100644 index 0000000000000000000000000000000000000000..61c996d091085459a65027e454aca31f81c9175a GIT binary patch literal 269575 zcmYIu1yG$av-ZK=-QDfr?(Pl;D-Ok7i@Q4qclYA%?o!+xTA)aADSy9vzklw|?Cg_$ zcXpD=ByX~LqSaJnQ4k3c0RRAsyquH<001HOPsiaQ|9Qk^ar6F}7E6%4mApJ70RCSC z7Jvx>1%UX6{-cBtF#lt9{$Zm3;xGR&{eNTrlLZU%|Ke>Tq5mh|>Yx8Vaw883IEVT_ z3kv`w!2zKEeF1W?10je|8(u2 z5S_lO{4)s7a(Zq603POl6vDb->E9^H0rFDfTHd)Wy$CIqYuY4T^S|dsb{xKFHVC9w zA2ZgJ+G9gtVOr&#*qZi`YDLUn&3Up>D9l_hWq>q-(Gyu!o=P_kO5+;(o#0Czx+8 z;776rGPvc8o32d1lQu=N)H*;i();1N#<$f$NuVjHd)KAaRhe5)JZH-x6!~s#dE*@o z)fI&`A*vUz8ufV?w!GRhm(Wwyk`j2=|Bm?1mXK(y6h41y%Tjr~FVTkjtxf_Aivi7gH{avPaydb$TOnNx&* zfTPstym&0Tt$hJ9g$trHJ8MQfJEAt*Zi=z=uH93~8vlz!dg^WdRflD(n2ash3i^aR z!SY$FD~0P+uU5BoCcDIp7S`2gllps8%%Nf=2fajzOF#T@K2*yKzYBjXbBF28j1mqd zfBu+8R8!&$^$o5$FZLA*o!wOt89chHN(2v^ZE9_YxV%IM;k!ZQ>H2ti>5>vy5v=Rc zrE|GjnL0S+Be*1>{Ju4`TsgGbH)K5hzYG8sKlveX#9W2qmg5>{kPz9t67F~|oHJ#R z^#!B|(f8ya3o6wCwh-c7?RW;lE5_NHGM|R8srHMuOaRAtn_z=ToS5q+{k8p*XDp0x zIJ+YO!+^-~*6k<(h^>xJ2mlpRNEHe@n$lr{{R=bzhZpgh-;a^pa5Z-BpnzK>q8`Fr z(=O)*-qG04zK7VWdHXf`DL1I7G2#12q*EP(gF4ZU@0QuaA}F-SA&&qmx1Ps}cqqcR z$-#6u;?~>7$CO<=hfZ_SWe-{k5Z@9=+sZ8fA-&`8uzz>pawK3!t_gJZtY{RBW65g}1_zC0Z~nQG zsO@~EW)|*4avXe_f;Q~iCyMl9YxD~v6lDlse&tKBypTNk-~%&VRsN|$se(B*!ZISN z;TbuW7^)`GCj)O<=_fK7gICk%80$mgf(>b5ypcnr3SPX-i*W#0k^EYajnN$^KhTAO ze@Tc%t?B5)F{Aeww~mD`)84aFDT!e>&}6!({`pFK5VMJ^t|B&2 z2vG1&VvHO}x$aCHRJV%UgBctxU?#&Whn}Riyh+Y%9_>LwX|PDRbIZB!IXJx|SD3Jc z)KKT4&Kc$jO=rfY@ivg$QF> zP{HQQ9Nz#A9^_fU8d74*91FpgUPdj+xT}8o1Plz>K)3Wo-X9_dkUJ0GF0wwIQ%0@9 z*bqtaZBG1-_WJ(x*eIg$2Jb4(nTtZ>yJ?ZqfP)fG9Fkyyw(bh4F$FbI3Q|1UB#+>>~+-j>bA}3-t)`apO)ef5H)iM<9 zt6h{}@7JKEsdbHR5cyy7hgMpSSe5K7;169`jUeDx=&L0@NTORI8v`t@xTTk8{$kYN z?EWLZj-QRS9B6QxOcy8qCofg>Nj!FY!yeCrbyS@Iv=e>GHsWHgJpl^XLmL(<{NMN{ z+}73ct8)1mWB?ObEb`_h>G2LDh5Q0tXurJ}I3*#-BgUZ)Kja8QgnryT`1!>Nc;uwe zG|bG5%3>lrj3pYKW+fAqdW=5mKp8(+aEC3qheiAmz}xEK&Ld?wW>uhcBfgd9?u*+I zlIOO|@%n+$H*oeCz4kYx+;8JI^@?I;Q7VctTYLue>Gs==SDST2y0xtK!5(3SEco6I zC(1((K^^gILtKJ)UqM`MU(@fy8kae9Aq(VG;rnxg;b^xizjN^2de}M?kLIZO(Kqry zn7a-vQFcusPOQwXAWtArqU=50d+OG*2h}fNXCy0 z>hPqos>G9JD5rLr7~y5&cdNZ*`fY?Sm#v4n5_XSOu$H>$#V=LbZ8AnoQJnmi(0r>_ zWYBMK&A(jluvtpz>XH{>UM^8*%xTgoqNi+>Eksnf#cI8cK(~Yji%o&zzh#U02LPrO zZ3lv$K8SUgtcFH7lV@Nk@q-1(mk$CSzBo^yD?3xd#lnb1mADw~7u@LjBq2eAmHMcb zmjBiz|Gh2VOqPi;C^l)sSv3#ivuaif@;M;5LI4I~k?~;$V@?t{P;(DpP%EYxRE%Y( zBV7`T@AB}~0`NNvw);}3Zd`mg&>?3O%NOv%kPV||K=K4hsgiebqr3Xhyr(rdPKSsC zJb9&V5#FpZyrQpDWG~C2K6mdQUfbS(ShhSJ?Q>SQZ1%4#kw(6Rty1dkz!pB;B$w)!pd`_%WDckMl1Pa+tn!2pO!=f+Tni56Bg!)=%IZ$t;`P~{&z3i_3l6YxB@1@ik7zV*R`nyHq|My@yblFi`|c&sbdB=oE=Ng zW8unUaCTm{z2btU-uc`x)qCrJ8q48=C32=Q=|&!(?x2>a8&u0HiAhz1gA8E8N=L1& zalD_f!j6-9a9AZZ9s=)_1 zhJv#ixKh^aL|2TEI?nY|`BDV`V2DYz2_*JN5j$KR`GYZ^ytuImm4uep{eJpy_dcCy zoGn4X7q`Q$cEyy8w?!n+D3~IuTfXwj^?%#*_4RS7e(ui`^y+&OzUKdKov2Z5zDlC3 zPG!i6jYaw{y^2i?m%Q;BMT9NyG`fjV&=@jR4FUeQdv}I!nGb7CjY*D=8v_4wo(|XMn`C~5t5r4%Sz_XF$?dJ=}wNX zQ=QC2F|5sS$A9U1JZWkq`I2zh2{B64p80)eeHKJ!WNC;D5_o~ou$&4z0t!>ttaYf_ zP|xHwPPF*PZK`ZqZMV>;ges2liX>d9pO_O>s}rkEdur)8)Hb3=Hr;~Y74Ww3iSWuD zew3mk8hsOG)O7E=4=_w$V|IcFf^y0jaiai$&wX)%XdE8@09XNl`8Q;Anj8{l30_oC z$3jID)TLr*l(5v=49lMax#E`m(y({iiFWyDdG0x+xaAfZTGe7A>VH{b4HR&f> z^wKV9>cuJ6at6f}EEg16hS_2T1f}K(xiJ+rdr1c@CZbG}Y*TuXO7-}goh{2olt8ww z{PtpEK&uh@!F^^ny117Lt@F{YO|@1kieeYp6t77;ERi1IK{|`~Iz>sFc0sMq98FTo zgq88z`B9`}$L*vnY3HBQ5+v;%P;=UpP4q8zAMu8&UKfR{zP}L--wsb;itOSV=NQU1 z9q-F|`+SCnB($p8MHI*&l5v$HO*n${p_kxI(Df_n@)}&)gM4Dz#5|^kDr9~KsU?^2 zZit&!Ck5tx{DsYHbJqdNg*-EhBuzsx~w2NHOc)!81FtH;MOC<<8x z6CI*9Go6ZPRqARH@q~#CChi~jbIYe;X?PbfnN(rvjnzAGj70S82Z@g_lVxOaN<8iP z1;cfbm4L0=uno?t7UIT#VX!2|V2A4>)?O)VQFe;6d`YOoJ&*<|J1JGu*Kf+EqciC%#kuiPVlg?p8@N>`6E#V$SQbm3$u zlvJ-68&cAFOc8M<{X~=PoBh*i-RBl>?~`((NaIT0FdH(g#3Rj(6=8a4<4(J>@g?1D7>y!Z5r2Yi+XMdZIqE)!1u=p@L(PDmpWSN=@ysJ*+V!GW-X@m5IH5RAUeZJd4!ci-I#3r{`% zS(f2otI!?FN*jn)=tnju6Rb{ zVj!xe?ilMUN6zp<=8Vp< z{B>}=Cb$+2h7y<~Q<+OMrhrz3w%;K-!HE>)unZoDlVwo^9DUXv%s6u9uj=-@<`PF* zV;Ja*o8)uq2vqbT7s(w=czATq+6|^QcIqGMn7&dAhtwcEg9&tdc|MGI`)qi%l^OVo`eUE{g(I3xf+7V?n+$3J3 zecSJ5qE9mLbC~QS!ReDHdO(4}+Cf2{!QgcW##26>*Kk-LDfaJGH0_k}MyA)TwzZq4I(aDtklMqkE6CsTc>Bnt}I zC2Qz{aRIG@Nx{-Mj@?*8mN?Z+7#!HvTArm2(IJ+V zPkO1B(!xlX-N;lSoz1bau?0T<`FyMBd-~SUA282J_P!1$aznD!<_P24AoM(P#3t%- z*+CI>y8&`-#sp`#1pz`I5?Y!j6)7H1X|ZZ7gUjx(vKE1yX1W?&bQ95;RR@zwhPQl& zjB2ygNES{le4@wm4%%|&+ieuoo^V(U$jPmR=Jt}Y)o|egjO2QrlM-vI^ zDh(rfOJQoPHuD7s1N9goB_r&x9?DD)VU!sf!UvRTXNV^T3GMR0J00iMgQ(?TAGVIj zU>wu!d*>Md4DGCHCLhn@!Y8NBk{HDt1|&WFZgCpVV?^o_w|v%0ys0A&O258{-Dhe0=b}m~T{8%%B!tuOx#V4^z5;%va~> z|0#(M581~xHSuB{1ijC`x?R9|3u=Xslc*o1O1zD!m0NmTt7Qbhpm>e%yX~f>_C?E zEq?DkueG(6&HvUqU_Z5h&|@c2`T zK94>ci%{aLDLNwMq!}c#FXSW=2dWMwAAF*GzLE)A<-TV{Lbz!>x5Up&KJxRG^DXuU z+IA@cEN&pCt_>1V;>FxE^0NHh&+X-B3{te2p2~J{heX9Th<0$SSc4uU)wvV>wgkNa zS%qAL9ut3$pr+nql2Ft|Ju{ZC2YVOLwTg!VBuW#sC@%Jmrmf!I-riwFJ$wmADQl;ezOevNZw+Lziz~$|0Fbd7pbg**1Xr#@ftoC- zyXe($d@c?9<^bF6K8WRekBk5~oi;reOT7_9`7u7W18mTh_&4q#y^PbDHs|Ixpd-9Z zD242Rwb<_o@qf5tep~xuq@^?Z%J8oU;OV>Wo^kL3@2Cvq`&RAV_Efh zWu%ROo5w^*xsaS!CJ&OjtWJCXF71j>{nzoTfjdWuHf6MBkvw9X6LYp@kB0qkGq+o5 z8sST#@8+L=4h~lteb1v3RTaB;_j@0!PTx+mtao1@oCJLCjvtkYXkuyOXYyeJ#DNwA z*3y773Vm^lyV7*!wrKxT%uybA3rN291)8O%BpK8pn4wTsjH&7jSgUNcH(YFNMDyBc zLuJBa_h+qh5%d9X)G$3c5f>QsJb47d5RW4cZHV4n1XrusHs?z8-NqYj7@bHI0WQGz zDZ^yOcw)`^M#}?*vP`;LfF=ZjPg(@T}vl)Fact5J>B~W#0ZHXo|)Tx zf+kr{y`NP3H%;myt`Li zPev7o0382=G%sd*xw{uAIJJS5fs@=Wgb&zapdTxkXoxByP!Kv(5YGFXC1rg`KiV7r zfr!#sCqFhq-Zy9FMjDhN%}}c9{3IEt?h_dm|7%nD5n zpMuLLDiWz|Zr5dP?n*FCgO^0)l`in_-rDNwm(O!^3hf5Jhl{`mrhZqGYtpmJ1FCT^ zhb^o0;;#^UApn`VTqKoKKAh`Q%sT9t?`ue%k)gZjbTRS z5OW*>z97bJGc*L;fxClH4MRMtg5yopDrVI;!4~=0{3!{U$Z~8fx!6p3Qk-PYR11^p zMeUTgZl`yJ6Xs@0%p01<8GW-RHgX$^5!uwZPMQuw6QwE_?Ydo$M4ex4!|jIsrYCb) z?aJM(ge%_g)eHG3UqAEt1GZL`4bn6k{EyZe>gqn``(JA^41M+!zRiF94t#Cszia4s zJ^T4Xq3Xi(w@ARgRR%;{jyCqUTH3H)SrBXUm}HdjW6SGrUzHvd-~Oe@c}DdtSBhtK zaI?x>M&dLyO;V;nk`!JRw$`|nMJ)T0cUoW8vW2jcUdKhX+!(~HME!SiTAvnV6INk# zEeYjG6<15~ReK&aL*-16bS=m4$^^yYAd|E&9G;61#|-oF`(MJn$GTZT3U*U25{Yn@ z*seCqUJ!r?&n8AJ>IjoM%}_(Cpj8g5qAVWvbW#c+|AZ*0X#E(%h`wI(t9h*ij$ah^ zu-MZti_zt%0SUb#NhWa*8B3h!vV|cnMW)JeXI$MwJmz>e0MGD|gfRse45P*p-FP2N zL2n8siVZ2nvz$RbHH7JrePbs}6{dSnT8DlQF%+tAz@fj-m3Tt2u#m#RBW&}0jmaK> zJ!20gBcS^EQ$P9st(T%;P_YXh($U)XR+)0UJfeC+P}_sws^BxvmBcU%N; zf8}qfTM&*s<(~HW zrMZ3=_s%1e((Di}D&$hBFkxDJ(VzjxdbzGul~!y|CPs0@CllvSWq_ckOQzM4fElFt zmkF54#<5TkkGUOQ9YG`9pv5{Xu*?kcs@<+vJtu)1?~TFH{+;8uJTzJW%p|kTiW0&yb+f`yLNe#LVX(m=zXe|z^vbQ3|BnUmoHAe?hPo_wp)#6hM$}hsLz{Q$U)RgYTm}w_vNwPx*QErtgKxbJ%t!9HP z5oFnE!HEbJ*o#~4R>xOz&|TQb;ub?il#|j~rxcrV7Ff5i)Z+>7xZ&Wm>*g{k6n;FC zpNzM#qn+BVPiKh*aSEb-FMqFc+JqirGT1ylII2qkSb5aw=8q#R<6YHS3jW94{b%oa zUQA`A0gF^{PkVk_KRMfpMeedQ`XK>dKph{CKlB-rZtPcLP7wg92F4>>$V1|HBzi6j zvZHGlO(?ELsP!_80~tm+IOPRw7mlO!Sv5&p!@Rx-w#6zT>EtT5 zsrjM{Nv^!MZL%qDO+Lr3_K68L3`72eeG!Ns1!1Nr%mIT$ z(hP_7Ugwj_Q;Q?Suoa!%LeQ} zUm_soai?=n;zi7gB#YBdwxOtsM5JC@t>2RKq;j>v1srh}BG|i_-4ZM~ zD}X81idJUqiH8!$Ox+>!C^*&^VPgc$Y(D7A0Wcjlg@I%H6fLVfQH+Way!4v!bFk<1 zbq?mas~(U@C=c4KU!`do7`QxiIbN-M+UQflGe{|c6o`c>w~R>J0z+Z)D9roU>_R0~ z;tDty`U)1)7U~lUm{LZ^OSnY_l+IvPMK4}eW#>DFnvV6jNzExOzStQ>xzj%Ud~PsJ zh|SyrrGgiE3%}w>wd>^!JbDPBV^C@wJGr%$v6PH~Tg!|jhL4}|DPmbW19hF$=_>wA zb`!U<5iiyEN_c|qyAdIUbLJatc4WAeDt0p^aAMe0?tMCZX>tjNA48erMz_;47u@4- z;f~J%ufID^XuB{d{v7!~fnr36{)ZEd;p!D>ug4XE&qXV-Xnp52bcqEW*MpIdzjvN~ zlD#H7`Fy^mYfKaI?D)fC;I|q#44Au8%d=U`?_+NiI$B2IdGR2g3i)xglTsj9a}(VR z2`OdM?UphuaL(JKHXd6IM^l{>ze<(Qnw(IYbUhUH7Z44#}?D||uDSv)c=jj;519PST5V+X(xz32*UO!eG z(ivx*z-5$<@w~J?^9O%@0FnbB+_9W92p|D1pkq8=Cfh1wBK|# zU^T=%1=Krn703;olDL3`Bq0dH1T;q)rom+Yl`Qa431WGnYc7lvQ;|}1mliLR#ikRR z6UY&^Djy$}rvJ)8^OA4=87*n6r_YD1IQg~6UFwp4o(=@b_rv64rjARoOg0au=&*d? z8=+iM8HHjR{+Qke2YKk*qM~*)i06s8_sYS^=YnYW=2i7$fbMe?FJLHG?<9zour$e3 zuS|uc&trN_S)uCsz5e=dvTC63+1y2e?`Vz6UclM)=O9_b$HV3{sTBRL>uj5*EUH}! z$Tjv){cJOfPN!f8ZIs~6mq=%{QlE#V>YsiKKU_O8dcJgqh#gz_orF65L8 z*M||puxQ8e`}Fwpi2mhf4EU_pfs=j2bydNe>h#rw){Ut*;%G?-ybO4b2}kC4R@U?A zbs~bG4awiXN?3Jp!JA!2{-g@`FYA~YBhXW3WJb~4(nr)St4hf;Q)$uz@dtjX8YEDx z?(V6}vTz_PG^#GtBp%h1kn@||vYflcvz97+``DU-`ZP=(NvKD6mMy^qQp zVdkf03Nd;x|JZ`DgnXR(#UtR+yNK!G4T@?-(e$obdh+@>nv zoD&#(|7IPb7h8ip-RE6hw34bPxBbk1p|TK0Gie!z6)VkasLUJ|kjbQ_a~$UAmI=|Q z8ev%RIyqg6Vrd+RsX&(^nDQN@)-5pEo^RX7JM9#k*)ieX#5(OjeNY` z{t01HC>^YX5!)_ zJP|*ox)P4~zFkd~)27F>D_U;R#&MC?H$_5+!#Y|N!Ia(+g@LRCtaruvyPqzIdk9GZU)hL4lM4(IOu%MXPw!H$Vvrd4+- zJq#2t0pZwiQEL{q7Ea`($y4XL$%qmIaCc#p$qiM0O1Mylq$=@)B~H_=KcqP?EdVu1 zOS+6VamVlcw^yZ2vsjQH3oxn2A)0o>vRGC^P3-uLboquuY(1D zx6k^|hUc*rRv=6f^JF5-YL*ZD|82=VyWZXAYrjRGQ$^pvPkp)!5B{Pzp>}q5q7O#} ziE~V4{}%CEYYs2QfJq#crK#QwvlecL@uDuCnJJAvQ-ol2JhnhuK2Q~ajSoC@(z>3}>QACC5jVC)1YbbU+_k)%zfmsPO|l z>nfNZH@+x{13yPv0N?osfQlIav*p&Tpp=P2J!>ck^Y2}c^v+q`5xN_7KsUM_($%}o zl!4V4&E%lb;)`rtfIE;vzxsHggbqeSjPbn(aGJ8pi{GrAm^Ro^9BP*J1;(c(Q!A3> zL6S#J_JInEKZuhZOtxwi*G@2S`8xGWER_ymGIu2kXDDhNb_T#q039ow%0=t| zz2aI>q%;3Vpm-I`4v^C7;AJN_7g51;xXDl{@Hy?>yZd4Me7pC__W5AYPq1Q71aEY= z;>}j(X}{_EUOvnga2f67`}x*?V08T%Z~U>s+w+$t@FTG9p9ni2P?i&#^<2~og1dO| zC-Sklz9_ioNn!t1XD<(Bj0LdrQw~in7{Qo$i;9t(9$Vo9au=HST`aVc09FXuH6Q!{ zPAMW*0%|Hdib;S`eUThi$2MmJE{@hd04ht4AAZha!3rUx1MPZ~l;ILd>4v8z)API0 zhKh#nM$6@}LKdSgL@p_2w#M7>6wQ%im~;Z@6F@#u6W$D13|ACQ0du7MO5!6_?`)C| z#Z$x~@QxFR*Dw%23P$b>uZ+d4cR6=_Uf$Fa!&| z07NkJwUIFG(9UEvZ+esjtMilJ5i0eCvzK2oo8jYhQEp#sVIE8h*J4{Tp^i46h&!Tl znZjr-0HUJNCLH%r$dLPnmeE#u$wh1_NF5pvB^VbcWLlr>0SbwTJDb6JSwtQwi?|Xy zR85lwQIi20d#Q|A1~SJ>e1tU|0xD4?X!>fFZTZed^!$=k`T2TS3EX!0_%ocs#OgzJ zXb8}^q5M+vQvCB4UtC`X8@itd@B;7MMBgr(_nuX5Cb^QuF<6_Q$#159%_OIiy-th= zJnRLO5hZ^9jTE^X54^7ke8CI6eQfW0$nU=9_qrCo`s%k=rd^YkIYeUsUBXACWaHkD z6g&`t0wf^8xklyj4!fnyp+7UGoqubNZ`8ykl%$-6l&aP-Wglfr#)+B1jAAsHrZ1Z< zc3&c>W%%x=q~ZT}h9vXOJUPTlib6!2Lpk^?ZPnp>K(H{X-Aw%0wFGY6;@`m|D;qA7 zGHmu?fDhO_C0EI?*hlf+Gx9c82-Qe!z???QjdDxk!%$GrR2c2={KmN%K@RkeWg1O{ z3TzP9YcD9z2~Hac1KXxkNZeiYoBkg|TLtew^~r3DQ~hEB_JbTtqae zs$@nAyQ3_rgE{MS9$*z{2fHr>7RoSbfV!Dcs(}M?f8h*JToP3`Yt+hDW`LZ>S1Biy z8X}Cbpf>M4Hiz{vRT#BJ@&oYzLOlhb@Wo-5>2p!oMFz9^Ytr`HM>h7w(T7B^v>`Y& z(@7Mm>r4dnajdY-u84`T?>Gt|h_e)cf23Lv6nbBa=>ENzUp@uCn-{$NRqa2pWYbib zoFEO!X%?@t1M}t!x{WX9p!Gji@%H_i)MY61itsr3Y z$8V;9ujFdA^ujvwk>Wi-}BZ`SB`Lh(js4?i7)BzO6@1M+o z@=(gH9enS?UU2R5-u6QQcd8A23#gbk($JS0*cCdQB4Od_nX zPM}@sScQD>`MOPG#+bSxk&hzppeak2rIT)$ez0(j=gvN9vP)bjnHnE^=}ZuRm?NTm zvLwJA1}&>hD#b0Ab?Po>?3Fg%EwFV-`M)s6rOhC?1*pluhi??8iK2a2&!gz!HA%wg z3WO=x^jJocZ*L^+#IjXkd+2G|{~V1c%xM<9cisZ6kUdBAe+c%!Yz6*jfiZnP?(KZL zmoiwRj4|OP7@dol9NM8%Wp*!lV>3ixC#*v`POqjnnY$(V+?V{k=@xlEH&5iTw7}Dq zy&Wv!mW^gmX>qJLc@M z288~!)T3WcNG6bRR-gHW5!{WQuAwUQeuyXf{#~Q`_WXWHZItu5`C_nLCDTgs^Tap4 z2&QZq375Bl*gEsAlFLp9vhsir)g~Ea+b4~oJ1MB{xy3c$kP1gc>&(ZKVdE%rP z$vEL)IDp{T0`P6K47cPZ(=#NG=3cum*gMqW3sM8K3Y$2h_7K4jH`fJ-F4Cn7dBOY< zEdk!TY1S2ob9fUOBeZdJ8zdCKZ%Ja!cOOheQtKi?A|uS#Y$$|c*nfN&X~JQMH7M`G zZeM?oeqT!fUS`@HpoCL`iYQsFZORwITmU>Z zAoJLoB4Xa(&(kBB#DWHLdZB-U)7N8g20c|2Sev>EFI%`yK6_`M{8sXoqD#+M>C*56 zIm~MwU_#*yOFCg~RPVF|FyKQ9K?V*|+=j_8r%e0bC?Y7)8T7&?S*{iEkT% zF+~1fX?4m$on=>e)!nn1CB`b$+5dRVF>Lu6eB2=6>&PsqggfSQ*?iv&FqyTGzUu}kJe9WFI<`|Ko zrC`}ZS0Z29gF0ISp*B5q9oCg87ZHGu$WyyHh4B#HnZAk*vb&w7SkD|pE#vUhfKxBM&x$l+us}>9w$HC+?76WuV-my* zo<$xIGT)$YXN$x7TJ%LoiLo)^;II>kXtTV=u55o}pZ=`;j8w#vetCCijg3VqS3_H` z@AOvpYPV5ys6lEUi%vz@??mJs2a|H{!^Im&D7~B69Cp|}G)~hHtph;T#~;MnKsO*a zyNd%M-bC@UBeC1i4p)RwBHC7e%j!bLr_rdZb3ITix-d8|dbxD7KmcI;NrNxy!e}08N_r+Q zyfzodwmfVM_Tx+l7OFo$7p5V->*BepA@JxTL<6+yA~}U0aX4;uBa3n2xa>J8$sJ5z zUAYZwj*M`X#?N2V30yVB$RiZywxXH=xFfugvhnhF0@5&RbtN6C#M?}9)xi2}oHMA+ zQW)7ZuEc;+I|sx=6tQ-FwGS=#B6fL!UiS{+8%8C`=P(Do1?yjaa^Ek%yB|-8{yTi& zx{L}16+vY^1iLYlB=BFT_&Wf~Pj-|-(_`IejR*$9aZC{|wCQOq2X5@3or^o~fv=)& z80X`qK&`MBZb-I^va@KWs^Fg<`DiM#7!>MmHfuGIXAm9ck=qG2bUqr6Cgn6o%73umxCOn@$pO?u{`lDrs0P;)KZX zNMOHEr)X>%-!L9w2lt3c?|eg!W_vfONt0)07fQ{8I{pK^owKZ9NNxqI8N+q>2*0dj zv2}flIA7cIEzTjJ#f~gEN`+gOue~{I^rSyz_rSW zI2nklT9rEv1=Sf!K_R@*E}3_pF&q$|v`V`3^_AWeLURju5o-}Ns-#;-5Y;_xD@VlW zX80G>^3c!ag4T9y{Gp%+G4KU%?(4MENwdIR^*)2>%iH|dU%QrV?Td$Q za*nekVe6^t0gMGOXPrhfIE{3@;v7GwIbww)fUKHWW(a_Vl=-Bu+w=UW0C0As8!D%$ z?Z#Mmac&P3mbMh;k4nkk?Y>jH7ZdZNettJo7vpGEN^=z9KSYV4(tiI&w{B`y9Wy;$ z%5;@%yBaZYbNpWJb$0Paxy-XboM8^o+ev0{PjRNyB{!PnN&(m8ZiZGI|21OUZuHPJ zzpTRBZLxmyEK3t>OtDNW#dZ>j&_U|vaRG<7<_+;kn2BYSkj1~bTzOAhIc6Qns*OIj zMN)$+lkkXW+ZM7Yr@G~aavc(-VwjGYmKe}IjPQ3=FewokUyw!v4QKUKEwc$dup)`W zDHO-x%JKuIiQ4TA#i41R4d7NY8sV-F<9F0LW|J&BNMk!Y$c5PKU(cAtE`i zVRD=_6n`Y;L&mDLHIZn3eeN=m1UD7=K#lM~n$Xj@5&isiiAQKtF7KmI^qGopHY*@N ze1Y@*_UkYPywd`LT>DfFK*)NJ>OzMs;BASJ7txQ$LJQRM@GRV{S|i_csI z2Z;SZ`~lS4DVw@doE?N~_!Q59n8E5(-}P2zzJvq(NK56Aw}%1OSb`=MHOPnJglN+n zb2Fj zs=JjYPqeVnC`QJ8_r^=NXLPjLy$R-N?{&7hor=)RZos-fdh=mx%#G4H(>R?K`1~2FKw-~_?#BB3r_0_EibS%ZY-Mg7kB z0e{}UYhY$V3|MrI>J}yrEESe}F(b;gq(wzS4DjtVSz_dQ{rh?45IhgumtaPvw5Row zSj;bb0C1ap%V$G~9`{9$cD)S*=XDF?Gk99=Tg?$bRZ%xn z#?X3DS2E1Z_}3anJ70wQ0G|4P@Obl7E?gY{iJt8n(ps~uoX)k8aET()bxz0$^J3)B zK{n}Zb;im80y0xoeTcamZ+=7M9<|MPvk&a*JyWxLU~O=&F}J7wiEa&rSjo)NU#Lei zg)nn^4%5HjF{dKkt|aBV??w`&@Uk3q8pK6Y&q%?H7GTCL4H&%tVq==hgSm%fOI=pR zm<(pQKyUdek*&qTp$S-TTjzj`9KGzbXNzGmBIZ#v38kgLJZhL&VG$~eB=n{?Ui+8< znY}=B%num5_D*p!=_?XMdZYTcI!dG8%WTU8G5sm)lS!F#7X8LNKSL_)BT}WCc&~-q zZa7)!>e?u;V{UXDljNm4t=Jc)lk00*YUu<=41qO7nF_5JPqDO1Z365d;rQZGT%b(* zSVqXsgO4`DqnhT%_dF_R##T|B@w4Pi>4rSrk}rKV`FS+iAV1dJhhE@%JxJ%fRm^PE|6(wDz~lh+c=d3E9ex5qi<@F<*oJL>u`qc$wqI7|9hbHv@^v?)2_s3s+MkmH5=5JiaWq=<)&en#i4Wp&M6u|!K*JD9Zw!N8DzbSMn%GFOlO zJ~Pe1@&l>WA!g`K$3jVWaAGY?H>=kwbe$J88$#&&!TUG;=~!;RI8ADRblzP@rJ*$$-uaC9gq-%AHzB%mEup$olqNn*XgV=+w(#C9{<>8zbgN4<_Z=k7RT!Y*q zDou?8!_hQ|xO)g;Qzx^Lw#$_KLXf2=!6G_jW?6b&GVS+yg*F03#yB|1&a|=C@Dd>h zflmG8SXSj)Q&yys+LT|Y_V7$@As0imcd9cePouU4gJ)9umHcT<`L?op#%BxDP^U6` zwHL8PhXGKx0lgE@V++o6sF`sHZWspV^jS=4!3RQ(^vW>ue@ZPzNX!`V( zqZOIL6mX#|yU{!i*PfD^OITcORQ{cM;f*g0`BSAvzxF+w#0cW?1`c*0{N#NNSO8kc z#GUC(nfT|+t~4u1$$;GCXE-w7C``~y9p_mzlk~Evvr5rNeae0)#3$K7%oiIRyRvde z9SG_})y+)}Ofhq!z+Z=dNoOh+mq&EO8si~Y3lH`5&j&&^KClceFh1!C}KN|-{K}$re zzx~rNdnCA$n;||2zzftZ)wxgTlwI%*#dXZ*f+i~*2JGTiLoKS-ZmEgVD%MIFlPBlbq-Gaf4 zX#EHiynfIciogqde21nEk$EWNSwS%w7Ti#W2muyNLC64SQ49!a=Gsaw+DQ`<>L2>9(5RFxB1)|IiiUmAYWKmvX{SC({&g&K;*~YHb6>6__#N9Vqm8Wz4?9ZwMfL zxd{VyIa@}Q#>d-8M=qhtEhjz6DbNdZ}OtskeFA}GxC_jGiaR->kDjEn}c zR2HczcM^u7Ph#cFAqBWwN;;f;z&~*uY&D7q49ly5f1WFVpYyYBR*8*45l-TVTp|=* zy(T21m$tPtBlv!{Nt)e|7O-H;wNsV^kb#sUYP~B!_UoA61!7J{A}S8t_t?y?-_L=( z)ZnK@Pzj>sEyO@eW4bsGzU6R@%ODGc%tb+1E8~s5jjk;G`IeoJ*rLRQ6a+Rac}1YD z7x$U~@KC4^zBxmoE_ zUnzcc~ihj4iO-8OW2-L5Ifta5_c7&6L#Dd4%UMb%WKn+@8tZv z&yxpdZ-fD4<~Z#cx;?X+pDA035RtIoYPHCsKRn{qElyM4$ z-f+iCVCN=&n36}}DT3s{GPENmGY0e3W~;G|=Nx=>suMHkD%F)QR|;4QFm3_x&Z5mj z9O4md*AU7vhgf{p>LX@)Oaw`iiyNa$Ro}ZsP62Yuh1!SGLc-Z#YwzTp8>fW-Ufh|Y+3Ut~xohDG3^uFC0LDRF-fXqR@SO-bp!XDMj%*Mk zt3W^gPV>hBz~F~O?W9XmQw+HRVC{;+DP@bo70Ro^sKOPpF{Mz}oQz?oC+Y$r2o)M; z*FYDoYxCv=TYBu$Bc$!l%mmBzqf1<`3tV4hp~Hp90H(GHm!ra7o3e5-xuEEB9>9r9 zGv1=mZ34GZ1Uq*|ObOOs|{T#*u=0a(&+_{{JISm?ct1;Ct;N6XiNAtR(g zVj(Qm-Umcic;bz*5ag1O@-n#Cf}=u>>SIjUtQ2PndW_tr=>c527_nI)L}?;#8604s* zWDOwaB}=S8ugxEA0O~B@%hU2!qmsIV>FJAbvS__dbM7F+M6v8zDrF?DlXQ${8zAO6 zL5p~pr5AauA8|5=wb>MLFaHn?26iEq2?Qptxp^bw!~S;Z~o&miHo?JM3S>~U?6##?KNa%EAGgh)C21cDE6&(B?OONUl399>;KeBs>TE0<23KDkFoED@G4*2&E#dy<$9T)U+nj0nHTke3Nmp^oM|67VYPMyZMzE*1tGp|yhN zPF2S`XtQ-`(|9y;28(9J@dhRuk=Zy#?%|At&&MzGd?L!f5y0z0YmX>a{PO_5>gPEC z&NS6k0OqteH&0u)uUaoGc`kriaSiPpS(V8~Qa(xm=VCRBF{BgI@1l0~+!6#Qls)Io zgfoDJti~YG48XGQshD_((QIWPWyxlxa2sdPKrWyLVb;}eH&+4nz-Fc``vA=W7~{(9 zE6CoQ{gx}>u4VwHhPmJ5wV%Y!X{w1@l@2?MN*l>3b>P4YRxcd9_ud2d+_V4e*_+Ov zJ#_b72f8Z4t85Oyl01v*Aa8Lt=W3V%46LHK^;(TmHLeYA0M%d5xEojr8n{H+&PkFB0Ra^>Rj<40fU z6wZ%yQsw}R%nPuzx-G27@&Qr!incduSYDLcECbk#sL&eQF`S#g#U^7X;0w?V&DAep zqA_?NAr~IBcp=ftVxDbq4-g_;!DUQ}zzLN>z>}ba-G;c&CB5jWQ4?mg2 z%DHitM(3_i&X5O_E!j>TVp{4MA1J=oRJW0#h!f>>8T32jDMLcdtg0a$h(VXn(<&1h0N6h6JFU8a|0 z*ifb{P?xeMmkwVBaQ1cR?IHl+e%*KMMTxHf0K9c&@yexR4?S?lrHd!d-h1G}xx@Ee zIey~S-9A?Y+^k31qAY_YD$xIo^tFbk|FyJdu-Y^!F5~{hm?!W<%d{yO>C)IK?@anR zOIi+6Kzcfo8!s8m8we#d(6?-=;d>!U6CGW|c1jYN05l};YQuEHb(^+<LTQr>SNla^8ivESA2;@b-$Gl1Q5s%3iEUX?}X7mAh7EJ>zl zJI}1lirYLw2#$m^MnfezY_paDj3I%%%c1N`IMimtq3H82{4IQQM48-AJLG=SQ0sEW zI6G`$2e$Pbl}z4q7??4ZuCWr*>8B%Q9=T<1x0yRSJk?Yw=BANs^)%So4Zvpl-FLZY zlOh95sNVY>A!-MS0tqdd)SdNIvg4El8E{L6d8ycFz{wf5Rm`6eLJM>F;I7M;kKc3m zO&3>>UA%B?^}>;JXAh6- z9hE#Tnzp`6Paonu$sjBp5=gG*maGl&V6kE9mJN%KzVzO|{mpMXb=#iDAHVR(i|@H~ z@s?A!?M8)+B_^-+*>*xk@S)JaaIeyCx$xU?SdRBt6N6hpzlvFggw4WEW$Kn<=~_dy zC2gA7Y!>q-*Z^Q%&XbJ&$Q0VhtbrMm;aR6;|3Q&j0G3)?Bri>ci3P~KEU6;u*qqa* zf)={8?6E<}K}Qb#H3FDPJT_naS^&(tT;yYa6M*^A#lW7P{g~dKp)PNID76zud9K6eF>lOCk z2-v86UH~H}1S2h_vXWF{SWk1^+uu2oI}95D;K|NnH=te!=*2HyL|we@xL)W3*%5S9 zs7h-Kr|Xr4$)hj1@Rm0}`hgF;`wKtu*>}C;4Xdk1PoBJ9_?qVcn7%w3Rqr)gZ!FxF z!b~)FCv3UZfzx`09~E&Gn8eE$s2(+@*r4r5NM_k#IQP$T^6!nEK(y~^5tTRn%vmQQ zwukD&S!W9DW*ABm?-{^M#j%?YL}otJv2pCxc`g>gf{+%h!BgcbsUu&EaWJ9s=Nzr_ zwn+i*Ja)cC-v4Q6HV8_EZOhq_Fc9;j#r?1`ePo$e$aWPR24oJLd&r{G$d!h8tcq3b zY_^-9gpRG8d{_tItoO>Z_(po-bp+6Bd4!sQ>y(1}LHeMAs?Ys7sFTd|opZK2d`{;K){iatu z`qHx(&K?;zB{I*sx*AXBg%cAj0RS9!m}bkqr+p5^y+tHuN)x!4)xg8PggoY~)tt2= z_>--C#4Vl!HCHG;KBVFm6c^?N+HKa5N>z09x@ObzrPZS!{LtU|zE6DQMGxNf)~8?j z;Qgm>KfO1@M%uhZfuU5c3xPpwv$kin-KFg^uAQ&7+h8nQFM^+X4?84RV(5=|JGf=` znWoN9*e^f`<%EzZl#nseEdbjTbB#+e(A_GML;#Gr|M)OMoT8^Fn!01s`)unF&t>pc zSz$Nul;A2!fMAoe+~`3HU3^slOa004J-6>!wr-Yzd$Hr`_eSd%7@;D10E-*&oHsy5 z@^b;qDXM%!&jVOcTDj21vivFlmQ9TMS^xPL%G|Q}OEYNj$bI)U&%v)#P(43@v+QXc zq9lHK%N1tejl;guSOR8;V!aJL_TXG692-hj39XbF6;}Z`Xx+t#9j~d~=x)C=nbV{u#WtA$mh7*+!t}uoTBmfxMRxvH;Xl-4g_-UDD_jHEq zFt7+!jJL=UNJmlcWFWVyAekmcFu$xMx6Skr8g;0Oq49FDV23YoVn13tNf;!w*;LG; z1{R$3a%CnDB(6Mi{VbErEiz6Z zuc?-bn&S{Oh!+(;Tvq@s*%p9CS(3716oGBwz6zDHJezoM58HmELEL0m&(BQldZ`KU zjQZBj)Kt9;i&6?<-E4tf3-bt(?4V5_=>TqXagidcl_wilP7lHw0{Rh zNYcU)9i@^K+9`GO-tG52aND-)ato8I~QryjoZu6?WLk4^|+Q8kNgA^`?IOnUfz z&dk<&cH7I^Pw@uh=pX!XWN1 zBTYH55=jKUa_RWR)uZ=aI(g~h(Tf+3J#gQt)3@zm?%Izu;ygrCz=BgL)hV^rxJ|~6 zxdVyg7*iL*h4W|M{@6=ieevAJ@u*OR5MYKg`b)6oRgYZ0bmsUGntsIhzwh09ZhWDF zjR^tlyj5!7rp@2_t=}Bc-cm>Q9eCe+-U(X;?(NucfT_gO^mUsy|Km^pz>aILg$qiq zbphbgIjz*D>C%sX=AXXisaG|+Y^v_MEn9x`*M9ZNiyy$D0(95P73gqCk~t*S(3Dc1 zMVk(pW0aXDxI+v5;Ek(8fUpZKZ$bV#0MqD*yVP-KH;KkV*-4!s1Kh(;Z!v++TSjm1 zGTr~o#2dm2^tx!aRcY2Cw6NUqb3R)zF3UPf7-+zsk=(Mlhi4H;X02G}abgA^fI4Ja z;j!*Q%FTJEfqClLX~39u_)HWj@Rms<;IiK6rQvlo>HTGUVm{_2sDVx{y0G7GP zeoJ27l;XC9yd6fQ0OY2{=F0iQ7tS8J=dJ^1?>V@7?(nfA*CU#-_k-jam90W`3jKpR zbYR!L_a1ook$XP+UElfS%deb2fAIXdBhw~{4MX01qvwt1HUyf}(qzZ>VIMQrK|-5Z z6ybR%v~13dWh+s$In27rS}@Efi>96Tk67yD(rF04qD~p{go`4`D79_V^5sh>R#%T) zxpV?ZK7Y#tS5BP1ZJ!0vlidYs+HRgXb@HD*`y)T|(?9Wz-|+U8uAL}VW8YvCENp3) zN8Nw^o&SU_D%RFM{pn8>*jWbqBshxaHhFBS{IL&y=xwigjZ*5yUDyBmZ~XeMUEA?A zkwBJs!X4M&_{;z5UzW}(r7quo*FXE`&%&LX^cC&XXH3V_8yCcE><1d>EiYNSCmX$8zMlu!nM_?z?jG{wpUgtscAo%E?nF z_YBi-714Y2(4qhGd%yG46EFSvH-7Wl+S(^S`Huh|YnQln=v1j)qw#P3_HPS-|MGwQ zpNIGD!4r`5BD38{0DtFqe#g6C^-AoDe);}i{H0$YII-zj6GC9{yyN=4U;4S9#lVnK zm+!p$$DaLBglq26EvooL0ADzDdTnj(&b#hpFh>Ff0Mj4OD1FUz>97CfPe1vC|3ngo zE4SaVwzl@r<1ZzEy|WrtNu_hFgh^3QM>5Anvv-U~vQ9u+ug@>Elo`WJU-{t&QP-^l zu;dR?B=<~p4r>kdv?WJ@Y&hZ-^5J=R6Cvb{T|0@Idc2+wDP8?jEQx3Pym< z*)09v@w+hCv?I!iH0{fY5CMN4Ihq?S?w#f4dcw4rCh%mhv%cqK0auY{&QJf-V>M?D zSuQqblqFSe-tJfkHe`1$GW=pEd+hfsI~B783!$V9=vkt z=;;%CF{0o(!S5)C%(DdA75dPDT^BDMSzSGHY4z3%7Y?tk9zJ*W(4>uwz?3)3wL>#c zG>M?HB^A&0IU_AM6BA{BMj9P9p*-nbj`J6XcQjU8PCJSCp|?`P0^ze!L~nk8lOs9s zpaYSbaH}!bYK-Ss)>)E*^yE^>h{-u2aLLU-a-_{r5k+^1vlz;K9N51Fqd=<)Tq)$MS|h`j5Z$*=Ik4 zV5`)d9(n27+S((JzXZG0J`ziot3ax!c=KmUiVVBX5 zlF3R>%5EKi`6s^)z=N`f9#O-_MOTeF$hHsc(a|5L7{+?DKb9b8P;n5M@RyQVd=Zf3 zVTSbpzRGtNvw=l#mC2O=Vg)Y5q~>luKD4>J#784sjrjPp8Zs>4qlts_LO zA~|fcAiHUcXEH2Tl-ah_J^93ifAoDHdit%ey>j1~^Y`9->Q+P{^#8mrJ*nef^5Ipn zfB%jvmyVq~e`s~}=*87rE}TDj?(Cs)%@i8)GTMFh0;xPpNC3rANt@s zUIzgG)K7llm9N0dIUyJ(F?Fdal)Ce#gFo~0KNpbNtBZHv`|M`{U^r>Wl)H5);=SJX zy4U~vFMs)oClORzMqr{NMi2UG*Yd_+|H@ZB|LkYmgs}h4*S+o!fB*Mh_UKEn7DVr~ z9d>5O4Iv8|x2Uh?=Y%sUE&GhFbKaS=ZWl2o+WqwV=wS>Yhj<7u6&Hm~9#NDB<&CI4 zKyV1KqGvJOiw8u8L8G7`>g!?BP^MBNBXEVyU19CpZ!(-mfBU9!b}-I^KK=GtB)vE` zL?KttPD_O;ZDbyVGf9JxV^}Ye5+gggH8Yhp{LmaL4=t3U!ITN%%q?d(iCmrL-2;Ri z;;svv6UyFBfCRL-sI>Wxlx@o9@zA(qQLxq*dfQ_A`d2^n#y39tv5$V>pZ@RXGvoj+{S#%j$*0XYV~YZEKKNoTne=@~E;F z;g=HHxnjQ;JG@}6$r8&Aqwy#pQN#=eO;VAPutXQ50v7N9KMcKOn>gMZcqV)4bLK%p zNpKH(5goDa2*xauN|f?#(b#QDZ{4(b-<1<*@4fl%yAE7jy%i+ZmD_GVwO3SP$rxLx z<*xg0zx%s?@jw5^t+yPil=}H+e{yYY?di9?p;juO-vdF&<_#;q_@ytdOvg&8H@xD> zAN-LYT2N{d0%gKx8ly!7e&$tAymUboU;fu$zG?qn;2%4!`lc+GyKz$LuKfpp?qB|V zmnx;y{pVH%z!j=o8{zxms)!W&o8I-#U;Gz8`Sdqn1G)yoz+_TFSw{#U>B z6Q6r_%kqL%>I2{S{{Qf6UwP=^7m>s&gJuLCMCwu}NTK^2{d14lUVEIC(qel#M^@9@ zDR-Rb77aZ1q@ z3+c>t<$h+rvnJDE8EnAah8;}kVkLo2hcKO&zwoYm4iLkK&!0Us zZ7T-=lYGh&j^6vus!FT8WpTE2&R*Hlft%J%Ct?-2=g9`>NUTQKIfXVrGMHg|T?%#* zqG3IoV{#7cPG0bDvR4UF%^15OmSRpq2W_M?Uz{7ysY1y#=4hhmQ4LcAgpW0~y#3zW7UFPu zJaxzgFi5FHWG-PVe&_ACJiTw{9e3RU^%)Jx$dE)+REe?b!s4&L{nn8eUZ^OC=!}Q% zz3*G^zPoJA3V|`lg!?cjav+Bj_+qxAA#M7<2eA4D0A5%%FH~R|?DMDsqYUi?0H={p za}I(5MnTFJr*e&LGv95{Lii*AbJ8#AqDUf3GE~xnQ_|XE0K3cQbV3^0tm(j*h1W#7 z@y1>kJl5d&L25n}1EScNQ?h8@c86Ff$cx5W;xqHf88%xySP-_;3tm6qL_ zJf-qDBJw?^@Dz$lr6g;bifz;b1Aq|&Die#MzPV)EMdxjJ<>iByUbtnXuX3=j7IdSy z{YpDg(Ts`}xM zRUDd7l7UZWmwu~AD#pGlf~1xRVm6G3Vs6;KX=C#6;m2Qp{iUa#*tLA=&`heUzqcAs zp7fUo`|E~+f(`9024m-S_I>!H|CpbjzxMKr8yRcUtdI=*r_#)vx8HiL+_7VfEuD%V zKKv46EK|_~*n!#_G`G7R*s)|Tb>-@fk3aD!)I;18p$DC+;rKe#()RM(uL0YUu@zHE z7r>w*q}wXMfw7$rZvWC1m+#*55dJPL3&GD_1AUd)QeFSr%dfof+_Q}>O$uX=?R?~| z0{ZQmn!Iz*UAt}DO?Q3u#+56E$0wT-iO!JMQo;WX#uLt3=lG$-YW_gM zZ4<1uLDP#UCDO7N&8u967DbuEK@qlYKu6(Ik{ABkYCy9f_f%HBs^&pS9iSH%b-3Tc zzc3{m(PaTwBpwW=%aqhYL6AgQW*< z9is+hrI(3&Z@%@%|M0z^|LEW6=jU&`;<7TvGE}1$7s%z2?C-pEY|!g57+X3${lW|T zX+X_@JE;Q1DaIaq@WJWX_@j5`Ho_z+_U&#!l(;iRl-ob{~3V6j010ARA(pnlj5VW1iHivjE! z;?QCYUeI2=VDrl`JTDY#m=_gf&ijQgj^{2AFpwV&{u{l9?p2|mw0L|mcGpnHAaO%u zLQ&LNmTH@hw@r;T#K#(w@wU#ka)m7b*j)-jd6xy&P@BJQW9rh2PMcfOyKX}~9&brZ zwgHiY0rv}x#kqQcFR@@O=Kyv&6_`bc;hpJ7Bv|<%fZ?Tr8V{tA$&z1{!n*1xqDO_l zj7QTm3@xB4UJZI|uL=g&m}5*f^jxOP#|I?9aes1AC=lgpqKm^8n&oB&W~O`AtV}Ln zK63B&+itt{s$`;jXrK<3Dd0E-FDgM2n?(6LPdxL}kN(eaZq~OB9+{t?f8dgfa~TV3 zMqEuKsRFkW=A) z3(RNiG-bv{THB7kd0Yl(LAHEs>c~q++&vk8VarU&3r6-o`|MC(&ud2yORyINuORTO zmsJCDj#+C}?UPSF{^ahxLsMe`S>L1EzjinMEvRH&AL2Rfi+;ZCN zV~_5AvFfvUmq)Gu@$uO;`Ze+ucIylbhW>`}!Me?Xp1dPsgesUxC0!sJg z+fAtsoXp7Q5VpO!(eP{vm{`eV`=(8^XKh_Qlj<58Xh2ggwA9kH4951He!=|w{CO9j zUCh|uzV-F_`T1>^o*!bYK+~oqIfJqKXx?AH@`@+mWsKc&!woxb{u0EmLLjhIAjpOa z9rB*LzIxl8H@|xHh|h*r1r4-OyN@j=0Jx?7wKrc61-ynLoxAarg9o3d%@g+6s;;O4 zVg@ZYLz+h9yzQ=a&-mznL`CMf*x^MTc9bdg?#VPAy z<7Ie&=EbMqMQ`XNvLG#55D)Sxm+zuZ#5tK|((-BX+IdkmrXw3tRe+?f!q-XbLehWA zNdR87$HGk;)n|0&;}OKf@!+wex0YvddT*fT-qp15F1??^67wF!B4m( z=moBbt52ZBE!u3kCm$Y1jP)54w5SMk5duZXQ$#q_9}+yc?m1A;fjEiF8YgHPr^8}90eF!s)yZ_dxpUwYXk5GbW+J)+`ctR*M^oA10S zDlopj`_9{^ty(TI1}+<-9F-J5vX>ve;kHA^4t@Q#V?IP3+G1h=3hgNv>#wf)^>04< zw;%oBhd=n|KYjeieNV!uY#9&>N-+zJv9_w}!$)3-g~G4Be7v}go zestfi?YG~ycGHHYqM|*!cJBD{T?Ami6VNqbS6;JiF4@Ao3QX z4?rq5+>i;tFrDKw#>Mc$;tF~LjCHd$%~EwTl=kW-#E`oSAsU7kZ@G{Hjyw}ePD4Tu zkGdkD;*tqRH5`E59-ACb(g1c_U&R8IHaZ~-$OHm~Fk~xZ=~g)(%gs9EvSDufPGpU0wx_e;!@; zYlS)nQLo>j*(#~L0!u>w5S0vc2v=? zNkNs&;*em62R22Le4+7P zxfNFmK{2-)4Hl^ z9(`iZZI@qu`uS&fHZ<N2@lYI+q+tU_ zMg-_o2KTYF0*M|)yxB%}G|3+D>B3P>3oXjsAUP%9A>#~rvJnriH!*Ss{zM94NT-mV zM3W}k27VUpPI;sQ5%g%3MLh;;+ywZyKq&*SZ55avP-8LA!WH+ye2W@1kE(hU6*_IG zy72cT`X?1_E*0kUEs7tBlYkARdqAcE7{o&s^09H4#0WK%x!?zIfdmkkT#H#W&B0qM zoR>JC1OvWNflNu$_>9n*kRNhV6SeEsj!leJ4-J%0j?{Lxm%z0Qivyl^1PhoIz*?1= z&{S76J<}eaYE2~C67kkls$)9Y5%5_GK1U$J4vAJ|A8TRH4Bv1_(GU9@bT_B1a7cjO@{xQ&Yf)*b<~l_~whYEPIiGZz_t!1gOBNu4KF;mF%3E?oPrT zxPNr0QB!1(s8nls(P`CPDw_0gP`1Z(bq)B7g~#3V7HffKO8Mrvc0p8+Z(yVt~1!dJ=$f{{fXBPeGDb zhrC8?iMfUW*!En_Psbh*{c-vS<_~f=bB=Iuz=1PndPs(QGTFWgz|DX#Xsq5u%;34c zl=|xeSQNYpm>3d=#Ni4j9uZ7#IEw)cO^SlUvm}A=5aS5&fdZGaT)%eK=IJAc_iw-V z*0JH*@!{%@R;a)PUeeuk62lSkiZSiare5Ia zB|fvv>&dsRn62cQdXA|Vd#wV?j+k0dQ?m>;Z0i}O7E(1YI!Bf}90He9`JA&mM$3>9ct1vrGklBmX|SMX{_RtU;So&e*T@8 z4)S!(aU8{AVBk-FLr}>-YK@53f+6 zQ!@m{9^Jn47a#uY@4xq*Wy??-1e#vB@%k;>XW9~DiLRcPkHP(kF}7~`ipQSE7FxqY zkD?iP1is{)b8h?Uos6+#`=5^|6F4LTRo0XxzYYwL?&`Xy4?eRtp1A$1cWhm`>Wb^H zdEn-oR&9bMR=~ClS%(=|Mnf!Djf8%=E`S%C+NG~bR8+a#pDX?s#VMr`izr2VVN&7Z zm&@Rd~k>Ro!Y^{k)fITg6BuPbt&PlGB z4%siGmm<(J&=DH=x}3#HoDJYzQ47som*|uN<0x7J06zqKUkRqpOznZd^YxJXqP$Rs;`6z7|FcOQb^*m9n{^WM-ygYO*a6Z%<9N#iv>) z;SOxW)BxXqOf8|vLQH1CDH`J=kf{cQDoUlHYqkuMT3gn=lImBi5DdNGL*UiqJj*EY z0(vt|qdM%(wVf=}uJqf5KC8%U8|Ni*L12qmF%2ObuFLkA0BNf(9~iJhy-TBasH(gIWN()kp6a{v8K!8s54jei9l7h4D#y8_5yzwU3kR5*h+0{gl1cXbt`~!ryS5!T7@af6U?)!H>aPgKcTh2Uv z$1S&>e$G~bv7pD%6^+hFWH|W01F);w|6Kl{+cQ&{1&W-aqPU#m+aU3Ur!zqrhcud# z0Gu9x>DokwLyMD^@T^64o(lj@=jEU!0Jq3P1_%eR+m3QwF0Q`tcTeO?ky}0ehyccz zm9)_`-Gl`A(*cM0TmUW+?j9D1zIa=4P?-QRg9$A(_W*D$sTIf*_Gp?8b1At`NRS=LC~*2?cysdSItkd ze*TMm)r=c9oOKDhnAF4pyx7i?1DgO0O|PP=Fd>jPAE=!c=p0`FV6wTy?Xi0{v0sRqC}GGlE4S zyEq6XI>#_$j+0}X8KxXHG@q&jRV71FyU384i$g?1lClJRz9VlbCmL9`wEv9LmL{g! zM~3T@iMHj-2I8YFURhHFS%%0Cu}ac1C8%y;&;~)YOa@~af*>k#2%S|7#=3Iz-#d1^ z%`gSVZoK@;+wZtZW-J5_^AIy&;Q0Fd_MJ=TW~cgxzxn<*1)zE8wxaoT!xmJp3RCj* z!0^E%hk%X2*rv5>9^HokR)KD<$fBvi7kTWVhd{CT2WaCjy5drTCD4i$&5x6IYgy%i zm!4~^tbTIeqYrMsx39N*`^`76-aujo%}}`~)`A!qFGHv+LSZo-iWdDAz0Zl$fba!U zB=>Djmr%%PRTW)@!4(`{$Z!M=HBA+q08F5p&~};w7+1%53Q7l^(tJ)8brDZ1O}*yt z&@Z}CloYzdnZ`U_lCvPvj!HDs5)`?amWv0(UV7t&M0|`aK`6S>+Zcd^t~&5y0>*{w z^MZ6}fIT@&!qXI0haN#fF9A6AS^}I~Fv`@ex}e7Z-47~?;V(&PDd_|jJYRv62Ap%? zazocCeDr|ghbl0L6Nc4<=GFD2KrYawV+6O$C`Mc-Ho6 zO4v{Xs+?u(MP55*>-mOO5pW88X0g{U4|pqsR!P7v57-qxPodu_^jRfdt0L;F3VF%{ z{!CqK%nkR{73bP&mMMo#Eo5j#vfPMC4#*p!2re-IJ4J8{Qkakv4J=*KyL-T;hBZrQ>NC4ivZp~g?R)OM|f<9$QVC>*?&%F2kJG&mZ@9+QSTX%f*c3NWLL({NO zY}i&-dFbf={EUnzc0KagQ%}^@)IMIQ@pVW_dr-Pui^ArWP;(q z#{hsGQSx9jOtswucVJLfU<(bKDhDx)?z5yZ0*a!ez;ZR+HoMG9vhqR75iN~n7r?Th z!U75UUf6^wqECkdtOFI2&khhTC`tukXUSAke7rF}T0cHiGci`%+fxZqM%;-C0Zhjy zu+$Nn8cJtoItGWTQq!I1p1o;yx+k7!i~2n(IhGN zl_ifV`ZPJy&@*h+r^+$M%D1gTM=SGNxwet-vl_EQ9l5^tn7=;iY0vf5WjZ~1(aM0Q zJnU2lopQgYD&*8<1bQk8N`k&DUCJlbk4X5HA z6BDhQPMy2=p4)G~_3G(l*XVFP(COj&2L5$H%%H52lv)8c*m;ta1-{VWN2!Z)p)BVz zR#{N^_8YG@sIbsFYvZQ7@4pA`uBr;NgRI!N&3 z)v)ELj4c@(JMiKQa7t$E^mS|Z?7=Oxf(DeRi*i);=#j&Hw`A_|XP)9)XmI>g zO&yq6?d4U6Uw$EK*^lph=d=h z8|bGz_XuD8ob^F(lvzqd696ZoR97`aRZSIw)ree$=!IXg6g2EZ_KCancApMB;R@YA ziD8R%n9}9bQnDcmMh7b=Mrzuc3&=Ps?WJx3 z85S-{GKv{yXSx!Jj)}>Z@zJKKcvCXomJ#$S0yf*O8wN5);bCDzumgM>y@u*BbP!|V zbHoOAr0iGWXcW+th$d$la;~l9Syq;17Ws|hfKlXi8X}>ZaIiM)Y031|MZG<_q27|r z!NTZ3VWd79=*SM#X9nvd;o7jbBhNonnOorV1i=t8B`6O_K^24uFCJ*2 zBY{++ZOKf}$Vm0F>u)?|%(Ctj!baXRlcmk%!YdDVS zn7ZweWs|YOg2Hcq{cA0_D86{ZrU!RF2u4MU9lSkM22tH2~?a-+yn#YS8!D zs%c^>A}GBWn;si|{>A6XN#l$)8+PsjKBlc2$oc@$V$|n-_pLVziVGQI$)VB1M}cc; zVTpEB&5MnCds*eNH;!qHJ+WiQ8-MkdKM;8QtJ{{XgFjAJ!5`ZNFocqw0AK|fi-xQ! z$d3LzfFUP>*Xl*Z8S(iS0PLc70l=oBQO@e;0gSAD4q%=@vry!v&jVOH5x_+K=i#de zV2YWB%EJSPp|H3Brr1tindzX#_g-*#BnK?E@GIPjLU!EQmHXznze{{Cl=y>lEk$_K zr8J#jNV!UBJlp6sgI)sM?t=hKd}E5?<5FZWWP#PL0n|w3U}BMnS1J>7L(a@pxtL*34j2exSEFW2hvnH!s|k7wRp_Z477Bhdgyz-m&UfX~0+Pz&hHN zrI4!lRMi0kQ`aI_kt3jw7iA{qWrSu@?b9jn6`7kGSideYJKZ%r)M%&zTK!=Dq}aTSo`4KT|vg`Wlew` zr(mMV#_qklx2%R)^x)Ia+;H>t9DBHbu~mTh^!Vs=2cFRcfiZUG+6@oy-boI4#3>dS z%g@UE);He`g+O)O-_-iXtFNM{7*c6VKin9}6JK>$`40~j!jZ!U%b z&&bf?32v^(C4Xe#;vV-^Fi*lI9mPic!(7!a$wX(CpWU_r~sx>jyGLdKv1?-UR z$qEEKmPK4>4^%(JYgn4Bcs0xAH;@1TAOJ~3K~&WT&7Q#jKC%LbB0!hqSCojRW}8Z; zrAKu=OI5NQJrvQ06{$M7O@MhI)W%(VcdcCo75J9xu6ynPXo+l)5yC`i zWAU7uoOTWLciqMZcaiN5xXBU?tG2xSYj3^opyI!|q~fjDUbnqA#5fTm5WsEa zRmYEk8{V@I?f(8h{{v&}@%!#ux^_AG?*RRgfrJ7g8$=mvFE^A0<6E?z6!6HjVAsB= zg?6VLvgyp9e5iW_*L49+L9V)jhrRc1?V^FD%b-S9PLWV5$|G`|xp* zn;UYz2M^l8%8Y7WH$W6UYt!71wB0PN&4|UZ;A;abD9C_>^%CzPx$`?zSBIP>;78zT zS>P-Wv$2+5N+Ek}kt!XXLy(N&;&Wl`OszQ8WS^?y<{4QWU>S=s#Yo1YCkqv4j|z{E zpl&)Egj$fy2rQQbu`udgmTFH-wGH=I4D^#pbIx+XuvrmGYkoWBnLB<%{yLZ>>mCIm!U9s$MzWqK*f^oP5 z!3KQ5?9|k=&%s#B7(2(Uz$T~;WgS0Yu($W%;R8^h87s}odGpoRvU0N(Myg)8;J_BT zy!z;oLtw#m^X+fH`xaFG?e{HTvy9KclFSTU4|shF5ETf(U?{5)fUz!NcH#d5z$AG1 zp92^$OaLY`YggNp&ORpZShN&Y3<5ApnlNCRx6r&#<{1teY5Knyz+8vO$(ZgKjM>)Q z!=28#aFsDcILtUGnM!94xv525KnE^-B<@8~I}k7_6Q2_YO^;ZrmnLUk zz4pTRc;jGSHCmCRQ;vvq4VQKieW5|jFkF{Rw5O7-@yX`+RBOoV$N~mi0&5+Zb_N_C z(XbjlrA^qV0d0f)fIPD7)8q_O&(f8Mp@c0Z*D`R`?9H*96315&3^Zo?n==Eo8G-r? zZ+9#-T%I*lo;BA|G}BO!YABqnj*S*%E^jSd*H<=Kk=q^%w`B$DBc7#gmAzHDS(Xvj z^{Al+G>~mz>;p4o=mbN!XdnR3#@kcz&Sbm=m{^I9=~Tz?V56n!8fdK}Y}k(nC|L_)Ic1;u~S+pixFVIjR_FmdSl{Q_eVNe4bJYzhg$ zyH~7QsWH~q(EQ!+e>a*D0Vx936(8DJC8nnKKmQEj^YpbF_UyqeG$xPgI9)HDS^CUV z@Mw@l*>4(Of8%Qn^TM1px#8 zH5PIVIlb9+rNp2b@pJK_JPI5zMkYHv+ac|41$frnU~bYsfS(pTI|25lq=IW4)?2ct zGep%NH zni2WgRuy>YAZ608$UarhGPIa!WExtoZH6_Y$hK=jc2U484|p1K{q@Q~D|rRk@?3*;D12XAW0y=qVW}&#jM!y9#1MWx1`n!H6nl z=xU~+M-0QKfUZl!2}{ACK%XQEkjxO8Np{Yrx~Jo9sdyKXu{y?vn_$!zAbB`ydsNk~ zZ*D#F%yX8nUU%2s_dfglfsXDz%k&f%lw@b+3ZiH-))Fgv|E)LuCII!!_Wr;7#y3q_ z%Y^g~e0d|VAAfY$%9V3EV^yKd?|<*_+FF{aLsn!iSBg(gJ@+`!CmCZ~R<7K6-&Yx9 zJ_ESWrUHBHjjPt&eFqS&Ee*hWVBhWz)H9gK7}FG(ldDV0pL*~&C{)&qkV>exT z@yz%z-hoX;61kXxa4Tcs{XBrR{|$f_TT=dS0Zc`hM=hd2A}!)aE+Mz)*P$nQv%sD$A2EaZhe z%TleW$+pR{y0PJ^xm4$1Z!L<7TmU0cM8WHw!dQKEZ28iGd+)mb_@P6WU9e?jtZs6$ zE#S3efrTt9Bj7U>#jESwHk4xA;GT>jM%ZQeH7#taepQL8Ql6pb+Iohr=lea`rdnW| zHPJv*MxZM{vn4k$T%0jdp4C?vs){=OCDG+=Ma#R3S2X0E(pxcG7aOn09<9tjwXbY_ zckxhhPFE~2P?A#__BZEd7W?c%OV8BwhzVk*bhM3vcXFQu^(sFjJd^B5raD2)km>;d zC)>tHnjKvQ0E<%CvSp^6dfFLB-+c4U_ul*0|NMUrzI1dZHRtz*gZ_}; zO-rV;u}-#zGaoo*eZaOQf!Q$Jfp>+-SVv7oVP;eXGg@`BqcJxelo;sbG)eJ_s>-CW z*HfLJBTKT%q#Tb^5{V#Z9wrW5mPA2{*iN?JFCuq9m6Z%bw-wo^nI@S30=RTVu{1Sm znmQ;5fbFNq3LGz?QHKjIsJ|sak|{b!QA|xsmtvmJz`0r2T4T@GJvg)fsy*nnU!0336;XSB}0%Vd50~Pb+EREY~0xbog z!30QjYZFO)xgpQ;nU0x6XKJE-da`wCs%yBvp8g4Lpf-ikp^}UVwbikuOZ(3}cf)z- zoN~sN6=UNKlasA}FtvlM06;#fop}B*Nq;ef85I=3hz{slo@01bHPe(MnjF@ZT*rvn zdTGE3nr2bh*PRn-&I*jyIE$<`p*$QI?WZaTiu{-ej|=jT8EKX3(BA(fl-^D>l9uyKdAu`EdX%2;epTO%NhaFNg6W)+P3F?r=)fL7k&7FZY6)G@p zr?EI=xZ7d?V+~&bFm2IDbx0GhAbE7tW18^V;xTKkWYj(Fy6a_%{6?2NSUVFtT3)c8 zLRSq?)k~@&fOZN?q6(o3k^tN$no$uaP;FcUrtXrh@sEG}?9)#_`rU7L?%f;qhyDJL&l}Rf?n|x_ zlqlqQ17}VVrCdcVl_WtD{a7s(_-??fQ3Z~T<`We!q;yU+&lVhMg)0=NQG$Q$2AQN%B>xM6(_j@cr*)fWL+<{h=Bo(y0-Ba^fAVgU0XR<{C^X%5-eKv6|&f9$a- zc6|ZBK;p+07Y8uz!>Ig{{XciZgG(cl|YbUiM#|BB0~5kjPf5iy9+jLP}3-{3?GQ3bK*{P^jjDyN~saJjsiyx zHylu6EGOidOM)j>YO-Z=v>`RsG|*c^qY!p90-pV0XKBik%*49t*zB_Y)6ZCc_cOArAouF zeRiT|ZBN;f#?pbpte%3*wyaQhaaOrk&$rba%g8WvzXJK4NDhVn-7!gyFd-TZu3S2F z`wf?V<<6^~f8pt!kKDas{rDMYufxhDdnG9>NY(XCAN}SxpMCb(C-@)e3g3ILuC^&7 zoaM1S#j4~8aF_ClGAy@&J}LVpsTRv)5YK@CDpUdJ@X*wJqUghypA3vWpe^`u%}GT? zk==M<0IF!I@aWMY!v&@PkjDhi2o)Z$08tgS8Q?jEcRs)xDFQ&B4{2t2hli&N12$_3xav|Y{@;2Ds-0ASACP%&;C$&xV2LbnXz;)GZNXPVTZ^9thQ37i{ifC2$%VZ`Vp-2y3-$sY^HT|_&FI?t^qFH8J% zSK&ZjHWbc)T0aZ_UACg(uY*887WU62+9pRDr>8oS6D{$Hrrz#yelmdX2~aYThz3)! z%*2|?ywz*QzIxmBd-pzc%@ya5j@2dNtzn;q00!e|GL!~3wFF#lpoMcUJBovl{cy^X zg0N)LVy2#LDTR(%?ziir-rBIguOO?tD093%Hq%;?YA@L`ShlgdWYb9L8KV_zy2>VN z3zxSRt?n#c+fi^zf9Y4&HeL{~JTFnXskd@jOVL<)Zcl!+B|9)sl3(HX#cV68%VA9q zXgb-@!c$3+Dn+p4%!`Jm;~ir|4MPJ}Gl}-qOZqpgpW1rrQi#?S#E7g!m{i}`^1I*v z{?mB?@TZ@A`srt%{rkWF+cnqT(9zjvsCu)k`j`}xM2`Xhu4F8P2a$R}wdO^(9Nk$Z zcml$AJ|HVFJ)y0QjeDY?Ao)ObXMhn8(j^Ef`1r%XR~dk+N}?cOFdza_6mU>gu%MEC3i&kN@WYrd`)x z0I*K!!6yJ1r#_#Q&Y{8lTDyTLSm3xY*pQ`A<_YSPvM3~azaiQlckWZ zW|(S*W#oG8a-UJ*vuc8#hESkBH#l0J9dF1UtBS2?Dq7uFb^d7W){*jaC(2Llso2z4 zvZ|weLtpih=HlU4^r~dV?aS(}pRK!YW%K!o#`WD5W7Roxt@%A=xzjBb&AGu`OV6=% zpDKq9;CsOWT$T$3DI|i5BR3M5PPC@tosh0F+dCcaNW@!)`fI(0E(t=GsOCsoaarZB zfAibV!0G0bPe1+ilTSYR(Z|la&*2EK?Mtvf>k!pagVlUD2{)xt3|jB6>e^k-Vg0y;B6q z)V11{CLLX-LxEsVf4|5C4Ty<+2doOBt}5+49a(uf5)*V;^>{qX*RL%uDFIYtod!=V zzE4hf-f2 zItscNlQleC!yu0X!3^yyCam+AbK3NB_etVrxyDzne$}lx6rOrwJQ^`TQ2D_XdGG1KvE?)G2;4!@5>>&evkK#EcCKW_HD`(pteBkzHp5D9p zl$p`tn!et06`2p{3V?!s_%<>_5hccIYhu&W?a6riRH8kZXq}pDjZd`(y-7=fH)w!S=imye+-6IkL=!CUS4S!W}~D=B!xlSrM+|% zVw1LBcphUcC~Gm8hrz@LPDH~a&mVq4WSE7(*pZiy4)%h%Y?ca&Wv?vTOtdupg%=N0 zVQz3b>Ohwyzh#=b28U&@|Fu_M2}h#fr&nEl;2^k96N^Jv6MQ5L-f+AiGYoPiTFfte`bxvs*v%+1R zFVY56%T2pk98IaVa5HfW6y%~K0rwayEr@R*MS&-D@r=T>BlIF$SwyL8)?t8+Gbd&d zfH5W)feJSvKGxEY=tN1D=-ecL+I6^OaAPh0@g7P)p#>fQSj-Q3<`PZI=lZtYb@jy; zZyp$|?CL6k$9Xy&l-^N5nIr*~vT!=tIx*2QG1(HIY?+*FnT*33Ndi+l+$K;~D>&DH zm`ry~>TMZ{KPdP$E$Z=RTUK?*uFdvW1ickOPr2Vy5b*a_=+qs zu}t`f=1Ov=;i#Iq|G@tF`T0Nn=}%OF=Rcc&Z1=vpsybQG3uLtx(?ci5C&;zqAO7Wo zJda;xtP1L?V|NVCj%mFSAM85OpTjiDIGGlo_`+;W779@{mynOV%I5R>3er`X|wxGxX%HVs>;Ghtm|Kp!_Jn{emc(kV%;zgGO z6U$QdfM!5HEu&E@nd79F)MT-@cplcU7~uB;lc1&MbmNB z+*}qx?vX^lEC)0VMqi3J;Fj(rfk3mUbjTo4sa&lR&)VeX8{`>)n`(F;0E`rikBsl^Yim7S0I&AHy|Qh z#f79}Xa-ErCr-R*OiAyY^p@_Q7dRXE=E8O1ICSEa>n^3~w-CLbBUkcjOiux-+hipV zyL)%IC9lbJs4R?&s_v?hPD*Crhhpx7#XWbZ#RX;vCijy4B3~)tmGMtOzu95tS{0<;PVBSz-|m5=6%7g3n|bV(mB&Q z0l^0A3=;9v+%9_!3nzcNuEEEHF$gY5BqkI@o$1M%^=rnze8&~%oUv-KzqGTxj23d7 zkA;}T&xI{?ZB=Y~rekERX=1D?fu2~2cuUA3Zyc+p#) z?2POWKKRG^`T5`d{`bE}H;k(;yHa41*Xy%nxd!%FZ2yx_Z(auxMDIMY|LoIFjWSlC zs9}^7YapLgzy8J>W8goI z_UyXns>`?b_m;G_6oGd!ckM*d3qBMK2(}mNtMXIH)}Ee<{-LTwqWy~jjJC4mIZn?e zk0M4*EyH#~rWw>hNSJAwMLws%F$#VD&VsD*%G^X_;fC(YWLQm(2WIQU#o zTMJ|CsY|cF`pPSH#)=fRP?G&%r2tIHBQLzL^~}>jFns>`yB-0dLP*gZ87@h-Ecpe| zr&-639HJH)#!m>xRfCq9;}2Ao7BR+R!RWhhycWsA3Jd@~#F$_L&WSe#04~bP{?0ev zJ9y}zVS&r+H5Xs>!N2^&+$yNRwrOdK%ruZOkO>wYhxoBKP07OMSxvvgM_&XdXD?Gb zy?CsJtu)_4FaC9C50o@SRa4V>;ne}d5Zs0>G^o3TjZpiJP1B{A@# zjHPtugFE+l#kyG@J=de>d-PJj-JT!p z&iBu?70k6(CR&QdYO>e1=U+bEa7C)=re*b4Bx<%Tt$t#C?X#yf?m4CH(7A2jxVGc{ zYx>{3sOz;WdfvaL_g`=8|LC59zrMEbrn!bIm(*__u36Pwu&k-DB`Z`N3gmjMJjV#@ zvPY48=&cLw2@~?OLQ_+X7hH1I_J{A?a{AKI(faA>_L1R6$1pTe@&jiU)JT3HY-p3i zLwj~UaP5_sXGbzE!}fdpUdJcNYM8M?+jID(7t0W4_ndOhU3WnCr>@AEl2jqcM0MVE z@Af-xzm+le;P&m;U2zd(EFx;WMpSE#_qlS_HTdlos}pjv<<_M zH3TrZ)hq(=Nu_u(fYXC62eAA0KLN0%z*`!?*8c?nQ#b=zNxA?|s|y#5qtiw_i_G82 zAa;>8!fH5tR08Ez71Ynd9ix%{dC>VD4Qo1b3f>;po%uLizjMR+|RuA-*j*V0y zfRVMvZ*r840)x)+P7B6ovJEgnq`q}C)uV}h= zee3<}T6Uk>`sjxG1Lw3nxwZB1MQvX@zvKO@d;aR`!FR73{^2eCKfbH?7x(u6>m9w@ zmepUCtUtZKVns_~s=A;#D^wq{W2Tm4>e+_oS7bkhn1bb)m>>2nUDkigO&4$b@^z2y zdtl3HOB3;?@v&BDp#>=*O15r!JprF5#Ta|w-h03C>dWglKyUAumaZsZh_zUePae^#BujW!c3WuPMV|y4pYdgPvP1jqOcK+RsL;vyRq5s-8 z@XPxLe!pw*$OY}^50#!ZSh1?5c)C7+yfUxIYZrNpn4xEAYFLAKVd#dSg%(P~!I|0K zGq$d5Z!75SEj#n9btLF_3_}$pzaU|Fnq?bKSU0_v6R>QLVHm2eE4rp=x}s@d;42nu z<~N=@SWr@^GiJ+@AP63~?n0jxb{EmhMe(V+<00F8m_)(iFCUA|$t+#tNxPWd2U}8r;L(q3B>YSB(oBXut*$ud6Pkoi>F< zxJYxaN*w&I9T%lz)6tk*awH4PE@AGa763>TXbO*KC-8HpRTjBJx&W5*LUwYjdUU8} zY_N88pn7Pqrllz#6z!y-hFd!H1;i^58LO?#UA|)A#%nKn_`z*gTy)ylXaiX4_#qp~ zl`hazhAexzJ)i?d&5~CZ!>Sz7wM<)$IcA|_wig7KwiPVvE88^DGSL9{+jTt^XN^={ zoUFcPY295b8}D6Pf7wX+^^;|ftf~I;((?UV8lF9^=Dn-CzI9FS*DvY#;P%0ve0AWL z_YMBnz1_ds-u+kCbe%g?e8F(}*1odkb%g`P88sn)RWML&J5f!JXy62b-w-fRFOP;M z#+oj?VAIZB+i$q#nrpASFqLW_9cl0wy2`|$>z!bTViD${VXPOv&LQ4_(m53-O2th3 zn*%R(^mW^eWymV}TYyDeNR$gjIfOHCQ+dVv@4cIomGk@K}pF5;enLR zq@bZ4f9YUdbtT1r;}!<~7wAZfyC*xpBWKK1yX&1Y?OE#O5a{~0l`yztZij{qhpB1Cm~$bBJzbqxTF zdn^;*ES&=zV2m<=1prg9gPXrdz8q+_N@tt@2EZ;W1AvNJ1w|J?i|@vrr2)*#?;^PZ z;TF>d@uv`7P5m(7O5;&!F!2I_QS{5d$YKDe-Qm(^?!@$RC-5|Yd2$*U<&f+awp^$v zrn4~aw9+Z0MD1DxU@13XB_L6yetfuYe5h`CsHUZ{P!$B2X1OgEnhjwRipYe9n!;4F zy|=GyY@{|Z(Ka>Nl9=uY`oR$j(vL7pMOFBf4ooLVbgjs?CR>UCx&jF4*0XiBz~i*# zM8~W1XY2B}j#W)InJR-0Kggu+GH4RB`s+CEZqc{`p9dAVwDQQ|L!t@*#uv8A0bEe{^q#%D@4avN%H>{J-u=i!kKBLn>06P=p=oAX z4@?6vCv`x8#h+P3FIqK|&3T{+XRp|^axDz7o@ z)P;NnUOP+Evot*bYHcJTGgg=pUXtpbneHANsT&!roEmSLo9T{Ew0cblS+FFbSQHyY zHLPes#@g~rs#;o1(-4^8f$AtnBq=CLWS6n;rmy_$qhEF8Ll+&BwITtW6$DWVN^+qf zih^j%>NAf&e)}yqKl1QSiLrnRdm>#{LL&IfWr|{kt{*x2B2{3DxphNxp`wUP@Y~Mo z$B)JGVFs=%E-@0k@mYhIv-=2r>|MJaS&xY+c zoRmq81aA;fh|)wE1d!mQ^Em*gSI`Rp{-W#dLJO^G*24QI7wZ28z;IPM5x_vkqTFzv zQ4CY@=K#!Q4=^y3fj6>rkrt-P&a|_oXZWeh0fXPr2?de>4 z@B;e^{y5bVC<1CBW)1UVkXwQ)?k@r`Mz^40I+kKg21Vm#itr>)Ok5@-Eg7xnO<7Tx zm>0BWrkZZL>GI8|&JOjJOpGr4{wpBx-J$t-f)t=B)PE6;tKsC!4nPmTwuVTv8X?)LHrbmWKB)Z2ylt2YUzrO~P3t3~ZGo)byeVgiNh=-nadociz7A(hG0} z4FGl|C7VIh8WBXt(2g8AR9_3rCrAQ2F|SV)1ixj!dh|%FARi3wib{?iH~^3YWj=Xg z!E{qmSp3}Hz1LiP;RTmmP+3^CZ`T8NTzlnN=bwpNXy9zo3=E&+sv1CrsDcF96*(Y1 zi%29q^_Oq3bX^0DvFkF)msJZ5Hh>d4XV^%ixBDVLN^G0)1JVG7*cfb~ZC!_U+TC^W zDH^0UxCUDs7fwc#@DGMHHQLAX)D;3S>VXlTX#g)Ahsl5rcVj$o6pt~;jv{&{?3`Wi zNNS0BND8qm-PW3>*0gw#Xq9A7iMwt%Ex==OQ91;gO99;qy)tPq!@nDe2k6hDYYnBD zC?M!V`UK{Z(mDjs$Qav=mXx^eRs}iJqixwZx&8iI9(v%8^{d84hpIbTD+s`(HBE<`ylJjxt|9lV(UNn=tIiv$>MIS8 zm*;KnFWa}N`CC_Z|KO(XU*FgLt9!aWda(C54|f0kwXNHh)Z94RaN1D$83PrG>b$O4 zurcgZ`g}3l&d@Z!4B^AD4q$SAG&r3EJC4Lu`*gB>I@vXy=$IUDaZJ-;f=`sH6~)h3 zYT1fE{QM_VQGZo-?4u7q{Li2NS6yyS5n~cGo>D2guVe}Gw~rp1pP%2oZ!bgvN)TBD z3ItFZ$qur$Zn*SvAT}*e!!5V#kJbdbQ?F%MBcD8 z)z%Hdv#o2i&@yNeraBC z8)G_3rQy9ODlKiG#ivsqxq?@sw5Z5sKBobkP6h`hgF^lRq+v+2Dd^$CY1sI0L9VDv zHA|OzNe)?#i7|29E(`f2log61MD78E>uynHa<<=Gv$X4q%g&o!(zSVGDlyjF(@_a^ zV}S_Gh1MpnLu)JYl8LsV!Ro1rmdPocf#YqFpaW+l+;u_4rX&P(KE$s;3vl2qc-`JU zYTAWfyD`I46ZZ6%<#d4uPpgwh&zg91gDc7(}~XMcso$CfDYXrpJ?%zriXzX zzf#t0#-2NP@WY?}WXyIH#t!U%etv%byi?W-*p#XmMwcl;e%WMM>4zVDFh4&(gux3r zvK9t8gODRiArLRJwM&=(zqGvvn4RTyF8a6c`|IvydhcD+G)*Iox<;C=f&fuO6$VoU zreiS123!%v1q_aT5s#wBCr_T-vI+CyJla8QACfdCOo6%Ozx4dGF@R$sy=7S` zH*js4@cpkp{d7x96P&FMj?vmUr*@;3E(1oSy#Bi3jev z<@no=BOlmvJ@$o0&MGMx*0#-W0bsb~ln=C40l0iK{GS6bbd~70X)Z@^;`IUSp8{Z1 zU|ROFE?4CbuLdxg@9+?)1Yq>u#9KNQz#I__i9dc9MJ^Dna(w{n`n(>1;nLvZ76X{P z>Gha8_I3|!D?{nHN|{XV0z?Q?TICR0ZN)#>o!b7nsK*^hkk*FO5r+ppQX zbHkQReYBZ~0Sp-f1K2T)p3d5X2ew>x?C|XF(Or8G%P>DTR>*_|U=@I*wCR{>F$0Xr zWGHWIO7Bf`8{U83&^^mN43AkUVm)n zvzLy(_pFhtXF6}()pyPAzCFFQQ+3(?Vy4PRc44LOB?YVqrC~Ny)iMSiR!iO|0m*zTfxJ|TF_#8h)tVP=BnV+3f77s<$IffmlyUYq=Z0ag3zeT5 zRuMNtsUvhyIHpjg%pOba?vy2X24yx#!;b&f~MQgG)=}`E-PEGa9X{ zlQShVsaR(6{c+LEgVU0h@It5zgT9JnCP@eq!pMKECA_ zk4(S*!O5RKH2u{1{U1Cua_!EJ%eJ)Ny3~K!j{Z&cxsjTDTQZfAe!=(o$q7uSzL;O! zH@2{EcnP(4a%Fk*{^hBCvxAAiOBi;}a_Vi7HO!ZvLvOsYuon?WPd$Y``H_b>TpR9F z7B(GeAsbvjX&8?`{MZ*i`)LMY5;}f6Z8^2H3hrrX`@`S=?Q}A*4Y>UKZz+QyYy{WU2)e9H(Y-+E+`(E?5qReSl@r<0FJxmGA-en`;-72x*|lG zJtF{zz_1rmi6;LY1z-?~2(AoZj;QC)=(IF)%qfFztb$<2W>--fUPo$I=MyyJ!)t=H`Czi6WAn%TbP zp~m6rL{C1-Ns)8CG^X^zH_ZA%Zhqgy^8T$$%bT}v>)yR~nR7Eo`|Glkf(};F5R-7H0gl>;Afv6B&S$D1H%?ic-lrsq`l~cXTQ?k(}4=y z)bheJPX~!ekj9uWOaQL0f9|E{+v^*i`q;+N3nOJ2bXvXat9%anCTJy=0!#j~TFtXC4FR-sPdP_G#F9R3|r^Rw8Tv*c=*X2efOouF2$RxCEALfHjKZtP zTa>j!V{Nj_QODYnNhSMZ&_x8RCYHP@hf10@^YIlI4%OtdmjF>a32ahTOr9e^Q(pkxLlGdeOQMNK+Y5rmzY zXsV@lu&HvQr)I3VVz$5fqOr!~+q&L;aPXnCM;|*p^yuOKyZ5%gXL<0-JsroU+K$b1 zoHg2d-PVrJTr%|+@7em(_fNfkV$&}koqprdO>aE9`HhFBzH!CKy?c9ZTk3z{?7?@g z4D9NtUhJ;f(NWix&$K1N3fIZGA}d5BT-S6OikY(yZu`y8efqPX`{W}Z`@lW--3(pf z-mx(7GX}WOHMUcrF;dYm+VjO{9(&>!|LY9^_+yVz3r&+K=!A0D9>1=}^U{VfGrjrM zS6;*k*%tNWSCh9y9j$^p8k)ZM?U$nn#(+H0OqtNkBV85SAosbazce`9&pc{&vk@F{ z?;vnI?FP?2`<1Q%?3wzTTfhF?GeH^&7#PIjv_fKry1Ex%{#tb=^VGwSe&#cunw%cL z{rc;V-v;WnOCHn!@Tx;^8Gu)LtZM-C7#TsKEGjyc2)%BJE?XlpfOT)IygLZM3c3Lm z;qsdzY|-IC&!Q=CTrH3kwL=|ciAu^bXwWdM0x;i4C4(!XMJ)Pb8WvI%*nlo3a=>(! z6^P7a(u56Ft4Xnds!n`KD0?+SL&=p{X@p*y%PggwGI83ch+wssrijC&kYxbI55d)) z1vebJ!MrH^5RQYO=@eNsb3SlgHaajrHos?Vaqq^ty&D(zj?QfAg@Fz`1mOToazZ$A zV2IV*UAKREVs_`?*$1{9KCorqzOnhaQ2?0CWz@rPkW{Eh*GPwfOBOlWz$7H$*cA!C zA`H5UsnOQrL`!k7Iyci=fALJ;#Ptx9eSty`MPRf8yN!>vweAw!7=ref?KV zH{Cebf9X``&D+~PePr-Y?%e$2_l^JKhc>=`V*HIqH^2VSrk{Oy>KoUL-#y>^?)`(0 zoHKOWT;GM8T1SiNk*ZWzA?nHn1y>Y35Go@$+MD$ixuvD?D=t59!TAd}Tz~%i-gE2X z-1xq|qfy|eVY_41+GeekMZ-96Y~ug=@sB?B*au#I?wOM(Pd-5{w1@Oi#P-%#*S+@I zcdDW=V;GyKCcpLa3#MUY9MKH2mX&m@3JNajsBiqP2r$Tn#M1}x8i z<%@%(NDqZL5KJ%19UVr^@a(EB@K-i9|z~!{(H2_9(lC!D;v*)WH1NaOM#$^?_ z1Yoqv+FNH$1zv?-{Gay5(!-d6B~8S#WTK^7s5mn+&@6U$)vhd! zE$oFO$F4ma78b^rmL@B*2_0z2wU?sRXcmn;aY`Gfs8JYGB^^8Oief0+@=;f1VMj}# zciS5P03ZNKL_t*Jczxl}NXOMX`)^sCykghbwcEPyTj_b^P~S(-8@Ttt$Sn(9H_Z=R zwWa&gnch2AhMqdw_r%%#zk9>@8;@au^vlOK|NN27KRYq?`iG{TzI6D(h3>l-25#Ha ze(^-(1><%=HJ?f} z8OF@iy>NkQAJWXNSm0-x@3=P4y3AW!0 z&wgcae`no;lT`?xldJvuP1HOg0d6Dn=J^&nfUgRRSyqx&O79T`rAZ9P#+^PaI z04UK@Q?hKA5{5CTDCK>V^oK2-Lz8W58K^kcCTQ@3_b#=*3~Wt}msSD^i!KwOktkP6wXA~diT!uVp`;OV(+m?H7+}m?}ci$I|jlO5T_fzNg|M4B0es*H=myb^U{Glzc zKRo@$hbR92JyTDe-S?rD!Mm3R-Zs;9-gxUmZ}pCr{BUKeFPG|0gf)RzaAgIB5+_W% zzL?p+G=0O>M~+{6-jh$9_{d`qF3gWD%#8LBgk#uu>zI?V{caz#Qzr8yE82vDW~Yd)IXU44Y_n zmQyV>53ZTa#1)tU%#jO}Y3MS2iu&;N08Dc=24rj#tL*RSPf&iebP{GKO0`ld12Ff` zSnMgAyEOn78o=XEe&WMVJ^9#$7cKAEH845W!2o7&N@`N*hZBIiJ8Bn~#%A}7%L>pHcOTjh$zgf~!3j#Z{MH5YfZ z*PlJqdj5Fx%5d|sZ5_AI_g%lO^0xPmI0s@Z>K)GV{hmoB#fOn?JbJck`~Ux(e*36uppd_bW*m}SeaeeH#WO_WY^9OyLR?3%uOsUOw8@w7)3$CfX+T?7|rS8 zkAL{1lP6Cu%wY=s%`X7Jk3aTdB@eJIVOmKUeDmd(MkmJ8hH?GTqo4Wge^KXHlsZtJ zV9#P0tu+8}2mk}vDe!z@Cr5U(X4aBl{qmOwhOtI6EmLU1GM&O8hBpl3xi5V2+T%FT zUUK2lmtVoC3_fr;967EdEyHN6s($sG-!cs2f!pr<(NBJ8n#Q|syyf;gZ!ZD3to*Wp z4%xVqN5`p_I&@(u0oaqC@A?|CCD~w%XWs?F`Ebjnn2g;4THgAisPaD<#(oLF5pG@r zHy^>4o}W*r6RzhA*K^kZm_&sbK@%eqeI~@iZ2QCj#)%Qo;wP630Q6z9qzwXY1(=Vd zJ)-zBq^>CIEM>ld=A72cbX}l*5{03F=S*dphGFpmn*=}$d2E(98PA;DeNC?ULI)L(_Vey=8hX?T5nnGxMQj3-TMcw z+u42RzV17Aw?BS<-)D}F{@|{uKfibL-`+R&_wSo{L z*>LAV*YQ1_S8VQ>tWE7|t(s}bj#Q+(6KwKK#%JDB#z{3qhst zmFJ(o@s2z3!IPhQ;C=U#B#_vO7>=S9uQH6b+Q#pF>l=wgAPh^|(qld{tvRIaWKHq) zXP&+Lz3;m4q6;s(=E~ZJI*Z)xaP4!wQ~-yK6YstM)z@CLZ0D7iUV4Nl;Sw=ju_H%q zvH0rOzit@D@k=iM%fJ3hgqz*)j$7}zmCn@ym)}$6galxVy7sjI_5?RkvYd>i0j!7| zuB|;yiS3yXzKGa}|CvYB|`-w`4wiew-Aq>tvu=R7F z{p1H8yyup89NV$2dvc;($$Ck^Kocx{9MH_Bzo%|xab#hBbYX7e(!$8%(#GYb@nR++ zC{sqWrptIBWMmgf6Ff@kPhT|r=)vxhy}9-J?ajw`buVpbo^35|ZOHay{I1ArioD7|R=6(Gm|T&t z%+{*H^5W!yl}*b_(<@6;D~nSrOVfL2hm&Ct!J-{~?{sz3FMsk6Cr_Td@4fdf@7ncG z{{l~}Pmm`Lwi}ko@xLE>_kBP6#V4aUQix{$ePxx&%>DZU;pJ_aJ|?M#wFgg;KO2f06(pryH1`w`L}=bx46U7 z99={_TZ7It%(`OnJ1?O!pF6+whkyKEktcrqmfPNWm*>bR@Puuy2VlCQp9a9VtsXn8 z4KPJ9Xu-oO00YBNDG-=7-AVyC@O{j#_@7j$HFWVoXfJFhwibplZ<^3%E56 zR`#=7t(t^M#IjJa)k`TRPI9^$-(Rhi*^W!4vG5B_rj~s9P@;bhVIkbAQ5zcm9o?h`0%oIQm&nGovaiEPgZ(P&ULb$Xi5gHiC{-(!@<$c zvq#&{-PpB%sP2lX)@x@6u9|6`-%x+^Y{z?+M(^4;@Sy`6K6~MYSFRZO-5bY#`;JY2 zcE`})-9P@f@1FRZJBR+^gA+f0Xw#2BwB`5ip8mCShwfkLy>z@+gc`=u^D7M!o zuvoTTw4Dux(O%o|JFkA50F1bXT58-JN;69NZpO5}{o>1He~F*7va+NBjO#4a7nYTe z!vFgH@0~n(@&|wLJLzNsJ0yyIh%0b)KL1U!+Sxfe_S_dgkD%bIuD<*BThCcMkWM5- z%!OWi=oK)37DAAfo`PjLk)wPeyza-0UMjf~jWmiacE1c@?xNRBo4HWyV=!^BJT%hk zNbI_iE4cz|eUj#8(g>QpqbYq$`bd#sCwEK2`-Wp2&=0%F9MnFQ?EH#>Aqr*y>ZKD{ z62;R-5+skK$^opFPkhQH04HpRe8m{0Fs~-{0z5QX&%kppZa!615;-SvCKDe`<43}B zNIq}lOs*tNoFJpJV^mBb>4b3wAzNwghkaqYrcsqj%O?b1$5kQNHAT@!kR zWSDWxx`f}EP4*TtiyPXHjC5SErS+nz&T}@lT{6>t%ig}@^P`U)+3=-HM;|+H=*w4a z{Q6bH-@9h;4{n_N!JXs3b=BZc-nZp{J+}3y@1OYL0~0^If8u|BVEX&FjDPmx4fh@F zJ3ilY(M0QPZ`ID$+Kt6zd)n(x2X%p)^+bg$GEx>jFS4*kT0sS#URs!3njb&3f6IZD zO|yGOa>*cLi98MALc@6X(Th%={I`Gj(O=JPo1E|H`S*YN<@dk)QcWRavGN{;3Pa`x zZ@u*&{^#G^aBvX`DY&FMwWcMA=Hc2_y=l}&iM=zMd?_R;Thatz*Fy2|ZPPW4rK!n_ zm*)>`pIO|or8=Lp4ay&+pe9flsMJpl4E)-O2ge3_v1%rQI(%pKS~rkxXI-^)L?Mdm z^Lf{@s}qUJY$olx(EqX;>C6EfL-cf@T|>|M{}I5E8zx*oIt9SIMmjBk!7L>rGzgD4 zrK}5JGInJ1R|-|ZqycM^nBGkFjWQf`P$=IJ5k@JbTA&YiNYqSIL}X;A;zemR<(`K}wnsIHXxIlQ{gd8Ge0l;%RHZ0E%@7~_MYe)a?o&6J&?P~yx zB@lU%ssc-~lJo?^E{jwX_&l?_7{EUH`w)O-LQqbUbdu7^xw0r_D4l9gbZ4WHVtT47 zv!knSe_zwS_Ua=8&1a9cowc$4icPK8?r3}9;NX`p+xVTEr(b#d(92hkzj(#S^KaYu zdp8aK_jir{;;}7nJiO^2Pt5%BEu-JOwBs-Cp8WRlv6rtKdFW8z+c$TdJ=n0nzivx? zVYDLAnhIM2x6*ZM0`c+7pWS^HtaGl2i2oO;#_;~Q`{8rC4q$}ntl2!Y^)0ZiLm z$97!EjB!eG;T!89!AM{iZ3|9LqA7FI0GwnkF^!``L8KNzaR5q{3B(Qzqq$LC(tajBY`LO7Y+6pXc!VTK)@*r9%0ok#gM%Hl5q+zvo zvGXYSSFNCF%FTy!s)s0Unq|ooY1SKq8RFv_q_U2+wN06=hhioxGg@icg+wsFYs2i$ zzMWgUw#{_!+1WR}sZHses8F*#ou^HO*;ytw7*=d&x~F!oRo0nm=I3Ao90+XA@~%@Y zogxO&vMva^GTEM7vcH(y+EUojQgNui@xrmDizeI8n(VlEWAjrN3_g8q_^WRp{rww8 z|M*=~-@JC@#bd*--8lJ!J4XM}{}1q6{PEX{*6IP1ACqB-H;s`CIHOuT5&B_RS0 zkk=^y*Ek)8$H~OUk|BkmG3gM|Kb|Wn1Cbe4xPJVf1~6Awe|-R#Xd9;jxO^y$KwzAf z6@aPs5-(avP9%r{T;@ty0WZ8S@s zwM@ES-s~V9YCiyWb5pkY^u-oRi+vfZiVO625xoW<)thNCq-h0gDW%dy65wqS?PNg&aAB;a2GQ{NNYT5IzXeofs;guINd}BOSAx0(KM*;-^mb(oaZD+!T7%s98yiyi);O zVo)joV{a7se&Bma*H3yS04MNsD2j#LEy18B<}+DpJwPzM=U&1ABIK?cUwLXSSaz zumUjm;ouU);$BL1(*RbaF%?zA%0c2$^MH-FT#;j?;K?k~uA381))jfruRtbnG?Yzk zC}cL)->Z+NZ_pJ0jcDU#JH;(_|-8kQV>$;(@Ts-*s3kSb^ z>ELTujr`R;6Mug9#19|X^pg+IeEaIY-@j$(#j6INJiGJ5XLnsN+Hm$j?V+KT%?-t| zs?<;+*ARwP0XEMSQl_LV_#m;@<#UG*?KpnprMJEFn&-aurTgx^Y5&ru#f7mKUgU*FZ z3@WQ_;F^Kbs|H`EdgxGYD2~MJXs3cjh0WvTH2~&iFZx>-Q|0n5nn%#(`y?Jj>#v6D zlSJXbp_lGozE#rfOI3kM8_Y{1oO_|jSqY<#KdCEzNOZ#$X` zJ^od;dqf?LEv(5g3!z8KS=zMHp6qI_=x8jqH00V^symyjn(ET(cj=vwQd}~gYyEdcS#W?2m69|Iz(p z-@Rt=Pj4Ch=Jn&>yJ_(Iw+`Mv+jRG0$K{)w7W%4|2Wq!8WX5VTL%FCiaH~C86MB`N z$V#`$_e0xiuE_429l!U!n?C=WpSb*@l^d@=dicQh#re@}G6)Rp^I;4|Swan#tg)Od zveZ#CYmnBcYFR02u;b+I*h)b~DG=fuY9UpFO#r4Z#SW7&VXe4{01Tm%wuxN>;CP6&E`R|`uXOHRb2|mVZe;sw0c^b)fZ6SxzN%$$%4pgu z8!W4`$y7)7i&a{`bpWi~u(%k~#T7rCj-yP;RKcZ?F|m?K{Zeoq$+Zh1W=Yp0M`$zt zYG`A>P3bK>aKf+Pt|106jGCBe2^EBqh9(wy5m2QYhNW+&Vb~_52xFgYtup6F^kgfO-JL^xxRdIqBgU)vwDAj)u9cw2l^W?9&dTu z=Jxljj6Qi}!`H4E`^F7Z-?(=4*~hoiDnc+gJB@t8yvchw!1E=7NJdE|6rmFnX z;-)>bLwj~?SXh|2^peHph0P0dW0?d27;g`Nic{S%ETCJJX%%S@L`+**4q#XgIdHKg zr+)M)Nx1flRwB$}RY8tap#{%cFpen)2OTT02PCI(dbZX87?D(5Z%N)DKyH(30L;T` zMzRkJZumw4eYWWd5a1<77`9;1g!dEWbjqS2nCPj)!e&6ul4jWpMJ+XBT+4qq@CK>Lf94961 ztmC9zC+&KTk>8U`j#uS|tFkk7`JJtm2SyqWZ>YO)yydKc`rEg*|HjebUq9OS%vBRF zTs8PxH*EaQ^;3_ZJ@DxZ24B8%{8MLjfBovQkL~aN{x!W{xN`X87Yx34vG>@P)*~Zz z=TCR<>8hEoOAl2fHdY303BSs7YCVy6ouVhICG-Ogh1C4~*wV`M^78b;;`sdB*viu8 z#eL)XWat^#11Bt}0J<9l@Rn^<*iO6YWT{b7JsM_9bfnUg6x(*%K&!Hn7+VnV2))8` za;8&h!k0N^ixjMDFz6C=_GHAmQYLV4#dNDIIS>Oac=y0%7TvE;qh_K)Dj1W)BsJ1} zNkzT~NeqMqD**L*N^gKwCK1F4!*2#)DQseE;4_fpmK}P_0bHV6y%~U^PvD};G!F$} zpxXbB09XgC@b<-~B_4G(L>0tW&kVqhun;>Gpp_y>C;;>NMCE|T0OoKrevL_ipxtDd zwn3(iD&QL@WZn~!8*?cum{G^|keq<@K$yOZ-KQ0R<5d%%>Q$&W3H^EIgx`)$kG8h7 zOaM|HylIlhp0ZSLnc@-ym~SzFRdHq!g_>h=eP*Sk!2xdYf`;WS#K1`hROCv}t4pVH zQq+gCFO!(6NN;T@3>MRKowetVHXYg6a&)Te;;mix?(KQ}aNlPy8-3!e))(Hk;p^9o zKYPv4uOA)x&GR>Y{^-!dOZ}fc(*J>#u2-)b{K307e(-SD9g7<-pXpxcYr1@U-|nuO zv0|bp8}y~3rYNWnWkcj8!1@VC9^snH>!9eKK;c2 zhNTYqLi6A&9zkO`u0;F_z;scOsK7kO^7RBOo?2-D1F=B~z?enr85Jptm|sQKlfbId z?$ZPd7*_Y8L9_yJG6<7Fh>zL!oqntWFp(%@0DC34as{SY0=ZA>Kc4>&{ zONI*3SxeC>Iw2{J0*_O8;$5n2SFG9a_kxK9`WNgt61JTcqRR8?^VxjhHTXeev{a#kw=z0o;auPt8asn;o$?lk1TC?{G5?b9T|G++~JR( z)A#vH`d>NL_ebxX__a%hub62*GS+-#q;Yw;d8{V0u_DuzNwr2^W8^l5eyuO_zRXdk zNob2)7wpMOztL|wocD59{n(|30JyV7zO!+LDB!{-sY>7rVTClYsk(}}V zph#Z3iEgLES6PiI&Dw~Vj_)bb!y*o*b-x!CFq{yvcwA=IC3~; z9kt2>b+~qtW0tnTk(g0=m>ko97Oyf}0;oz(1bEUfXkTdTj`3JdjR-FB0{=D_001BW zNkl1hFYuc}K?{nqCmYIX2K@>q(4@nZ>1A>(X(T z3vZlhM&MV|DO7TkOBUsH)IB zJaJ^`#DSql4i7)D)c4pqeP24-|H9?{pTD&C$s2Y8;MVzG(G^+O#S&QHG@djq&oEuXpgd=I8<>XS8b(B$-iW+b@>C&1%Pv~fd$K;9 zx-$tzkO*wp)9H2^wXtA}D(bNd*!p4#oT1sLw9F2A<7G^MkSQEFG*+;iLn{qJ5h6j* zh@&M$6?N!`#}O!}5Si8I(E{@z+SUgmVpWxGd}$B73cz$7TCMN@RRGM>Y5zOQItWPDmmbx`NXR(>E&Dl zU`+|eYeOk2tW08}rK5dk(M96|Y(iNYhT}cm%5+7eBd{Z_C9>L|FLri?>o?UY-{&ORcfFZZ7e3cQ(;Fs*%JEI zo>Sq8oOIF%3YKZaYC9=POHLr-M5Jsxi3KlOiM$DhX$r54TMw|A&9p{xY!mww%SjS- zkIEgq&D637hf*wrQx4qlcvIlo?Dv56kD#a|7kEYuphb}=n%F65c^?8FLiHsoBm}NF zPHeK`6OfN2j*z-D450IWhrxtAuTk>WqcfvGx>0Xlx7 zXQ?e8vn;8NT-{oi@<(Aw%gWv|p%N!GQoKuLR47SW&@UDfMq%N`~*zLQn&*_RLz>5l@@hkpmk=QX`rkBuZ*a z91p9x*~2k}AGSUi!@vLuFD59c9Jc0A%%@`)5H;%5g|aK$;sC@7mY)?!}J#X1hOnVBq1iH+a!_n-p{`hdcw9RB~Ct zC75696h?za9%~?QU0QkTqlY6t0E}`;7d5_?5sDPi0_6546ZhdoZqBmOR0bl;sZqa3 zqfRN?I4uo74Kr$7UTI^@xTORy3>S%yQCj-M6^J0}fUw2PEjk(5CQ-ObAZOTW0PC6c zsUrR}2QURScy9(^79^b+fVoyPfRjN;hi9t5z6wSoNe6HX71&zjM;|( z_WznSsTL=csGs-D(1T?mwJmwCL3I&bi(@_IBPm*LTDAuD4Hh z9N*q`-+b?bO9Ss&?!Rt(*W0E$ubJuGQInmhOt)vE&a_t_dCdvg7>S(c`!qWIRqb@eUj-1aAosJ4IklnK-95m}aMC89W!z?~^3XStrlmVr3X5@?o_TnH;4EMBH`SOB?TVG}4uBCruqZ3cX$O?n1 z87m4@I(g~0YpnuXVbPD(xr%HL0I`7s=CcZmplKO@iAK$A>tWf3DJH^skc=dtX5Xf4IJeQ1ZryY@TWljpDQi1ZRWzuB& znW70Z_`}qF&^z4fvoJk|F_jv~<6V&^6E)?grqX9h(Uz8Dw_2ha(PJTPv~ z(OC%BbEF1voG`kY$N}a?3BU@gRBu-?H&`BoLxlISvlq5FzQhQCbTV7`V)uv@~ZzrLKN3Z7bEl_@=`d zG#`^z#L57GV!<`=bYw1v_bR?eyg*g}j_Wpw6Uoy@U8L1OD|Wi&SczD;uT=XCVEjdB zF_>IIhH~txhr_UpP~;hkRAVVDcgM`O?vD1_iel3D1?8%!f-UnD<&H6+}U&6&YpM7b{^Z(a^7IWf!?YkBMk?8n+^}vo-?*lRbT)KlTV5`)}#~# zY4213b3{1K^DgO@W7f3f)QGY@5Oi*T{wII-Fg^xTfL-(E}&7W2)ax3MBUUXvTE$nR~bm~P6?)Mt0K zX6L%Am%D5Cv{&uzC>|SaylrR4g9kR8GhVl=qxR59(^7Bqj{3rQb!Mb8KTxdb$!43A zZi6o?JdmymQf4LFLCd5q(~grQJjcn}lIM{g(Umi%Rf(dfQN#cy#HR29 z@orWF7%MuAkvtnfZLpk9s@u{6fN7Ks(1s*qK%%I^KWbxyIT>#^EItXhSr~=J!1F9A zh;e|OGZ8c5p_gV~;X%^jh-pG(NJ`sjD(SQnhFN?ZFS;7QYVz#9g=ByA2wDcXzvoE_ zC7@R(MPl`@TSDfl3S9aaG`dx{rGaU?9M%xT}x~Gus*p3+=^yoyDz9+1Zxr9bGk- zjnyBY?KrrhZd-G1Yg?`>6?CV);X>G!44M;eedyHqZgt>i9g&r?Qo4DLWpYFc;f!_y z7cw&Rwr$I_4AV%^sf76wo@KWoy0%rvZD4Zw*oLJtL9a#mE&-R^#EKNGU>^1yo@aau#(hs157`ePPr5yv<`sd znOQl8nTDl2>j=P*p^zBubO2U;J{^E9AJiyx2Xs|Zhd0+UGrm7LG&s~X(ACk^-_vcv zlkHaqFrSd=+gAgaTaS=-HlbrDgq0+^Wd^Gqr!D0*g;8zfwNZc}5$&X(ey{+cbd>D}$cEp^##4Ta8ZswwoFqo^wrHH2<$AnJXm z+IK5lS&(qCEqXzP5DDR=1c`w$QNs=?fbW%Y=biU{;>o9AB}tgHaaa{~sF6HUQd9~x zsbbz6e`Y-Z3rD0ZXAnmfJwqXjH7ej%y9stQ@jz(11K}Y7l8!t%)5=o8pfM*MdiAt< zO@Pt*0M;@=CKrMS!cfJ}!NOuy3mqT0*QBVZk130=+&-4ZTG|(yoHt6HHV>bDr9HwB zi;&4v=y;@DW_*PiRXp5^?}N_=VJb?bBJ|Q|r2UX6Tx4X)$%YhX9zfqu3t?t=@bQN9 zC2EAjDnb?yEAcdeA^wu;d$r@NLaBNcxpb>haG75$3+^z)Qe!HJ7T_E}j0mKnQM;>h zSa=FeUxhPEsT9)#Q)gX8&sLRIf1Y<{oQ*;!aF!WbFyA644fyG>WvOLJ!}#i#p83T; z|L>o^{?nBuc(RclbcuI`;~CSWHCd9~EtxE>2UqwNL@k7q4wJ_C0br{^w(nS9gNy}M zR^VW_6j)Z?b1OVq^qt03P#=1|sjx92dNTe%KG;whZEMJFsxM4br?%E-Cu{TLHR(-t z>239eJGCPKEClJW&;RElF7)$gFF3Wc;>t*qQPN3;s}5dZ;qd zUyL@@Wp{K`Y-y<6-I&`5t#7J39rffPSEz7hOVV%fW!{qo&rQ=AJLma1*UP#x>o^gN zscpEI!9l^K#LChD##WgqSv1O`GcW@<9v3=>h1!l+pl;bagp;AxE%5r7kP2dsrl3uMfdrXywR3=Fi-6rsXzS!n%<Krx$|a9ehDpjhy1A_9h~ z(lRBT50G-Si zm6EDCu475b%IfkjmF%)$E>8qqe()h#y+>>ruj%3m+swL7l`EPYO!j?JYA&`Ly9V>hrPFDG3wxL*S7)G<@q}h#~=vDz8HS=U1Mh8p%W}sED zAlN2NSm)Fc!;BPy+z&f#mZ-wQNL}~IMhBFvz_#1)8*riIw$8^^+|E<$2M14Eq@>Ky z^nn?;hnO<(~w?Wz6wu#@Z@6T!^eKLMi0Fwcg%2hZME37jF zFwj^A;L?q_lWzfFh?8iEbs7NEM1Bo`V`V?�+5d)e?ZgUj9!27>n0n9;4WdE;P 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 N002ovPDHLkV1idCFAD$w literal 0 HcmV?d00001 diff --git a/API.Tests/Services/Test Data/ImageService/Covers/manga-cover.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/manga-cover.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/manga-cover.png rename to Kavita.Services.Tests/Test Data/ImageService/Covers/manga-cover.png diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/manga-cover_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/manga-cover_baseline.png new file mode 100644 index 0000000000000000000000000000000000000000..b24421f4e290b82887c64d07d1cf6b6434e1c246 GIT binary patch literal 314481 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

    p8;?Ba za5n|>XeSw)_+8NbeQW{|7;sxS+!tS6{jL|o-+*1&Up>9LYm7tA`)G6_H1q6K4ocBa z9J>=5Xc=qql!xM1_#81Ht2j$xHf$8F*KP6k5My?pW<3P@*x{fuMzA|&A*H20EfE&k zW^Ws<-i{Qoq@s)cvEHV1%EQf3FO#sF5+N+2?bGAB>4b-qb?MEn=w$V4wF3mAyMxlW zpyA~3c0b63r|u@vaV-hjhWa#1_gwNf=2(7g@^w6?#-zL3O{Oz|N<#=}kD8lW;EF!y zOBV+d8@+y|E7+Dk@6u|0_UXjdTFFG0e^qfp^UCl>ZPTKuN8SJP&mL5=%)v+`7Ct>n z?=CyIY4z#2m7eC&ikzeJm_?>vlNabbi!9xpSr}+;MQX6^bh_PiSJJ!=%M&iM0`AHp zdpwQTvVz5(X^$-__x1RhO8Q1?=5~d|D+nu$l{o#_XO1_+Hm7kLh>Dokhq-P!I-HfA3rPccg3%zDPeQ9;@Wufn0l>R=^ zmg8>F7;d$jW_L@qddzWO4mX?+P#d**9a?j^q{d925T3T+k ztSv0(qQ1iT*XMXf4VG4s$}LOH0%T9gY+yxbLK-Ik(?H??88zYM)L}f&rH6w@3!0m$%#Vw@YK9 zjn6HdR9<*cw1u^snrVcEeTXU8URmGtwI*D}ILh3Ki9qMNMdiCk=ekAZx<;}PLBU4$ zR&R6+{-kdI!O%k;gj6y^D44)L8pA#qxvNP%wE%Eg= zz(PMiKZ}o##Am>$;a^@&_Xl6{d-mkyWP_LXqMvRfM7ao|aZ(fYlny$~fUV&zAM>2} zrS3avHrphtr%YHYTzLqraLoYiCYTIce`rRk4Pd}mJeSi9k0O85$uN^{yN@kS+6Oa( zQo+S?RepD7+T-m(F9W}w8YwQOZd2oj8Klv=^0VzZ$=Qqeh7SOEKaY&a;j!|b0fO|27x6860uk;;7f&1GcC16`0$&Z z<(_QvG%uJAGu}##6HRps?@k&C;l(zpc}SCfPlK~u^nGd6V@=#tgl%VlrKpqq(3C7_ zC2o}B)(0vMb3*Sbb5jp zv^)27vQ)QWpzaN75@A@acdXJBHSiTk(~#y3Rz%K97IC4f{Y;6>& zIvk%2^-HjFkGFKSer-&KBOh{z*Y)|LU%SEI6~kWv{2L6*Q#tX;>htB+#m4O6%IC4h zik_VGz1Gsf)C3j^mh1rYHvlDCyAf=?qfMNHbsd6q?7g+Xj;a1u27GpaDt%3W7sby8?epw636_u9@W%qDMXdQb%kDVE zs>b}yoU8g{7Hl%ZJ4spIa-jH7JjZ<-B zWtk$uWqSr~IW1ODl*Z4aEaF2e8IX)JEdB5w{B>=DUgystKn& zzg>=BkBiQrhi-46dAE;Ij+gyVTtGL2*p$chGttSjRVjq)G(`Abk97;rHl>$4;;WtY z@!rS1Me)h{b#rN@v)PcnVh{L3wWn|=c&vKr~OFGK)JD`FlaklrZDfeXo zyNP;zu4?nnDp}e;G&Hr0Ru@z4&1HTq&)?pT(=a6D8-sl3h?)>mh#rgv^$RgT0s*-mz%a9p0KBs@fx`BWs)-_1N8rg0{^7<> z7%Ml(JJVnz>+5XdWdVb~J10DmOR|1j1OE|*zvj|_KRPmhM(R>&%XS zmV@@YA%^dVlO$q5sPLr^EPIgL-iY>hB?4fk>~HT6Kl7iZ#W?h8xb~pSrv~W98h_L4 zx=2A0Y=;ZqC0ac2U=KNn`2^F)O#7)o?J9#`HiPx~Iq+G}PkmPJ$HUAPQeYcth*Gr0 z3Daep>NpZ+HtVceX7;*?o-BPlksNIdCWb84LB@kFpE@J$OJHi*P$MZlaV|Bsi4-TU;&qW@ z0n{(@fjakIl$;`Cpk!Jqs#`En_D+^!bsNb0om}99h6<~6n8L*e^ zmm0B4>~h%YocyxCX3+)JxG;ldqtA|`{jd^I$U^msP}g&4%uxS z?d0vFY+2$OUF4B~)`8HVerSDoX;5NmD84w9P!>TdizIhaa~kmMvN%c&j?tS_{)N6` zd<)=;c*#uE_=MU7sN%W5>y_XztfB)=DO$TgOaPomvj z$?fgm6o=(`*WS@tZH(P!gkC#BwaWHwO}O=AYowseVTWyhl4^a=h2CV?&*Lm_nRZRK z9~#Wxo#HGWd9VpbjUhXw&#{(kRCrC8#ViH6Lq;5Dd(AOjSAvW?Vahp{N;A#nQvSuo z=iZJ`?=232pPzP?k$jp#IOgJx8HvrQSoshc(9k0wYU;Jr@Poyv(tVYjz)Ho+R#$B=vcSrvjSLSq3p*$84q zR7y83w>F7U4(!OLc4ZXFS?Cw-l-w^C$E=8>43;;vv-2wxsb5^6<+x?m5P;@TErD5= z%&NiCOHIKDap zPjd`$(*Q@CqukX%w(kr^@R5(%q??`^(dE|n0REN7`W3og&u^udzZS;ZlYJ}wt%qYB zdwumI8L1iGNTjAVRK+mf(gkDX8e@hE)P=dJ10mO49polEuBz5K2mPp1&dvK{gS-D^oKTKo zu~>X`C^|cPNvQq`7|S!Rk%9ivIO7$xUYC`gbrCE zSRN;u6zTuc4NAFI8ye$rxWw_sD`*hf=Hb9iC z5IW1l9n#0s^{%ENOz;I4zeUIFv9bL8wC7^l3^{gxdO&&swEjfE#L04dZFM~bQ*5O$ z<8S=g$6(A=t1G~=2(GuC5Fn<;&Suj)8OfsN;^&pmPm}%anbgC*EugP^e`8~26ZFY- zh|oB|X&5N@Xf;znDqXeKqKzJMU+hZ>w(g=7$D1ZE+Kp--KVyiM;4zZ;?@ZB*Yrk zt<+X|+Sj1nNh8@>XEZUGgD|f4Hy(?&WkXcs4PK{OsAO2H)u6PRoit~I4ezUCW{WUc zY02wjJ(9zfr>@#s7tjzwxy4RtDk-QZ5uIZ6i#XTiF3v+RMoK2LTPc-tUsxI@q z_Y(}Opo(K~wIZ#5BVppDQYqe2>+?Y0SMKUXM0s=!Q#Z$(IG4CZbDbkX^zA7&-r;7b z`Y2LCKw?P^ToIop zN386;kWxT#o6M>wvg&Y*ViKz+R>;J?Z1&Tz5Ui&p*rz8E)zvr0?jW4t7$pa#pqU(6sf`u=S7wSPkT^ZsV*5 zg1ewvGFKA~XS{V99aNeiN>fa`=O)*uGQ>X1_J(f5=Q(UqZ08b<=R7nD^hAf-X1Y5_ujdAvt@x4%2l z`TaI)0;?l3}fy~A^h89Otnn>J65~qpGX_8^M7E7zg(MqF;d1y>&Foy0J;;IJr z)qw=*!YqE)E%Naa6sDdRGK2?9Kq>pZ1o~HG{(c1h^H3~n)EzCKE`J_vZ^U`{`)gYS z=~|%;t#^BhCTwjLR9Acf|^gR-4M5O0ie0Wngs_>Pn=K9ZAI z&k~XNE;U26x*>lhkxK7qJkjjzA84UvS5k8KAllm#-vQ~gtS9ySpDhHw=a*yLkMX8` zPMX#BD(!HkWu~pP7ImNRu#;kS%K{5{@VR8`sSxdMTg6PhSNn;k59!cDf@PQ0yA8Bq zXM%06pUG2+|8;TDCd>V%5Is$Ee=J6K*sF4Fl>3~uCQzyzCyVX5QR(I8#p?88d+mC0 z`ZzhNGcjbjy%wMl_m_l+fR;mYvUasGH`CKfxB4{Zp)=vBJLRd@=JcuD(daZK_KBHv z+*3c0&l9wjKTY=Wr~1W9h&ySc%p`$sS+Ij-s~rk#C>T^~_P)>j=QxC|L@I5~WTm)3 zzr9V%6X~wDPMe=TrNaVC-C|hCkSHSr-r5}p@vaF?&i79!Mkkeo;wmD^E#!>K_%u0y zfn5r?Z}Vr_`UO0*G9k5{TR2?VSdoxgLtuRca6O>VU{oa08pr^E>+sCHcyc|HS5E*6 z-6k1g6XC)6k)#?tqam3E02sh9t0Ez_E{R$iNz4vNC<;oTI|jR`g55Q20(D@Zw}x0- z7{4&}EQ>7InEbtN@Hgl9FRHb^$n1|-j+Q4jKKJj9b>GhP+>E!ZRB-#b$(%qp@LOH@ zM+;X~8y`(uH+2h?vYG8WL%)v(I3?q$t`_O@`Sazr^m1Ezw)K3mExkPuT<_#F>7ZY< zZC>d|m?H9=!Xk_v*!KQuc0SpTp}9_Bp32tY_FiJCSi%opJ*YD-eqvP#SrpH*T7MNPMt45Q(@*=}lTIF<|2|1#H z9@3#tned4O(+OAA2Fnj6=I<|wme&mM4#vFK>itx(A>GqpB*seAmUvShw$1juFGi11 zT<;5mdK}bpoOF7845mGG``}7U59rDItn_F_vbQMQTN12(7VR!PpKb{E7KHnYg1yD3 zy#=7Ta=doCzlw1Hbs*Izee|cj^coSL(jlJ?$&nA~_=kzkvC5L$#v;K~pI~N?Kiw~y z8@!t6x||x2Y|V~OF(5n@zetHE(F8;sC1_-_akqr2|b!#v()dErS- zSpmYL1FBvKRv{v+*-n-A2&#eI>_eV&i7HGZDqzD0rF(`~mY zkdblZAi%&s%WUdP-|sTx0b-F zOJ+8b0RWd_(i#C|vg--VjL4+gG=RRUm%+7}!bwD+b3&74yI1JVDd|llb`zOZ8bhhW z(DH+^T<@6tfOsl0$X(seRRa{D4TUL~1?XE{1ERg;$C=(gYbO2C`OoE+z#E>=)}&Wk z&zGCh%dM^HvF6+?ZcKEHqbnBWg|$cc=o_H4w4jPw2o-ZzMPtlcT}nWRfIhXz48Y3lhn{U~2cJMwYYFH#fq)UGe?h?;3y0^XIBO_I8TZYN$yKRAtCZ zs|up9%7#iy9d9zA=S=V;7k0yQX!X__hJGwEeA9zcKO*{YB~3J}C@c3Ra%>HkkI-ueaAE*nL_fhCOmg;<AIQmM3QWR+VqqW>q< z|NO82V`XhwxVUgwz`L)n7)~XYft03_99W?~%_zNQq~;V2Bh4i@M)^?E63EE2_=eVQAWwY9%c&~hYaGcr)` z3^S1-C9ctATdyQ@XS|gg*EO^(5R>N@UlfEX3C2}KlG-Q$fXm~6?cTLGV2!AJLipDs zW1A~6w2_KNK=4d>sd(#&07PqW^pZFVfJ`!{A(@pPfvLcyHIq5jct%r7dTSat$uXFR zCREF~>t;%NBaziYX6J+wD&r`5Xl%MiRIYDq8Y0L;-43N@>8A~KQno~>81AOyuX*^p z-dfT5mds)2uM|?WMlX*x(c2=q+~n^sNFNTCCwsKsDWuy6hMU+E ztX$|2Uz(k70V*Qe5b3OKdv|;D#X$B~*6Q25_pcr&l)qId6doN4j*ldAYWsT(-#v*d ziT-!#&@s1974|9@JfUlH-#M?5c!*iZL*Di0YXx?bl84k0yp4%#igMvG{ZNiV&A7wra^ zN*YYRE-&MDXHj~xED5j{Ng)@kHSMK0?WNz~ zpoy{89KrZJ@<{yIfyc$MoBn3eLZ||*e{qJ6wVI;XJJ^hI4WEo6D*I3)+Qud zGs4Z8=Y!?v;}sEL-}8ssT7VQeT;gxd^3QfeyG#7C^qUN_WUz5&po3|rbwYC$^rW6< zp_ly)%VDZyH(R=$ZW~;+BM`n-%tl*j>He#Bd&x+0&S-pA&&K}c!^30S@P@W`NkCeU zi5}_y{XhPDV{(=iZnaPjWE7efrMJP7Yv7Ji$lN zJjXG#*d-E3o-JL-V9yNKkYeBXeBZdjKuj?@sWOVxPR%I@!gA7oE|7K&wX{YI<%jdjGys^o0rZg**>GLH-cyQph8Dt!tv zu!O1T#Hbhr{L6==&=4vAh`+ZWI@_A=YUoJ}{v6|b%7`DJ#J5(LO0Kq_cITx}$90tj zQAV(6Q-@e{LFYFcY4d)k@yRf#`3~-l&aLWc3%wP^jl1hXeDY|>Xh1FF8AWIfMxJ8g)6?n^nXh}z?N^DD#0 zQ{2uneMg-?@!;AGUPh}Cmh(X-9gZ69&br4@;9}6nOiyrYTHIJ=&OmiBBO0CL;!x!8 z{OMo*N2tHM^ltxoe{pMWf{d^panqXh)~|u8WjKNA!=0|Uc){GDaB&0x?dNX6bgyu# zM=;(gnCb<7XQm(c)43th+>j8!@VIboLbN_9+ME$?&k6Sy001BSandE{Le91X8&lVX z^y`{D!TOZ&Y9}=u-5X%RZ^0k2yk^`#WmzdPJRI0CqdF(uaa{04M|CMJ38D|`s{qu< zIjys)?4Amn%<8b(&XKj+`gtwaGEFVotN*In(a{79Q5+wg*H)ZXlw_l9JHu_VLGNlj ztq*%!q*=*r;ogW>|KD1$;eC0IG)m)n$gL~A~=Voy2jL#SQUxX79fRYwNW@suh{$$T&90K)hUGQ z9zlfpd1*lH6-_)eY&Enn%@hWZN%sb~2#!LK~l_uSQsSJtxp046(!O9GkIj_`0v ze7y2}w*GtpWXobe4e~olxZrR_bh0MB+LfMfir2>_^Sx6AX%VV=q>n~MKfGRF7?PfE zNKV)H=0;`)+AlX|mOJYkiSZ{H*jaXRUsa*xa)-aSB)vPR<#6F2^t^PfdB|V})He?m zN(Om4C|ayeOp9o_qC=Ut1VCa*9?9wAl?{0%3n;OunW7oF66>$|l367IrL$NpK0Of~ z?u+HOZP^H6U2)bn*KRw(w9ZzcFUoYrU%L>l`jqGN$g_XQaJc0_g*nJB5A`<7cdb?* zi$L#J(OSoNtNRRO4I0!ASAO6+jZ$4NTX2&8l*{ValT1H;Rpb!C@hIJM6s1|UFd(T(fmE}bl zfzG=`_+GL@FI*)HVOSI4+Y#o~6X(5Cl_{F;TOH{wFW@54DFxIwsuy5?!x`ubzkR2MQNrRjA-<#5TrO#T`20Tx}_x`)D%** ze>%Y0Jr;OBwv#rBqvy$XH%wTcn^wQqCoaOcB+{|l{NIv;yybXJ$3!p9PW@)Jw7e#Y z_6QBrgk&Q_@}0tnAooCKMxdPjP*E4|y3Tx~xbt)8z=p3V&Hk96E`O^be2bbS|xfm^`AGXHQ{bhh zl0TSu|E$PPik9B*@c|LY+RAA6?)32K^5oOW>eJa)e?`GXC3m}!xz$u623XG(!P%CR z8OTvVKhr5R&CWXy6`Bh6hP^SK9hm@3bA&>9Aq!|FZ||RFFJJmZw+|Ab@NabF_?l%s zJ_!#GMW?6IXUWCQLk%nRz7Rg`{i)bSafN6*;H**Ut|`iMyw7mB<00-d;I}MDk@@Rh zTZJl<_r=!l=6%%fnUF~`ss^UH;Hq(x=P|_azV9GP22#$eV^6aD_*D@-i4KQsk5T7O zIVgh`f7A6CoB3d~5kI3QxW;E^jnyc~dI5ct9XI5vy%=QH;i?^BsD77DkmOP_9l>cy zang%T>A})WcPkg7x8SQ^22-sI_NWhWYmf4h&h+tThlDfzg2^u6jI|3tcL~Qk1)sb4 zB)xZe4D?&IM~@*n2Mk9)f;ou z%(GT3@rEprqBqhohxG-5xgpWo=cj{Zso)}=#;sx0JWHilgR8Rv6`xbP9!{>RJc=%5 z?p#M}7a&D~pQEqL2I^-cZqL23mKH>6`k)BSKyye5`t^MGIN43C6VOH*tGJP6hL%AOfQKg zS76d)#<2hkT}NPMge3CNNlj!KI|UF?0BhAk0kl@xVE}+zQqt>)taumS*3|UUgw%p) zQU^7&n~_EHiHQdJ*AW<%Nz|$&Y9oQsPDxL83QKhfW4J}egFLB@!B|^w4|Rymdwrym zIZD;i{1@#6kllG!${s!OW~%q`bnA9^@osPVZg1)SVCC^}<^Evhet+qDYyM<)YJXvL zV{&k+r+K`iajc=DFQ3#$a*= zk&yr)6p=t6k%fei5IKpQbI!>o=bWPe34}yOWH31@fwq3acDsAJ`)sy z3<%l7qM3dGu2bEjsV>oEr)aWEIN5nO(IH~>2**2wQ@x_ueld4c%pI2ii8M>RHYeWT zNVXQl+lxSh@wb97Adq@ETICFNrv(R;qqHlK`o#`bBSD_m<*Dbb<+sbM`+dG>b6#|~ zD&6DXoeC&eAHn$Q<<#au#l&n(XQzE}w{zuKROjW<1!r-e?A5OLZ}xN3K!Pp3zZczI zi+P;ovO=e~-*>wjj)b9B>Pn`jcSk2E2TwM1;sZ;rPoKKG)4lJ9ofF2 zad^KEA&HGqX#ju$3qm22f=KvlqG=-@Z0{s%oX6qx+Y42oJL@wz~O_ zy7m5+WA?C6u<&rQ@o*-1INkhh^a5TyH@jUm#lFwf!xc^9tjwwc$$P7_KzRSSwmjay z!y1wu@UK^9w`WJi2P+c517zi3aay=OdpprB+2M-!mL+F`0B1tHrE{DOu@LQ>g(RUA zOs0B<9awK_Y5y-9y1RI-xz3;@y3KRy&_hR zJb&&MO>~N;y6+|c7>g!4?E!po4;NUw76>tzMd$XjYggnII>){t078h#;K4rA%`p0gNyrbJ5m)#hZp5YnM zo{-&}lwZh*ig586Oe@Uv2`vwl12{Q9!zGh(n{D8REIC|xu*l#A0Izbcej?NeYA{| z-&v8Bb=K2@d7Ta7lQq#U@BV1Dv!vi;vSXv8=A1V!1D;$2jOO?F52u?S=SEJax`q2I zj}_?;h1p@on0TmL9+F&up`a9udq1?u9;9NC=n=s84>IZH)q_Ni=f`npS2t353jM!? z-o4%Zx}1RXD(5+(dZopyxhTt>5Yuw->w^O1Z2|JC9DiNxaGq*Y2Uch{e>GvJR04WA zj8;0xayUwaExPISS}B|)!*^;RMN0;&x5IS((rE2U?@2#a;$s%uy>3t=H{o zDu}e-kHw997&jx-SIHK$E_$P$=G~6^Jy@M-Z*vY&FHrZjAS*`7o4k9Uw-)Lh=|Q;P z<%yPOWJep#(f$oK3f<|k_r*yIfgT-Mi8B7!-E@y|svF3bCp$$`U1B+mg%dy!7EX1E zrh$xFG}kYh=@&0f07dCX01E&BOLhPN|5gfpuyiGu7w)gfj#g!yK`FaeELe~ntV#El zfcJhXWdoO}hlAylgJoB1;&IRN)zZ#7cZauqGRE7RXr9}y9A64(aZ^qx$Zvc^@8izh zU0?RSf1m1%&NF*cj5cibK|r2Ap{xJ79jRVt^Y$PaEtxLgt`6SH#(j)|zPC_H*ZMX$ zE_!8pu)*ZdttGjS+qb{7y*M}fx3lm*4aYe-L1+k& zv2rYRVHDzN#V(9e*Pwd8*erryp?63%mHFO3w#X~&12eNOJUO2f@)^LXjgje0Cn_E4 zmg5y#9UR{t543aJVzb)gvYTVF(!9dk7|u zV4{ITB_pmaCM(+`w3-pq8I$P`Ax7A{r(%8Gb&wGd*JvA8R}DMsH~Lsr3pXt&O4)*H zYJQl{yv&ceYOA>4f~5XY1-yc`yPL4i#sX1+7Sb;)O^X| z^7-Z>w;*w?ty*}vB0CpMbTx%rU=wUzbCEv94pi)0^QMBT`>Zn2wtO5RP{fj4$7M>F zH-9vCPtF#z=l_0J%E!EGpsS7D5Ppi!|s2`rWYYo9kj{UIlftF7SRNQ9PWqRTFSgMY*V@ z*0~z*Ww}h?b&7CCgA^-4Bz%c(HS1?PL$u}65xg+lkM&F}k3xVePTo1vD@#7NV>{L>Itqs-!UKVIjqJ?3Jfd_BYE zx`Z%{(aJMYD?$O%Cl~1* zV2VnybIU|{#XwyA%`l}d{&|jmMb7>u1V$q@CexXk?-5ugLDGao0 zZ%SSW!lNNPxho;3KP4~QCls%XDyA}yIn9YWZ7h~)YEuii#pYn76alIrpZeyx@ z{6kT7ASr=>t4dE2eq5IxE(5yPa{;HLp^4`CadAR+zP-U7jfJ_!LWsFYpAsw`uV~R+ z`d%gyONDabs{CM;9vnXazD}P)u=MEk;Xm3&`js!SMB3d{`LR7<$ydACMzI|JPFRc@ z#HkkH)rF1jS7rE{_wMIec5FAzV*O|1(6?QduZm0+PKpS3d5Dj3kYSrQ(+;ZQI?{3# z?VveWI-DRLN!qFoxcWdluc9;%jdwF##_)PsNc{?CN~k4p+Fwk#R%R^pAmV~?CC z(C-=wMC?JJ>^$2C$PcEvMU&m43E&Pad`zbSV0Qv*vRlF#lCb(EODyrqjCh?bkvEfo zJMh;4{slhmkA+I$mD`Iz0c>*~_%7gn1z^7HXmy7@n&#`2U~AY+aUNwR50~_fxADr! z)#!hM_m_4)T7Sqrs^wl8GTCHT>teLOtgBkKK6^CXXZHNBsYX9^AT^536*m$bB;1D6 z_rcedUh4tiG7BY=@*lI^ko^AUQuA+z+L|6W?)2n%f9DrR(S8E(5`W3!g?19eH6P`Z zgYXKma7cs_GvHofW;kCXcq!gL4@)g{q7~!)>qwD#1bV((0ARCD4zHufB;x#PW74ub z1L}j5>O+(2LzCO1GlP&G7#(O7mXhxiA?ICXwZ&$(#b$OUDq5RJEa-ID~gFXT;M zZO%)9;U01_>>gjZyLf-Jvd^2kSZ7N=&D{D0nul+`tdP|!ob4A*cgZ6#upVPCfZ>TwF>nX&mRG0y#Z%o9F3=N}tgyZYu>21E6Lrf^ zm8D;Kxa{&JTU?+EwZ;b4S4RM_c6rkM<(n#o zJbWIq0T`S;$OOZE=}?2tD5``%_b@j=wJ-=$|MophwHl(-hSk3A&$u4RzbPYL=M$|P)6I63A1LH2Ncb^UeW>aooEwHp?9fot_bH_6&9ALWw;_X@SZ z##+0i!Q6vPFLXHGfu>PLOLS&$a()Uipn?(Cosvg};<8D>?<11P zR`?D8-swGwIr-#poHjhgh4CRYAr?cE|^L{{7+d$K6G$B{WIJ!tbq7 z2h~N!=@%XWb3(olKY&lj$>u1P1{DnkE$06&_?H#X!SO^~VC>+Gv zih3ib3n_O)@uJZb;ZV{>1zp@3x|>C8CIA2)bm`w-az|FOxYRD~>OC)!%wI z7+zto(QL0W?5NX$)L8N|$}~~v$xW6WEZr@!L~Cr>#{5QN&@k}Kig0zSjZs9D@^%YKbizYwE+s`SqctV~+Pj-u^dd0H?V&1rzJ0_37kDVl-8~l?_ z(l4mPf7Gu@c6h+ed7Uj@os+DyrCVH}pZrJ&+2sKmj4iHYiz_?c*q)o#Jqafvm#R`?1c;$-bqwS{Y|34PzIh`=lGIRbZ)DX!e>D?{vS~ zabD^3P)(X}H}O&VYnkcGPVnn;y6sdZaeD)hOg>JVcr*b10^Ywpn&((^R?l@)q3!^H z?LDKcoZ@X=((OI`3=u>vNQomY2Td-(Q41YurTwdr-b1&?c*G%<^ECO_MSO(3{jz0 z&e(U>?nYKjdxWEk4d|&B(+*Wnaj7K{1Ub=?&3Wm`CJ;7(EnAixEZ=T(Z?_lOLtRKP zc%q|zuBVyRTr>H;WUaSpd$2je$F;vSTei=a9<4r{3B-ph@;ClCBn?P_?{*XY=^+aG z;VR}SwqS8{p%4K4&r@;9;quMF%3^oJOmW&&O5|)p=t4B3H=M>9YL{JX9dJeqanPCS ze3|HwGt?PvjZcBOr`ow!IQf&*ZStbhfbIBHwE`|rQlUsbG5GV4B(eDZUi$X{_;Jhk z4^BF3eeZ`oXI<1AZ4`6NUR-C{U+1H1-F0`H+(b?8w+&urd5A@CogAH~JrKnSsA4lp zaU%&L%!XcNL#MIoLlDKw0;ltiu=Q%j{bZ_eEcL1{e!C{%eu%l5gl!}m3)1kTM7=z; z!F)J;D;~S#2c9BX47%vgd4cA=K}}fw0i5n2R;v@OvFKq`Z27t)D_(ZAdbd0)US&%J zJkfZ^R0#QEaZ!9bb-P95K=>6+bck6! z!ijExn>F1FgkL$oQnEHH2DlvZBGymD44+7pKhcPP{i~ftAbDP!m8{N+R%S02#-)G~ zL*7r`UIc=(K%QN%vG2FIvK^l6WP7rrAkV>epeT2DVXiWg&MMEj9_ea}p^xSzN*BiO zriWyUlg)naUT?oy_BN`8s-&5`u12WcOjlfXCQHgacp-2c$EL*B(7nCAU0mSKu~_mjESIK?j9Wg{%7A(11Ac2BvDVJ9*3RiL4<9{yCsm6Q zoL@HDC(psR07osu)3Y7O8CX)edr+A>V02zf36F%jWqSqG1w?=FkFE=dX$wzIck{<; z+c@b%VlmVlkC67*%6g&qlH znT&-VHu|9)Tx2*${5??sk_BS7v9%`m7B=PBwORB?YDFsl`c2RoOXN;o-)L zit$1I>wWe7n%woOoP*XX;nLK@9$;AcslK(dC=(tu)s%X^&`i^^_0=}o&Il24hW=(C z)0Yp%^23?n=3wn~WB%jv%+5rwAU%AzC{=o~yV6KE|`<+5~-91 zdeMSW5|v4(v3=ddNm; z4E|2oNIGc?-D?Pz%;t*5)6Y7Y+qDeYcmgjR*5qcgmEt&n*Uds1ti)iq60nQD=IuD` z5qBdF#gs!fDYa2yd4NWp^*T@hfGfdoYU9FX$Lqr7S@AkYyvlyqUE0r!-K@xZ*k6)v zvd?CQ`m>TG{0ZTB=WRnFaNT9~0Y&C1AkqSf^dzu==V$uG(|w|uK7fz4I40(di1|~Z zv6sGYwTXPX2$^s_UEYkwO^@FDBRuKo98a_ zRyR9)`d9e8$LB`|hAp0IX4(Uu<0&?-(O~B|TS7X_(_I^iRWU2Z`Q@N|axs)V45bj~ zmxb|3M7Wo^29&rm$~*!qyn~a_-XV6x?#PrX->^D=W=lvyfp;)U9pYtb9gX&h!_hio zGV=XG{n5mxsH6gN1QX?(?Hk}>hOP{X4nnv^AiR4MvsxpQyW=wRd_r)VcA<8jrIaw5 zm2()>J;RCWqG9KvVM8`VI;qu;kNTBT`#z2E5d_z@mDtAvMNZp*(Q zJO&JF_kegVyV#NKa5pj|2eT4n;?vpuq^X?5`}6IG(~S?2@gc^j6ohB0oqM?>El?j7 z;YyPo9e?(>0uEit&cVYY{ZZZ&mTd3eUtj&5DfGp)u)B;d9ral8H|n!jiqU<__cORD z!_G#4#}jR2QxxG)=viCHc7%Der9v&}#T4xAnCg3})N1kF=S z7iku`5al79=9H^p7eo5tuyAfbINc|n9Td$B0LABpG4cGEWR)dZo)!z{ zfq)6*&%lxVx2>aJ!7yL4y&zqim9Ea-@FqnYb7BFIMFZ8W%>~IC8$j>U6hIf@O#*9a zTC&8F@TcyVSoi#C*&0W}ow%Fqy_@P4&JIiGh7WS$q72`4qSa;zdNVG%#g=c&oI#S+ z_M;-ob)kEVrE;47_oG;?<9NGHj9QHDGj{)@4bYEvIWn0{BoYcXH)rPN21iB*MkktD zJH}Y-IXOcMXfAa3Sv}Qy1aKmR5DOv1SUacMxf3;Q98}DT9jE|+(Ug1#-y8=@HpYhu zB@`2AB}7K4TR@2iqr@YCY=S9pXVlZ8YW*S_7%^4U2$Y5m-3k_mb_+-NFr7R@o!o1J zV!PwAim8!)<}Lv!g0Hnxc>vSP3=@s?>59#44o~WeNze5TcGQ9~z^;|l2!F6kfE6JE z>PparIjKS1wP83FFxe1IH^rF$V33SP_5@PV?<_$-s50$f)6w*gxzWc3aSwY-hpY3w z6-Atp-mG}0=JVI)FV&n4tpc4rES0r%pS{)o{&)0R@+v^z){yc z%+WJ9D7=_PuJv}F3eGa7H_&a}cO!Q7Lfu4OpC2otP75^#t9 zgx$aSdicBUz;aHeJp3-QCl||5Hy@nl@v2qUig`vq955}93ekQ3W(y@K*-6IrO4i2` zrxjoQ61^YVEET5g-VQ?*hXPIR>K$)$VM`Q)R)c5!Q0x2gsP!89d4HUkUn?C?*=z{g zZ48l3q>Q>7b^! z1$V<%JEe+1lI&=j%bD=+1uzrDw^)vJW50=II%i@C-@!{(I z-m-Xwb+#}Dl)*OVfwWm5NAEHVkW4I2in#KdHg`-sKPp}rlkg@*^3+{8J1G48CrRc< zuPZYPZ1ijFlm~G-vo89>IBllh_j`i{=Pd~biKr?kqX_lCG}xN|}`0LOt{Vyy|$ zV5d~5J66RKqikC2;G2W=$wQF~9DI}PJ+sl|NLymQBej&kC?PTmiTS$1~7T%g)c-xt7ihfDovrc#4q&LDLSWV&$P@k9lX|t&b!dB20hK zuOqpShBHvgmRL2z2wU5g)bPVB=52N6LVsIrWbnZfD=9i&1h zajN-x-;dh78HXy4VN@oW7UC9{i%jT*qiVC^v#k^d>3Gaa1?6rk>u$bMI+@I^3OVg! z-jAgX;B|&WU^`6vdW2Suo7sM<6E7G#=LPC?)}Qq?U7(uJ_?j2mt4$Mh2e4Y57|nS% z!(OCH6_p@6So%a--dvDu%!_yU4-2DnVU&GV|HJm8c%3a>XG>OQ#oS5J{3sA+Im3^4 zT_6WmoB27hTF{jycskI?<`$7S)yP0{I|A_S6RgFxt)1iRy_1{0lPeByb&SPk&#^xNxTPKPon{u?y8!9K zw8TeQ;iD{^l5E}V-x%Xm&5AI-IY?42l2n4FGOb zv^V6nejd@cmJ(J=32kCTdw>u~4O4Fu2wvZo4#jpvq%;J@H-{xP1}EF8SXe337eR{_&Z?ckf>Px9|S@|Nc9Qoz+HVLAr-4-q_ecK}q?W z=U^p$$XnePe}000rJkc_@B7-Y)z?+rkSE;v8@1EVxR&y1A*J!met);C;skbP+$v{E zw8`%B#%*-h%Px11RykBNha7wFc&K}lom;H6b1Bv@!HVEv=5TxS6Pf4d2VYYu|C0cI z48N-joNj-^t47xqAKf;)w@La>7l;}wiO}T~cte<(>?rAWHSI9Xekazt&H8l}==qrS zn@Rh(<9L37|6gIIM| zGA?R8;FdisoEZ?$^oiL+KnH84U&t8}KIRBZ)3V9V3Y<-e)f>KtVIy2) z*2{DNrRl2oW_Pe?BgDQKu92np{cb3YFTLX!@Qg3oV(6g6l`~61xT8>i(TD) zQ*)f@S_QBcB-s#Qu(Z$x$`S#KI`rgM$_QDdM|G3ee%^i^MG_&E}g(ympDJH@S z7jI3Bw8UF07~s^*3(@3UBq`6*n36Hc z)V_)o{DBhQ3^#nHQS6QnhZ5b4;N_H%){w-e!1(5nB&@DA=!JF!+9M1>^ai1$ zk)(82dTVH6TWDgA8w0Cu<714i^a+kfdbw)BJ$2wN8nzB9Rvua~w6dAAIs~r{Hu_$> z%bzkDM#H?bz-t@Sk%(snah$lI9=h9VWm+(Us`_tF6#w+(Uw`)xMo&~cbRZ-hOY4{V zx_?np`k&t`{g3b8{J&3i|M-?-WB2O6K7H|jKl#J&{)t|T6-m6|JA+x$AI z{?7s!2-z~3Y`7qbmjXU6a$w`$<(VtgfL|1<}A*a>!{c}Uq(wf^f=tsU24l>VsBMw`GM2pozR?hWwf#7|}9V`F3 zDYOmtb}k;f%CIbhskM;7yJ=2bhV_KEaWh_C6f1eB#*6{?M2ZRSG2!!Ki$O(BQ10T$Pujo7?$v-fgF`L@hQYD0JxR##zovQ zAeCJlm#4LI8H0c$-sAxPpj^Ws`FUsm*B3B(3}AXljy5D``;xN*+4=UtMEB}o2LRxe zS>O=iO#&fzVGKAv-$FUevj z5$mh{?0pnrEfSLrdfAWD?1HPVg+g2DkV2zpTpy#`0>^QJ)_4?bXML%)Z=|hnX!qoN z=i|lB@zvhh?Y11Z+ea7MN9S9Ir$<+!zQK`gf&B9!lXW(=gTK?rvIiotpAmuy!N)?J zgG?N36$~*dCi!TeJS3^mfn0+1V?qeY_8vhNIHoneoZz4DL@jcrCm}r3ab!;eJIE`e zXgHzR-9H@eLNKs4Q_!1c`ldR#VKuGeFz)Rki7i3#Mc#pKAbZer?FbZ+X648L6B04x zPs#+O-uhqry z9SWtvl}wycDvtOmOsgG6VCO^?v9RI%f=$;*T6)P^_zobFEECo zkE(PIU_zWz?Y&~H2_+8H44Ai*I%Ior3{amv0{H$x%v*Vo6Y0KS_CE*U#|VD7v(pz0 z*{^cm3^nbyf17Und<3O9?4f&H;y4%u*=cl@4f*e;pm$R3S*}{8#?SjLUys`>a`39- zB;Ct)$Jbg=8w>tUK(aD_q2_5zXYDjpc2=4-(E5pO;fUKosW_aZ41F zJR6lBf9PJi^8(Fs+{>uj&0yZ&e1UG+>uFqz)miW`>cwbwVKiq6dMsz{DhKn679jI5vMsJ;jRRDPmS@ELDG6^%#GL>%gS-h5SH1uZ zFbj|$El&ek?iL^+1RB49fB|UkeP+!52VuMfXiz>1?oR|~ya|41y=0d!-2hSm5q}Ce zFXl&qG#=nx4+XWY3AFlZIDD3g&OqrV z5-q0vt+PPShtVn*#cth1!$l9HB74_OCz)B~Uif zI}hno;z+G<35bRe<6-U*5Ep+_RGBla0PmacKjcP)E@rY%;*(nA{tQv^A_10$47mA+Bv3-av=pkY*v18vNB z$sw#r|ILiB>68ethPKZ0x22&BZ(Cd4KfiM~c37PNNZ^ie=NEr`rTs!j^*gnTYXDpaFCrf?3))z6>Ss*oJ&N@I*M+mdwzR> z_+wa>UF|N8^v))Q-WA7R*5zGvRdXWzRtMT;=UcL~&Az&7>mT$ItX(S{840#-srFt8 zwys5JYL2~+qbg);bL(eXv7bXycK^RU2G#!%faPV(*_zDhNR!hlJcp=WWu;JJ@p9Z= zY0wvRR*A3kHxspcUN`uxhg)w%L%QKgg+@P2z!c|+s$5q!R;aP40kxNCHG)>H)p;sN zb-bTR+G`0s?T;7nKZxcEjs`N9D;SsKIfofe?`@QLNm#CzVLnWQ6^YzV#m`er$Gk!P zo<{R@GcLoj%N^8CFyML^_F%NSG1@Z({TUbCDkSJ^aq515=@Wprc>wUXxstk)hk|{yqoNfG}rFIsL#6SE>J9P8v^ni^!&kUy&fhNR&Sb7s^|Gc zVQavkJ7^BC)k3uz3W4(cO>f(yhCTGU0wLtccv?!{#Nztl*^NLB-N&@~%i24=UfS9p z8yWdx&C%1=1%9fPg&^f3d`Nn*cpH}_TQ`~^68h2zrEHRB?_Pi;7h=foJ;Gvb+@fs> zi7*czL;E6}Z!w;lgC(aSy~7~*Fc|*bH!rN7>kxIJwLU>b1iJncjrSzi)%0+JjxAap zRN&@c?h}$u@G12QL8@Efbsz+7cqZO23s3U|A>UKOS^{En2{Z>)YhPmrx(PNF?d7cx zchiL7RISj;W}Z55>sJPjst~j1TB_f^U@-$L+}()=W`(Zk)#T9iw6Mec=t>{=zy9-| zf*l-g+v_0jG;CF%6kKRVdR=qhys;7N$-h2-`R&`6e|q!cUtb!&Hjf~sc|rW(FRd;9 zMI`|0@F6}clN1n(#FJiYQ8%8K+nZ4|XziqWvTu(=^Q{ZjEmaxYYvHu-^7flZC>9v-&bglSk6#FimCeQEZxFJNQ)t=*7u;lDj&0h*_oa~YN$ZpH(i z#tWW?Js9mCwDv3jV7*3^;R$>A{$Tl2BE8K6g6B3@vd6z$Vu_2BW`n&3EAnMWYo8M7 z^?B*W{HLCiU_mNa5O2(j*5`nz`zRZHq+9F&=mmJo$LkyHF*=33u`qeNH7`9{lbvnI z&NifnE1$@;zo8J`^=o8H6;Kg&*<2`xH-S>4*h;vSyed=+~?t*yh;o#TsX4tHe< zm@*@gk87+dEVKNpMh?O!$DTydhbKeb5^P+3^zC5^#&AVYyp3}qiky$~sq+j=hI)ls zI%cEEUIzA=2(NM%dOFIx0O#wbYfCn@vwo|ER5J0wSls|GIh3TO?8%^^_HDpph@jE_Dl3E@FA z#Sk>?P%36fWm9);1oW+eqZ-8IrMj!W!9sjUlpQn#YBL_;cU6)&8AV@74X1Ma+fW2X^&kGo#6o4=Wj7F?9tR{6W zJv`G1agZOg5Fg0uZUS^sA2+zHA?@#!i5fPA4%9L%Jqtm~LU_m95DHPgg&05VTZ_@& z;a{K6@EO3T#~;6pVEF)(Ky1J1NYY;jzue}Mkr3mHM(-7W<5s)3MP@I??cO%ntK7XO zc7=jx5@F(2@=B!5QaE_RS)b)l%XWg%2+4FFNTIcR(ONSE{aJ#33qt$jWcS12iWtayxpDyW01Mb{uJmyAvZGp( z8`VnlSQu!R?Jogrtqq`3E!|v@%E^pCnQCiMyz?pj2Hv>)<=zX}Cdp1W7bg3y|K$(q zWb*jL@OW?gL}%mDRNuwsy!3EIcDgP-0_G$>j!OKS^Oz45z_z%+NwLP3@F#&H8Bn_# z_|(S&BCwp2Emu?AjdutqyJh@IUU`=1+rLZ_bfyWqlN2jX6sjDeTn&4-9$|l3O_`%Z z`n}EXTLP{=PzQZ2mucohiH>caped66-Dvt~0JM{47F}2oS=G`!HVXjw=zQy=e93RW z+ehbugOjDLgVFKHyW86@AD8;7_f}6dv*DyHdv9+YyHu!qJe26I3x&NhfhmHbtelE5 z)LaCq(I+wsNeM7R7vucgb)m5~#7Y7q73o>zM5P&H;_L`S15@+ont|pRDhLy8=Ze>a zP>n4gYEwV>c;mHg7!XJ_lGsd-O2T@9m2?I|$Yc<_niQ5xpd;UzXFL10F=8_uDJW%2 zKM=-42OeeZ5@C(UtJ;tw(&O0sE_xtEnT+DcBL} z4^hRw`Q3~95A_L5riX%7s^@$y~NvSyTI|H%8eUfw%}*fk9u2Y^JW_Hb~ggFMbn$18GOvLInT1` zg)21}KAWPN-%rP1_Jv*a$4N%wg%b(4eUbcp_wCNa+x~=Ug2tpDcqbhqC;Gg^0VOby z5dZ+*;7HeIfe5@XE@F=W1!usH>$9~0s~4~qnCKME4#?()BESZ9aHUCS9bTaAN-|+H z$746eX_OAVY7Lk6L{0ga_j{N~S{T>$egg~}4%wJf?u;pwhrjd){bqhR1?1TVwVbL_o7ki&}{{i%lb<<9;ddUiqP4iyA?HkO?0K#sB| zFwC)3a|rmAI{KY?u?sEO!WpA#g;h7WEKbz8{u zXFEB}#RmA87#-xtTo)(KMF-5q2e#5Zk-EBbg*g?>lu$D`?WGPwK|A1`vD-V#P)F*A zf=Z+VEV*>czt_y#!lVD;(INW zvRN_KuNX}&L{oFolvo==0h&^RrQwwB`bXf$wK8)6OM>U{c*pySI)KltwM?sf!%%_ZIr zpMA2*7Vq-!0d2}-Ui~A0ft-AEK?-E=Y@mw91z9$$ml?3}2ow&M1ogR|S65usuOc^E2!v2faG8#wdl@3~o&C}8Ab}8Ei(h06%nhYvNd;ltwC%lg5XlG+7Y#dm zB}2TLwWppP-3ns*y>>@`0K?eO`cL0^f2ZR6Tr1WF zSDTm?D9?DPr5bQfun7TIHE;-o* zM(}L&WY^ndSEB7R11kl6yVr)r4%8xyUlH0j4^4@;1tuhw;Qa84<{yfx{{0ks=jeR* z_>#ZA)7acGGBqBO=okh*w^CbN|gCANfUk%y5 zt+RV8Yw^vafLTdMX`3&bYQ*+69LK8_TfOXqE3NwJU!+=XhZrt~nTqqFV?@n9^B2t) zuZ~-2qVd?<;b>uB7y#h0IDWeGOg`ykB>kk&r3b3Gl;XG&WYy`Q)l3B+7JAP6nX{>8 z10JBoK=1+`%<{EpCKxO*z_V@!9Vm?+w8n^&4vVPQkI@t~m&x{*0WP#)0l@HM=XOsX zgb!Clt8=G$apIzc*)X5?>4~zFP07Z*M6f8{0+t-Wa*5ym*JAz8j_rGV>EZI}Cb!ng zdPqyVU*E7>U90Bhn-Wutk;=;J8^DAexI;>%XUE5f8!M-)Y{}88>~!O9XYtVuT(Zl% zKUf0N^>vPPjSbYyxD!AZdbVE-`1=6!;Zsl=sY?`4$5%w4X}V+m&FEK zBg3r;PU^N~5WLniD8vd6S2Uv>=7e^40&TV6E=yNXU&y zY|d(!YMx!M9-d37?(q+=v@&%0{r~aLnQ4i`J?+`ds2G|r{f&O2nw6We)zM(%WmWol zRr(!g2(a(}O#nXzXVJm(!{v_Lsb@=eCXnp&Z@0KI(NS?;y8gGy?ppTd&vmVy>*S-! z#qtz73qeYR0vhZRoFCz>WqwTRmx*(~3gF$37u$#D2dCHE6+vNsNoUUhXKACZsWm1t zs-dPU0*SHa_ITC@`&vj*!jag)qes?1-!;%*O?sf;Yxf=2V zz(CrJ(&)o#^0{UK*86j|q_ch#RD z+L;#g=T0(Vm#mV$SVEt}o5PU;L1SDW)qt6r9#Pev+1P!4b$xJlz5VfG_xN)6_)>6q zGCDmoJT!i^zyC7;cei$cpUMFY^`z<{s|dkKXb-%qIpU3(gR;5hbFCDZN0z;3zB8?f zo)Bb;O-FiXAV^N?kT5HJi6b=&?Nf~Rqnl!AX1Ep_8KtHNerb@4qqO=)_*-Cdop6Ua z(L0%ue&&`fRJUlD4cQn`>m5ea249rKO++(Xv~689Y+ZG&i(P0mV+`z#u`dYas%7V- zY8?i4qL{#4bJq9R+ormntAdHD_DfgwVi) zg$bGPXso%8`r05`$t)O!6;F1amLy%&fxc^Mgd# zU=n61(ni)vT@13Eao3si)TxKP>33FJ^whn|w7W_HF9d>i6D@aREZV`Z2CQBT`j|*q zaW`Y}cf+x_-NE9Cgp)ejke}t^d&WfrWy#mL7p1XVK$vyaX(t#irnzjVyUdY{rYWY= zzNQ?SB|iY%;B3(A1>yyOr-=ql2=#6Ut!@YHDWZNiN}X4h2BgqyY#_95$}7oV0a$Xl zELvt=iP~IJ_&c-rt8H}r-z$+T{ z*u$L%NA&3Fx2mYTfBm*ZUAqM9*q)v>H#l^C1*Fz}bu|n(m!l}>ORb#|(wd1b6u zw!;JdytO%?5zU_x&jYMUz*c@{KsecTH_;`TA35s!K+}0OLo#N2g4hh`W{Ll4ZSc|i z;H4D8c~{z4gafZJQhLdoV4}(psvn!fMSJs`6SZT}h^x_5Rz>VM-FB-zX{R^0gQ6di zn3&l*G%&Zgws*XHdcFJqGxnZAZ6@fR@BOm3_S4?oQ+0OEIkPi8Ju}^I+igrnfgljd zS>&KBfe=yTWO6iMFt*8<3baIAsv~T?&+5dONRdQSq9+k($Umr^4Kr~xOwU7gP@hVgvHx9alu5IHjx7+ zQ&i#pSX!p7dnV7PHZUoMY!_|HFW?5a7+D1oxoJ%Id^^8%rbn`^D+_8`CIbc&4q-848 zLkI_ky`CtV0A^vYZ{~%jMVK+1jfqT_`^4w(~66Y ziJ^9L11wXA$cB*kx~AspC=*S@8=6}o+b@gZ8Ee9iGv((py$iVk9tM^HcAj4?7=8`l zql-;|;j@jsll?R4*u+e6YD#W=TyA1wUq{#E(pq$6WV!%7R~P=JD|9>Fypg1yg?u8m z0acl4H_)^X-g2tL%(pVxr=>1Kj<7K|V-a7kiVGcahp&bbq;GA{l1#=!@z+_FqaMZ` zrW&nOwXt~Gm*J$F?r2$Wg0w&JPLjCzirX8_T6!OFS>!Ne2kUV}Zm07kcCbcQ{7R1d zVjN?_3)kk5O|kIHwJ<0}=G4x=88O@NBw$v5oF}p_g=XcyC42m%z9h$?bgY zGZrT}8ml|($@w4vXJn^}mp9*3HjgcB9-Lio9iHzTU#;vMj84z=bPpUK9{yK=d)m6p zel$w6b$@B$;)|qLdPc`uJF-Cpx(<;8p_pl)_%QMdGeHX7z1%A{*~&Qp&nU4E^hVqI zVrk)2`y#$y3Qd^H^Wi|L?r7`j#4x@wf$|jo&gogXTc{t^f^TfZFo1DjaMTlxKogP$ z7)*y^qRgG2nVB>OxpKOcUjSpa~4j91YFw^~}P}7%oT?2Sbw@ zf1!ywJdtMM1%sLV`(FYL;62ZT7w=PK;)e73!n2O5PiGsq+tbQ@lGWRzP5H_0=j*fS ziOIN-=rC80oaZs!rA2Ff9kQhf#s0Ele?_sipje&0n;W~G8@ZbwRcy|VG<*z(;CwZZ zrOzWi*XA8&L`%gjKrQxIIZ6+y8Btf4vnrTkzo1FK*=uU{YW{si#-L}CAIbMJKTOgh*< zSoS)(GW~UabXa*-MnhV7SGucwF!?gac7&^&hkjaTqS0pyE~2VUdm;v2(yG0&vTBdT zIIBUv-k7UVI~&sFjTv)<9i-V@XIdR3nJ&bT`il! zO!WG8EN4E3AxU*!$rW7W*mc@~$DWbbljveQSfe|6Bim&v#BAIP*X4p9@*z(8lZU*C z?QXcKK zmvoUCbCwlzn;X|3=_hHdQXDQR9doAci)W>2#xMOuf#btxYs(!?8(u!r$neY1kjs#e zi;$4($nevkz+NJ$P9N3`N3$sbJ~&exa74re79 zK4Q^eMqOuEe#Q7Q0PyC)+3v~J^49*Kcz9xD>=$p5`-Dx>C$dm6PPY{%TR8h888!Y1 z;THBZEi3~>W`jvKT39xe5J}<2nmH7^gl99o+)>tr_Wt1%o|BQO2g)|aoS(z>&a?CN zLfe|EBg@@As=b_a|Ev~9;WhZixFAgpRP~r7bB#YfhCk7M`up!8e|hrbAHH)%TH1i% zEJNK_c2-P1l%2k50EWeeP)s$E?#8wb2Ef$?dp$E3q&ZWU7->N(778HWs@Oeziuu<+ zhBB-VK4i=CUL4irU5$5NG?Yr`#XsE^RW=}$&Samq=jZ!7=ek;CYtz8J&)p@(#@y${ zaryMHbW$Xp63eGVa^SmIHZh=Bo9Hdh3)Vn+Lh$13#LLX+#7-A_-vTbP%2iB0A`r(X?=8TG+SzXN4@^ zOu9!Hi5p9?f6ou9a*6advhlZe`U04s{puKL>uW<&In!4@yJt5|2dADW!c~7Gnf<_TeWsC`_G}o%J(p-$Bl=@Sro||1&dk(u9OPmZx9AR~Q*hnyO z(j9SG#J$R}+DS6oe`U7woLq)g6#peQ#%HqLxKCrn6VdGPT;WlX z<3S3e$wYHL#%d|bV$j~GJ-}=;({bFFFy?_95aLFCiBtZRUJrbS8*Va?GUZ3<;Umgz z^teJMS^K5`&d2U=! zxYu}VHL!|!&pfcY@HGwtZLPm`CIG|dleNkD@DO=e=tWrQWoYnaXvjry&`o66X`o*_ z3fpLe=rA<${qbpJh@awS=kv*mbZc61v^J9*)mvVyP{tRjoog{ZUhPhefHxg+(mEv$W zoVlK2UuXp$k1#(NczdTf=+E&yY)G1Df0u{*uK$Gxu=XvzDXwaac~=)xTrHVj+dsY9 zKD^jCI9uJ?>uT+|?{WP7*FC@^gF{xTsB{})vV}`9iq#bQDv->wP)D*rWG0wM*CAMI zW5P(BC{uo(Af&`DzyocQN_Trh6IyAbp5f_y18O1PKa1lXYc3$E8u{R@dn5e~9zDW5 z*83ogNTNGp)WIHRmf#1E9{uiH@V8I&zt>Rz;}4I1_ZLe|gvx(B)cZjN{M`c@2lnCoWzU%2H8~q zRcF<`?e>2f<89A=+L-yWx1iWt{Jb(LpC6UYjmT$)Zl@*ki2>PozigyeHry*4>XMK4 zUQ4lWjOpn00w%Wvhs0r2+GxpJuRvo7as_e?r7yYS&na?y+U+%Umzf5u&%-)bY zuh!2457Hs;EVLyKx{U$k1|Q;CiC{U&M$Fe6b%pnGbXx-mV?x9A1k0Ne0N|}8^RoO3R}h{K^F7a_s&n8F*D001BWNkl4KR71;;!^vS+h8b_D(n&y(l_DN-UQqx8h-`9@9k-@n)Rk4LD7VDJx*Fi)V~2o~lE)cBFM5ZFL6 zcQ&+SLdlr|P9ViP-I1&I$M5VPYB*>?H`62J6Me^}>1Q>CH#39M@m}e8_ubLT-@1>$ z2e;B2bYlh(+bxaVE{@$TjNZ)--;VdoMtkKWy|BzKo0bxBvbxnD4;kConTLgsBQ#|4(YT z2M`{F%!U%}bxrK`Oc-DyLx;fEHDT!BO*AnXPGQ9y|6G=DAfAg>EnH)qwY-?R>u2*%K>Rs?0oJ=^xAV9H__|nA?ghtZ5&cJGqvROwLHgr+eBvf6Z2QYL=#j z%TNLwg5k8rrE_3pOLa65fk6Zwgkr9Wus0w%8dGy!Ba3+f$=0s^L`DHWFp$XR>XALr z)~-m4B8Px1whtRhO=hzq%xth4dgexOx-KS_=JnRq=d`ZE1geiyG0L(RZof|K3=Sh` zqWc#?$<^!P_HV@1A)qDZSr<`;&x&5c7Eh` zMj{*TkqvdBp(@>4~8;4^NL!%jYy0EN}p z8+X4vQhC4JB0F4qs`4Y{35=(2VylC5Gqe#J+0Y>bTWu^CN@jow_If6Ex>Oc~MAOC! z^h{Yg1XB&nTVZU4Agq8B5Juvb*oQ2|7Iyh02clR!xaILr^ZEZ_rJmhNsUbJP{l=z^Y#r!PfW4!fa-J#Z3l;+Q{q*oRVWhn(=EjCzyDeaO{p z+8~I9;C`8^{&dZ+dxrij2H`7I@2|bU03qFx`d=JlSO-V@uDEhh1d4O zLFS7E-dnkDAG!J!JlKRk;j%sAq(8AY$n<97!$?Vdhr7wiRMSkhu=aJx;NoUUYk%|b z+~m~s_}o%ML(|nca6kILe%%ASy0pvy6EdxZ$(GJRXm)dAwv7(jMiYAT}wFZU_ z!O?ZmDbAq<+<-iepS>X^o#`HL(r49vg# zw{H=TKrTjBfSV84B*M!26_4(sZ)~Xvwo}m({GeXIvbn3xlWoqOH<#V6PXYtjUwD%3 z%mKV@&fMO6Jj{*UPDyTOhGavXGI6`KzfCsKF70lX_B6}e>Tfz4u3Bmoz0D`xO@X#_ zuP0DXkl|X0+i7Cx&1lapV34SMI{|w#_o{)P!|$x(VxyDAKSasAhv4c`sV3Msi*2%!!4oWfCz@|D!vDxy8J+6jnGz%|{ou zcq9g5*tTHOn(_|L{T}{526*TAYZv6Y7Qy|sr=g;Dp51rk?7u@h|A3D(8a?~Alp4j~-p@#G(cSGlaKw~ULo zj1I0|C0=F7R=b+4zE$aWJO1XXCFuNv=WSoiW*V=-1u+;;-^pODy|iw#)){j%-bl0- z^9}m~sjF`|)4?V~LiDgVama@>9zgDOCp0@^C;X|y-sBN4@<%qJmTfF{CiFVulPO>( z2^C{al1o~?Y)mV5mlWFzUp8iMSEuAFld{!G+4>YH1QfAZQ!ihvRTSSWPJBM! zy6<)UUFkJr|a^g)zlX;Ea>BIJN;QN z;;=Jn&YGwp=lsI~ zpYa47`N`|%YwWV_?-md;twy7GNsY7JykeFa12TY>t5ZF)x1A+~*amnNOy>|$* z(JWbw^+W1rH?Z2o?fl#Rozd%;CFnT*7l21uw zqdl_0R#|_Ww6{gp*(mF5k~Y^~x75qq>J`P#F8aKC%4slMi%LSz^d#bJg8Q z_wBqgDy&W_HS_o5$)9Vh8O6os^8hbv9Ss|8JRL-`)I>QMm^$uTSbZ?jXff05v(T)vf}D3p zODEo3RC}IRdfs+NPKHpLe2HTTwx>DlsW5VrsoMNA{BpFZNPy__qinq4j`0qW+y=>bRD?Je6s%eX!X;<%H8gQbg1p}UFvy8?0H7aS$foER?Jyyc-Ql= z-AR#R`#zWcTO3w)!jvk@dwbF2mFwfxwHdMWaOv~;#@*qP{O7>?_wS*DrMu;^{j{jg z#K=#Fi%QS_)s_#XC)@LitKF`qDhG3-tM<2JE{F*?tXO~?bw*2^Fx>*=up4&51K;O_ z>T*QQ29jI&#`DPziusc6XwG1m)#NkEY%*`vvHU@}vO!~zppAR-$?rUUnQNxl|>6uM`*ASeLG62K{$Y)?MSl%Yi+tH5l*=y#5R zX-p4uO-u&YI)%$bJ=Lj>2tKSX^|R!75UpnmGl?qNCJ#VDJ)G^gKUjbBknvEHrw+q? zr$*BNallCA4-lh&`#(%=q?rcF31&%oq(l1lJMzQF&IbBPBvZ_TC$#TX?Ej+Dl$)tI zn3t}M$$u$92Yy-i3RYmausk7~7?e*+WE1_;{ubF_o2;uv+R-R&tG#aeC~c`z^tBu} zR2KVsvsJX|-#+rv(yV2e-n@yGb~WBg+HNNYZ>PmT5MEXKy?hn<{UozJUYQ;1OL5`& zkufGu(M&MOTm$K7U@p)%V}eOmS{NSW9$-@*l){BlY_)OrP%0ZtHdRF>IfwTIr!JH> z7dVBdTYFSGMiy}c{n0G5r^t~3@vn%nlv?QHi!J43e|_)d=t@2?JkePBZsuL^NnKn! z6B=fQtfr`mn2?2FjF<)eV4>Y%r z-@C{g17(dNHzkgrO8F!HxMGaTARW|7Rc&*_%V#og#7U>6&R2~A*LD6~ZpJmf_}O^N zi#*n(H@e+Sb2W-G?TZz;V+Z`HD+#tkLd>uSZq$b~>`j~qpwu{^`aOvwK9mt}%Ak-` zN;ht_!${o8T~5TeR)%R_9y|R#ixP>bqqDiXs=6SrC^5byE~+*;uIY7Le|GXjar#P4 z;a=N^Gja3HWY6bC$>({=ZDX-CJ64|i{5B^}{xk{% zi=VGk<+*#iP{q;mr(K}R`p4o}X|%4kn>X43O8H+w($ zlV&=k!bZQ1V<>Sz4m+VnoX}&=m@Yn|-w`z`#ElBEE%t~pFMN*+w#5;-8pjw2vu^aE z%!N>!4tkhw+2Uf_^^A4Yozf8HkX6~>5FGu! zd$6OqNvgCa{byhI0H2*-cvHFQ7A~n4&gs0s_b*cM51>vkbA}EPXkvkgTrkB-6Jw`~ z3ug*_37jIHZz|oLs&16Q5_0rOOfbPp6N7vVjj^yVbqscfTV?Y(x8*5*BojXhao|;K zWke|AuWFcYAJ8AFp#S|(mMX?VMgSL2`@{eE-M{|NfBWX&zDtUF<;V1R^uHf~9{gaU z4zt$8*=l3J-~1_#VLkUUhlB|gSFXT90n?vo!Nh!KPx>zZ>J@< zlM-M=Bx;urc1SxLr5#PO*7{Ffjf$S8>-M_dmx;azoYfzH;C-)>g-0AldMhgO<-KjP z?narY<91SfE9n9p4uLFs=I;Skj(NAsa?-})0=&prGgUOu=hHwr8kjrio3bHfYb^{H zLbcMy@^#GsfFWdSO$-l0W`RhiPmCkDer-Xos~y6kDfUU`&c&Q(c}zcFG{fviM0aD` zZvlJ)0KBn(a&&n+C>bg#%$li)IctAaX$=Xrrdb zYS;@uq6B!vmt1X!9P*|Nd67rFC|%BkcXULX059>Nwm1+fSjd;oj>84{6ScJ~{e4HP z%W~O`?6dsj@@!{!YkFdAptHTcq_{dOz4CS9hlHq#=#bi&(C*}z!B^3e#K`{Wpn<5s z!Ki@22>+4i!Ck?gnLci7q7J}?M_CWwt^qx*ug_O|o%?8fzqIsA9V|fj&YWy(Mh?iT z7Zi1Q{o&p#!ySr~Rr%J8{CM5joREWhFd{%syP^v$b*t(6U0mZ4XUwo8YRnnkWd{H} z;)b1Y$F7XmnY_tDOAPgZ9^ zB;xlF9~6zQMPP!J41#>m4DY^S|)Y2j;H&@xKV9V zv)`AsOcRVOe)r%0sQ*}#ri-%BLK41vLRHsF<+IKU5~ZCVN0TF8xVySBt?fauI89ut zqu2G(^5=s^+25I#$-e@ubkAQJ12X3ckyO$t>uZ(wwSMkz|1#Kq*V}qnRo3%7HUdkr z`O_2Tw-1uw`m??cUy4%YEwwitjnejdS$C6cq)#U5kd61t7sr(E2j#At5@2AsxI2Hb zyI2$vcvJk!0gbd&!?2+Mw2t~_4tk~>2+39($AeNVbujk2W?Tq`10mD2aU2Ml2_jmn zq5W*!n!-{_n7+O!S`3+=P4muW`ud>g=8xfRRSmxxfiJfXFODv6d%C*vGG5G7#axe- zrLi!HKCYSG{3h(<f4r;Y}yv<1ZxRP!ygN;!Hf5fqWL;joKQhrRxQo$f%W z*-k3svdl@|=yy@!dYa3-D|Og@ZBs;0>9*AwVu7phs;gxIck{XD)t-BA?PaaOb5Z8+ z9F5z2u-93(EAghCG_crCZ#9Y{7QouP2x3p{cpy>aiX8DJ1NpNbrN@o%(GD@@LlJvY zdR>W4j`%_vywjN|a;MbU<34gx&v~4wx`t(mWNU7AZ)@%NXz%jk^hSCilV9I`mVH*- z-F=o_U7enu931X#?{96c&dn{1kIfE`P7I3Qr5C>reVr1X`X)T(O+;!&Ty}P1VR}sZ zLVt_ma7hN-CA<&9zhwBAavWusTebygU%wo$C@!`X$E)WHBcqw|v1CMdX}02WQ@Syw zxY!(M{P4jF(#{ffO1u>VyW02s^~M5I5n09TcKoku?&GHG{!A zd8TlYr*W+Vtbk@P=Z6~%G?@vv_(0dM;TrZhz?NPyFT0{Etf3oz=wTmJitcwC@1K=> zVw}R`L(=o6#{OyP`(FS)^dK;^Y=p0=jvs=Pi^3Aje}o6)S#)gz6GUW!hzt;s2R5Oo z!oBgf*4h{=Eo_A#*b8e*RWl4X;o=|bGa*zPRYQ&n$kmwq-YJX@#jv3|E}A-efBcRO z)eSSJh)ePgx3_v;y|`(psE>-mKc=A69Af#>F_k+9S8@$<#*?b1k1Sb!&!;bhPA(L+b8q2GEvlb)<9PF7?}<&$)4R<<=O z+nkoID}8*I$3D%Ed|4P)Ob>pZ7G3r?uf8v8cozK9#MV;_X7z^$_J8>y1CN*rboo@C z^QEZ@NUd8wN;~SM9rZGCmvpFGI?^kj9Rix8fc=B=%B1qcLOEI7=x?oSekT0%AqxOl z9m9f1;Y z$bDle%whWYpc$r*46AbA{RY5?XV<4UiuR`3lGlMdEipUYsjs}9-zUFl$VxB4sm*&K zHl9;@Z9y$`NF5#g0lJf|I6FStb<(pg969%k6yiQ$hSOCqy0tKOxVd(8xO;Yfe0h0#b$xbueRgqudU1JjdUkkpxO1?xzQ4J; zzqY)yIJY@BJ>4(q{?O7~SW}r=R+?P$E~)t4%i@yclG4|u71_1VLlW953l(RpO1<VXZh&;@c~;$WAa5% zEZxe7g0J&&KARdj**{ni+|_u~ zT7T10bJJWc>u8X**C{8%UBJL;Vo)|C0VD>1xZtNnH)W8QAFsZD5xMa?;;isBAA_<~ z$FjgAOLerPzWGl*XB-boHBp80pe7tJnFk?TX<=yEI2MS+(!$ep$?rm6l`sQb;g&%- z<|_-=w+wGzG}GjfVNPP&ZvcF7epOMB_3@?CSzFj}vES>2g!a<1j*{Xx7T5tMX#b6+ z*iNsT4ymT;ydNc(vXm+zfq^*$HnuA3R` z>q4%ynA^lRswAn7av)7)wN^gzZm!^FAZaz;@~p~hGo4*c2N$vQ&(o}SlP$#_$Yu+z zS#RX5AG*&K)#Z*G6{1D1_xUq%*o!pcO|Ie@_qh=y9^@`(e7h6AoP~JLKn!_O+nq_R zPLvi0Tr!V7QrEP%ws^3ydbqcJdU|+qd2)Guesu#p0LGW6XJ<#pM|+2RTL(Mq2b-%0 z8_Ngl%j+}KgQ6ueEy^M~2EJTU4kE>@haegZt%|8pX( zY)#9y=j3~fUryE)=bLw%GaJ3lt!W9F4zvt2gF=c{8v1Fi4S3KFKH~&`$0Z#ujRTq9 z*0kbctGBASkgP6pfX}<5a?P|`Ij|PC!Jw1Tkh4)49pbK~kp&{Wc}gyO%9uxoZE>>? zJlUjEtZiouHp4bgp!_>WP2#eQF8;W2c_k9%CgR4zUv~af8!qRo{EHKcEW`oHr z9SR>tHBpDNKt#e5SU%T}3Bl8$SQ-@Lr-AeX5@y802n7Ybh@R4(RMGUG+CyuTNH_y~*`-s`nFkgAvcQaB(>6yr7V>jP&xT*mOI$a0Jy=MVJ1~W7;2naC-P8)Wu##ry`q<=HPJ#Q_Hm0rjsO^K$aV9o>E9>-2d4;{5pX>h$U-hOe&9E-p^a&JK@{ z_YU{B4t6*8w^sMpSN7MJc2?$BC&!0dyUSm{D~!)CjL$E9T^jEho%|!ULW>*y1L=Lz z%iE)cFWa-y)d|`9lx%HEx;`b}oc(mT4D`1*#+IAjHN-v3=b5Jx!MS*~N;Az7f#H-Z zs?^G$hNjoShK)NRQqWI3-@R2_ZOOJ~KJU(*Y|Or5Q=c0?OvQtP^|ZY;H3Kw3NxDV} zPSpenr=(#TtW476u>%#mV@AM|Wv>+t5gRejCPwaMr&w5nB z001BWNklzTq|0eEL~JAlZ{rg@~AJNK7V2Ut2dK~3IRyIE;s>Do9Jn8Z>>V3rPvZ$Lpl*0$HB znyDfA2Gj&gC-?&`TQHKTswdPm_SZ(T)L=9njFlFOuTL^jHKJ&1pA;l+Wjz1r>-?5) zlj*|UU7YN${a6+h*pr)`m-OOzTBJDJ`neV zsbe~1*ZOJR-natr^Sl&S+F%TyCYCYpT=B zF`9S7l$)u&;9Dy$Hl#a1d8WAD`H-FQ{Cj*2*r^HP(re%&K?RRtBPOts)95e>%v+3q zw(DCePwrh0dY`V(uF&L((Lp(GQT-jQYqR6S&0WE^)GCHnzcXUg6Fu&UuH)*9eKCi* z_LbPDLoDd52Rsh+a6Jr_A~d&;NbVAkE^lmCRo7o$UH&tMzs`YI*8H)wY?^z9jYo5C znG4$5A4$uwb+^$b*lGcC4i<>S29a1gBrcR_p^dW9!8qxglT{3bC~J-mN}vYAJ=Ap7 zFr+>;U_r^QFe`gK6DEk@VrYi=UPpj7mcLKkNQ>@3L^#^g)mimj#n*4l&oz> z7LCOD1f$@7dWNBJbQB5`i^r!}n3VBp&0hTB7~h@zm-41}Uj|zgqrG>Nq8oAN%|P3A zccZkc@usIq+S4rUZ@cMhllHaC`rDQCcF82&vXNf-_<(#$d^84cN5y7C}=P6d2XlDZp4w!7Ig|XGb@}N|zI)V)XWH5G6inS)j zR1L`iQFsul*;C_0@33^54-ZQ5fLp~<_}O%?KpdO$$S~N+_cw$bt!XZewN>l;mkaaj z-Ieuqxg|+Vi$)H3F_P5Hg!HkXl{Vm4M73UfU6B*)6;XB2*GE^&bmxfr!@p=96?wp+-09o|a z+12$uz)FU%PA`-UpPuX=AMG9;Y#;1y9Bi%bZ>;RCEgh^ckF@ucy(%hu^FEU9o%W+y zu{JALgH{gW`hR02fa3DsB=*#m&9+sxr9O|hw&0`iVV>df;ctrdoJtYYUI$nY7e3~Q zcxMhB79eJvP@|5B*F^PF3#}Kpr$JCnA1#Bk@vcwDEApLLpnJJD-yNKK?p<~gP=4_H8 zJ`c+=QJL^SjkzP6?F}2adb`Qi4VF4BMD=w)M26vyJv`kSA3QHCv3+cAa%S;kP2=43 z+^-8c<|n6p5LQ{XZn-?a_sN;|x}uwB~U3Y54ar4U-wDFuG$l8d|Ll5W{>4-k7N27vMJ+>lbmF$Rb+ zmBJq7qRZ^(v(3KOF-J}Bfz`$RCD~YCf*qTtjknRpTB@U*4J?=-qNzHPp@Zi^DO645 zSAZc@YfTJQ6~O^h?4Typ$FN`r&rIGkh9=GhW*$N0?!$M1~Vr$KD4`Z=2e8d&r z<6zLv0*n3d=QZv(Rl=J(@ALOg@6s5iub)?lBCL#!3+RR=}R zb3ugDD)&|vG*s`0UO(jVOOw?&;(&`$jfv)n05%;$YIQU&rs)m4A_km{MM8{7h!eS> zyB(45>4qX#{D2d-Q-H1JAgkHP94q}EXF|U#sfLTLvBSM*B2(FB4cX;ev(x))%Lf~) zNBi4A5I#G)xHwTJ(r4E{$Kk8f^UIU7^P|(#gOlUEqr)8_6mJ7n@&5Yq-rC}5Yj;CI zZ6Z4$`-w%dHalN~UZ}xHP%+I_v2^&}NMF~`LIoWCJv!m9#9)?pMPY4OVa;2LPlgVu zTL5dYfxI@+^Dxj#!fI7m>g1Ea9-10HPhl_B&D#w;8np#|aj}Yvb=eNksa6~>uTBm2 z2W4%BeVBY!Fd9}cp74GssjT;T;b1~ZXGlhoxz9U(FTpQ7Gd5*? zdU|JR?qGTGWbYt7HZ0dvWzHAVdyG0dpMAC<0p$QhK<%b8OSFS0dUWV*n+N+ z*`YJ3FY{>H9my+wQ#lVEOiN_3mKhc57C?sdO^hUzQ)P$oH3ktb6lb3q20#u>e7( zNIu>#AM2Nm_9;ctJzu{M^#E&0qkXcme%Yi5m>$m#-_DN!|H4&e_*I^mz1^HqoUSbo zwoWC6D)ts+z~8-hv6Mf;gcT1T+DgDdZY03|Fgb#GCnrfUEA55 zS6-6*wuk{2c^QoejeD5THafJz0WNZbi|zIM9AIxKs)No(Vi&mB#c0G8Ue5$~^Yxe0 zneuwi>kqCs_1-J5ZI;vM(qcP#iQR0xS(&BAk_Wuc5>$bCvi>gk(_H>sD0w4=xsl4s zHh!3FqJH?2di;_y6^iZT>D5uxCOy%UL8N+ncn!;7$jNBH$#~EW)8~jBaz)p&4Qtr& zK_~2>Bc`5%{K!ER+ro-$3}a#fSS_hd?^Lw!g8wIX^Qk8qRi$&V6E4q{A-MWaMkm z6P}oqYO&)qt%R;#F$iY01}9C;+L!B5npIJlU6GD)%YecfX*ww$c3}+1GA&Ly)ILYY zx>$!(192LFdCek%MvZ*M`aY#t_T_y4-63!i8gLlB+-NKd#!ytTHwys)z{dwy_`8W~h# z`E<$$UB=MucQhJgfC~^$dM!cYe7)z;2eUros!6g`;}#9w_MUCpBR)5b<5l9x5OP0M$}*sp*KJ%Yc7@bR>>ue^3ADV?$h6k zeluZC6|NO`%24s^W*_22= zBazPm^a7`Fmd9_`rsSK`O4sq(&pUJ1`%B&Np;xUHwDqYGQ=|Ld6;%Vt29uN#m_paao2nxKfFWdyr^W&@{asKZ_B*J+z&sGc%AtG3 znF&abU@jKCdo%Yh{|sQs;Als6YeR0u$GneuPK+KWsMs4VvWKjSa7imzJZHwvB&0E z8}EfZ5|!%o}M0@-;b1lT>Ab27`{^WrOz*p&(Dv} zP7jZdzE;Ki+v~e)OZz)(%}IIX>YQS2PLU=vSDhCB#H37%9iu|!GC1+Zj8YYwP+e;m zfmg-b>YP_aZ@|pwWU7l5DIqev0`D}c@7-_UC4u>l8v6CZJX&;}iqx2udXDW7*K94? zaQX|y>AFm*hX1s;aJ@ZWUGe_v=IZv7R4Ti?xdER0&zg&iswD-cD6s>Fel(qs`TOhR+TTQo{VJZJyTi3_h~-Cb)WKSk(%w>NHz75Aj6e2rl$A z2`Q|r>lSC{%lqRxH~m_@d&dx3XEu$i3)%rc86Eav$6plyjNMqDt3 zsYBp_DHiGo%2OjAlxh#9nyVUfQKVM^L6Jt3mjU()O-8&^OdBil?cC$moYa3t#_mSvVnHFqzl+F81B32ZB=Yc z4i@J`=@27~NbAYrr~OUq^+k6*6}Q{K-Q9m?VD?|UGL(uAKoqT%Rjy0|s^GxB|)8o%9J z__97z5a?^EfwI=b(6#Z-2IjWfIFgEy9fZPzP$+5$;Db*IFhiR_RfTiGWGaDzTFeZJk+2guAYtT;A3M6I;{@qNE74I#DeM0f&Go8-Q@)!2yd<(?rk0)?w+0;+&8p- zkxT#7);c){(rIOP`e5g9e`|kp_4s5*lwT!6hBd<6OEg)z8uTPp^Y_}E_{S#h9R6!8 z_uUhlXnk5}KukqWby{djk_t77EO_AB|JRP*uZXjuN*+DzpV9!ztUX|9oOiu3Y?fs__SP5`@O`}lHB#-Hx>fq~%unxws z-qPKRIewvdHsOgO4@_Zc6YL;VqKdwy2AT_@u)!oAglegSvez(h*EEjTr@S&WGkv5( zR529jn>iYobD(59T@x2W3xU2V14M9zSvkVY_#e9uF(c|M$c#joMrjh_$YxjhNhh--HLntG`zr6wHa}hM-|a6cuf6`G zi_L%cIv|hRnfr9Fiw2aHz-H3j(l`)`@3#x*hX2zuKO$cM_ELf6VWl$!K(Dg-EZ+oV z@QV|JHJ%Q4q9*x~@UDq2_iV_o7-sP$f|HYl*)sUiK=iWh;Uy6 z)3tD9RYM+xYzHROw6IhSM2=Uusj3k}o9KsPMo`(her6kMq0?t(d^R|>X5lkHOM=klF`YbeSOx0J4o?W#+mpB`&zP25A zg}>DMq2ATt>W%4oEO9mz+s=hnQ`JX3&=b#y&CbYTONiJJ(eH#DbVm2_jU|rgax2|; zR?uEM<9>T&1JkgEZdgG#%r)2Twnz0kVA{B-CLa1F8Pem3v_~6Mr5KA~L?W z>{fQHdXKOEL>z^BF2ennKt6M-o^$G6?H}zTtlW|lvKvd-q~*3h0GtvI}4m z{^|pMdC=ETySTU@lRaPGe#7wn_08=CbB8h1Hr!s?zA-a*y30Q1vIJZE-r)dj?ZW3snrW)es>lL4wl#EseALqUPTlywl@q{bSsEzQ?^MX@=58>cg{FWIR2bS7iXy8TyTdV#U<;b273Q-J zRYPkpIZD`8FCOc;?Qc6P&AV!>co^!IElr;cx2L#M1C@1Sl#D6LP z_j(=+A)LvyG|=6@tPl85q8U zIfDSA>qEt=B~=-zRGrzrgfTXxkON%u~zP} z2HlOM9OgT(cxvv(BfCvL)LAHAWg+;9ru*?Gi+)-?)+)m!^;I9kwLs%)BB&9gy5y!e z?W{dV)tjMcPE)kW`}w4+`X8d8aYwUa?k z7RZU3fuZ8I{jH6YUABPBKG_9s!onk<4g=h*3*pTTa2W>buzWTBc44}```Ji*1#Gs? zg~ylTv*GxXU0mM2O$is9*A4ehe~0WRE;Xdz#0|590!0?Xv zOi-4ZU!7fgd9*KJvA1Ssx>}mT^}g@10nJ;hG#Pv>(0oU?RP076F}xs&7#(vL@3^Go z-?M1>5x|`l^#K~_JiJGOxs#s`Aqz?xQR>t8_j1?WU-W_Bt7m&p8shP|4(4R8}S zl@pB2DPRjbbu(*KL-Tj4DKJDF#1#1+>L{lK+pC*XVOH*Nw2g|18w~9XMLK~koS_!R zKR^nx=&Rb|tNyl!k*?>Nq1V~rtHF+ujI=OaM9>dfA&Le8+FHxOE>}76x6^|c+jJ>+ z`RPvhbT0w`e0eC3fc#UN$giZ=r(*_yK=~Hx>1!$81g2u{IsjMTk$>g^&(l}o(OXuY zym>wVbPIzmwe(0&+4i(VzR36nV1|s(Y>SJ=DeAv9CRB(OT0zg?d$21Q=^zKNt(qxC zT@GL+L(5OvasXQ(KkAsggL!FVke@YhipGAr_;Qj@0p3&ZJ(#G4y1TjiBLH{S40a|( zG`Sm$N8*+uQNt0KyTOpB(Qsj{`)Zi^jJtL|QelP)qkCx1xxwe0pz{upR#Q^9pxufE6(SVJ zZQ)aPnqzia)kxJ5JIx}5Y7-7J?Y0bb?WosLIN`f}N_mgd~zAcUFSDs)0MH!RzJ(>>nSp@kmt9#cd+a(0V*Oa%_ zRkYVtv^Q0DHWW5Dmb5mPw6#}t4YUk1H<|RQskP!%*~OleFPFR>Y{`yUyR*aP`MF0Y zN6#{;k!ac5l$L6z{!aWXaY1xzDzBDpvfeoUQTwu(`sRrP7R@dX1Hei7mCXq~s6${@(!HQdkk7VU>-g zCL)|e49P%2z|l&H-c^pF=+Al%>ImR~^g)xLWN52u_I6LQRW$`BIyEygV3=w_Qa3gI zPBjT)mIOr*6|^Z}gd^C(8G>?y0V;M^s1+4v!qE<36YOWhKs`bw*(V=I^C3Fh7-HU{KYka$ZCM#Do~DE0*sUbeNG$PY zF!*^a;;uL7pqR4gqu*=`?WRCid~|21@L4D5f+J*rpgc;|xNr56w);sseD0fFPP4J6 zCB%ms+v_T_q=7Q$p>q&pv|w%KT~{VIg^qrnwVnFo;wt*871OWpJH|;Kmtm zgy_vUL;FbJ6+eR&KjRTM{XC=3bQi5z7wtJ$of(SeEJeG(RIvrCK5ef#W3SbM1$E-V zgJg{?Lxnyv9B82(b(`_<6ceRJtZGAH&M|{CSkgL>-#S*+&1W+Jeier)_!)-JP5^cX zfZ>}9Il)6-guh`}-b({B=k?9S-R*_wRx}k}eCgF7@^85EZ5lKQDf)=nHc80QbEEqD zl^HnCKs@t2VnX~POEYW2K3K+mL{+HULB>2{A=h znz2%<)qh{D`+g3o(u-6Y#wg;9ba;Dve{foS18`ktp%=uW($+uD%-sa*NB{sJ07*na zR4Lxvp_JfVfb%HAd)7OLlU0qJAxNT2QR-f;L&rlnO(8z_C;)%3TBFp@E~pv2HN5w-(OmJ=hftod^%FvZH+gnFfB) z4*INBRgm>#}g8keliJTsT>nJcRUJvZF7*E62nVEr$m_ZK6v+^SL@ zH}|&LlG|m;{}i@>WW2Wz@+vJ)nf_`5AroXcYXzfFeNv^3Uk=ti)<4450DE9U)I+5%+^(H$6ey zap+MuttMOWN`S$vyXLGjbj|@h?*OhvDzGx}kNqK!bNP?`VRvQrCk3RNTDzMnTXCa3 zKMP0q&^U=UnKV_az97Utc8kBFMFku6qw9}Zh*Q`dVP1NECRB&v~+WFxWB$RQPbO<-89uSaI~{?!ea?|ECH7#*kud%c_RM4 z==A8~{N(aVbbWIHw5f8UNGcp{r)ma}qE!G=H2Eh0%N-{m0D$e)&7HtVYh^>^2biC-ZYB)jqpXkm ztc6z8!z$|Il?`lEjcip6>=borY6cNtQ;*LYxc~Lh@1tf6%&6ELIqU9vl9nK8tG^%V z0Z37^!w+-Al8xC%=JLz#hM2R?o0|mqSYJKX{{_G-`NQ~Y7zWxO`Cz>D1hmR>k_N!j zcyluSW-2a^#Sx3x9YwhZ#aXx0gO6w5n&)qiqauAG6*TcRLs%8I61x4(C_9&HL!r*+ORh43_=$=R5!a zFF1m`t(AIcI^rhx$9CVxktj)%$90kIWwrfXv(t42Nz&@H;;XwCZLsDDnL{fMTPv)6 z!JL=cExBo~`{)g#luC8JoAlItEI@N(jMsy7M;$=T)~a-0{Uu*Rx}RZ|i9#C-)TmpwsE7l(I?f?MvwI_ovuY7}jEQQ5KhKu<5f1ynrKENwWqAt$x4 zG_S7I*e}mIEHyDNJ1V=esHHNywjjAED>f%Lv9KVyq$sttD7CaSqoOLern|n6KDoHh zJCHuz9|{Ch$&t^?JeQx$%awn6$JUu_$ z-`|MuuP?67GD1^lyM}f*HjX*W>zN*g3jDm|(enyaIjqZP~V3dlq ze&{gxG-0VUX!b+9HT?LP|A!SA*h1$;CVN4U9exQ#1fNoZcanuuHrB0-=u__;5omx1 zbn0L$yb@q;Oi}@~!8UScrHzWIBiPajhOkjJrKlqS|5U(A#vG??WUrtVr*0ArGVxZ@ z^-awa-($n@ZK5#!Z2*~ay2PJbO zlBIDepd|oyOD~7plD*Aa&gzrgHt>HlC*ikCu^h&7B+C<_zd0z$og0n;x98WZ{q~Ad zdSS7kvqOBa`6y&bCa~mRw{&B`Tz*vMn(< zG1B`>S!QKJVQWWaZ)f&2rR zmxm^~%ggMgrC>Mz+Vt|(v6=S5=E}6!_FCddBkZFjDk*|ur zyNaHTqBdSxpP*u7140;oue6pJ|1>`#nH?67_diVZNyhu{CkGzpM;-uD#|&`qVXwZ0 z<~$$mJipmZ{{I2+ztnBH*|6N60T_>ufBO6e@5f`N?0SE+t!bjDV!yRhxG?>6{)^`M zM*zQ`arY-j$WVac?5TmXRx-3f;t8ro-kJn=7{=t2mNOLX2E%&5tpT}`qP`~#=LA9h z*Z=W9$Vw*mhG+yt*GCf_Z|+!z^GqOnOia$WqipZxxp}_};0b@rzIfZ^+Q8?jn1|sA z$#D2VF=fI-yUQ8A5Na~-qr2b(0C>SevmdYA>t}LP=k!qTAZ?+Ft10&tcK1y#;(Di> zLhFYXr|nR~<9OpOXV|Qz;v5A=_tCv9C$D>JFFHboF-lc>KMXpE}SPe9;v{J7!Q=(Jg6C}vA9c;o57O(rh)k(3b_5*O=`IH#cnA*8g#4dKTJnX~&j5V&Bzs(+nctw~T=}-1den+&wP}jjR#itf zHaplgB*58w$1013D`?KF$VkksFYidj22`5*mE_iRRrh7b=12L(<|P#rCKvZK^v#UT zGd6cbqLUl(rT76j4&4G0WuVT#$N(ROm5j*!rgHJ>nguyPC`LOY^cD>uc%rbIWThxv`le4Sn0w^vi?22*2RG$ef|N?wa(f zg1D?>F7u4XSs0(`Zmg{`{?u*!amGTi+eD#T_wW4?Zf}RhKTsn@xdl-ajoO3L8^Ti? zz2p2furZdD8cJXVB_yBVNz=4)gUbO7RA6I3oTO@M14xuioWWlKY_D$a0J6X<8In|t z9l-!2+7WDlS2jd`(!?t05>yOr)J-W6OBa}xD;(`A-%ewd48u$iPvZmkb0hbY{nEK% zKvXa{B3Yi0uFnB<3(o2z7l?HJYBR|H$qVJjO86#k__kGIN{*R;*i*jef8fi>mA_Yq zf9C<}>eb$2SNlkA8MnQU+tmk%D1K?&^W$&*BH{>^XUQ;gcPN?)LEC9z5~AXXN=Clg zB%lI+(xk$%?l7DO9FJ2q#wi|~x%b}*dE?Og=deeUTD}H(lK05PW+H^N~i!Fqe zLz2`xJTy2+8Xd$X_=hIP`&Kt`0sgMe;k<;f6QV1KF`PswtdP})Y{2_T#{2}cRd>xL zJ8-+{$9nV6JCR1x9LxQ9(+L;Ym>ayq0k+_+zu;pq=b@jW|7j4fK0^Xe62WwPSh1N> zqRtO9cCbEc=qLeJVx~A|1D&+f$TL+Mr9eaBpfDxG7bUA$6{6!iBbIf}tWI1w$ab`E zL~?sB5*)r!nosw+d?tgtw8B|j-rHC|V#$jzkA1q!eZ%ni+40qt=r;i@l}R_JX4#Gf zXP%9xZq)+kicAG;4}`joN)PY|j`a*>#cl27 zow>1viJ>V~xwVC+TzjCDJ?I!TxA^48J68zrC7WUn*wMpZ7OU z*Xf6=v-d}Ue=V@85ip)l*|O^c+4aHQ(e~!-&}e&OOJ#9pOjMkANbZ-!+2Ntg_~eYB zjN0_NnzY*Nh>X)+_9g$IHNSq9zS0uu-KzPwaSNppbA>kD?;6n1lhc#m?V{y>GsM9g z|3RnTC%(!*u{}81SKHcG58F)pQbG4fzpkIs;vg_(~h>}LT> zcFK|o*{?#*>p53;cLc;l9{t*Q@ORHna`f&lwY3aom2kTTwi-Gvw^p9c-V$4W^&`iO zXCeEBwc>7K?E%MO-s=WCdRLWH;6LaE=-IeJfMpd84wPRS9Or6EAgY_uz!+zJyt54j z`%b4kx75x6gZroxY(z*mb1aBX-P~c&Fkh*ZzsrSQ>5e}ejFt|CN`@lt`hr>M_(7`n zu&2(bpW#Z7$+Evb-CJkbTYJzEG7w^RS7UcyYcFkfdZ-}*q^nlvhboG=m?&;{7bIGo zXIQLJz|)A&b9SI{7sy$%2{YJm*%>y8Q)w}M-)j%K%0ynLnXQKEcVbn#UEo8`n# zE53#uWN4n=hdB~x0uP!YLk9?;NX>VB)@oxUNWV3t!BVvqqq^**U4>HXBB;gbs0^6K zoRX`Bq^d()X_2~ntTO44T-pl?h%j>9X0V^0?=LP+&xFTs#O9MdfPQ$yW*o5?Z|sgE zCi8%?bG*wDogQ7vavSrS8Vj3SOFH6%5{gsH%QC7nqq14s zJCDx~H@_OT?;mbNR~MDh;Wy=f(lU*ZggI%5a z-_M~{r;*An`XAa1zF(Uh{c&IQ%NIi4May2j@h%qsgyz}!#~RqwJB9_B*-^lzT>*&& z);=}%!6AkO4{bSsm5qtY0B(Ujmb?PHLQo!BRzxKuiW-8VZf>Jy`eOwK1WEEsk~0+L z0!6vO0q9cUXlEG8T?6X~vGmr#+kz~jFr=4Fx@3Dn?%pgH5-^s5(HFSQ0+F$R5sF+1 z^!W1)uz(=}F0t&}gYDbB4Il<-YyN(+hu2ZRSzobTUAkIcP?r?PnH!SHjY0nkhTm%M za(jDcN&&ZLaHFB~aBLhG+!JY)pp9jFkr%K!C4Q+0h;V>|hyp}!8U>Lv5c_zSk#7nP>roRwuy!{2W=%>Hvt4sILY;@DxFS3!=*+10V zOIj$>THsv=4jg6Avz~p88AP28G6-TM>%$M9>rgG)V-7!N0FC|GZ!iYelQo zBbD-vKQ1_GwqYSnR;rmM@6$jwd}7(0SyBz$Cqv1h0_>Hq=31rcu}>670_Q3or6l!()wrh5l69(INK8>sDhHi=Kqzbs&I$C7EXt}b zzy(#}f(z41>NDyp(`xz}29EiMxyiYe+0|L`*@4vH>+1`#Gm8cQ3`PbS5b%ly0Q*ilk{`{q;yhW#g&5T$zCNAg{Vl@~zdk zm@E0QO`cBlEsCA_bptG5@@I!xT}}Ma-kaIstDd@ptfZBopk)s)rkBrIK)|N6o3pVQ zcYaVN;{JMk{Vg=^$gXxd$hso08L5QbRK!?;Y|fS}1=V zY%GFetEj&;IlHy7mE<4sCxU+INSJsiIRPVX+#cIM^zsJ*XJkV@2!feUkU_Hoq zBfzkqsy!Bmk=8kgtL-H1&X2A3SLN2X737CvlDLp0?g_mqCEXU{S8dg&%s#D9z{76v zlUS48NT7cn$0&E96}xQJFES8k(FV*Y!&zlJT{>I+!!TRFX;oiY9pU>Q8uC8xac*L#fMy{E}?&5Q~jwNXyQSr3&& z?XTC}Pp{I+k>Q0HpRvr?hi{JX(u2*X{S7I1Ri5ARCrea$Wd@!{0pmr)1;XI34`o+} z&m!LS?)u@{)Y@3jKviL7q;D+^KZu14YW#b90QDJw>&tHf`2ImcF~Q-~jlzv>BaCcH z30_nP604wFY3rYj^Qa{G$C=xC>kt5cETDfjCaIdfwa~x}3_^w*k*yUCDC*{K2FP}5 zz%c9rK@yaWC}4ye448Nwzz8ZF?I^Fnt}v9B7TyVpbl1T6>JYrNtZgACo6Yqy0bBYt z9B1o!Zxis5I{*&0zs@CkI%Qq+*0_V6JFPYTMuz1W^qjlfMQF%FWcX`rwDd~^FF1%@ zU2!i@pL)wc`4jZ=0rVa%O?Iah>~!{TxA$%|be*j)KA*GX=^VfNgMj1z^+9D!N@_y# z!t9((CQJ5>bW^kPgy2G*d^~NO+(2kgsC6XCJ2@nl_{os?$z)_~yd*X&!zcFSOh^P7 z`|4n-Z3BI@&?AF`3)AyOG3kF|sq;AYMKTq4(d5lavz_xb?snJh_BGy$GN1F)V?-dg zBPd*5mYrMv6UX?jaE-4cUO<@)dE@M?1e zwN^`|FYxcCiJ)PedWD%{q0#3)YxQMEcsouhS09q5j&Fwgty-q>a7Aov{t=;!j!J0` z%UGP5=x=Mw36F2G3_ZZ-6)M^$c*HPwcv9)@?N<^1wOD?Ac^g5mZqB(()^Ph+U-J-S zbLaN{>Op$jjPdyK^}|x;T%eF>JOHShM}W`l<<_y zhJcI%d;x=$UlyMi>>W{>Rb7SfPqz!L$gG|19Tgt%F3$uvx4`zlud}l%uU00L-2t4c zp9Wz0b8~Zh`6PRq?Hefdb`!CeUPNr*^*Y*qIovuNY`t2adVaIH{Ml6YTicgkjA0T! z@VpfnU~&c=e|F_n`sh)(d%i(m&Wh4BHpLmS2+O09H13P)Ed=*;^~<1B?y;!+^@4 zqKvA$no{riiR?T)_nrtaOL(I?@XVeoGPnDG;Uul9#a$`fPaCPzoc zhete&34U;bld@%gYIdljx0|{ZRn02dCpsZ83irK^t1j{2CZse{_gfF$glm zSCRaJE$w!X_WPRJJ4o{oewo*5u=#|SVUvf!Rswq7Uym7$-%TJd`|7m$ z7%%3Ko@?#Jm1OCJ*K?oKUN-iwm~dZYeV2p2YjV3Sz};q}mWirU<_aq|pk502AYA`2 z(rDQgK5D7lM^f!}f}W=$_Ix!D5==^sJ`@{%nsCut1jPD!QzS?#s_oUSAm7oQDTFZodLp{>aIRw|AHOyS%BP>7n+~wWW>o zOOf>X{^?b6|8T?FW{z}>H`n&2B56a4PW@`GbGjjH`=a#s$Tj1r4os9&P*Ag9<_W1} z+pZuvI3Xc2B`G|$6%|(Gm%6sFAUfm-c&w8>-W%8Y`r;%#Drsh9{uQ7%|H}+~b0@xj ze0i$P%VRH&$*vFHx?;d3Z*_bjC;4H2^~W@}A1@rgerf*bT?G2=9YDf*uqEAHf8O1A zJ=~F<9q{HyyK1YhidN$k z7;I=ug<%PbhQI)65*+@xcZ*RJfIus$Ok${54})c6<18%#`eJ zeyhE|Tm7W`omUa}a(&@ydH$@idM+$zGBoIsvm@hfzpl@&4)q=HEWZ4LKK0wDpxcA3 zfxMFSrmmgN{*Bh2?bhBq?)sDHYg*dxq4(_zRCdng4GuVw-6CT0BEO^s`$V~FVJRw> zlT*`C_P!oatgD(;USf8bTOj7|S_!V8eGMBD4xI0wE1T`N^zP39;d-BVvRO^bm;{3UX04PhxSI4 z=}CkE$5(5?PbUrhk7l&m5>0oWrnBs%Rjv2FQ0IqbJa`JDUa0qR0HZcXQZF<6m~QYc zSz93q3J+H>id7&Ks=F3}o%6xY6>zUwO}`fHpk9NBDTBBc-?Za><}sVeXYTB-t#jxr z+2N_>7XDWb6%88BIYDs@7Wd`~FsA$l;5W+T^&7|d^yHYiy)!?t(BC>bGcqUO^B*KP zlBe6N%QGgISK=HwuM;#1^WH`$vmA;t-=nq>@f8+mLyTDuk=3b;%@hjDp!ax1>NI6Y>rEu@B}Zm+VX!0q=Z^xn!jRpVo0GXrh1 zo5MGM34m=YU@~(O#>;YL=UnNb{45EG-~by$fcN(o=C@zGTR=$%_z7v zcy+46KXRjS@LbLlcmbgM)v)sN>I$!e@FqCB!sMPf?*ITG07*naRIR8G%R*~UH>j1t zcc2Jkat6{Z#)Q)78X9OoZ~>u-U-ww@C6=5HO;j>Yws5jj11#^}0Q|Lu2BQE!WU#r1 z2F?wJ0iX*7vO-1B?jn|ona`vlD@kUrlTPDa(7L3 z$A1xW9*%(F{|$lCpB;^!fxL^om-F5G6BciAW}&fG)KGaxNektezh$+cd_vZxA#6hKl3sq`e_jzK*-+iek#%i`w#7o%9ew@gE(4b!Oql%>19sz}G{Wm($HB8E)fW95ym3yhIc`&1JvPXVOD+ zIml=<&Gw{@`cRC&ZFYZRr`=rhcgU{a~X3t}iXVFRHmhrfj7ake-Qut>FVAjnziEJfVm@Er ziEnQ1t}ZXncua0*ZC_1p(|FJ1_S*KTKrlW%(PJBPx3HG^HCx`t_ z%>}U~yUcy*(;J3?-}hDaw7Ip}Tu~&uIRNww0+zfvWIZp9EG0*tu@+z7VnL2}p7u9i z4!7ja@|(a42guI+>lg-tt%1=IFr(N6MoR9gWM@$(U~Y6Z<$y?i{yE1-f}cCiUS3^U zYng>Q`#AugKw!VYQ1)u(`B)kahSC2H?5~H-v~rIyvZ=NUjI^+E1fgtI%!tayIKbx4 z^v&(bTG1%h+`$EENmd5fl#cQ&Tst*07YNE;-GTzM@YJ++hhZJSmhKwXmI`{dU^72G zl8+AFLks5!MZN*pPnYPgM|6f+`RkK?w23y##&$a9HCgE=I}5=2{d)gJ#C|+wz6ja! zysN!u5$FDx!Cjs0X{zh0s~GHP?5V9Bs4U$Y>RBG{VN4B11^5+4Mak~?Z{p40Meix= z@su@LS-nu*!ss5@XzAH#?qT%|%B^`=f9Zd<0LbDHPE1!sM0XZM!MgFPf9tYPTBd5vJHVImpep?jjR=)>5_AfqK801z)qNMEpwkF;?1Fg?=?9LO zg!dQ)cIgK8=!FazMogH+(Ge*ts0=1HkAciuMx_qhCXduLP7d_8RaZ2YmyY*#%?=E- zm(~qdb!3L6=Ue$pf;w05X~o|0+{3-wyQ}Nl%eNN#4axG)w_;!_zLh@RiN#m*6ALvt zjm^caQ=|04@bo3Km=otJfqTuRSJMhHE$KV+BV2ZqOPY6lLJrR7#IbS>k?5m^o0y(H zIy^{<&MdSGKk=$wcQ0xTPn#MVJUQarUY?xnbNC#_`4O))Ijb(GQ6v;e9>u_&`SD(G zDyS;Xk)8qvOn|I+%zQlDem!L~N;6LuM_^Qa1;8W#!1{6?rHQSYnYDsZgt3j6CYq!K z=;oZjNSv|(PEpSdhPGF?aD-X-XcOJ#^dp)U-bzsqqo^CGXXCAd_tdd=09yv=lc^eL zZykJqzKs(M<)w=cHnIybvUdhq5)|}pHO-TJ{T8~KZ&|AnKI3|Cle;>zINmok(9zdg zJ2%{U%3OL8ah^_Cujjij=R5%6k`wXSZn&@S*7N{Set%+a{W1;b7>Ip;7O|IGTIiL{ zjPAjW)}GDQUS@mW9dq?r$o?znl^*Xr2|0JnHBM_+dQe1hO~dBF0sBliFu9qSSWpz7 z=A((X{b15n*Un7zXNDL2S+lJ#1t%Wak2MV7gEu}V6Z!O2Ck z(vVmA7-=ctHV-H2iIt3`+?Kko5L9Q7iVIk!8l2KjfF{cgN++oHSSby7Ypp~WpCy=d z8oob@G|n>mSPuV=PKL}=G-fcWBbI9Ux*wN`YSZY?&BpJdq3?Y@>ej4{Vse#_SxU9AL^{=Dv^J7(6@&3jX z5S8@S5FP(_cE6H(i85uD{r!Lc^A$2->ksj0L+z3CRodl zjD;uO+Qa-{u%c!SI&^P#<429*#pR`ix^7xzytA&g;*&+SU#b%RmDg3dDj6w+hy`b%volw5)>-?fi6! zE^w5eE-}K`f%t^~I3o!n>ITj+ybn=`}voAj%_t!EJsJ^6gfeiATc7d+YJuIzH} z^>S}vqSx2Kaj3o;a1jIo=zcst{X#|bbjo@<>rQwzYE}2SG^`@ox?mEVX3FE z*zcss|Fk@EJBhSY;&57OcbaB)HxPHf-mq6hTu;MoM4H@Gkt9W!<08ssU;I;V>Y#=D_dBW$gsuNh1HhajRzus9IatNci>Hr>efJG?%ZQN3MngpH2snM;J z%5{IJ(tp=z45=_h6zto9Z3BeZ0Q85}xz-j(MzO4&#Ia@T)}oywhFo5u35Qv%%e34Lb9Y53$pY$8G;oxHtWENmIr)H{!+T!ed=UnG~o?rRu`_Kl78{8C5^Iti+Oi0+-F zW%tAvuFp=L^0{aHz0R7N?UnWOvoo3Od3J7gqQ6ykdmuTI=j$9ZUPat<*2+?Q{qs5J zYY_1<^YLI)CSV`c6?P`a%1#+T#?R0AgMY08e-+I=9{|S~poM0yJ~5Xb8FUG2MYcLt zXJ_3@PT~es%O(v6F-`pV7^s?g9Xl7*Ne?p?n>_hZy)c@a)ivRw%45YiS4n76p zoMz?ZrDH`>GO<-PxA>rCs{&MGTX`ex215tx6NpO2P9RGcu$7IPIpl8&G`O`R*vd`Q zDnQT19d7LkMLR>Sh^nUg->V1c*?4Oc+%y62=m0%3&A{5zkQk(AlQ%{A07R5dfY1~=Mzx7+(RT6#~$N1pc9q{pm3SrY;T z5|-?8?|NgYJvlu(FtlrAge^Q}orxHyLe`mRfw|k(HD6lZo#-3&@(9paD{~5W<-PG} z-@vH=V8x9L43t+?B;l#e3Hfi=`Hi{7>GqyK&awgkTuFv9lCUQw&a8OythdpWyY@*i}X8YANX}$A#0&*3vvWQceMky7V zeO{x27qBXGSmjon@>sCZVLF=aV=#zRX4ABz!T;zo{y2+KrCX~FAU@^ms+Jg9jS<2b zZsq3z?YBYgm;OzM)Kacp-a0;I8JDzyPiNZ~9=TPWc-9I$YJ{Hkw_YuD>y+`9o+sJM z!^7R<)5D8Q`XYN2-(4KyOww%k)|L=;-saR45X5HdH^KbHC*n0$udODTm%xcelUs49Hx*B~$a6 zFRSCs*r4svZrRC>^k56vOaIOi<(I?QTaW#t)J86Ll{?5ha#kLhOOHErz&w4tW8B|q zoSOL%+t(eDe|`(>rSHUdW=h(5X*nK9d;PzuhZvFrwF!D3A<&PCaV8Gl+SUo?j;Tne zKm(i|09u664^XnQ2}Knl4-EkpS3bIEJ5_TRxo(aMMJxZuJ3BRm3k2(?VHK!ProymJ zfSw6ytB&~iKmX~kOZL_w(6n$Au!WB%!5`%qPN4Z~5~%>)5)*7->!$+*f>Trw(M~=W z0*37R;054I0{5PWqiyN&&a03uyV`rb+})cW>&q){Ow8=e%pWbNT&!rkV{ClwoPQD? z{tSNnofi@3@q#y6RXJT+$LJZ_Z0*@<@7-wW<@NPTm267{EQ(yWe9}~p>TEosB3tsrE|W#vWLTBKRn#C)`5QKTzJNk=XXCn zJ$1IVFV4)zVVuv7f%r(7Og7!sCp`Qk-PkuOWrW2+p6zx#Zh~eo?PWOSX?R*_cUkX# z))a74W+UG0IIi{BNkI&F>kK-8o;%#{vQWF(#FJX;eU8PN9cUV*w1icyH2TA`#zSpNoWh zZ0_pp;#k*kLt#yRTxQ;v^um~|!l8$f+RE%2ADq*wSN==N zSTQazJ~EZD&AGV*cvoMI<9>Q>?#0*Q=lj~k9DZ2+Wqc1mpyno|y;*?|F-L)y%#aBuY@Dg0?Z47pvZcM*k{>)|(Fk#!iEKWIW zE`K`R`DSJQSIh6yPZm&s()H^A%vqJPmfryU$Xa>nuWhv>uDBOIrjD|GDw=XCfHTs~ zt>I@_QFgvR$zA_H)x30Yei}HOmW3^z5TtAE4o62C*``@Ke=)P6LQ%FV=34))OjI&* zR6|nKEgV6X_G$=Mn1zEn!WD{fg`mAO37`Mhca|Tu+%*WUaHPL3i3-KpgDo6DmaZ`L z-~Z2lr)lDRb;%yu*7jh82OJw<<^09o*H4q=s)40xVuK89LyhdP@AVVBgYHG_XA$Q? zp8WXM_sVV#Ue9=!tktcd-p1%8dktf6ETK6ieWR_L*FAKxy!d*(C#R!+U0nbCksNNz zuJ^BwwhE(TM+>VseZw2A-CJ$F!0orK=VoR4aeqt7{}vYeFVC^O@fLAqXZwQb$>zAU z!qnUu`rOgU@yYe={^ccGBxH&J^zH~x*ZBO=B}QA%TuT={FR$)k52(E_FE3Azz+W<% zY-((LeQkZZcWAN|I2y@bWfF;mwX*g{-SdwC-p#aL|6)1ru0QK#H0GwyO~PK4llN0C zuL^LI?auR#a9*N8e}G}C=HJeW?Zwsh7byr%xzj@_hU>32j!>LKs|+C&GWFlDIe_P_ z6lZWM4fb$$lBFoxtj^&53Q2Xx!>q%`aFqz#AgI?FevW#t(G4cGsNu?#ktM3=dJX$t zGe0^pY1h5%BDnQ7qWkVk-%UjKNl>#esD^QPoEUHJ z?yYF-uV@}??40Wv-k6wUEv|4^*10Qd?4=df;u2?RnX|OQSy`Rx9Il8ftw^i#$5SWl z5@eZkoi0wS7~Ee)`3BZ>A{h46SSO1@Af&ToZhDPjG)Zsvg%vW;rRiF zy|A1Xn39)N>ETq@1}?#&C!11~>(O z-A)1T2TwBE$}CR5*- z;`+>bw9Qdq^Lbp4BzdIQK4D~V^i?J^Qh-+%m&JI5>ikb-M~I~l9D~z9M90LqfK6!{ zxFCIEI@0-zscnc6iK1$$`41(cl93Y#QHEi{e-McS&Gd1~TKG<2Qer~q9uO%qE2ThJirP;2*i_YfaAfu@Cb*T6=Y zQ0x^=W4wbNulJr$*TM^sN1~ksbz?@eXdC z8My~@lg~n~>|*yx$obmt0XF}LJPX}0Wq@1C(fm{ph^o;W%&b0icT3s2AP?gZD@yXWWZv$MDIyCXc^ z6bNP+yuOLG)}Fb#wwa2GCh^@@mz=k~|JC`$qx7+_x$Rzj|80Z={RH5R zD8#tC-i(*gn5+Iqi0M^{^+6`)DhF{hnDj82b()DB3@}dC_$MK8m|RET=ZN4$ z@PGJz_qkRLSEuPXXdb$RPvzJbUeFrud|Ji+?U#PdN8UADkBUu~f+b28%e8o%n9vfO zI#$y(Gd#d!?+EvH4;VXw{awHr=WzE@aBzKkczt?!dAxsiz!U8QA%h~|;mCjG3ir6e zeU9*evpzN7n%h*KQWaw3*?{tsrH-$<=lWRM7o=6r4$(JO)|cm37G{=~=TqFb+W#;%JdB*jzBy0kCU(7~Gi;qIORO46ls&8dG&SA13f#9V80*OE)#E;h3o zzi2N#Qq~95iBs{ZZ*!(kxLCw|e)Alc zA29FvjHRxoC!ySk{2O{7MVuGj%4A{$e{)tQWJ!;HY5u_9c@c7C=UllZ#jgPTmOTE} zIRk4jVETRJtxMP|66TV0hc4ZrKQfn}7f1I~1L}>OuA_TJvE9O$uG_f2dXJ<-;fWR8 zxT>h6FdaVLtRbTxv^-6XhTvih8k{62r?s;1Vk=2HC=D7yq<7gMcmgjp6urM zS;T89%Jd|XXUl7Ny+a$#otv%QK>ys4wf zJNW-m_8w4CCTO~F@9yl`-T&^LJagyvOm|OrPj{*0y*al1(kFDzRFclq`K!T)ajWG_x_&ulwTJZn&ZQpp7(j|;)svqCfw53 zpTk(;wTjq|dH<63iW-TGuapXuVjjq20s^*N!UsiMsemovFhwjn6ik|s0ZBj$XkO&Ycbd2u^Xklx1T?Z-z-cT-HkZ{1X&V)(C8PL(<^3`!QH z?k^y3^P$NzZM;vgGO;gXe9EYLA$ea)pRdT@7$xq@=s&%l*)oz13>u!4w#Z$X+T@Ww z%F291@<-@?kcKgO5$I`)dg_9Kvufb1?z2{QD6@LX{9Z@ZY=nRB!#H(Dk2I-MnuJwn z5EnwqOL7aI-nOeKg4LB)*?eK*Vs7he{_v^Q1EE}!9}yb}3B=lE+4-m5_w~Q{AnLm7 z@h=Y99={l3D@9I%+2OefTdl?WvE~dzyHkh zyq%ZHJy>?>JKwON^Nx`>JbkSm1zOs_e&pX+fef;AdUwai)8t-CXwe(^#}%Q6PjoSo@o_aOO2Fd5TL!xC0nuxo8yYew`k@I=Mj#1;!Go_|7OHD+BQ zWe=q92`D6NM`KDJ3emz4iqtz>La7*3tCX8MP^}iLlsvhd0fIC@N|s1A#83t%NyIAx z{xqF6JipP^KZ_e$DK2kqu7?P=dmqhSRSLzne*160`QPT=zXEvh`PF_O>tSzeVcuhI zfupk6Nse&en#>RtI@GwIZ@6cIbvPqJy;9fuYV+?=3Y_HCZi4vhgO5zd?ac-rnHE_8 z*l%}s(b=rg_GIqO6JVxYkK@_Cd#C#C&Jc zTkRYgT7DSOOZh9+7Gx^9q}qxyRCUGDaF3e3wtHX;@YCFDWNty^EjE;Rcj(Io5p|cp zK_z15>dIH^;Na}w<^*927T@jRz2aV=%IX1F>lMJkT{uDL3w(DuUR>H;{nx!vXq z6GzjVUZ!`QF4!t{nr0Zn@9gcTW_OeCdFMQS?sMT`#8qdruaBF5b1d|->x+vJdV{T< z{VnbNtnNeFG1gCmZCqTA-RNw=B)*PyJ9$0qs$1AKx59`-A1izJQ?`h#l6pA8+vI-K z&1ZfW9|u}Gys~}P)rNU{$H&L~adPnMO&J5KoDT>XBC-KwZc$fr{Ue>u+)D`vAMR*T z3&;jF7h+IwNxLG-K0q_*M6CY4`uFJ>-Y?(Tx#v0rB;NH(z3cwm()QWIr@rn15A56? zUcV*o8(v3ut+irFZCxZ|-%IVnOGk$`Hx~5VzkbaRvhFTG-xt%k!~K}7LS#YZ@&;9J z*w^iBfl7sHODETAWEwS~R*94f2*7O2TXg^cAOJ~3K~w-h1EiFL3@nw9BodNDw8ZDn z&{-o(lyX^O76f}^iiSkq1vIjh13_yJKL$@J^S<+_-u;asdx#QMTkHHb z!#@tyjDP>?J<7y=_EO~hoj7G)FF&k0%-RW2TD{OaB4yC!e0Enx`GQxTJcTf6pZxrh zPdS!QQ&KnPk*N%A7Kbz`LR&|Cb62LuK^{}erY{bS)#g;nh3vKQnYD>oA)Tz3NpUDt zXKsSQpxc79)JWUx70pNoMAkoOSr5>w;4nvU{pAG^Lrbs@5e7dVm+u2~=F&u8SNrmK z?;ZecI}e7jM#TxE28Cc<&m(O?xJ{^~3m`NrL0GCr7>9wkbRkT;fT zs&j<-Zq=kjRtsbE_wA=WK(l9ADYE z1z0&c{CG7hDrumv=gG+{VOQNktekVg-Ur|Ja6fUY;C0f>%#^3;owql=11>rEU9bzj z;yN=v8TmLc%E4z#N`bhI#{a=xfVwZFQo9Ln-TV`RBi1Gdwv|kSin}8q>v@pct3k$A zQI}VHiTJv@p~`ALzMtGdm~CpSdYACx(!B^fkAm#tdURg{W)wHP5g8gcR8~RhB&@b! zN$9S1bQig!i{IC)UYy!uEE^jZiV?a{FS9RZfYW1r#ih+fm9z7!s+}FBP9s+<vdQAt%Qp+dox$ru1Ymr6g*v7lM>xQwDz$ffr5-&R9`;thwP{@R zerShxjX(DG_6~mjTbep#g|oN!w{IHuJ|ntZpvKxtik*N0$IUXwohHx81q_lYFfEoz6v@epO(7ti<&` z{7rCi=&K5kVAO5DfvbU|H-oAyoU+Uwgde+@efnmbr5pC*vkuE=Gxrn3{&iqL6YE*U zpm$bRV#!P=fw8(I;MaUd2S9>QY+;$IBRRHl@%_T#M>ZAx!jtJ z)Rltx{plW6XPvAfQ_#}}G5Cx(=sVo?T@iJQLqew~$v7*<C4!`OVbm>XRWf$KIj~|_~ z@j7=u@`}d`E2k%?ZiLzU4fgiAn%)e%;ud`A$=fGEVfNlnPu)uNcqInK5s!Vtt(-$_ zAdQY^W)EOFB~|h1!uipCfW9Tz*aPYNAVa=5yV8Vd$}gJg>ojP21_gVYx3S3~?aNu) zV%pjeVXOtU(9yx39s?q{7=bPI`5%q-9N^AkFtGJ@Gu)MRNyt|}o0MR&s!Yua;P^P9itIGn) zh6pk}f&dJP1;+FXNop7Ytk!DPx=oF4Q=`@>RZ37M6+*fk5KqfkS{r-g1Jyg~%t}>G zx&IxPp1^EF!5GiKe9}66>_W(h>5KV0@uJ9v+#PG$c(9^H}r9$fx8Nw{u-?-4FGGoqGWjh|B2&aL@%@x+LF@JL!Z~a zjTosYruB4)=Z0l#(`xdfhPv0)>@YY+u9B2|$erz%fC-*qQF$&sbvq;VMOdyF&zqfG85y{_F@T+;7t?(S~a z)>K{j{>%f&&Fr3|$<6gO62$mR$$Id^g`mrhnRWp=j{`lCqhp%b?^yRR)i= zEuk9ZZ2C}deQrLku~tBufvOT20}?qA7{8V0-8f~K{h*&VGwZ>MH{bc?#Q&E43MFn2|?&DnvCs#0urc274R&}mhA z9gE8nfl}3$7J{x$Wekl%tCnljQjJEUR*97gzFffq!Gk1BlS-+^TAC~slci89CQBtt zY)<#k+B3h1pa0^E%htDx#X{lm;PBw!z{v1$Wo7ldm?TfnfQJvA@7%G!chAAz z;c0wq{M_vH9t7qOKX!JvKjURQ1PUK(V2&&CciY`AjCh}4^tT#yx5V5(J)PpP+4fSI zb4OkHXgu-WQtJH$Kg;3A<^zuA4Ynr-AD^9cFhxDNu`xLUDuKbGx|M)pNpQU)ycKwb z5(d=^0&4~SwOn5$*9R%`sTKRx%{%6lpYu*Wax2H;agL*R)|8S}8=u;c zlGU13h{-F(mm-D`jZ@9o1uS8$cZfVTL7$)Jt&@Z_iU>-fbP1aw;j$!rjvNrFl#o@* z2Wq4dz#m>Af}}#B0w8%Eqi?7L@(pE*jh+Sq21U%F0ZhguVJJEvqRi#BFsXerjmQl_ zlAh)%;<5eCDX+c5y3yTyCY1AqR0?%&0ax3UlaLv6!>jwGC+_@<{!6bq&p-En;F+3R z@YXMk5m-L$RV4I9q?+1yG@}F@+S<$%z-4mR*LdW0#?lgZW1UakkT9rHKDRzUt1-m8 zIM8jIyRt2y>gfxRG_;th8o(^o!1XewacT5XnTba-D7o0mqKY7&)^~3(@o{;9!MUOD zDq_>Ir4{|P4gEC@?Rh1H;>vk68dnM{4Npi5h^|eCXShWz*`~K$2&g>mQgg<=>crE$ zYY&xW3EI-cjido#=k%J`Hz0* zA32=9v9bu!57tPeC+0W2%bZB_p`PyFT2V?aAtJM0l#F+*~cG zFIAu`)qQB)!myr2+7r_DRz`%#yy={T!S+@j2y7b+bS9m_W~jDwsx945(0c%2$fpN@ zFszoSltP7)E0?oDkOhD&0AK=ABa$UliG(JRFr;GKG{xOF;y1tj=JPL3{`q?g%S*Rv zkTuE48K&mfzdvI6mqX{iJAD36Uz`5%>oZ?|Yxd{wEx!EL^owuKeDSsEuYUXe=YKkU z|DJtSWi^M*ImpJJ@iOlUGw%%rULN+bWWBw?e0O8W?E=x>jQHZJx-&wNbzfKdn3`-$ ze0zN{#Cpiye9+ON>5fUyL(@f%3#eNs=DNFd`+L}!0%jmg{T>U%wF5C|AgWao)+Bmf z$M-{WeGo$58lig?>2coV)13a7Mb$BxSz+M?elKA`ku|U1;|k$p4J}KAKI-HQdtr%3 zS{G2rVkS+>XM-XhDB=SG4m7pmG9}R18L~Sv23^8o$s~N05>RVDh_io$@h2p!)R2Lw z4ARyCgrHOah505WQ z&){3J8w>Na<)yiS(YfA{Q4FEE2$6oxz4o*_(eh=VdEl#a51)JbKDlwL=tXnS5FLSi2nX)*?AT5(;=V|i`l({A5$_l`tF_)LI2uw+ORjmID3cAuD+|X~# zZLUn|hB`nLe3M7oV#4k3%$?8J&P~li-}U!)gI&BG zj@i7wF$nQC1)R?ZSg)13q%w>ss1%V@qSkL2%VyQzhZe#x)CQ}eIhItR6e|=0xq>H?b7eA)OvaXh zP*E+BazUVTnQ_fsYs zKc2KXdiv_I(^n54xBBtaRh? z!gW1!d7A@iVnJ*nIg7hKGmw=mtgjR{BMIpl9&o z#5LEP$3bszx;-+vnH3oyZts29#5(edTlf{%pz{tf9>GyQFa4c70?$2uebdY5uCx0M zdxOD%LwER`y&r1n{Qg;ZLU5GB_g5zH0~-`7zPZ7mxJ0chJ2 zNNWD56>JOxTbj4J zv7t5WYqs@l0guh+DRo-)_LdxKca6DM`!PNM80v)O8Yn=u8dO8mEU{80k}E|Dg-{_E z%H>=T1XPMqrtq$B#J~N^&wugDFTVNS;`o^x$4*~6X?p9V>5U^Ntq&c)c=&|%;S-k+ z9k)Jm;_{J`HeY{l_J=Ge` zDUS>MsGC_AzHP56SsNK&2rSg35oGDz%Hlp{X}_YdN1o9QzHgUBweftbMgsFkTk#wb zSHGiE%cTI9EoRWfOqxO>Adie9-zH}Ig%*XpD~yaSdzX;${B>sF>%5?MIWOL21-{FB z5nmjdR1uX`pIF+OUEN)V8m#Y{=^9uWnxxJyFqf9OP+d)x@R*R9AmoZV$xuJ;f*KVo?N#}d!KRR;#Irj7vX1|I8FtMz-Iil z2>3bgHOIo``?(0)U{qiduB@)Bxl<-&wU;zkr&ZNtRilcU(8#XbgaY{McO5a2Em1Ew zsq;HR$_{%4+5mB(rICO#-=31lfES6IYsT}kVKJHg$PRo(E2;q5R#e|s(%4em(3D@> zTF}s5f|`V^z+p9sMaaaW(wNK&hsf!hiHI|9NF#s|CN7vWzQ|i{UEa@#W>e(T>kEqLYg=^FkcGUzk0P^?v1a z{r=S8_?}_!&13&p=bgfAJ>mH!F2`=91;=BWYPEdQu8^`1Fi4|)0DaM*WJ3dX2%O|y zfT36%n=LEBr>9N8N)}Lc^s(L@DQ!Y@Dhu`jYI9XBWtx^Ua(--oQReK_lpNwW z?u%sro5z#LWorGVQm2K|>`yD_j{sIeb1coLPFSW<$qw=`1Y;#&46#C?+SE~Gsym)9 z{`nvOzhD3QtM86nIBIh3@QKUEpjWFy$E<%mdF9B7%ZE=~{^8iABPXHP-~9gDpZ)BY zzx(5%s}J2{a>}~LW+@^-vZivDK!64D;K3j^oJRflf!o`UsH)s)SA; z8Opti(tag;KnWXAaZeX87YU(T3Xzgnpz$inI9aS>ggTD#v^X}~+e{>&2;HbILR;6;^pr%%W|BxfjktDr>uk>`z+tdA*1F#mEcoOL z11pQHJ^UTrpFV!_;MTn-x9p>A+|c*mB>s4(#==M7h9Ena)t~Xin+FZvexG#JgILoz zF+5aP&`_IIQ=3)O3Tx@C=|)#~RTVbIdw6lGvka8EZRW}@m$buN6)cSn7iNm8N+gYl z>B7vy$T&hp8=<@vU*3w!K{RGnb;8^5RoLE|?%tX%bYTNLuAsZT4V8mvNGXFSyX5fZx@Ul;eS1@DX>xWCp~uzawy(8APD)lrRH9KSrZw>F!{-){F?G!eey`*F zLbm0!9WfcQHJBgeER7rFj1Pyq5TBH$B27Feq3?bOtndjB%iRurHx+i>R9-+77@nD7Xd*Mb_7X41VStodV0v z-K|KsbVPD`K5~*I?M)-`DB4R631Pr&j4SDy>?r(`dJKpUu9y zOpUra|H5j}?P8zvg>lbws@^vWq?aoDt|aR|&i~?M*p*QyixEeQA$#*_FUyr6>sc4e zJnN$+uU`?eVC**yU~)H@j+5sT6h*ztvVK+RfU>X`Oz6Nx6mmo&0hcFav9@=&v>Ler z6a$dzIt$>iq->^`L6dTrAeYtG&{p#%vnRWTG&Cs}^8h{@x1D&zoSz6=yA6@Xd|!C0gHU|$AZ3qsC=P*yQo z0YeR*MlDw>LA6|}1jRBj9}sY*TsEIh0E332p;{+5DQ{90oYr@u{SEnaIA(J#U z+gjROn^ilE=_M~O4OKKT;+pj7y#qd3e)nD79i8*s9{Jndzjyn->+L6RZ+MQo=1xDz zC_QH1a?W?&J`aA(q2)p#At<*wy|lgr*#vKDfw#4mwzR=ox)2yxN_m`{$86FoNp+r} zxk9JOKWjmZDwU{RwcTmiHN{< zm7_~y^O|#!b;&S9YH@?>`{`TBRi>WEGakq@9#yAZ+s%F3&iTPkIIP=eh=Usi&ym9C zHKG@_l8^?zfAvfqQMRK6)UdZGnf?G z&i-Dk^UFZfhwptOJDVF+0_Y@YS4;uO^Q^g%JrE)-|CJmnqHOZkH$^mqnrD!+q2nO| zqULxoLM-UpE5Jp&jdl`_O?Q+5rP1N%jHV}`Q|35)qon6T&sqPXXCoa7(a#X zhd8LBKVaAhVfw_% zhmJ#K^WhVhkDK25{qO(so8Nuu`tt4IDph0L`D*o>(Dwy3^fv)DDp0MIX`nw5YREUW zpOGVM7G78lyI<^idJ+HlG|>25jgJG_D$^0QtJyfkQU zKKRHi{OWmhddfs-u{H@Wjqd={aPn+|B9EXb>`@f6+M638}11s=4=pS*vifRIWduP&|3P0o(>kM`sG zdNG9Vrq0d=ECz{guWV_BH#HX5H^UlR;Ej!ib&Ume4f(b8d9}5<$boh|pSmFzGFQf? z8}jQblFG5oo#@CsX)JaxcWCJ)EF|PzO+?^!+554#&oe!ooNR6V146SRGt#fS7oB#T zxt9hz{iNF}nDV%E?mZ>QFp`_!EAs5t|5!krd=)uwKhr=QSWio`(; z`~alDA1MexN?z2HeJZGHbg51)k;_1p0#qqvDw$jffO3fx6beNgg+knoXpaqftJXqX zeC^g|u>G@_rVpy~O9~USglyiyO4Iw1v%^uF$SWSfcFzC+C`->(Yt-psv2Psx$m?VR z8f!2Zmd0mYPTj6Z$=H-qjZCujZ82HHTHO&J5W@coU_GQ7OWhGsAnDu>M@7_afVIsh z@6zU##Ky(K#F2_3&hjKgr34__=>f9^(!P?h3_`LJT|S-u4iWb@Ga>0!TxN6yI=5#o zpEw04j4f`gixdjw{=N_zJIT0GF+(8W%jFWaTBKBnRZ6K^1*##lS-BCMA2F={&tVL` z4}FeOr%`G(ph~%G*k=iN)>m);!)L$v>g&@-PhByh_u8SOmd8zQ96f!_*y~<7e8T4N zNt?qbY<}_Ye>i;ld;+{~g~jIbIi!vC<&{;b4AknkRQgS5#0{;fWPG_yq>uw@C8*VW zwj69d?sbviV%hrO6tBoxH=YA#-P{ zNg^5r;4ncx3*a#&Y&yhqW>7>Q(R;w1Ja|*cq>0%dSHvQoOe&N^ZopEd66%e$I+bqI z7?380La6|y5+RSnqHe4&PS1`HjN;llTdPqu1&GQ_czIe`Nn&AHQgK;gaZ!9;QG8xO zOm6<$yo%%sVnZi$WeMQYXZwfYnXvRH5%ag+(qB~S61!?&ra!W`C&dRFsxr3A61kbL zo1VKyd%Agf`bEErs|hd2Ipx1C=RSUhUmeShTDuob~iRA#OKv%!=5B-}G1eE|F7fV5*Sj>}%*b+VmnSspD$lKo0 zLlY@%m-CS;*cw#6f1}bGY}+E{9xv^33AV;_M=AgG@kU3_$TcV}5)`!8I5Q=7VFS zEfIAGWJ1pAtD`f8$$4+1W6J7NJ4Q3{Gev!iMfmAr++-XAom*9l?C!(%4RFK)jzUfr z@aYmUM=s~eq3JPSE*C1~Vx>X?eJP;MR%Xn`A39!R8~ic-{-iPfxLA^@l=}w5%*yiN zV+ehsAGbPs>dKE$*=+N}QLDozFMoUZ!mobu$BXx! zD+!|_86Z=Da+ON1RBn(dm^L(~9lb`S2viChw2P85K#(q#vZPWr2!1yH%o6Wn+2Ux* z&9c)?6oW;Mn*4i$jC+mFXXk=1PPm>Qd2(*r^Wud2g+T|4LC14r&gY+BIMtdMUuo;P zdiU+>!}ko=9C0ubj6(w{U9wc1IHs*9txCPE(`wWjjb?XuSEW%e%*=LIH#eul(HT`E zbzRiSSrLr_@>l?rjp&IGfwyE=w`3zQg>~I9R9{`kB(`s5 zWRkhG!rxdIQph4YMapJMSx_&10AK-wEMSn0lCCtdaX=05Igq2b1mapMp^Dnbwfv~X zq1{w%Zb7?Zr4o>VB2X$+$R%opL@~5=o^X*s3H|CnGos| z_V8{vsEoQ?L~B89OBSLr zy`m|jvMIBwB^%L_Tiud}Y%8e2d`QA=_=}9RS$51H z#`Fnv8lDso$z>AgOC|?YGC%G}Qry`3FMabjd>=+?1w-Y4(EGKvib!w|lQMWtC20Xs1!1;1w|U`HK@5pqUhgf zME*C|^B-u*V(K<~c_uFrpP#lg(6s|~zl?3+hjZU80c98D7}!gS0rVs+uOulop}Z*z zH&slWDd--{>m1GN9Lw(%WUaMD=A-PGnh)~h?fRHJ6i?-ut1Ov zf?T;wC^uplO2x*e>!70kn1q#oKa@5Svs8+GgQ0tL{D1xNyMOuDKO8!4^&_+sx_;8^ z&L6)#`t`RaM^0WoblmFDacgMceDd<=UmXAC&;Rf^=v~tsS)x{|H#ZeJ?Ey)1Yi|!! zDd!jGmnj>oJl-;oJ4q(vCnpKxKK`hffUlLXi zyzkJY5hlanB%{v2_U_J>ep91SZE7`p``cT3Er&rL#Ne9J%aJjK*uuIgOfP+AK|rBM zSPZ#rhbF1$45QMT1z=JQgV8$Uu?mK+*1RRVmdvg-FCBudmI| z%uEc9;M=<}^{rivn4UJ=D1KmJWP-lDB4#m_BAyNqZOSE^GO=31m+@IbDw)2t#3HQ< zsbrN@P+e5={^ULES>HjkfDp%^P;U=qL&4SnRy)|G?rYy9wipNv+jV)%?*ejM>>?wh zv#U|C;_52D)B(qg)%!UtmrAy0^~%HSKAQ;SMZdhe&+~%cBeTj|3u{}lk*G{Wb9Qx0 zF473#T1;VGM^Qbt6x9t!<0>%(1eREZ#aDH7SG0G*(b%$<_Tr}Yf(C3+V`ouQV_HRg z;F~BLr?S&d?Pfkh)}fQu5hbUcs?A(k9&nBig5_1g^r#ScAt#^a4Npw*-Q$Jb6GhzS( z1V3KVGnL&joLW*j&!jL_3W`JwVOS!eNu>~s0gx+`3l)FO!yf_slS29~tfv;FexQ-Ap1-yc1F{m9A72aBQ~j$ZoHSEqjW zo3EY1-q+7;Fr{KCG{lB>QJ`9-+tDj^8m>sVu(pC58mz;0cTZ0iw_^Mfvy&?7sxh7Y z6XT!NIG*BX-P4bk%Bs9onKyMM_FX=fcvp)V-;0CJ7iPSy7eeokyIT<)&rP~ow%DGu zJMkU-bx4Vw``Fc&Gq)pVZC}sdiCT9^6}&{qldz)b)}Fiu-M)TxWPD?4RtX7TZ0ff+ zwOXZ00ctf0wF(q4s8fXgwnAi8d=c_}NmohpSTk{fFh-qTlrmYHN@*t&RhwMal7mEL zRd<)S^fz|ls@hwN8|!mws?#bf6XE6WOUvSlN@EKul1h;o<@LE$t+0l^X6!_F&+_mn zV`+)AvBsy7g-oi1%LIg6xlC*vXnkOM{GE)BR;$!N9Wn??1#BjDePwBKXatY$Y$~sZ zXO*;@Z?SkGuhL*=n&sDdIvkJe|ttGEfm|Xq%wcqi#u8JVT=1 zO~P^v*H#&o*Wr{rdoP3K z0OxzwaonoN_JxfWKFJrHvVGqo(qOGc4Xt@KsBA<_UQJs;ZCgdADicWofmqBF3ppYl z3lOqK@Iy_ow!E|gxg0W&M^)FqkBBlD40JO2fNFmrjz^=@>@98-C1o271{RG)Y-?dH zPZ|Yr4mjTb7wG<1vYUDld0z}&8t6Yl7djOdQ4Q>+rP8zzZ@)woF{ZLPr>(!dZ?>pw zycjoGik~dOPZ}SliiopWZ=zZnoBC<|y0Lj!?__B&gyDC&#S1JN>l20nC>ztIQYHZK zWU>Pc8~fpdLfSZy{>eT`p;O5ptknceRuTI4@WPaGQ0EL zk@LU$)t5gUvpRC>%8?T`M^0Y(`g`*~e`ogF-+cQxFuH1dbwkMK$>l<&Qmj-ex3;wU z%@xwx;P40<-GS}wLbSDqmNnJ&^wkVZK2OR`ZYE*|$Mic}pK&wpZ;cj#HU8?Xo9d!R zeJ?NLT+D|(EIS^XO}SsfJ~k&hns+*!8+1P3YRi^#7tJr?=bvS*I=)|g7&Ck8 z^@45GhC`|_vO$r8qrR?N9GEZ|_G&W=GXkT~d5Gz*LC!i&Ap>+erA7tHAk#puLd*lX zEc(LYU~^|m8lM4US-XhT8~J-Mw?ffu+F_+Wb6wb%jS+hmH{WESXebnkwr0XYM=6E+?eaNVjmTuSklA+;u+g_nJb1LNy* z%dwTs*wRLHVI8Klv8%j=h{Oz`x@Yi%>*LeRvHdCjqbA0uUjoqe>@~wA zelRiI)yqGtd#rY1BcU1{T~=SxH(!FEEF(-8c1`4A#|k>f@(J_hMWwC5UXyDiG6)V) z=v_azCDe8eq zzsvc?d#2+~=4DUrV#~{W!gJLb1Wit#ECUawbn+u>={`mLP^3DUu$MWw6yLz%vJHm) zP5svL+(IX!xew7Xh{Vrg$LMn#LOKf+@?=7elndPn%J^)BkRzm0XYhk91$EWQu*Qt4 zrZhxbMoo2cSz2gfb6yj9VM)lM@n}>&gC=0oL=ciG5)KRCaRt!!h-SPogyz+J8kxJX z##vwGZIA>siipiP$h`+^q)*NapVq=3GVlTKQl~yZ^}(u1p#VTYBoXq2JQkZqX0ESs zDF+B4uT9MMwcu)U5joMBX<^CPZ_*nItFevk=+`;SfO2sHM);<&B(~s9WZ2f))b9H1 zHfdTjHb-Mz( zbNRxX?`9eNUyA5 znp+NN9(#NHdq$UVu|(R}-#6Na6OyJ33g(U&I>-O767NrO`(Fe2Kd7-wnVM7 ziAA$q?g)+1wM1&1TF%7|#pahTu^9}7>|-8=Fw6u1u1qF?rqfD5qXs`LmOf0R574XI z(kZsL-CxK4<3IlLPhT86VSeYwQ&;}@r=!PAuAeZw{fEzweE!AJA5E?uK6&NP37a2I zSpRU$`j20o`paSSq?W!O5`_wgR|GtUT*ed&yM{*I6xHN5<0clD&;#Qn8Vxf%7Kg&a z+A(xM9-UW_*3w(j+@(~?Ra?5x>9rT>o&DQK7XQj7+;I+DsLwux~4it)<+lEOC%BX;JA+g@K{PQPfVlF6UW+1nkth@ zir?lSlgiRV6Ow}xN7@G^LbiZO<1=VHI+aJIh}f(xoemTUxl}TrPUX|72f!A8yl?=- zd^sS3nm83`G*$VSU=IrBPtV3w{0YE6d0(l4U@TKA00k(P2stdu#>&j>WZ%#jzJHw1 zH%{oAAoNe-2S+;kyOA9&MNJK`7PxoZn8l0nE3ZipQ>&jPypBuY40RglOZv4bJ!x9M zJ_}h?9|R;QH>)R2Z>`VjX^RHt(*A5e3zmU;;aQQCl!s`j!}eCkRS;b=7w=|Loytg0 zN*3*MMsFu}IJ`$E6peNh*5?;!3(KsPRUVlvV$#JNri2HbYDjr32}Ckw8ROFfG${my zY?+uN7eXwWrRllMcL{OF@6#Ofg?{x6S2)k3YW8vVNcSL3D4;Oe3@(o&67nS?fm9@t z3dIt>Sj-a%xe@_K!e@!Obo%NFp_$lO+uecetSYY3Z$mz5>np1g5u}i6Tqf-d4-Z$u zik8O+yHeU7@ZWX3f0uRt69E6c?iZvK2@Ra3Ozljsq9mc=ZDf26It_&@#!VCxru&yD z3Y}KJyDbv(@|rtJItP^s86XGbN`-b)E8pHG(Z}=y&u&wd$^$&p-adFTeW6 z6{q|9J6~#hov|Hu?3hzW(93^^sFo4xh08<_EJczCQisHzt4h!(rcq z{D!&pF)DSI!(I{!hL`4Y8(JgEP%R^Kph7-8w=%y@86O?(qjBC=H?4D+vt(vULw9z^ zaPRQQ&i)<`06$Y^-=!qnrY7B355H3X$h^hge8}}u`sL&0cTO()SP|SVce>h)JTa^G zbw$I<2cBnZQVBq6mkdI0H<;cHq;;z@iKF2q19&`@OB=v;!DEuMf};>=1;d>^YzlpI zQ@6Xnqt>VyG}piuaf4eIp#Kz$b~rxuY6 zhrcfl_Y8$4!>0zOR%aKP>+4)9l|!b8xm-{z;xlMMCQZVDM$Mp@FNZcpVx@6oWTa{+ zASW>+dUcyx=vAjV=zf0+;E$ovefWeAClJtQ9uPku>ZMd=45eHwlL~|!7Gq<5X=-K| z-_zaP-dfpEl?kuOugecj>$&)H>dNaG>+pI@A8)^~?rNBUv$nZ9X-vNd(4F6=%#xL6MFI+gN#}A{d>&6E6p95RF<&I&iG^H| zfGvS&rwjp;vNkhMZ0YT;!}YfG!80qTC#Dbj-W{WilTgSfbhIrD<8~m*6x(0299qN2Fu;NaBukp4PxL`L4= z!kSzm8=skN?ILodVlg0ITwGAv)N{sZ~yFPzy9V2^CKqLPMY8O^4rsY{PNf- zi@V<*I{%BGfAOcUP98aZ<((^o(L@}%_xcf`aBdTwcuMjvI+YH;1r zl?{<4wZn_6`n~Om*|{Yeb8&HzFOl?;>F{p6a#M$$S}y3B&#A*OSPZp(XO+qROqg+3 zl5=PAwN1rcQ{RG=Ib@j1-()#dSl zp@you*8Aa0cj9JmMh#yKdUnS%D+7|YxAMLm5% z&s^T&t{LbHI|J?H>^OLUS8QxtR9;zG7P33Ic>YNq+pCfvSjY9RA$yk1hr*{3?QGhH zQVS}T5>O%lB?6gLAd?7W6243-P)LPJP^6NHH43Rt32v$7P^e@&g;WI!K>=5%l+7KRTYgBf*XA(ujDFxV_Mm&4=p1OlE=$Q24WVgXykXGwT05r@vEtj`Qi z5>P$(Mk20>*j0-wFRa+oZ$eD0&8^-Z!emdohP!4^vZ2$zzwsjae?{+q0Px@Za!Air zLe+BCw%bZ`pSeUKJL)FZQ|daB5NPBiEhr{~*gve_-_`AIlgQ*Jp1yNSOJif>27>|7 zLFgQtq0y;g86e-(N>xg}T+ReQnpi>+i5N-+Nug}%8-D5Ocj?!EhzNK=*J>EXJbVCP zj!Y&{D5NSSs5zi^K)DxMl1)y0^TUaM`PbhcI(F&EX`AC`Z-0B(;y1tj`bU%N|A)1= z0FL8K_r3kybMNk1NMe>PX0liY$#yu|>~0)%U^n2zF~oLajF~~U#TK&#BOcL=q!BYS zOT&!aJ=DXfx%KwQj&si5^PO9DKr`U+?A{_*y{N+kU}s zcn7}f?z`K~E7IL>&)u@d%JKOQ3Zr#l;X!WRsnp`Tx#f#gn$>1oSXx|RvKU+rl}^`M z%&ij>GfW1AqGf~gNyMRwhDMD+FIA~&0^vH=qXUEI-YWY%y8N@Kf)68;{uew!6OeEWw8KYb$ol*&3srPGnf@>$Pewl`+uNgU_sZ`Y=1^ZhA>v0Y0OgkXPMV+SFdu*j7j+WY<(AmlQn8 z$+?$N5?4^2Qr?h(CuGz#=QUO&RaK-^C0|c|@XKRTWqZxj27F#!LkXdm)H^#iMOj$p z&=~?A4}u_>OsYiX;Cz|(1wQo{mTRJ_(P}lCwZ3RfY{9@V#`g1{d0~+FMbl`38rua~ ztyZX2a#ST%DiN$!3aeByoenjdbw-0)^u1}-a=jL{SWK%n zE3j3I#cVJc)H;=10SQ%7m@g6KC#2QAe+l`vc=&1&na5_ZSZpqv%j56`T%nLB5^}{t zKrbW!0k)XSU@R^UwDh-Cb+lJ?v{tqC)c2NVmMzT9D^axcY3^)43CtltbTGC1e>!6P zU!wc}C%;&|SHduM)yzIPbMorlqJf3n&dG;`_?+&!TXEU%?SF&V(W|$Z^j6En%*>@r zm)Lx6{(oWs03ZNKL_t)3c2Qa5z}Wp_eE;my^b%QTHp;O&jZiMgn_D|M<6`kNwC}Dp@jw4nZ8LlqUlKLu)l*U^bhrwxNE@7MDN2z!OYB7=9o@8}x*$}7$e+XF$n6{z~Y;UDt4r+?J?^)4U$@Jqqq z41Q{{WnuAFTGr{z^5l|Au|#CG*(h`xjmebAWK1?oWzdUc(nT5#)$5jdqU^4jjD`*> zovJpPU=$T86zc|mdb9S+eHjNL0rc>S0ix` z<8uCTWbVUzQy<-1{ydg@BpbPgm&Pof6nS@?=4xZ=2i?EC5IH4x7sZy8rBvNIb+?Pu#TN?bbnMf`4O&zol}N=xxk^z{T2}hOFO;t{7lx)d50%X$LQ zroa)C|J0uGU+1e#|0i_+b?%B{gqg~Y8b$7d)8Brb*w~lfIhoZu7LRYcotXFO>+jro zT3uS-q&4adR?FnVLFhJgZurVbU1qDqa%Vp~l~Oa1-t z{=8|cuXn&+@1R!$!r%1{*uP<;+lCG9KEZE#`0w4c$z%O1&MqF|e!;K%?0D5L_|<^0 zH$D9JxOzq2uV~5bnJ62cx|5lAEw8More3W^EjAmA!(LdNhZPFGNT|@LRYpBWB!HDN zrB>ZNPfu-`sAwd~QMp{Jk*U>Ug<@Uq2Rmcl3B?`U({uRE@uRQezucelumZ*3~5txCa_#O9aB7nQ{qm&6tp zKFrU!nepU$YVx%wd5?yF@S&l;kC&`-7}1zSDx(M zT_%fx!KU{gO%|RgWgjhEIFxbz*oBPv`!+GnxHxK=8`Mtr8mW^;#vGuRzXIm<*J>ej zus=;g10BB*9dsH&43mTo)^rFcIsw%xqM3$gP8rRAN}w8;u%`Myb=OjYi$7&1|z-UjWz&wk%oA7PCRGMNt_H z@%ajctfQ;_$zOgX9E#y`IXoVl%V7$6EDsn!>)>+^(q< zV(;zL;*SD@6N>8|R(Do3wdpJdtw~Si@(P=}b!LMOOS8?g#pS)@dXrABLe*xI(qfqz z8LjyFOkQwgonL5I_}-ep@Ee{1AHV+IjjW=U6=q8NGE7{HvpL@cZBXoYFp#+A@44E&EDpPDNvrN~_jeEg}RO868$?G;}5d z)oYY`tN~jKiqB%XqHJ(6t)XXZY+P@(*!KsBmCAKlAM9%X?zQog@AQ1TJNw}4X;*H? zeEDI?`{9q@4m$tO!%MB*Md$7ffBk6T%h<^e@6LYsVBwR8vmf4D{y1jfz>Tc8j>a54 zEf)z4I*r+CHm#b?HnYiMG@1&84Lhpy-sT|=*?!MR*fnVNG|5f1Z7W^S%`c#zOWDyhR~mJteS8uIJQQt+77q~6Jynwn*DI9Pr^-*3^OeV z%L1NkGr~5*Y%|0%OPEFx-5{o086qxI%w>W)JdY*jGFU6i6TM^YwcYLbuJ-CR1#YYBCuTiG=KEHa9HVIE zUnHFV1Hk`XbpLZ7CE18@wDUuz(!`|izc?M6Q9QVm+c}+F*KsAa=+l6ZliwV}O|c%} zNX>&2I*UpU%oHCazpAkV@4iBNc)Ve0p%*x_@(V z5B2olyVb$hXUD6)!EZRZgs$J9^Ns$E)6f`}~4m z^Yq>0;T?Isupy;=;%s8pPj?@cH#MUMz1Cz%=md4MwwGr&pFZ+%5JIcWb^rSHjgb3@g=YbkwOZ&6jjPlrBbC*X;f+r zP+g7vsYL<9&s!#e$7@eYtDXhGYr?x`_IJJ3pw|Hp0`nc#V1Az5`fY@4mu`^jvP->M zX|Y-RstNO7J>j0f362!DpGyDXr*or?RqClhSsUKkPaxw9HA_Pl-V(Oz`T3{ZF26K0 z%&^Efz@%&o!nPnB?8!DG923Mci5Uh7!y@O|5SDtdx#_``*gJRfY6&fb&av1s-t7w6 z<3?Fhn>?X~bGv%rdUkJF4J^lQbNt4C#{OotK0nxDUNtL}QXZAI)HA$P(>abKJ~?_R z{qlV>eMKn~iMVvT{EE3u=E}-c??iV)ZwJ1+qq?gd6XA}!UP3`Fm4JIT!TYb%kL zwo#^(=`F?uHt${;X_g}*jZ9t1Dl6(5MXgry%uMU$8~9zXHu;4%1?|l74!jW*dg{x= zm-1_#POQxE1X8t1Wwi{>&V2HZAJ%Vhd+ATx|L6bQaP-7ah)#YU)YPOHhGj~H5SFOHuIaT_O@&si)@uoq z}-fK|$MZU5L+{Po`o<8!ME8dARgB<;fGs-vG~zPIDjXJ0N2 z4AoppKp(Xtk6UE1pzte?Z&RmqGjHSTa&g#M2=K>XHlj*7!WW2En9R||rLOTYLf;_1 zbBxeG(>}g9I7gXWVa`!mi*)u9jWbW?El@-hhL8e;S3sfi$zDX1EuGEmY+RT^Me zjEdok88-S0_QIelIaocb0Xuh|dC_YgvK?Izy&LQXB6*Bqt=4Y8y4){zgV@txYdZrp zYK3{#NL^YU`aVf;vXuK{@%*vu8`mC{7i8Lmlodh+LMTCrI0~uSDqJ!C_OlLV=pe8f z)*@qBP@zQ)SrmM$lw(CW7Ae;v<5{F!vy^K=xMqlHkTA`1o=wVuI%{hlT)B1g#*?Dr z=E~;flzP#FIz>{O{7D;{*hafn(s(JMqn-pyWhSH1px2vBX3*hiKj&dv)oV0rI-g%Z zxExnSxm?WpxsdZiKKoFP-~^6(D69PYyQGFDE}hOIuW)D-`r`6L_gGJTZ&z(^cU^C1 zZBJ)SPiIYcYi(a&UY?0IWg;k(z_En@4o zplwcJ+g&3zZFFD%iqpmo?he~`xOwjK@{9Hk*y|Ixf9p2yOF5PIt6IN2^nLqaA5Wte zDHNtv%iQ7ul|k2;&2%p_xjep9&Vyxqsa%Kwj3|{-rBbF;$^pQ@k;cwv1Aqavs8pdqz~&7(4Ej3IwJHB! z<6$jkdW}8zrq^o?nA^Vw@LEW3=iHvYTKm1A(J0hvrA7lS^SLMd zW5t|PCA9BzGH$0|yn0ndom}NC&e!1hPm_e@Impu3>hlAlc8nn`0EU@ngb5T_4VjH9 zi&kn?@U3#5MS+=|XOZ*Ga-K!XF~LkD%(5tiHi%AZC{8*3LrU6{ik6O!^46vJYWPuu zEU87A+M!Bnn>e3YdoOFaYh-zWPG4ezWeOsMHor_K4Ty`o8eooyBEt>tYk<*m)7Egkrt#^O49 zU7_uTqcH!E4NUDb%rM(V9jB$;`Q`YrZy%(T&M@wm6E9{IClY&#XSff_NbmmXrGI|< zRnZ(bzIk9|2@KL$uvW0@fvNNHna59G{^~CWzS$A-$;%tgZgD*0?sat6?vvjhe^5}J zJ-9?z5zGr9y+LcU*(d`3tw)Jrk#B5w3JnN*J9N+A1H<3l5%I3Occg=Z&maD{`A>h^ z@X{aGJ9~t=`b2Np=J(1gP8&D-IC(_6`9`_>M!We$xp?h%_T25{8op_xD+qu$dxO=J z-n+g1qJ4L~wspJz{=a=0Q`>vs;1@mPV?3>n0YiF=g~4XdF3g*4HjaP?%VlcdJg@av z%e5-KNk2^Cq_&Kfr^C~Phvqr75fAsD0Wp_le^p9Gv z*BFgD69_}pN-4ylQ04~5x~iM&GOJ1+KP`DwP!?Z|dt6u=S5y{PRQ;r^CbgnAtrDM7 zflsZhOfD-=#NiTfMRB`No9}>7%Y_` zGDIdv(PJXH%R775t)v^G5_0U`_(7-3luwpGe8t01#kYc{DY8i@rJSe1N>l5bWDEJ}ez zA+X5#0Aq-0gqW+SXjMSw)fZOXzL1iZoLO2qR7m7Js+GkyNfSuwCml#kL*J#WhV07j zj-l$3n(AxGi?@rEx!vmg0W_x0Vjrw-c>`-j{+ zap~sq^EGK1_0`oSxw&V)JN)I|Hx9V_e6(@%q0J7boZU}&c%St2KJ6WN>#e_E{OI%Z z-yJ`D@lIAnJrxr7(HWQGo`mmz-^o4FKlqK%onT-n0Naw`AM&Qpj@Mk=BR6k$edXmX znD$0Fxkql?8w!4PAdhc=fiN-9--FL5xTlmKHE*m$xZ*>T8au0L&-s$NV z?G+Hc-O2ykn+czMd$_r~Q)DtOi-dB$UW`EFljFu!DLcZ2yd|}jSZGAj`bUD4g zdwP1(xN4QF(Z%IuCAOwZq1UV{OfRU+FCFNf?{A$eOUkc5{Z8D+p=lSdj?K@zpON<#1qlt6!_7*!x(Ak5yO1w3avzyMdHQExQpjYj>yVp#QDY_V>3nHrU1 zy;-0Xj4EMNi6|AY91-a>iov$_;cw!3KbMM5mA4#8K7TtQKQ+mQ@{P=Sn~1j9(L5b@ z6>cs!!Hi!&^Z;PYg+@Rg%ZhNUQnpnou;>*Qv&y2ES=3@HDg@p&DqMR}*wvVAf*1yf zwJI0cI7`fmtme2I*?HNxQo?*T5sD>14++xvW>qYSa;voZVqDwL397^}9+ zQf784Q@}PtX-pGxrvbiFW8ab``l*zEq8K_|Iq~t6M@MdswvScj*Tkfx)btNm&Cm<` zXY#ryaHGqQGID0)uh@8tMhWfzdL-~aLyMgtGQkY9lx>7rCf<^5x<^ke8h?24SX4yB z`VAjAyIpnjO7`?i_X^Ao2rcppsqhbtb@%N|md zb_flK3JiPOC-`;0u(yLF{t^JDcHeaM*|W_tV8bihUwL_}i$?_D(|b1tu;+%29-FuL zJGudY-F%|le4<>vqg=f9Y~LQRX`}m=ExwM9zJ`#{PsD!g>7^7{pYE(J!4~H zqhUoXZtgpj$UNLtCVXC&tL_wWCijojbDI;njd0dADMVK0ZJ8-rcbS55_-;nf&O{ z>?d*Lf2J+}kdbjYMq|=wHTGn$J)H~UL=70mH6%bqdLkehW<5EwrL7-Luf1X;m1OF3qQWs)!q5PMa|w^62e zxb)oHm(y^Sql7^yy+w4Z271^aiy_J%G$`-X!6_~BieW{1ha$B@_M}ao*eZQYLLM~2 zck1EGHIfTelC$OFpUVU%N(3j1IfsiL>^+*EkW<^xvMl1oq~)ATD!5V97)Kboj3Z^9 z{n5~nr-zs(Da$CP8L%xjV3Ey#pICeN05ib9LCVq#DLU$uakkewM37f!bX+}k;q85| zIXJwze)BOmkNAL~ys+I*LL$>ccNK;1&JNjm$!EuBF7BT=I{#(kmd`eC`EbL=Lr!ja zp?h*dcc1eP{KY#E9}!&@zNaj7cX`?^A@jdjzP|D5iTB)?q0jR{P+I; z-#6|E4jo=t5a@KvVi6UFWqR$v&=6lDHriHsLcSQJm-R0!?$Ic;YQ0(Cv&4GR(A(P9 zrZbyVMm>!|gXL0{L9fzj!7#%zO)xaEI5AFc#rOa4{+r+XM-Ts$(fvaL`B)a~OcCvP z?$TE&lOH`E{ri2|7f@ zlS8b_Z!XKIEl#b$WfF_i32Cvoqu>7Y_)c2v-L&LKS=q^jxZKJbTtf$;Yp82zX=;v3 zqlvgIun-Fp$|0dlB9MxCh=?l_^QB_YEi6NX3Yi3~*U_v2ycYNBb-w~^1X7GWfjtvp zECyC9P&tf3Ekxygh zK8wvh8+Z2FP0DbaP0BWMmQ2h!o0vY=+AtAw$;Oy5fVuXy27NogYsDliePxyLEegIx zB?6AL7P6`(7F6^MV4)eq7?D8nIudD%(1~7!*FX;%+-e}sUuTleo-g;Vkvy5 zL2#*DoYDdp4M6wn6^U)2n?05!d)$mXY=rMOz_;q)D>cwBRS*EUTyPR6I8n?xT3q}3 zjW0g+z5zL*J5xgKr?clSpum{9c<ZY`x&T|KsvR?)~}G4xi86jdxO zFN00DdY#%}P#g8@T043fJaG}O?$CiRllL8F{3ABH zV42LDUuI1$Fec`xBQxZ|$>qNBh3?VW_Th=r3 zUb}oRmVnE(X+#F@vYEFG7?#kdYf5KQ?%5o*_R?C`yt`$ccRTYiD}{DLm|1f2Ht$p{RK_Y3;c$@ReI%?CGbITZco z$)7L%c;){2N7-E*X+E*#UdT>D=}V3rELK9JUt(MI3i=f6u{b!^>Mb2YDnYntcQ4Ik0-0k4BW1G_sN7qnC*I;M2Fc**TKm7Y9 zH&6e*g$03LyCM-$V3>m{$H~GVT6@Li%bjwFq3md28# zaU>+JMVrz?zg3ZUD^4bd)Tk7Jgj_m}zPz|JGd0rP+fQt1%r9@qEUil}sZA)Ziz}I_ z8(C?YZOv_H$*pfMAa)iucNLR*%Ub&?+j}e8`zzb}E8F`j+k47dyKt@TxE9h=Vq+eG znAcdBO{mDIsm!h=mbMObOmaD5quHp@>BVv-7ec!FMz3DGH8#IIHn%*xLR)5Vn0%2C zmLdwdR3VcqWimM|l}cn%34(|bLi=qbdt9>DQ$CA%lgHdaT z)?|Lc0K+fF!|Js-UxV7`x;6HxLpiLFNn|jXRg#K?v!kOuN8`xHGHJ(iM!rjY6q}u% znq-r6jRFcdLCD2)7-kOPa^`d6z`6t^#8iKr6DUBx{be zN(Ay-r~v4&64PoK&n)Gd5sn#1G215Puxs+-&YbQWpVW#FxNAvpshoGN6uw=D+^a|K z*28z|q1*NFt$OH2Ep)9$as@BBSOs0EhJG#=9w}yjleheN`s4>0vAYg`dF|m4gQvwl zVKOypv)Kj)M{=GPsjOxZsuH1U9xSDCc*6@z-4oLt!{el;#`zY!e3-OK8MCtIb&Of< z@~CFK(=ys>>8wI=iQMFy^KsW(Zk#DQb@=*wZ+){P@W6Jbziin2?uLyYI&44U;T01Q zoE;Q?#@*}ScBf-*o)3J3szP?(^7Vgz`}U7FZ1^HD_~e;uF=Y(^$v`&0xX!`8c?wf& zY^lMg?~G~=+VwPKXI4-|b;xeg&izN++`n{m&JT=e4391hjr_^W=Tk?=j~pEDc?Y)d z-2ccw=nF@u16#Mf@38&gcE<$2pu8OsUpcviZQAyym$s~bdF#tBZP~ol%gr|mn9m+} zEXu_I-tFeS+tmwTjL{4HXAIydSFb%zuA#1;JDuIbUECvF+{0h`lY^7HZ`a%cU$0w{ zh^R2cQpl$`oVu<7Q2sTWxI#Wxz|&&#Yo}MG*P=SDLaWvp^f#s7%IHJ;C8q%WBdqh^H(X~~w zAX%kX=?uR%Gh+=NFC?E0I=umuj)22#(!GeT*`haF3}%Y~!`NV#Vo)tJ8BAuoM1yad zjG)8JvTC;4ELc$-bW3n$g0|~v(ra~+n{}KM1-W-}Z#|BMsZ*;k3*4;~Gr{NrYu+NJ z4-}+v3B@)A#~@l+tB!)MEGAY`!T`22EW;QE81pSkp#>FNF<%-JVZb(cR|Rv&|U`yU#o?$*U7FoC@$9{XDV1n z3YLG&8@rI-aXzc^hsPxc?`7}(>A=T_uHoBexNJlNj^dhZtHa}C&7`JPn@y=vsdO5R zL8mhtP?J%pR&$i-vKShio+DM1w8mVfBwg3ylC`BzR0WR*uARs{^40B=CrlpYv{58c%fxi2js?1ODi4li$g z>7^~}*KgbA6ksn7+1+FJXYz}Exd!cX?FHr&?drAL&2y(KxJ3qXSAYJqgNx@5($uU- zuOmxDR0O6=5h@HlZRue`u-0glsnPkR1vU81sD17{+w)i$$-1iX<+PG2l}f2Kn_;<} zLLuucW>l|N8;nY|7J?+}>i>FOcCQtBNJQeB<;iV|^e#nKmm;G>p4=u+Xq6?jN)uWX zaji{P(#hmGy+*20z*5U&q7-L5{)%FKG)GY)Ku5iQs2|xK0Z6SNTpC%EC!Fqg<+Ax zUNL$$ex%d?rUC0WgHJU9(6Zj=%&}!9E2oiDm0v4CYt>ij+ftVXgK}k^6NOfx@c7`8Lawn(v#4!aa9Fuk=0z(9L} zS%DNk3ykg6COOD&nZ)!}om|i7G{wf1R8@Bq+opd?SKe(DU&TXLYA~ynU8#f5S5r^s zPoK*fyi+hy*2tQfr_RmI4~`Bu_jFW|8q>-@|I5LnPoCb+FKwJ0UlIweHd|d?-Tcym z-fBkmTD{d|cli_=aH5_qm4bC}sFJ1DOfWeWmF1DhF zWB=dpHbpwP9Cmg4#@X$2C+Dx7Tp#-f*M{$Di;Pb45Bbc|`AbLFTfPCf&`47FzI%Ql z?`_!lk&FA4JMlO3tHzedHk%Dm$eZGm^1`DkJ%gGf_7fuZJ@yOA4-PL2*+qegL^1>J<9xEWkH{^pihz0El=x^C$-Cg4wK}G z?eYhtrlXG<%PSb<1tEhXhedk5+5(1+EZFIAlhL5jC}m1mtd=9-@Gv-7ysAYN3W)&B zkxPUMSfqqS3PhxkN|eCFpkND$5Rm{9aK${1h{qA~I6^K+EQ3EB9*e_cas>>Ykj4{H z`C>LKpJT8R60%d`aY+xJW+s%E<~4RS4~-4ZF3nR}3^t#~6Nv;6B$msuSohfsgWjMu zne@;4uKpvySiVYcGV9EMUVyIEYO=0kW-(cv&fxrFV(*_bVSJWtA3W+pHP`%ryzg zX3Up1a+j>!McMLXd)yr(ecFuBu^}EB)?7(9LktTHd}+`Wjde!@jAfvmV@-|M{Oacn z1A4K{mT1l7e2W^f=v3ez93f>L|0cT#M<}SA{V`o}lPJ7cCAox`T*R-OEf{%JI95+$ zF=#TiRHau*5wVa;rjJi8bPSHxw9k+Rs#5aa|N4g?9%o!l$sFTx#ZsgUSEABswPvG0 zB3W2oR%leHUaQc7!y=Fh)XbE34t#HRh0ef~}0VEab7dF^y{5A*Qa?dcP>WwV!qqes)kw9ueikqBv0m;ymO zwURVB`?Padh^o{^14kmH(5X6$`B!?My(qM5qe(x^6366KAdpyRF{$*rg@pw$Ok*&p z3V8k4ifvwAsfGgo~#B8>Z$>LGz95R(nrn1NsCWXqRG8j|_oeHLoXbcXO!6CD_ zWR8H!6Vrtd6PD!_R1AzREKzw3o=BolC_yq^t1)UdI*l5PAFcI2zc4TaDqbLV#Y$f< zHg7yH9bpaIK!E|kW~0?&vSB05_6A~;#emAB5-Oe5H^y!qstSklVM61cRWMNv6|NCdEe zD`qo<)D_;sJZ)rrrlp76I*@Qb;j;@jK0AALT8s=1^ffg#+iW%}oi@KPKR7(xKRBSb znz5528kt4|4%JI#WQn+sPVb_Sd+Ah~PM4gXc|I<)bewi2so>xLusLFj)7OqpA8mKK z<`YmIzNaocswRBT74Lxe9kv~Ib$=KboEs8Z7_ze_V(%#@mxBQzX~aI<;A~7@rOa&W zuBlD+@NEpcV{RUwI6B_< z4rq?pUmp&1xH)_uDPmt;SX5=`?vjw5WudzYLn4p3xNUu9tD|R_XTW|tw;&9X82;s#FT4Qih^(`$+Kf z83sK_L~9IK63YlwT4!Bdo1QhVnpCJ9X0gT7OXA*15ouJ~G_E4eYDx1t(wv?&uO-f^ z8>Un><7m~0qHI83)F&_KmF09vGuoxeEpS{D^q@gG2pnmn2-9IP!z5;av=;EFpM}KFv(uP8mtwp8 zno+SC0~lzpL1k9iOHDiwNQ>x(hJriaf16!R%szLYawtQ3xn6j#0y$GX_Hzz-dPZwN zr4l|Y52Vv5~0Ms10UQ zTT3hYY!|r{8 zu$u5FeAu4q@I6<213%oh{X18;n1JA%kjR|ii0ZJYNB+SFwrsndlt)-(KPspgqjCfy zLHe$!=FmM2JNGw4MAd~yKMn}`&fPs@M`%&-&JVUZW(0;dN9?N)kFF1kZi(2R8Wj4e zqvIhL*MgvM@cppprtrOK0b$2oJdU}#fA8%2;Wo$MOUCO^QH;Q9 ztwv)stOF^llnzBkr!uQsnbV6F^s5R60MEdMZj;5g$YWb&k6S>w|4}pis9BfTJ$>MCj_9~hsQ zpIxD^aD`-^h>a+^N5WPKHv6vq9|L_aV75V{G8!}>;e_J?epFRCg5n0z!hS_wk36eWmf9vwXoep(Lig%L zH>y<0or8CAF)2AE`PnuDjJe0y46uk|5`d(%g*~sO&5-i5w48Y}s48J2O2FuL15*53 z9kyZ&TVy<|g7*TwLW@ddR!hukiA9T;H4u1Fi_I#bS-~^QxmMaVFZ1rL<40SDhOfQ* z_2dV4xjz>3PL_y&ESor!Ni$E1mwbPi1-&8vRe zF@$eyHrZ^8WO7YarOjqzaXDSxU0RDtp#?6q1XWTY(Kv_QhtbJ^yKMl z_x8N~;mfbMY&e1nNQUu_7Fstt?2;~R9`!|Rq`U};!nMo{=ao!#>T z!+ZAn1%_XUc%PB{tosV&pIzK{ZrvWR#liQbKW*LO;?uoC zWh-SA7@{ITe)%dzN!M8A(5wVi$keJnGJ`ZYVleC1cEtY{V5L@LvYPS3O9kb4t8Eq4 z>y;Yy;?knVWCX`V378&g3C--5CAUcv z+rZwO$0RVu0IXRW(*!?gR6TB;yjU_S;e*X@kU$RNa4{-_RdQIPkm_})!K_2o zav_t(7#L*|dqjjm1!+pxwPfg{=mw~|L8^9uqU~SNb}wl<7c}j2nwB|D)3k;-4FIm2 zP}hvBtH;ptA+%(`o<5LebV#4HNMoCz2lYr|i|k?3;>ql^C&l+3#Mm?t1|bu)A7Koe z`70*&f=#eQALtxvthFiF&ssZZrZpV~BtOe+*%`(#exbsIg}y?wN@P(>K=YOc0`zJj zixxc18i`pgwkWtpIe&G$LzH><+VLYfwS?QRe=+ddFLMVIs9$FYzbouJoJ5|Pl!-ZD zDKB<(jOi3Hoi^D(JaOiiE2;SngOdWamc{4CJ-jzJH(Op_DuQ7(c0^C6QFCNS zKZDjyrS#IMJv0hXV9LrABr0uc+Opa0fBoKJ(-yyP-2DmRy9wdZ_|Paq#J+1j0iQTI z9ddFxFdaI43wPE-?74mtRTH&ZdaH4H&@nVbSe7U%l-g{MW794mr7`_=gcA zUcKPy{nu?yPOmt=wBB{AYp`$d>j5Ee`GS$%{oaB5y#n@o`S0`a-)Glh%#nVc#9DKw zF=%(Yd4Y}&XU|=ZE)koy`fc3g?y%M8cmMYC*FT+SYE)E6LPcOQ1oKd3`|L_q>o8X# zhfw7lU!2$6AyXnMqfP;a7JhBsu)nJfTA9vxKd*vHqw1}zI9I*{@&nwtGMNmJNVG69-QV2VgsX4JtEo&WEl%E$ztMTbOO1lxy-fU_VxL@ba&LK!4bB4TjALMImsShT?rD!yG%*`uf*)3+_^ zx|g**%j%wGO)oHAKUq6~S&tpyd3DRIrfEjqIISW~sp=*Hz(9nD(b7R>QNJ>;N0!+s zO#>n(mMeI!u2s`KmwD*{+=LJvS>pp>xcLW!OMrMcZJn+&h zckafwEVFJrDNH4eHcTuR{p~<~U}OW9c`6Rsb=bx2sGG-GPoMk#JK_REVgiHi_y<*m z?j~Z|i`l-$@V#}B``_N?{JEpk6))e*UOs!aIKHypWsCC;-=J56BEV&Bzu-4~gJ1Uz z0*h~a0)cr2?DzEB=i#>(VC?g2G3lB7f~Sk;ZYTF$+nhr;Z}Hs#1{>VAZt?o}e}ARE zyHlXkk|7BNhFD6)6qEHB-#*Rdi%}&5mgcn$O)oAPtR`?J)K2ej{Aaa!)zZ7n%&)Ao ztXdU1z0Pc2p^#-Lsxg^VdhjJ*D3B`UY6Dmdv`!jF(mv^iW9pmEB{r5-%QOlxB5u5& z#=l*yO72D;H)9IijKnm;4;qnsMC48bm|VG4r@Be(|0!*8cHV|9#99S=Tfkpdt3{_( zArc{dWqE#La=dSFu&ukVxud(GrM;%Pr=h*0meg6_(nVI^HNSRgXjIIhBVsNL=;a||zEr|TBz!3(z(iQ6R?8JCDVIuKB6cvI zHp@!;b+uEvCLpcq)>(DOyt->q)3XEs?pfCLkpaVfE83oAZO4M9ZC*o~RX5M5fdWsd z>nGGTV`}^;S~Y@}4WdN@%7R{bR+lubO_oGLVw>Rm4U#)`vdh&?xAGolNHy@)#*((8>-jY7d6T?Dnza3 z$>oz=PB)F(Lm~H2$-`Xcpjdq5+WkNLFNaNAeBX80e#Xfa7rc`Y5mg@^)wFZpad*#; z9k>6}+4Yc%`)Mzqn82XC;E1yDsGqjGe7pbk%I?v`{E9&azi(!)=;P0710o2K`|HD^ zn^O9ciyzg^-q6vh};N~= zAkL7*sDdk#;`(Qa{eyaw4oockMth(8&uZ=JY67l)VR_lOYEv7Gh(f-wxS+RK?EbS_ zujdN|DuW)?X{0K2ohH6z?w5kP*la3`3g*1^8nf9bMdj^TWwfg}O=2hVh$M|^hVM7R z_lU@C0&=qsxlxB)tAVf9C@$8toJ^uJX*Qe1VANZ!W~0df2H-WQR;@JZR7RajtCFkb zFe-ykM67^C3RonEgos!Gi3Jj&P{9A+jJxp!{(DPRVXAecpR1_>&H zAV@Ulw!3ZZw!2NOm=zEthayvCC~}abl5v;=o`_8>Hv({Ox*M(O3 z&wlp_`|Lv@5lI9*5rZKtEMVu6m^mb7b{;)Fhn}26PtIeek+^9TZW@K3oX3n#%)|35 z@>1bRuae{cj8A+NpYkvX_GfCrtDJ@m#8^u=8HwaliR@(}gG>PTz9AVFhed;i%yce` zCKj;7av=#dKV8y@Pp#+Yc1p{KCDnjPg>|FCrg1HR#h+#+5Ccm(=cS!UX%|xbWnTPw zPSQ3lYMBx@LkwIuww^)*01M0e*8rT|$;#J|QkIjS^ z=H_VBTm^@$=8(XYh=BzTZ=jnsuOv)s$aDR5HT3ym4O_b(ua*9PJ-Yc7!`ht(f|^a# zcBH}Tk^riH0rxW1Qr3!;y&`3=%GfJ1@BqOWB4iOwEm#tdeqKSq`kw#v!|l7#aj>kz zp&jmbTix!AoroMf^`hWyVrOGpRcb-uyQIA5(PgjSSG{?k`RLWh+!78RKhWO(EGZi? zf@+(=&QWPgTs9E6K^}2zVE85^b9r-AYCj%3NG5iZNNFt{K9_#nv~7=$uBncmnXZnJ z`=;&L2aYs?GS#8p+BxZf#aRP`b2|-h8=J-*I9zC9Q)XeCc)&7f>-I-~JZ|aklWR1y z0|PnE?p1p%>ulU=EkTF5(aI_1pw$I~o!1SFZtOJv$=K|k`M%VHmW@^*wQjU@{%Gae zV&&FkY>F616)Y-LhT!K*S+!M z9fnG=z@VX-462Bq*VvNO)V;{zQhA)(aa49|H>?IRz+eejJRVEH;}>!GMJ!kMRwMU9QeEZ>eTXqhA)fL zvrH16vrJ?Vam#o#jeup42rL?fv+nlJ=Q4$SwoENy@#u3kt@CdSd8r+e{C-hUKfh!^ zSUDuB8WGiwiW|lOyS7b(X4U68K)GK4h9UIMiaTZ`t<&O6o)&g)c zK)8se<`6|+5SqHo;$N@E!V&Q3oT?+2YV2>NS%k+~2fquvkag$Bru%otUp+*}JR!wB z6TzZhhMazU!bq4EcNpxRK3Y;_{Zb95RnH$Kk@e z2mLPpxM|0}fBEMf8+CW<=$Y&4nf*g=_YU2iFLv*3vIdP}t%9Q5(!S8bw!+d5am2pD z!mi5N>9+o^APcMEsp(aXrX7}*wa23FplzLvOReQvaa(KYRAuFK*Le2@eZz|eM)yq3 z3oUG0terpFxO}v6Yp`-Dwy=xafB5ch^T3@(c3TV#bPTrbJb1|Fgq=%(m80JgyOT%k zPFmRbSl9pzAF?_I9qjdn*w_PDz#dw|@a}ysCIG|E#*iutz<3?N+jkz>w0Vz?uJJ}4 z!wnmDY}&NzzyE(5GT?yie$tul? z1Ud_}?t~6PE)y#?>ZzXonMb+&*B_bB>sXOBTJ$o0D`WgxO8=#Vez$~muVU%@{G$8s z={%NPDi#X30xpZkqH~!*qyeFq%%DJ%3DP2iLS!zH7*rB{iAc`A(Aj|aR8!hsS@5wWvk{(JlbcwX6lxYu&cMO zSbMzp3u<}%^K|?ZPENEeKUSU}C(VtMWWVR9N2&7?qaw~EKaD}e<&;FH7G`BVOvr(E z47JZs7~^BMwzZ>}xC?qxT;<`ak~#mJOF`R?B|5S75xq z@rYBcC71vK%tOU(t))|wm1~tHP-VpLKU{8UUt{G|dc;2SpjFh~13#IXp3pbkx5aRq zp3z2K6TNNwO!hn5xcJ+<1llPNz}g1P&!u}A5BPhnqP z=CnlTBVu!_Gm4r@>)Su~A%2M(IrX$J;7PCVqmQn)-`#of_;pN8Ib5TWD8T%dxWXcU z-ZH3!QA(ienAR-KV!B%A8Y{s}m@*HtYYGy?(t?uu)dUX=qSg{=4iPA>1Qf8Yqy;dP zKd;L9Dm9RTs+3~2oVP0Hf}|Rx(@eFP4pIOYMroCopIV zo5c|F(F}Ie9P#J4bn_$5|N1XGw(B3-y~oMa+`-7y&S00-HvPk!w(Q@uY41OD&G+aT z-Z0o1Wqz>S!oJbU8Dzyr9BM7W?B|L3{!m@LFfZSx-r)kDfKrnK&DO3Bko2P2%B2w+ z@KzmhsIqi~AGS@}XZgnb;6u~Bw|AM|HZr-j%Ou#q(0ZG}_RYo{H<{~hJFwHl=D-mT zd)EMGk6;J4Kzr9fJLl83P60Mf{x%N&Rt|oa_P&;OzLs_;0g|;zwH2_@*>}Y2FAPJ8 zG^mF`F}#LhLsPpQhDSDS0Svrh!%kftgAM=CH`{MBNg?4lEV4-0J~Z^Yvda(qoM6F(1HWCB9K35F7R$r@MrDnC^V;g}?k*}->q*AF`H8VHEXJyQ;D|&lpA4gw2zpkCVdYN#Vx{Y1^!-11W2s=G6|7OFp6CA14ax zhT%2ih^Fc0c4XfWdS;e@!{Z6Wc_eybe7g6`z{i@_+RCQ9hcR6z?)RR2_}Tm3XXo2* z&fLzfZH{~UMxzla1ymKE1YGO@hH-#tmll*@COpYs80{^{Rxiyemmpdu&8tZBD&o9~ z4BVt4HpW1^hd33R04yxIWT4hqPodZIXRcZyR4YX*3V~YATaj^B0f3=0SWE{)V?ISm z!-~IDi^`H$Ycdq&DZ-*eK}m|JCQtIQOwv&;{Zc3EYLIm`NP0hNI%}SX1f)cMsQpmT zo?Dd%%Z$uHlntY6hi8WtFjI4L-NQq*1LHA>7Po-2Teq5+?XkCZ@`o~2pCdMa+4fm@ znC^GhH@4ool^&A(unV zcWv8Zy4m1>p{dP&3wIkQUnh?I?8K_J{ftijkh;9EAfv^%*@#m5g>A3p#N zkwKlUgYIBNyw4RR(&nHk4ULyT7Hwc;y?N_?9X()ew{@%O|N5s*o_@gukpRPFkOb`L z{F0n6!*nsPYG^j8xxa606xb zWY>1qf60wZZuWbza4~!SVkY)dHtte3_EI+bJZwH9Z8|(&xD7hjpz8DshpB9xH|0*N# zO=jZTte9shQBM=2Ba@;dQ(|AF#l1+6j!aIAfk)nZ{p{YGcMs!U{r2wp?H7N3|Mze>q_ogDM)^T>ej-`iZyx4(-x@uc4W zMOWDSvCAoQ-@`C>^A;bMQD4_EJ~VMNKMUc#!cuJ|JS?sm1qzLZF+u&9sBuExI;Uuv zWmfe~=2ms*mh>QMMmjq&3$x@U5>LpJspTq2a=8Mj!PP1Sw4NoC%Oye~pTl7fwtXJC z5s&{dbLvv;d_;7z|Lx?Ws)U3jjhdqdLmkQrm$%dWWbb;~#_5!W?TP&s9wuh?+w>3VZQi#*clS;`rU zdiyTtZAQB`8|!T~-)3~g?4XOK?Qwh8(@q}2&YmGoo*_;iq0XKm;MpVC>1eQ%`x!^K zGY)P+cCLZ8fQ{EMY-R6fY3F;y79`b&t&bnHI=0`^3t;$=hc=P^+h7S=58i2NyKSe1 zj-IKmuHlv~rvLrF>D+qsiXs;i`P{zQncq{3dU3=d9R5vpcU8~85`9^!QVGGHpoA?J zvqU19N+Fa=03an|zE~tt$h$hfFgYBtN(IsH3KoZ>(w4#Yf(&d6@g*V(jmDEo*

    x~AfF^_ZXbj(fB= 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 B8Pxy) literal 0 HcmV?d00001 diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-normal-3.jpg b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-3.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/comic-normal-3.jpg rename to Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-3.jpg diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-3_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-3_baseline.png new file mode 100644 index 0000000000000000000000000000000000000000..9988185beb2362aca366e7f4a8bfdc4a53f6fa2c GIT binary patch literal 288418 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�%s>BHydPhkDC`W^j-j{Kv%!z_6og>?4VSH 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 TZnj%$Y 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≪8_nAC(gOpB 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

  • /// /// + /// /// /// Message is not localized - public async Task CreateAnnotation(int userId, AnnotationDto dto) + public async Task CreateAnnotation(int userId, AnnotationDto dto, CancellationToken ct = default) { try { @@ -62,7 +52,7 @@ public class AnnotationService( throw new KavitaException("invalid-payload"); } - var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId) ?? throw new KavitaException("chapter-doesnt-exist"); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId, ct: ct) ?? throw new KavitaException("chapter-doesnt-exist"); var chapterTitle = string.Empty; try @@ -101,9 +91,9 @@ public class AnnotationService( }; unitOfWork.AnnotationRepository.Attach(annotation); - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); - return (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id))!; + return (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id, ct))!; } catch (Exception ex) { @@ -117,13 +107,14 @@ public class AnnotationService( ///
    /// /// + /// /// /// Message is not localized - public async Task UpdateAnnotation(int userId, AnnotationDto dto) + public async Task UpdateAnnotation(int userId, AnnotationDto dto, CancellationToken ct = default) { try { - var annotation = await unitOfWork.AnnotationRepository.GetAnnotation(dto.Id); + var annotation = await unitOfWork.AnnotationRepository.GetAnnotation(dto.Id, ct); if (annotation == null || annotation.AppUserId != userId) throw new KavitaException("denied"); annotation.ContainsSpoiler = dto.ContainsSpoiler; @@ -134,12 +125,13 @@ public class AnnotationService( unitOfWork.AnnotationRepository.Update(annotation); - if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync()) + if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync(ct)) { - dto = (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id))!; + dto = (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id, ct))!; await eventHub.SendMessageToAsync(MessageFactory.AnnotationUpdate, MessageFactory.AnnotationUpdateEvent(dto), userId); + return dto; } } catch (Exception ex) @@ -150,7 +142,8 @@ public class AnnotationService( throw new KavitaException("generic-error"); } - public async Task ExportAnnotations(int userId, IList? annotationIds = null) + public async Task ExportAnnotations(int userId, IList? annotationIds = null, + CancellationToken ct = default) { try { @@ -158,11 +151,11 @@ public class AnnotationService( IList annotations; if (annotationIds == null) { - annotations = await unitOfWork.AnnotationRepository.GetFullAnnotationsByUserIdAsync(userId); + annotations = await unitOfWork.AnnotationRepository.GetFullAnnotationsByUserIdAsync(userId, ct); } else { - annotations = await unitOfWork.AnnotationRepository.GetFullAnnotations(userId, annotationIds); + annotations = await unitOfWork.AnnotationRepository.GetFullAnnotations(userId, annotationIds, ct); } var userIds = annotations.Select(a => a.UserId) @@ -170,12 +163,12 @@ public class AnnotationService( .ToList(); // Get users with preferences for highlight colors - var users = (await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences, false)) + var users = (await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences, false, ct)) .Where(u => userIds.Contains(u.Id)) .ToDictionary(u => u.Id, u => u); // Get settings for hostname - var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); var hostname = !string.IsNullOrWhiteSpace(settings.HostName) ? settings.HostName : $"http://localhost:{Configuration.Port}"; // Group annotations by series, then by volume diff --git a/API/Services/ArchiveService.cs b/Kavita.Services/ArchiveService.cs similarity index 63% rename from API/Services/ArchiveService.cs rename to Kavita.Services/ArchiveService.cs index 4285769bf..db1bbff0b 100644 --- a/API/Services/ArchiveService.cs +++ b/Kavita.Services/ArchiveService.cs @@ -7,68 +7,33 @@ using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; using System.Xml.Serialization; -using API.Data.Metadata; -using API.DTOs.Archive; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Archive; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using SharpCompress.Archives; using SharpCompress.Common; -namespace API.Services; - -#nullable enable - -public interface IArchiveService -{ - void ExtractArchive(string archivePath, string extractPath); - int GetNumberOfPagesFromArchive(string archivePath); - string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format, CoverImageSize size = CoverImageSize.Default); - bool IsValidArchive(string archivePath); - ComicInfo? GetComicInfo(string archivePath); - ArchiveLibrary CanOpen(string archivePath); - bool ArchiveNeedsFlattening(ZipArchive archive); - /// - /// Creates a zip file form the listed files and outputs to the temp folder. This will combine into one zip of multiple zips. - /// - /// List of files to be zipped up. Should be full file paths. - /// Temp folder name to use for preparing the files. Will be created and deleted - /// Path to the temp zip - /// - string CreateZipForDownload(IEnumerable files, string tempFolder); - /// - /// Creates a zip file form the listed files and outputs to the temp folder. This will extract each archive and combine them into one zip. - /// - /// List of files to be zipped up. Should be full file paths. - /// Temp folder name to use for preparing the files. Will be created and deleted - /// Path to the temp zip - /// - string CreateZipFromFoldersForDownload(IList files, string tempFolder, Func, Task> progressCallback); -} +namespace Kavita.Services; /// /// Responsible for manipulating Archive files. Used by and /// // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global -public class ArchiveService : IArchiveService +public class ArchiveService( + ILogger logger, + IDirectoryService directoryService, + IImageService imageService, + IMediaErrorService mediaErrorService) + : IArchiveService { - private readonly ILogger _logger; - private readonly IDirectoryService _directoryService; - private readonly IImageService _imageService; - private readonly IMediaErrorService _mediaErrorService; private const string ComicInfoFilename = "ComicInfo.xml"; - public ArchiveService(ILogger logger, IDirectoryService directoryService, - IImageService imageService, IMediaErrorService mediaErrorService) - { - _logger = logger; - _directoryService = directoryService; - _imageService = imageService; - _mediaErrorService = mediaErrorService; - } - /// /// Checks if a File can be opened. Requires up to 2 opens of the filestream. /// @@ -76,9 +41,9 @@ public class ArchiveService : IArchiveService /// public virtual ArchiveLibrary CanOpen(string archivePath) { - if (string.IsNullOrEmpty(archivePath) || !(File.Exists(archivePath) && Tasks.Scanner.Parser.Parser.IsArchive(archivePath) || Tasks.Scanner.Parser.Parser.IsEpub(archivePath))) return ArchiveLibrary.NotSupported; + if (string.IsNullOrEmpty(archivePath) || !(File.Exists(archivePath) && Parser.IsArchive(archivePath) || Parser.IsEpub(archivePath))) return ArchiveLibrary.NotSupported; - var ext = _directoryService.FileSystem.Path.GetExtension(archivePath).ToUpper(); + var ext = directoryService.FileSystem.Path.GetExtension(archivePath).ToUpper(); if (ext.Equals(".CBR") || ext.Equals(".RAR")) return ArchiveLibrary.SharpCompress; try @@ -90,7 +55,7 @@ public class ArchiveService : IArchiveService { try { - using var a1 = ArchiveFactory.Open(archivePath); + using var a1 = ArchiveFactory.OpenArchive(archivePath); return ArchiveLibrary.SharpCompress; } catch (Exception) @@ -104,7 +69,7 @@ public class ArchiveService : IArchiveService { if (!IsValidArchive(archivePath)) { - _logger.LogError("Archive {ArchivePath} could not be found", archivePath); + logger.LogError("Archive {ArchivePath} could not be found", archivePath); return 0; } @@ -116,29 +81,29 @@ public class ArchiveService : IArchiveService case ArchiveLibrary.Default: { using var archive = ZipFile.OpenRead(archivePath); - return archive.Entries.Count(e => !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(e.FullName) && Tasks.Scanner.Parser.Parser.IsImage(e.FullName)); + return archive.Entries.Count(e => !Parser.HasBlacklistedFolderInPath(e.FullName) && Parser.IsImage(e.FullName)); } case ArchiveLibrary.SharpCompress: { - using var archive = ArchiveFactory.Open(archivePath); + using var archive = ArchiveFactory.OpenArchive(archivePath); return archive.Entries.Count(entry => !entry.IsDirectory && - !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) - && Tasks.Scanner.Parser.Parser.IsImage(entry.Key)); + !Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) + && Parser.IsImage(entry.Key)); } case ArchiveLibrary.NotSupported: - _logger.LogWarning("[GetNumberOfPagesFromArchive] This archive cannot be read: {ArchivePath}. Defaulting to 0 pages", archivePath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "File format not supported", string.Empty); + logger.LogWarning("[GetNumberOfPagesFromArchive] This archive cannot be read: {ArchivePath}. Defaulting to 0 pages", archivePath); + mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "File format not supported", string.Empty); return 0; default: - _logger.LogWarning("[GetNumberOfPagesFromArchive] There was an exception when reading archive stream: {ArchivePath}. Defaulting to 0 pages", archivePath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "File format not supported", string.Empty); + logger.LogWarning("[GetNumberOfPagesFromArchive] There was an exception when reading archive stream: {ArchivePath}. Defaulting to 0 pages", archivePath); + mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "File format not supported", string.Empty); return 0; } } catch (Exception ex) { - _logger.LogWarning(ex, "[GetNumberOfPagesFromArchive] There was an exception when reading archive stream: {ArchivePath}. Defaulting to 0 pages", archivePath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, + logger.LogWarning(ex, "[GetNumberOfPagesFromArchive] There was an exception when reading archive stream: {ArchivePath}. Defaulting to 0 pages", archivePath); + mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "This archive cannot be read or not supported", ex); return 0; } @@ -152,9 +117,9 @@ public class ArchiveService : IArchiveService public static string? FindFolderEntry(IEnumerable entryFullNames) { var result = entryFullNames - .Where(path => !(Path.EndsInDirectorySeparator(path) || Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith))) + .Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.MacOsMetadataFileStartsWith))) .OrderByNatural(Path.GetFileNameWithoutExtension) - .FirstOrDefault(Tasks.Scanner.Parser.Parser.IsCoverImage); + .FirstOrDefault(Parser.IsCoverImage); return string.IsNullOrEmpty(result) ? null : result; } @@ -170,7 +135,7 @@ public class ArchiveService : IArchiveService // First check if there are any files that are not in a nested folder before just comparing by filename. This is needed // because NaturalSortComparer does not work with paths and doesn't seem 001.jpg as before chapter 1/001.jpg. var fullNames = entryFullNames - .Where(path => !(Path.EndsInDirectorySeparator(path) || Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith)) && Tasks.Scanner.Parser.Parser.IsImage(path)) + .Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.MacOsMetadataFileStartsWith)) && Parser.IsImage(path)) .OrderByNatural(c => c.GetFullPathWithoutExtension()) .ToList(); if (fullNames.Count == 0) return null; @@ -207,7 +172,7 @@ public class ArchiveService : IArchiveService /// /// Generates byte array of cover image. - /// Given a path to a compressed file , will ensure the first image (respects directory structure) is returned unless + /// Given a path to a compressed file , will ensure the first image (respects directory structure) is returned unless /// a folder/cover.(image extension) exists in the the compressed file (if duplicate, the first is chosen) /// /// This skips over any __MACOSX folder/file iteration. @@ -234,11 +199,11 @@ public class ArchiveService : IArchiveService var entry = archive.Entries.Single(e => e.FullName == entryName); using var stream = entry.Open(); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); + return imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.SharpCompress: { - using var archive = ArchiveFactory.Open(archivePath); + using var archive = ArchiveFactory.OpenArchive(archivePath); var entryNames = archive.Entries.Where(archiveEntry => !archiveEntry.IsDirectory).Select(e => e.Key).ToList(); var entryName = FindCoverImageFilename(archivePath, entryNames); @@ -246,21 +211,21 @@ public class ArchiveService : IArchiveService var entry = archive.Entries.Single(e => e.Key == entryName); using var stream = entry.OpenEntryStream(); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); + return imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.NotSupported: - _logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath); + logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath); return string.Empty; default: - _logger.LogWarning("[GetCoverImage] There was an exception when reading archive stream: {ArchivePath}. Defaulting to no cover image", archivePath); + logger.LogWarning("[GetCoverImage] There was an exception when reading archive stream: {ArchivePath}. Defaulting to no cover image", archivePath); return string.Empty; } } catch (Exception ex) { - _logger.LogWarning(ex, "[GetCoverImage] There was an exception when reading archive stream: {ArchivePath}. Defaulting to no cover image", archivePath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, - "This archive cannot be read or not supported", ex); // TODO: Localize this + logger.LogWarning(ex, "[GetCoverImage] There was an exception when reading archive stream: {ArchivePath}. Defaulting to no cover image", archivePath); + mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, + "This archive cannot be read or not supported", ex); // TODO: Localize this. Which user? } return string.Empty; @@ -289,7 +254,7 @@ public class ArchiveService : IArchiveService // Sometimes ZipArchive will list the directory and others it will just keep it in the FullName return archive.Entries.Count > 0 && !Path.HasExtension(archive.Entries[0].FullName) || - archive.Entries.Any(e => e.FullName.Contains(Path.AltDirectorySeparatorChar) && !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(e.FullName)); + archive.Entries.Any(e => e.FullName.Contains(Path.AltDirectorySeparatorChar) && !Parser.HasBlacklistedFolderInPath(e.FullName)); } /// @@ -303,31 +268,31 @@ public class ArchiveService : IArchiveService { var dateString = DateTime.UtcNow.ToShortDateString().Replace("/", "_"); - var tempLocation = Path.Join(_directoryService.TempDirectory, $"{tempFolder}_{dateString}"); - var potentialExistingFile = _directoryService.FileSystem.FileInfo.New(Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip")); + var tempLocation = Path.Join(directoryService.TempDirectory, $"{tempFolder}_{dateString}"); + var potentialExistingFile = directoryService.FileSystem.FileInfo.New(Path.Join(directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip")); if (potentialExistingFile.Exists) { // A previous download exists, just return it immediately return potentialExistingFile.FullName; } - _directoryService.ExistOrCreate(tempLocation); + directoryService.ExistOrCreate(tempLocation); - if (!_directoryService.CopyFilesToDirectory(files, tempLocation)) + if (!directoryService.CopyFilesToDirectory(files, tempLocation)) { throw new KavitaException("bad-copy-files-for-download"); } - var zipPath = Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip"); + var zipPath = Path.Join(directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip"); try { ZipFile.CreateFromDirectory(tempLocation, zipPath); // Remove the folder as we have the zip - _directoryService.ClearAndDeleteDirectory(tempLocation); + directoryService.ClearAndDeleteDirectory(tempLocation); } catch (AggregateException ex) { - _logger.LogError(ex, "There was an issue creating temp archive"); + logger.LogError(ex, "There was an issue creating temp archive"); throw new KavitaException("generic-create-temp-archive"); } @@ -338,7 +303,7 @@ public class ArchiveService : IArchiveService { var dateString = DateTime.UtcNow.ToShortDateString().Replace("/", "_"); - var potentialExistingFile = _directoryService.FileSystem.FileInfo.New(Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.cbz")); + var potentialExistingFile = directoryService.FileSystem.FileInfo.New(Path.Join(directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.cbz")); if (potentialExistingFile.Exists) { // A previous download exists, just return it immediately @@ -346,32 +311,32 @@ public class ArchiveService : IArchiveService } // Extract all the files to a temp directory and create zip on that - var tempLocation = Path.Join(_directoryService.TempDirectory, $"{tempFolder}_{dateString}"); + var tempLocation = Path.Join(directoryService.TempDirectory, $"{tempFolder}_{dateString}"); var totalFiles = files.Count + 1; var count = 1f; try { - _directoryService.ExistOrCreate(tempLocation); + directoryService.ExistOrCreate(tempLocation); foreach (var path in files) { - var tempPath = Path.Join(tempLocation, _directoryService.FileSystem.Path.GetFileNameWithoutExtension(_directoryService.FileSystem.FileInfo.New(path).Name)); + var tempPath = Path.Join(tempLocation, directoryService.FileSystem.Path.GetFileNameWithoutExtension(directoryService.FileSystem.FileInfo.New(path).Name)); // Image series need different handling - if (Tasks.Scanner.Parser.Parser.IsImage(path)) + if (Parser.IsImage(path)) { - var parentDirectory = _directoryService.FileSystem.DirectoryInfo.New(path).Parent?.Name; - tempPath = Path.Join(tempLocation, parentDirectory ?? _directoryService.FileSystem.FileInfo.New(path).Name); + var parentDirectory = directoryService.FileSystem.DirectoryInfo.New(path).Parent?.Name; + tempPath = Path.Join(tempLocation, parentDirectory ?? directoryService.FileSystem.FileInfo.New(path).Name); } - if (Tasks.Scanner.Parser.Parser.IsArchive(path)) + if (Parser.IsArchive(path)) { // Archives don't need to be put into a subdirectory of the same name - tempPath = _directoryService.GetParentDirectoryName(tempPath); + tempPath = directoryService.GetParentDirectoryName(tempPath); } - progressCallback(Tuple.Create(_directoryService.FileSystem.FileInfo.New(path).Name, (1.0f * totalFiles) / count)); + progressCallback(Tuple.Create(directoryService.FileSystem.FileInfo.New(path).Name, (1.0f * totalFiles) / count)); - _directoryService.CopyFileToDirectory(path, tempPath); + directoryService.CopyFileToDirectory(path, tempPath); count++; } } @@ -380,16 +345,16 @@ public class ArchiveService : IArchiveService throw new KavitaException("bad-copy-files-for-download"); } - var zipPath = Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.cbz"); + var zipPath = Path.Join(directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.cbz"); try { ZipFile.CreateFromDirectory(tempLocation, zipPath); // Remove the folder as we have the zip - _directoryService.ClearAndDeleteDirectory(tempLocation); + directoryService.ClearAndDeleteDirectory(tempLocation); } catch (AggregateException ex) { - _logger.LogError(ex, "There was an issue creating temp archive"); + logger.LogError(ex, "There was an issue creating temp archive"); throw new KavitaException("generic-create-temp-archive"); } @@ -406,22 +371,22 @@ public class ArchiveService : IArchiveService { if (!File.Exists(archivePath)) { - _logger.LogWarning("Archive {ArchivePath} could not be found", archivePath); + logger.LogWarning("Archive {ArchivePath} could not be found", archivePath); return false; } - if (Tasks.Scanner.Parser.Parser.IsArchive(archivePath)) return true; + if (Parser.IsArchive(archivePath)) return true; - _logger.LogWarning("Archive {ArchivePath} is not a valid archive", archivePath); + logger.LogWarning("Archive {ArchivePath} is not a valid archive", archivePath); return false; } private static bool IsComicInfoArchiveEntry(string? fullName, string name) { if (fullName == null) return false; - return !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(fullName) + return !Parser.HasBlacklistedFolderInPath(fullName) && name.EndsWith(ComicInfoFilename, StringComparison.OrdinalIgnoreCase) - && !name.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith); + && !name.StartsWith(Parser.MacOsMetadataFileStartsWith); } /// @@ -456,7 +421,7 @@ public class ArchiveService : IArchiveService } case ArchiveLibrary.SharpCompress: { - using var archive = ArchiveFactory.Open(archivePath); + using var archive = ArchiveFactory.OpenArchive(archivePath); var entry = archive.Entries.FirstOrDefault(entry => entry.Key == ComicInfoFilename) ?? archive.Entries.FirstOrDefault(entry => IsComicInfoArchiveEntry(Path.GetDirectoryName(entry.Key), entry.Key)); @@ -471,10 +436,10 @@ public class ArchiveService : IArchiveService break; } case ArchiveLibrary.NotSupported: - _logger.LogWarning("[GetComicInfo] This archive cannot be read: {ArchivePath}", archivePath); + logger.LogWarning("[GetComicInfo] This archive cannot be read: {ArchivePath}", archivePath); return null; default: - _logger.LogWarning( + logger.LogWarning( "[GetComicInfo] There was an exception when reading archive stream: {ArchivePath}", archivePath); return null; @@ -482,8 +447,8 @@ public class ArchiveService : IArchiveService } catch (Exception ex) { - _logger.LogWarning(ex, "[GetComicInfo] There was an exception when reading archive stream: {Filepath}", archivePath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, + logger.LogWarning(ex, "[GetComicInfo] There was an exception when reading archive stream: {Filepath}", archivePath); + mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "This archive cannot be read or not supported", ex); } @@ -507,7 +472,9 @@ public class ArchiveService : IArchiveService if (reader == null) return null; var info = (ComicInfo?) serializer.Deserialize(reader); - ComicInfo.CleanComicInfo(info); + + info.CleanComicInfo(); + return info; } @@ -515,7 +482,7 @@ public class ArchiveService : IArchiveService private void ExtractArchiveEntities(IEnumerable entries, string extractPath) { - _directoryService.ExistOrCreate(extractPath); + directoryService.ExistOrCreate(extractPath); // TODO: Look into a Parallel.ForEach foreach (var entry in entries) { @@ -535,8 +502,8 @@ public class ArchiveService : IArchiveService archive.ExtractToDirectory(extractPath, true); if (!needsFlattening) return; - _logger.LogDebug("Extracted archive is nested in root folder, flattening..."); - _directoryService.Flatten(extractPath); + logger.LogDebug("Extracted archive is nested in root folder, flattening..."); + directoryService.Flatten(extractPath); } /// @@ -551,11 +518,11 @@ public class ArchiveService : IArchiveService { if (!IsValidArchive(archivePath)) return; - if (_directoryService.FileSystem.Directory.Exists(extractPath)) return; + if (directoryService.FileSystem.Directory.Exists(extractPath)) return; - if (!_directoryService.FileSystem.File.Exists(archivePath)) + if (!directoryService.FileSystem.File.Exists(archivePath)) { - _logger.LogError("{Archive} does not exist on disk", archivePath); + logger.LogError("{Archive} does not exist on disk", archivePath); throw new KavitaException($"{archivePath} does not exist on disk"); } @@ -574,29 +541,29 @@ public class ArchiveService : IArchiveService } case ArchiveLibrary.SharpCompress: { - using var archive = ArchiveFactory.Open(archivePath); + using var archive = ArchiveFactory.OpenArchive(archivePath); ExtractArchiveEntities(archive.Entries.Where(entry => !entry.IsDirectory - && !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) - && Tasks.Scanner.Parser.Parser.IsImage(entry.Key)), extractPath); + && !Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) + && Parser.IsImage(entry.Key)), extractPath); break; } case ArchiveLibrary.NotSupported: - _logger.LogWarning("[ExtractArchive] This archive cannot be read: {ArchivePath}", archivePath); + logger.LogWarning("[ExtractArchive] This archive cannot be read: {ArchivePath}", archivePath); return; default: - _logger.LogWarning("[ExtractArchive] There was an exception when reading archive stream: {ArchivePath}", archivePath); + logger.LogWarning("[ExtractArchive] There was an exception when reading archive stream: {ArchivePath}", archivePath); return; } } catch (Exception ex) { - _logger.LogWarning(ex, "[ExtractArchive] There was a problem extracting {ArchivePath} to {ExtractPath}",archivePath, extractPath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, + logger.LogWarning(ex, "[ExtractArchive] There was a problem extracting {ArchivePath} to {ExtractPath}",archivePath, extractPath); + mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "This archive cannot be read or not supported", ex); throw new KavitaException( $"There was an error when extracting {archivePath}. Check the file exists, has read permissions or the server OS can support all path characters."); } - _logger.LogDebug("Extracted archive to {ExtractPath} in {ElapsedMilliseconds} milliseconds", extractPath, sw.ElapsedMilliseconds); + logger.LogDebug("Extracted archive to {ExtractPath} in {ElapsedMilliseconds} milliseconds", extractPath, sw.ElapsedMilliseconds); } } diff --git a/Kavita.Services/AuthKeyService.cs b/Kavita.Services/AuthKeyService.cs new file mode 100644 index 000000000..8df2b1d1d --- /dev/null +++ b/Kavita.Services/AuthKeyService.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class AuthKeyService(IDataContext context, ILogger logger, HybridCache cache) : IAuthKeyService +{ + public async Task UpdateLastAccessedAsync(string authKey, CancellationToken ct = default) + { + logger.LogTrace("Updating last accessed Auth key: {AuthKey}", authKey); + await context.AppUserAuthKey + .Where(k => k.Key == authKey) + .ExecuteUpdateAsync(s => + s.SetProperty(k => k.LastAccessedAtUtc, DateTime.UtcNow), cancellationToken: ct); + } + + public async Task InvalidateAsync(string keyValue, CancellationToken cancellationToken = default) + { + var cacheKey = CreateCacheKey(keyValue); + await cache.RemoveAsync(cacheKey, cancellationToken); + } + + public string CreateCacheKey(string keyValue) + { + return $"authKey_{keyValue}"; + } +} diff --git a/Kavita.Services/BackupService.cs b/Kavita.Services/BackupService.cs new file mode 100644 index 000000000..3f531b55c --- /dev/null +++ b/Kavita.Services/BackupService.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common.EnvironmentInfo; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class BackupService( + ILogger logger, + IUnitOfWork unitOfWork, + IDirectoryService directoryService, + IEventHub eventHub) + : IBackupService +{ + private readonly IList _backupFiles = + [ + "appsettings.json" + ]; + + /// + /// Returns a list of all log files for Kavita + /// + /// If file rolling is enabled. Defaults to True. + /// + public IEnumerable GetLogFiles(bool rollFiles = true) + { + var multipleFileRegex = rollFiles ? @"\d*" : string.Empty; + var fi = directoryService.FileSystem.FileInfo.New(IBackupService.LogFile); + + var files = rollFiles + ? directoryService.GetFiles(directoryService.LogDirectory, + $@"{directoryService.FileSystem.Path.GetFileNameWithoutExtension(fi.Name)}{multipleFileRegex}\.log") + : [directoryService.FileSystem.Path.Join(directoryService.LogDirectory, "kavita.log")]; + return files; + } + + /// + /// Will back up anything that needs to be backed up. This includes logs, setting files, bare minimum cover images (just locked and first cover). + /// + /// + [AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)] + public async Task BackupDatabase(CancellationToken ct = default) + { + logger.LogInformation("Beginning backup of Database at {BackupTime}", DateTime.Now); + var backupDirectory = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value; + + logger.LogDebug("Backing up to {BackupDirectory}", backupDirectory); + if (!directoryService.ExistOrCreate(backupDirectory)) + { + logger.LogCritical("Could not write to {BackupDirectory}; aborting backup", backupDirectory); + await eventHub.SendMessageAsync(MessageFactory.Error, + MessageFactory.ErrorEvent("Backup Service Error",$"Could not write to {backupDirectory}; aborting backup"), ct: ct); + return; + } + + await SendProgress(0F, "Started backup", ct); + await SendProgress(0.1F, "Copying core files", ct); + + var dateString = $"{DateTime.UtcNow.ToShortDateString()}_{DateTime.UtcNow:s}Z".Replace("/", "_").Replace(":", "_"); + var zipPath = directoryService.FileSystem.Path.Join(backupDirectory, $"kavita_backup_v{BuildInfo.Version}_{dateString}.zip"); + + if (File.Exists(zipPath)) + { + logger.LogCritical("{ZipFile} already exists, aborting", zipPath); + await eventHub.SendMessageAsync(MessageFactory.Error, + MessageFactory.ErrorEvent("Backup Service Error",$"{zipPath} already exists, aborting"), ct: ct); + return; + } + + var tempDirectory = Path.Join(directoryService.TempDirectory, dateString); + directoryService.ExistOrCreate(tempDirectory); + directoryService.ClearDirectory(tempDirectory); + + await SendProgress(0.1F, "Backing up database", ct); + await BackupDatabaseFile(tempDirectory); + + await SendProgress(0.15F, "Copying config files", ct); + directoryService.CopyFilesToDirectory( + _backupFiles.Select(file => directoryService.FileSystem.Path.Join(directoryService.ConfigDirectory, file)), tempDirectory); + + // Copy any csv's as those are used for manual migrations + directoryService.CopyFilesToDirectory( + directoryService.GetFilesWithCertainExtensions(directoryService.ConfigDirectory, @"\.csv"), tempDirectory); + + await SendProgress(0.2F, "Copying logs", ct); + CopyLogsToBackupDirectory(tempDirectory); + + await SendProgress(0.25F, "Copying cover images", ct); + await CopyCoverImagesToBackupDirectory(tempDirectory); + + await SendProgress(0.35F, "Copying templates images", ct); + CopyTemplatesToBackupDirectory(tempDirectory); + + await SendProgress(0.5F, "Copying bookmarks", ct); + await CopyBookmarksToBackupDirectory(tempDirectory); + + await SendProgress(0.6F, "Copying Fonts", ct); + CopyFontsToBackupDirectory(tempDirectory); + + await SendProgress(0.75F, "Copying themes", ct); + CopyThemesToBackupDirectory(tempDirectory); + + await SendProgress(0.85F, "Copying favicons", ct); + CopyFaviconsToBackupDirectory(tempDirectory); + + try + { + await ZipFile.CreateFromDirectoryAsync(tempDirectory, zipPath); + } + catch (AggregateException ex) + { + logger.LogError(ex, "There was an issue when archiving library backup"); + } + + directoryService.ClearAndDeleteDirectory(tempDirectory); + logger.LogInformation("Database backup completed"); + await SendProgress(1F, "Completed backup", ct); + } + + private void CopyLogsToBackupDirectory(string tempDirectory) + { + var files = GetLogFiles(); + directoryService.CopyFilesToDirectory(files, directoryService.FileSystem.Path.Join(tempDirectory, "logs")); + } + + /// + /// Creates a backup of the SQLite database using VACUUM INTO command. + /// This method safely backs up the database while it's in use, without locking issues. + /// + /// The directory where the backup file will be created + private async Task BackupDatabaseFile(string tempDirectory) + { + var backupPath = directoryService.FileSystem.Path.Join(tempDirectory, "kavita.db"); + + // Validate the backup path to prevent SQL injection + // The path must not contain single quotes which could break the SQL command + if (backupPath.Contains('\'')) + { + throw new ArgumentException("Backup path contains invalid characters", nameof(tempDirectory)); + } + + try + { + // Use VACUUM INTO to create a safe backup of the database while it's running + // This creates a consistent snapshot without locking the main database + // Note: VACUUM INTO requires a literal path and cannot use SQL parameters + #pragma warning disable EF1002 // The backup path is validated above to not contain SQL injection characters + await unitOfWork.DataContext.Database.ExecuteSqlRawAsync($"VACUUM INTO '{backupPath}'"); + #pragma warning restore EF1002 + logger.LogDebug("Database backup created successfully at {BackupPath}", backupPath); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create database backup using VACUUM INTO at {BackupPath}", backupPath); + throw new InvalidOperationException($"Failed to create database backup at {backupPath}", ex); + } + } + + private void CopyFaviconsToBackupDirectory(string tempDirectory) + { + directoryService.CopyDirectoryToDirectory(directoryService.FaviconDirectory, directoryService.FileSystem.Path.Join(tempDirectory, "favicons")); + } + + private void CopyTemplatesToBackupDirectory(string tempDirectory) + { + directoryService.CopyDirectoryToDirectory(directoryService.TemplateDirectory, directoryService.FileSystem.Path.Join(tempDirectory, "templates")); + } + + private async Task CopyCoverImagesToBackupDirectory(string tempDirectory) + { + var outputTempDir = Path.Join(tempDirectory, "covers"); + directoryService.ExistOrCreate(outputTempDir); + + try + { + var seriesImages = await unitOfWork.SeriesRepository.GetLockedCoverImagesAsync(); + directoryService.CopyFilesToDirectory( + seriesImages.Select(s => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, s)), outputTempDir); + + var collectionTags = await unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync(); + directoryService.CopyFilesToDirectory( + collectionTags.Select(s => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, s)), outputTempDir); + + var chapterImages = await unitOfWork.ChapterRepository.GetCoverImagesForLockedChaptersAsync(); + directoryService.CopyFilesToDirectory( + chapterImages.Select(s => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, s)), outputTempDir); + + var volumeImages = await unitOfWork.VolumeRepository.GetCoverImagesForLockedVolumesAsync(); + directoryService.CopyFilesToDirectory( + volumeImages.Select(s => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, s)), outputTempDir); + + var libraryImages = await unitOfWork.LibraryRepository.GetAllCoverImagesAsync(); + directoryService.CopyFilesToDirectory( + libraryImages.Select(s => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, s)), outputTempDir); + + var readingListImages = await unitOfWork.ReadingListRepository.GetAllCoverImagesAsync(); + directoryService.CopyFilesToDirectory( + readingListImages.Select(s => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, s)), outputTempDir); + } + catch (IOException) + { + // Swallow exception. This can be a duplicate cover being copied as chapter and volumes can share same file. + } + + if (!directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) + { + directoryService.ClearAndDeleteDirectory(outputTempDir); + } + } + + private async Task CopyBookmarksToBackupDirectory(string tempDirectory) + { + var bookmarkDirectory = + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + + var outputTempDir = Path.Join(tempDirectory, "bookmarks"); + directoryService.ExistOrCreate(outputTempDir); + + try + { + directoryService.CopyDirectoryToDirectory(bookmarkDirectory, outputTempDir); + } + catch (IOException) + { + // Swallow exception. + } + + if (!directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) + { + directoryService.ClearAndDeleteDirectory(outputTempDir); + } + } + + private void CopyFontsToBackupDirectory(string tempDirectory) + { + var outputTempDir = Path.Join(tempDirectory, "fonts"); + directoryService.ExistOrCreate(outputTempDir); + + try + { + directoryService.CopyDirectoryToDirectory(directoryService.EpubFontDirectory, outputTempDir); + } + catch (IOException ex) + { + logger.LogWarning(ex, "Failed to copy fonts to backup directory '{OutputTempDir}'. Fonts will not be included in the backup.", outputTempDir); + } + + if (!directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) + { + directoryService.ClearAndDeleteDirectory(outputTempDir); + } + } + + private void CopyThemesToBackupDirectory(string tempDirectory) + { + var outputTempDir = Path.Join(tempDirectory, "themes"); + directoryService.ExistOrCreate(outputTempDir); + + try + { + directoryService.CopyDirectoryToDirectory(directoryService.SiteThemeDirectory, outputTempDir); + } + catch (IOException) + { + // Swallow exception. + } + + if (!directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) + { + directoryService.ClearAndDeleteDirectory(outputTempDir); + } + } + + private async Task SendProgress(float progress, string subtitle, CancellationToken ct = default) + { + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.BackupDatabaseProgressEvent(progress, subtitle), ct: ct); + } + +} diff --git a/API/Services/BookService.cs b/Kavita.Services/BookService.cs similarity index 88% rename from API/Services/BookService.cs rename to Kavita.Services/BookService.cs index 05c4c60cd..f0fae9493 100644 --- a/API/Services/BookService.cs +++ b/Kavita.Services/BookService.cs @@ -5,23 +5,29 @@ using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Metadata; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; -using API.Helpers; -using API.Services.Tasks.Metadata; using Docnet.Core; using Docnet.Core.Converters; using Docnet.Core.Models; using Docnet.Core.Readers; using ExCSS; using HtmlAgilityPack; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using Kavita.Services.Extensions; +using Kavita.Services.Helpers; +using Kavita.Services.Metadata; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using Microsoft.IO; using Nager.ArticleNumber; @@ -31,54 +37,23 @@ using VersOne.Epub; using VersOne.Epub.Options; using VersOne.Epub.Schema; -namespace API.Services; +namespace Kavita.Services; -#nullable enable - -public interface IBookService +public partial class BookService( + ILogger logger, + IDirectoryService directoryService, + IImageService imageService, + IMediaErrorService mediaErrorService, + IUnitOfWork unitOfWork) + : IBookService { - int GetNumberOfPages(string filePath); - string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); - ComicInfo? GetComicInfo(string filePath); - ParserInfo? ParseInfo(string filePath); - /// - /// Scopes styles to .reading-section and replaces img src to the passed apiBase - /// - /// - /// - /// If the stylesheetHtml contains Import statements, when scoping the filename, scope needs to be wrt filepath. - /// Book Reference, needed for if you expect Import statements - /// - Task ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book); - /// - /// Extracts a PDF file's pages as images to a target directory - /// - /// This method relies on Docnet which has explicit patches from Kavita for ARM support. This should only be used with Tachiyomi - /// - /// Where the files will be extracted to. If doesn't exist, will be created. - void ExtractPdfImages(string fileFilePath, string targetDirectory); - Task> GenerateTableOfContents(Chapter chapter); - Task GetBookPage(int page, int chapterId, string cachedEpubPath, string baseUrl, List ptocBookmarks, List annotations); - Task> CreateKeyToPageMappingAsync(EpubBookRef book); - Task?> GetWordCountsPerPage(string bookFilePath); - Task GetWordCountBetweenXPaths(string bookFilePath, string startXpath, int startPage, string endXpath, int endPage); - Task CopyImageToTempFromBook(int chapterId, BookmarkDto bookmarkDto, string cachedBookPath); - Task GetResourceAsync(string bookFilePath, string requestedKey); -} - -public partial class BookService : IBookService -{ - private readonly ILogger _logger; - private readonly IDirectoryService _directoryService; - private readonly IImageService _imageService; - private readonly IMediaErrorService _mediaErrorService; private readonly StylesheetParser _cssParser = new (); private static readonly RecyclableMemoryStreamManager StreamManager = new (); private const string CssScopeClass = ".book-content"; - private const string BookApiUrl = "book-resources?file="; + private const string BookApiUrl = "book-resources?apiKey={0}&file="; public const string BookReaderBodyScope = "//BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]"; - private readonly PdfComicInfoExtractor _pdfComicInfoExtractor; - private readonly IUnitOfWork _unitOfWork; + + private readonly PdfComicInfoExtractor _pdfComicInfoExtractor = new(logger, mediaErrorService); /// /// Setup the most lenient book parsing options possible as people have some really bad epubs @@ -125,16 +100,6 @@ public partial class BookService : IBookService } }; - public BookService(ILogger logger, IDirectoryService directoryService, IImageService imageService, IMediaErrorService mediaErrorService, IUnitOfWork unitOfWork) - { - _logger = logger; - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _imageService = imageService; - _mediaErrorService = mediaErrorService; - _pdfComicInfoExtractor = new PdfComicInfoExtractor(_logger, _mediaErrorService); - } - private static bool HasClickableHrefPart(HtmlNode anchor) { return anchor.GetAttributeValue("href", string.Empty).Contains('#') @@ -215,8 +180,10 @@ public partial class BookService : IBookService /// /// If the stylesheetHtml contains Import statements, when scoping the filename, scope needs to be wrt filepath. /// Book Reference, needed for if you expect Import statements + /// /// - public async Task ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book) + public async Task ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book, + CancellationToken ct = default) { // @Import statements will be handled by browser, so we must inline the css into the original file that request it, so they can be Scoped var prepend = filename.Length > 0 ? filename.Replace(Path.GetFileName(filename), string.Empty) : string.Empty; @@ -232,7 +199,7 @@ public partial class BookService : IBookService { key = prepend + key; } - if (!book.Content.AllFiles.TryGetLocalFileRefByKey(key, out var bookFile)) continue; + if (!book.Content.AllFiles.TryGetLocalFileRefByKey(key, out var bookFile) || bookFile == null) continue; var content = await bookFile.ReadContentAsBytesAsync(); importBuilder.Append(Encoding.UTF8.GetString(content)); @@ -254,7 +221,7 @@ public partial class BookService : IBookService if (string.IsNullOrEmpty(styleContent)) return string.Empty; - var stylesheet = await _cssParser.ParseAsync(styleContent); + var stylesheet = await _cssParser.ParseAsync(styleContent, ct); foreach (var styleRule in stylesheet.StyleRules) { if (styleRule.Selector.Text == CssScopeClass) continue; @@ -274,8 +241,9 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogError(ex, "There was an issue escaping css, likely due to an unsupported css rule"); + logger.LogError(ex, "There was an issue escaping css, likely due to an unsupported css rule"); } + return RemoveWhiteSpaceFromStylesheets($"{CssScopeClass} {styleContent}"); } @@ -340,7 +308,7 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogWarning(ex, "Failed to inject a text (ptoc) bookmark into file"); + logger.LogWarning(ex, "Failed to inject a text (ptoc) bookmark into file"); // Swallow } } @@ -462,7 +430,7 @@ public partial class BookService : IBookService } } - private async Task InlineStyles(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body) + private async Task InlineStyles(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, CancellationToken ct = default) { var inlineStyles = doc.DocumentNode.SelectNodes("//style"); // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract @@ -470,7 +438,7 @@ public partial class BookService : IBookService { foreach (var inlineStyle in inlineStyles) { - var styleContent = await ScopeStyles(inlineStyle.InnerHtml, apiBase, "", book); + var styleContent = await ScopeStyles(inlineStyle.InnerHtml, apiBase, "", book, ct); body.PrependChild(HtmlNode.CreateNode($"")); } } @@ -489,7 +457,7 @@ public partial class BookService : IBookService var correctedKey = book.Content.Css.Local.Select(s => s.Key).SingleOrDefault(s => s.EndsWith(key)); if (correctedKey == null) { - _logger.LogError("Epub is Malformed, key: {Key} is not matching OPF file", key); + logger.LogError("Epub is Malformed, key: {Key} is not matching OPF file", key); continue; } @@ -501,7 +469,7 @@ public partial class BookService : IBookService var cssFile = book.Content.Css.GetLocalFileRefByKey(key); var stylesheetHtml = await cssFile.ReadContentAsync(); - var styleContent = await ScopeStyles(stylesheetHtml, apiBase, cssFile.FilePath, book); + var styleContent = await ScopeStyles(stylesheetHtml, apiBase, cssFile.FilePath, book, ct); // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (styleContent != null) { @@ -510,9 +478,9 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogError(ex, "There was an error reading css file for inlining likely due to a key mismatch in metadata"); - await _mediaErrorService.ReportMediaIssueAsync(book.FilePath, MediaErrorProducer.BookService, - "There was an error reading css file for inlining likely due to a key mismatch in metadata", ex); + logger.LogError(ex, "There was an error reading css file for inlining likely due to a key mismatch in metadata"); + await mediaErrorService.ReportMediaIssueAsync(book.FilePath ?? string.Empty, MediaErrorProducer.BookService, + "There was an error reading css file for inlining likely due to a key mismatch in metadata", ex, ct); } } } @@ -550,7 +518,8 @@ public partial class BookService : IBookService .Select(l => l.Language) .FirstOrDefault()) }; - ComicInfo.CleanComicInfo(info); + + info.CleanComicInfo(); var weblinks = new List(); if (epubBook?.Schema.Package.Metadata.Identifiers != null) @@ -564,7 +533,7 @@ public partial class BookService : IBookService var isbn = identifier.Identifier.Replace("urn:isbn:", string.Empty).Replace("isbn:", string.Empty); if (!ArticleNumberHelper.IsValidIsbn10(isbn) && !ArticleNumberHelper.IsValidIsbn13(isbn)) { - _logger.LogDebug("[BookService] {File} has invalid ISBN number", filePath); + logger.LogDebug("[BookService] {File} has invalid ISBN number", filePath); continue; } @@ -683,8 +652,8 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing metadata: {FilePath}", filePath); - _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, + logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing metadata: {FilePath}", filePath); + mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, "There was an exception parsing metadata", ex); } finally @@ -697,17 +666,17 @@ public partial class BookService : IBookService private EpubBookRef? OpenEpubWithFallback(string filePath, EpubBookRef? epubBook) { - // TODO: Refactor this to use the Async version + // default: Refactor this to use the Async version try { epubBook = EpubReader.OpenBook(filePath, BookReaderOptions); } catch (Exception ex) { - _logger.LogWarning(ex, + logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing metadata, falling back to a more lenient parsing method: {FilePath}", filePath); - _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, + mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, "There was an exception parsing metadata", ex); } finally @@ -850,13 +819,13 @@ public partial class BookService : IBookService { if (!File.Exists(filePath)) { - _logger.LogWarning("[BookService] Book {EpubFile} could not be found", filePath); + logger.LogWarning("[BookService] Book {EpubFile} could not be found", filePath); return false; } if (Parser.IsBook(filePath)) return true; - _logger.LogWarning("[BookService] Book {EpubFile} is not a valid EPUB/PDF", filePath); + logger.LogWarning("[BookService] Book {EpubFile} is not a valid EPUB/PDF", filePath); return false; } @@ -877,8 +846,8 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogWarning(ex, "[BookService] There was an exception getting number of pages, defaulting to 0"); - _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, + logger.LogWarning(ex, "[BookService] There was an exception getting number of pages, defaulting to 0"); + mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, "There was an exception getting number of pages, defaulting to 0", ex); } @@ -902,7 +871,8 @@ public partial class BookService : IBookService return key.Replace("../", string.Empty); } - public async Task> CreateKeyToPageMappingAsync(EpubBookRef book) + public async Task> CreateKeyToPageMappingAsync(EpubBookRef book, + CancellationToken ct = default) { var dict = new Dictionary(); var pageCount = 0; @@ -918,13 +888,15 @@ public partial class BookService : IBookService return dict; } - public async Task?> GetWordCountsPerPage(string bookFilePath) + public async Task?> GetWordCountsPerPage(string bookFilePath, CancellationToken ct = default) { var ret = new Dictionary(); try { using var book = await EpubReader.OpenBookAsync(bookFilePath, LenientBookReaderOptions); - var mappings = await CreateKeyToPageMappingAsync(book); + if (book == null) return null; + + var mappings = await CreateKeyToPageMappingAsync(book, ct); var doc = new HtmlDocument {OptionFixNestedTags = true}; @@ -945,7 +917,7 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogError(ex, "There was an issue calculating word counts per page"); + logger.LogError(ex, "There was an issue calculating word counts per page"); return null; } @@ -959,7 +931,7 @@ public partial class BookService : IBookService // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (body == null) { - _logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath); + logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath); doc.DocumentNode.SelectSingleNode("/html").AppendChild(HtmlNode.CreateNode("")); body = doc.DocumentNode.SelectSingleNode("//html/body"); } @@ -1002,6 +974,7 @@ public partial class BookService : IBookService #region Count Letters Between XPaths + /// /// Counts the (estimated) words for a given book from a starting xpath (or beginning if null) to and ending xpath. /// May cross page boundaries @@ -1011,8 +984,10 @@ public partial class BookService : IBookService /// Page number of starting xpath /// /// Page number of ending xpath + /// /// - public async Task GetWordCountBetweenXPaths(string bookFilePath, string startXpath, int startPage, string endXpath, int endPage) + public async Task GetWordCountBetweenXPaths(string bookFilePath, string startXpath, int startPage, + string endXpath, int endPage, CancellationToken ct = default) { if (string.IsNullOrEmpty(endXpath)) return 0; if (endPage < startPage) return 0; @@ -1091,7 +1066,7 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogError(ex, "There was an issue calculating word counts between XPaths"); + logger.LogError(ex, "There was an issue calculating word counts between XPaths"); return 0; } @@ -1177,7 +1152,8 @@ public partial class BookService : IBookService return textNodes?.Sum(node => node.InnerText.Count(char.IsLetter)) ?? 0; } - public async Task CopyImageToTempFromBook(int chapterId, BookmarkDto bookmarkDto, string cachedBookPath) + public async Task CopyImageToTempFromBook(int chapterId, BookmarkDto bookmarkDto, string cachedBookPath, + CancellationToken ct = default) { using var book = await EpubReader.OpenBookAsync(cachedBookPath, LenientBookReaderOptions); @@ -1256,15 +1232,15 @@ public partial class BookService : IBookService } // Create temp directory for this chapter if it doesn't exist - var tempChapterDir = Path.Combine(_directoryService.TempDirectory, chapterId.ToString()); - _directoryService.ExistOrCreate(tempChapterDir); + var tempChapterDir = Path.Combine(directoryService.TempDirectory, chapterId.ToString()); + directoryService.ExistOrCreate(tempChapterDir); // Generate unique filename var uniqueFilename = $"{Guid.NewGuid()}{extension}"; var tempFilePath = Path.Combine(tempChapterDir, uniqueFilename); // Write the image to the temp file - await File.WriteAllBytesAsync(tempFilePath, imageContent); + await File.WriteAllBytesAsync(tempFilePath, imageContent, ct); return tempFilePath; } @@ -1293,8 +1269,10 @@ public partial class BookService : IBookService /// /// /// + /// /// - public async Task GetResourceAsync(string bookFilePath, string requestedKey) + public async Task GetResourceAsync(string bookFilePath, string requestedKey, + CancellationToken ct = default) { using var book = await EpubReader.OpenBookAsync(bookFilePath, LenientBookReaderOptions); var key = CoalesceKeyForAnyFile(book, requestedKey); @@ -1318,7 +1296,7 @@ public partial class BookService : IBookService /// public ParserInfo? ParseInfo(string filePath) { - if (!Parser.IsEpub(filePath) || !_directoryService.FileSystem.File.Exists(filePath)) return null; + if (!Parser.IsEpub(filePath) || !directoryService.FileSystem.File.Exists(filePath)) return null; try { @@ -1415,8 +1393,8 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogWarning(ex, "[BookService] There was an exception when opening epub book: {FileName}", filePath); - _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, + logger.LogWarning(ex, "[BookService] There was an exception when opening epub book: {FileName}", filePath); + mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, "There was an exception when opening epub book", ex); } @@ -1430,9 +1408,9 @@ public partial class BookService : IBookService /// public void ExtractPdfImages(string fileFilePath, string targetDirectory) { - _directoryService.ExistOrCreate(targetDirectory); + directoryService.ExistOrCreate(targetDirectory); - var settings = _unitOfWork.SettingsRepository.GetSettingsDtoAsync().GetAwaiter().GetResult(); + var settings = unitOfWork.SettingsRepository.GetSettingsDtoAsync().GetAwaiter().GetResult(); var dims = settings.PdfRenderResolution.GetDimensions(); var pageDimensions = new PageDimensions(dims.dim1, dims.dim2); @@ -1477,11 +1455,14 @@ public partial class BookService : IBookService /// Epub mappings /// Page number we are loading /// Ptoc (Text) Bookmarks to tie against + /// + /// /// private async Task ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, - Dictionary mappings, int page, List ptocBookmarks, List annotations) + Dictionary mappings, int page, List ptocBookmarks, List annotations, + CancellationToken ct = default) { - await InlineStyles(doc, book, apiBase, body); + await InlineStyles(doc, book, apiBase, body, ct); RewriteAnchors(page, doc, mappings); @@ -1555,11 +1536,15 @@ public partial class BookService : IBookService /// this is used to rewrite anchors in the book text so that we always load properly in our reader. /// /// Chapter with at least one file + /// /// - public async Task> GenerateTableOfContents(Chapter chapter) + public async Task> GenerateTableOfContents(Chapter chapter, + CancellationToken ct = default) { using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, LenientBookReaderOptions); - var mappings = await CreateKeyToPageMappingAsync(book); + if (book == null) return []; + + var mappings = await CreateKeyToPageMappingAsync(book, ct); var navItems = await book.GetNavigationAsync(); var chaptersList = new List(); @@ -1585,7 +1570,7 @@ public partial class BookService : IBookService k.Equals("NAVIGATION.XHTML", StringComparison.InvariantCultureIgnoreCase)); if (string.IsNullOrEmpty(tocPage)) return chaptersList; - if (!book.Content.Html.TryGetLocalFileRefByKey(tocPage, out var file)) return chaptersList; + if (!book.Content.Html.TryGetLocalFileRefByKey(tocPage, out var file) || file == null) return chaptersList; var content = await file.ReadContentAsync(); var doc = new HtmlDocument(); @@ -1688,22 +1673,16 @@ public partial class BookService : IBookService return path.Substring(startIndex); } - /// - /// This returns a single page within the epub book. All html will be rewritten to be scoped within our reader, - /// all css is scoped, etc. - /// - /// The requested page - /// The chapterId - /// The path to the cached epub file - /// The API base for Kavita, to rewrite urls to so we load though our endpoint - /// Full epub HTML Page, scoped to Kavita's reader - /// All exceptions throw this - public async Task GetBookPage(int page, int chapterId, string cachedEpubPath, string baseUrl, - List ptocBookmarks, List annotations) + public async Task GetBookPage(int userId, int page, int chapterId, string cachedEpubPath, string baseUrl, + List ptocBookmarks, List annotations, CancellationToken ct = default) { + var authKey = (await unitOfWork.UserRepository.GetAuthKeysForUserId(userId, ct)) + .First(k => k is { Name: AuthKeyHelper.ImageOnlyKeyName, Provider: AuthKeyProvider.System }) + .Key; + using var book = await EpubReader.OpenBookAsync(cachedEpubPath, LenientBookReaderOptions); - var mappings = await CreateKeyToPageMappingAsync(book); - var apiBase = baseUrl + "book/" + chapterId + "/" + BookApiUrl; + var mappings = await CreateKeyToPageMappingAsync(book, ct); + var apiBase = baseUrl + "book/" + chapterId + "/" + string.Format(BookApiUrl, authKey); var counter = 0; var doc = new HtmlDocument {OptionFixNestedTags = true}; @@ -1739,18 +1718,18 @@ public partial class BookService : IBookService LogBookErrors(book, contentFileRef, doc); throw new KavitaException("epub-malformed"); } - _logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath); + logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath); doc.DocumentNode.SelectSingleNode("/html").AppendChild(HtmlNode.CreateNode("")); body = doc.DocumentNode.SelectSingleNode("/html/body"); } - return await ScopePage(doc, book, apiBase, body!, mappings, page, ptocBookmarks, annotations); + return await ScopePage(doc, book, apiBase, body!, mappings, page, ptocBookmarks, annotations, ct); } } catch (Exception ex) { - _logger.LogError(ex, "There was an issue reading one of the pages for {Book}", book.FilePath); - await _mediaErrorService.ReportMediaIssueAsync(book.FilePath, MediaErrorProducer.BookService, - "There was an issue reading one of the pages for", ex); + logger.LogError(ex, "There was an issue reading one of the pages for {Book}", book.FilePath); + await mediaErrorService.ReportMediaIssueAsync(book.FilePath ?? string.Empty, MediaErrorProducer.BookService, + "There was an issue reading one of the pages for", ex, ct); } throw new KavitaException("epub-html-missing"); @@ -1774,6 +1753,7 @@ public partial class BookService : IBookService } using var epubBook = EpubReader.OpenBook(fileFilePath, LenientBookReaderOptions); + if (epubBook == null) return string.Empty; try { @@ -1785,12 +1765,12 @@ public partial class BookService : IBookService if (coverImageContent == null) return string.Empty; using var stream = coverImageContent.GetContentStream(); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); + return imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); } catch (Exception ex) { - _logger.LogWarning(ex, "[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath); - _mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService, + logger.LogWarning(ex, "[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath); + mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService, "There was a critical error and prevented thumbnail generation", ex); } @@ -1830,15 +1810,15 @@ public partial class BookService : IBookService using var stream = StreamManager.GetStream("BookService.GetPdfPage"); GetPdfPage(docReader, 0, stream); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); + return imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); } catch (Exception ex) { - _logger.LogWarning(ex, + logger.LogWarning(ex, "[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath); - _mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService, + mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService, "There was a critical error and prevented thumbnail generation", ex); } @@ -1902,10 +1882,10 @@ public partial class BookService : IBookService private void LogBookErrors(EpubBookRef book, EpubContentFileRef contentFileRef, HtmlDocument doc) { - _logger.LogError("{FilePath} has an invalid html file (Page {PageName})", book.FilePath, contentFileRef.Key); + logger.LogError("{FilePath} has an invalid html file (Page {PageName})", book.FilePath, contentFileRef.Key); foreach (var error in doc.ParseErrors) { - _logger.LogError("Line {LineNumber}, Reason: {Reason}", error.Line, error.Reason); + logger.LogError("Line {LineNumber}, Reason: {Reason}", error.Line, error.Reason); } } diff --git a/API/Services/BookmarkService.cs b/Kavita.Services/BookmarkService.cs similarity index 53% rename from API/Services/BookmarkService.cs rename to Kavita.Services/BookmarkService.cs index 9cd72dcba..c4bfdc5fd 100644 --- a/API/Services/BookmarkService.cs +++ b/Kavita.Services/BookmarkService.cs @@ -2,71 +2,58 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services; +namespace Kavita.Services; -#nullable enable -public interface IBookmarkService -{ - Task DeleteBookmarkFiles(IEnumerable bookmarks); - Task BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark); - Task RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto); - Task> GetBookmarkFilesById(IEnumerable bookmarkIds); -} - -public class BookmarkService : IBookmarkService +public class BookmarkService( + ILogger logger, + IUnitOfWork unitOfWork, + IDirectoryService directoryService, + IMediaConversionService mediaConversionService) + : IBookmarkService { public const string Name = "BookmarkService"; - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly IMediaConversionService _mediaConversionService; - - public BookmarkService(ILogger logger, IUnitOfWork unitOfWork, - IDirectoryService directoryService, IMediaConversionService mediaConversionService) - { - _logger = logger; - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _mediaConversionService = mediaConversionService; - } /// /// Deletes the files associated with the list of Bookmarks passed. Will clean up empty folders. /// /// - public async Task DeleteBookmarkFiles(IEnumerable bookmarks) + /// + public async Task DeleteBookmarkFiles(IEnumerable bookmarks, CancellationToken ct = default) { var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory, ct)).Value; var bookmarkFilesToDelete = bookmarks .Where(b => b != null) - .Select(b => Tasks.Scanner.Parser.Parser.NormalizePath( - _directoryService.FileSystem.Path.Join(bookmarkDirectory, b!.FileName))) + .Select(b => Parser.NormalizePath( + directoryService.FileSystem.Path.Join(bookmarkDirectory, b!.FileName))) .ToList(); if (bookmarkFilesToDelete.Count == 0) return; - _directoryService.DeleteFiles(bookmarkFilesToDelete); + directoryService.DeleteFiles(bookmarkFilesToDelete); // Delete any leftover folders - foreach (var directory in _directoryService.FileSystem.Directory.GetDirectories(bookmarkDirectory, string.Empty, SearchOption.AllDirectories)) + foreach (var directory in directoryService.FileSystem.Directory.GetDirectories(bookmarkDirectory, string.Empty, SearchOption.AllDirectories)) { - if (_directoryService.FileSystem.Directory.GetFiles(directory, "", SearchOption.AllDirectories).Length == 0 && - _directoryService.FileSystem.Directory.GetDirectories(directory).Length == 0) + if (directoryService.FileSystem.Directory.GetFiles(directory, "", SearchOption.AllDirectories).Length == 0 && + directoryService.FileSystem.Directory.GetDirectories(directory).Length == 0) { - _directoryService.FileSystem.Directory.Delete(directory, false); + directoryService.FileSystem.Directory.Delete(directory, false); } } } @@ -75,21 +62,21 @@ public class BookmarkService : IBookmarkService /// This is a job that runs after a bookmark is saved /// /// This must be public - public async Task ConvertBookmarkToEncoding(int bookmarkId) + public async Task ConvertBookmarkToEncoding(int bookmarkId, CancellationToken ct = default) { var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory, ct)).Value; var encodeFormat = - (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; + (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).EncodeMediaAs; if (encodeFormat == EncodeFormat.PNG) { - _logger.LogError("Cannot convert media to PNG"); + logger.LogError("Cannot convert media to PNG"); return; } // Validate the bookmark still exists - var bookmark = await _unitOfWork.UserRepository.GetBookmarkAsync(bookmarkId); + var bookmark = await unitOfWork.UserRepository.GetBookmarkAsync(bookmarkId, ct); if (bookmark == null) return; // Validate the bookmark isn't already in target format @@ -99,11 +86,11 @@ public class BookmarkService : IBookmarkService return; } - bookmark.FileName = await _mediaConversionService.SaveAsEncodingFormat(bookmarkDirectory, bookmark.FileName, + bookmark.FileName = await mediaConversionService.SaveAsEncodingFormat(bookmarkDirectory, bookmark.FileName, BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId), encodeFormat); - _unitOfWork.UserRepository.Update(bookmark); + unitOfWork.UserRepository.Update(bookmark); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); } @@ -113,8 +100,10 @@ public class BookmarkService : IBookmarkService /// An AppUser object with Bookmarks populated /// /// Full path to the cached image that is going to be copied + /// /// If the save to DB and copy was successful - public async Task BookmarkPage(AppUser? userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark) + public async Task BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark, + CancellationToken ct = default) { if (userWithBookmarks?.Bookmarks == null) { @@ -127,12 +116,12 @@ public class BookmarkService : IBookmarkService .SingleOrDefault(b => b.Page == bookmarkDto.Page && b.ChapterId == bookmarkDto.ChapterId && b.ImageOffset == bookmarkDto.ImageOffset); if (userBookmark != null) { - _logger.LogError("Bookmark already exists for Series {SeriesId}, Volume {VolumeId}, Chapter {ChapterId}, Page {PageNum}", bookmarkDto.SeriesId, bookmarkDto.VolumeId, bookmarkDto.ChapterId, bookmarkDto.Page); + logger.LogError("Bookmark already exists for Series {SeriesId}, Volume {VolumeId}, Chapter {ChapterId}, Page {PageNum}", bookmarkDto.SeriesId, bookmarkDto.VolumeId, bookmarkDto.ChapterId, bookmarkDto.Page); return true; } - var fileInfo = _directoryService.FileSystem.FileInfo.New(imageToBookmark); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var fileInfo = directoryService.FileSystem.FileInfo.New(imageToBookmark); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var targetFolderStem = BookmarkStem(userWithBookmarks.Id, bookmarkDto.SeriesId, bookmarkDto.ChapterId); var targetFilepath = Path.Join(settings.BookmarksDirectory, targetFolderStem); @@ -149,10 +138,10 @@ public class BookmarkService : IBookmarkService AppUserId = userWithBookmarks.Id }; - _directoryService.CopyFileToDirectory(imageToBookmark, targetFilepath); + directoryService.CopyFileToDirectory(imageToBookmark, targetFilepath); - _unitOfWork.UserRepository.Add(bookmark); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Add(bookmark); + await unitOfWork.CommitAsync(ct); if (settings.EncodeMediaAs != EncodeFormat.PNG) { @@ -162,8 +151,8 @@ public class BookmarkService : IBookmarkService } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when saving bookmark"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was an exception when saving bookmark"); + await unitOfWork.RollbackAsync(ct); return false; } @@ -175,8 +164,10 @@ public class BookmarkService : IBookmarkService /// /// /// + /// /// - public async Task RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto) + public async Task RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, + CancellationToken ct = default) { var bookmarkToDelete = userWithBookmarks.Bookmarks.FirstOrDefault(x => x.ChapterId == bookmarkDto.ChapterId && x.Page == bookmarkDto.Page && x.ImageOffset == bookmarkDto.ImageOffset); @@ -184,33 +175,33 @@ public class BookmarkService : IBookmarkService { if (bookmarkToDelete != null) { - _unitOfWork.UserRepository.Delete(bookmarkToDelete); + unitOfWork.UserRepository.Delete(bookmarkToDelete); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); } catch (Exception) { return false; } - await DeleteBookmarkFiles(new[] {bookmarkToDelete}); + await DeleteBookmarkFiles([bookmarkToDelete], ct); return true; } - public async Task> GetBookmarkFilesById(IEnumerable bookmarkIds) + public async Task> GetBookmarkFilesById(IEnumerable bookmarkIds, + CancellationToken ct = default) { var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory, ct)).Value; + + var bookmarks = await unitOfWork.UserRepository.GetAllBookmarksByIds(bookmarkIds.ToList(), ct); - var bookmarks = await _unitOfWork.UserRepository.GetAllBookmarksByIds(bookmarkIds.ToList()); return bookmarks - .Select(b => Tasks.Scanner.Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(bookmarkDirectory, + .Select(b => Parser.NormalizePath(directoryService.FileSystem.Path.Join(bookmarkDirectory, b.FileName))); } - - public static string BookmarkStem(int userId, int seriesId, int chapterId) { return Path.Join($"{userId}", $"{seriesId}", $"{chapterId}"); diff --git a/API/Helpers/Builders/ChapterBuilder.cs b/Kavita.Services/Builders/ChapterBuilder.cs similarity index 94% rename from API/Helpers/Builders/ChapterBuilder.cs rename to Kavita.Services/Builders/ChapterBuilder.cs index 75c86337d..5eae3d9ca 100644 --- a/API/Helpers/Builders/ChapterBuilder.cs +++ b/Kavita.Services/Builders/ChapterBuilder.cs @@ -1,13 +1,15 @@ using System; using System.Collections.Generic; using System.Globalization; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Builders; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Parser; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; -namespace API.Helpers.Builders; -#nullable enable +namespace Kavita.Services.Builders; public class ChapterBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/MangaFileBuilder.cs b/Kavita.Services/Builders/MangaFileBuilder.cs similarity index 90% rename from API/Helpers/Builders/MangaFileBuilder.cs rename to Kavita.Services/Builders/MangaFileBuilder.cs index 480785d8f..efe938f9c 100644 --- a/API/Helpers/Builders/MangaFileBuilder.cs +++ b/Kavita.Services/Builders/MangaFileBuilder.cs @@ -1,10 +1,12 @@ using System; using System.IO; -using API.Entities; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Builders; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Helpers; +using Kavita.Services.Scanner; -namespace API.Helpers.Builders; +namespace Kavita.Services.Builders; public class MangaFileBuilder : IEntityBuilder { @@ -67,8 +69,6 @@ public class MangaFileBuilder : IEntityBuilder /// Only applicable to Epubs public MangaFileBuilder WithHash() { - //if (_mangaFile.Format != MangaFormat.Epub) return this; - _mangaFile.KoreaderHash = KoreaderHelper.HashContents(_mangaFile.FilePath); return this; diff --git a/API/Helpers/Builders/VolumeBuilder.cs b/Kavita.Services/Builders/VolumeBuilder.cs similarity index 88% rename from API/Helpers/Builders/VolumeBuilder.cs rename to Kavita.Services/Builders/VolumeBuilder.cs index 13f8aae94..559a628c8 100644 --- a/API/Helpers/Builders/VolumeBuilder.cs +++ b/Kavita.Services/Builders/VolumeBuilder.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using API.Entities; +using API.Helpers.Builders; +using Kavita.Models.Entities; +using Kavita.Services.Scanner; -namespace API.Helpers.Builders; +namespace Kavita.Services.Builders; public class VolumeBuilder : IEntityBuilder { @@ -16,8 +18,8 @@ public class VolumeBuilder : IEntityBuilder { Name = volumeNumber, LookupName = volumeNumber, - MinNumber = Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber), - MaxNumber = Services.Tasks.Scanner.Parser.Parser.MaxNumberFromRange(volumeNumber), + MinNumber = Parser.MinNumberFromRange(volumeNumber), + MaxNumber = Parser.MaxNumberFromRange(volumeNumber), Chapters = new List() }; } diff --git a/API/Services/CacheService.cs b/Kavita.Services/CacheService.cs similarity index 60% rename from API/Services/CacheService.cs rename to Kavita.Services/CacheService.cs index 9df4d5671..511b5d390 100644 --- a/API/Services/CacheService.cs +++ b/Kavita.Services/CacheService.cs @@ -5,71 +5,33 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NetVips; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface ICacheService +public class CacheService( + ILogger logger, + IUnitOfWork unitOfWork, + IDirectoryService directoryService, + IReadingItemService readingItemService, + IBookmarkService bookmarkService) + : ICacheService { - /// - /// Ensures the cache is created for the given chapter and if not, will create it. Should be called before any other - /// cache operations (except cleanup). - /// - /// - /// Extracts a PDF into images for a different reading experience - /// Chapter for the passed chapterId. Side-effect from ensuring cache. - Task Ensure(int chapterId, bool extractPdfToImages = false); - /// - /// Clears cache directory of all volumes. This can be invoked from deleting a library or a series. - /// - /// Volumes that belong to that library. Assume the library might have been deleted before this invocation. - void CleanupChapters(IEnumerable chapterIds); - void CleanupBookmarks(IEnumerable seriesIds); - string GetCachedPagePath(int chapterId, int page); - string GetCachePath(int chapterId); - string GetBookmarkCachePath(int seriesId); - IEnumerable GetCachedPages(int chapterId); - IEnumerable GetCachedFileDimensions(string cachePath); - string GetCachedBookmarkPagePath(int seriesId, int page); - string GetCachedFile(Chapter chapter); - string GetCachedFile(int chapterId, string firstFilePath); - public void ExtractChapterFiles(string extractPath, IReadOnlyList files, bool extractPdfImages = false); - Task CacheBookmarkForSeries(int userId, int seriesId); - void CleanupBookmarkCache(int seriesId); -} -public class CacheService : ICacheService -{ - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly IReadingItemService _readingItemService; - private readonly IBookmarkService _bookmarkService; - private static readonly ConcurrentDictionary ExtractLocks = new(); - public CacheService(ILogger logger, IUnitOfWork unitOfWork, - IDirectoryService directoryService, IReadingItemService readingItemService, - IBookmarkService bookmarkService) - { - _logger = logger; - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _readingItemService = readingItemService; - _bookmarkService = bookmarkService; - } - public IEnumerable GetCachedPages(int chapterId) { var path = GetCachePath(chapterId); - return _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions) + return directoryService.GetFilesWithExtension(path, Parser.ImageFileExtensions) .OrderByNatural(Path.GetFileNameWithoutExtension); } @@ -80,7 +42,7 @@ public class CacheService : ICacheService /// public IEnumerable GetCachedFileDimensions(string cachePath) { - var files = _directoryService.GetFilesWithExtension(cachePath, Tasks.Scanner.Parser.Parser.ImageFileExtensions) + var files = directoryService.GetFilesWithExtension(cachePath, Parser.ImageFileExtensions) .OrderByNatural(Path.GetFileNameWithoutExtension) .ToArray(); @@ -110,7 +72,7 @@ public class CacheService : ICacheService } catch (Exception ex) { - _logger.LogError(ex, "There was an error calculating image dimensions for {CachePath}", cachePath); + logger.LogError(ex, "There was an error calculating image dimensions for {CachePath}", cachePath); } finally { @@ -124,7 +86,7 @@ public class CacheService : ICacheService { // Calculate what chapter the page belongs to var path = GetBookmarkCachePath(seriesId); - var files = _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions); + var files = directoryService.GetFilesWithExtension(path, Parser.ImageFileExtensions); files = files .AsEnumerable() .OrderByNatural(Path.GetFileNameWithoutExtension) @@ -147,8 +109,8 @@ public class CacheService : ICacheService public string GetCachedFile(Chapter chapter) { var extractPath = GetCachePath(chapter.Id); - var path = Path.Join(extractPath, _directoryService.FileSystem.Path.GetFileName(chapter.Files.First().FilePath)); - if (!(_directoryService.FileSystem.FileInfo.New(path).Exists)) + var path = Path.Join(extractPath, directoryService.FileSystem.Path.GetFileName(chapter.Files.First().FilePath)); + if (!(directoryService.FileSystem.FileInfo.New(path).Exists)) { path = chapter.Files.First().FilePath; } @@ -158,8 +120,8 @@ public class CacheService : ICacheService public string GetCachedFile(int chapterId, string firstFilePath) { var extractPath = GetCachePath(chapterId); - var path = Path.Join(extractPath, _directoryService.FileSystem.Path.GetFileName(firstFilePath)); - if (!(_directoryService.FileSystem.FileInfo.New(path).Exists)) + var path = Path.Join(extractPath, directoryService.FileSystem.Path.GetFileName(firstFilePath)); + if (!(directoryService.FileSystem.FileInfo.New(path).Exists)) { path = firstFilePath; } @@ -172,23 +134,24 @@ public class CacheService : ICacheService /// /// /// Defaults to false. Extract pdf file into images rather than copying just the pdf file + /// /// This will always return the Chapter for the chapterId - public async Task Ensure(int chapterId, bool extractPdfToImages = false) + public async Task Ensure(int chapterId, bool extractPdfToImages = false, CancellationToken ct = default) { - _directoryService.ExistOrCreate(_directoryService.CacheDirectory); - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); + directoryService.ExistOrCreate(directoryService.CacheDirectory); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(chapterId, ct: ct); var extractPath = GetCachePath(chapterId); var extractLock = ExtractLocks.GetOrAdd(chapterId, id => new SemaphoreSlim(1,1)); - await extractLock.WaitAsync(); + await extractLock.WaitAsync(ct); + try { - if (_directoryService.Exists(extractPath)) + if (directoryService.Exists(extractPath)) { if (extractPdfToImages) { - var pdfImages = _directoryService.GetFiles(extractPath, - Tasks.Scanner.Parser.Parser.ImageFileExtensions); + var pdfImages = directoryService.GetFiles(extractPath, Parser.ImageFileExtensions); if (pdfImages.Any()) { return chapter; @@ -198,13 +161,13 @@ public class CacheService : ICacheService { // Do an explicit check for files since rarely a "permission denied" error on deleting // the file can occur, thus leaving an empty folder and we would never re-cache the files. - if (_directoryService.GetFiles(extractPath).Any()) + if (directoryService.GetFiles(extractPath).Any()) { return chapter; } // Delete the extractPath as ExtractArchive will return if the directory already exists - _directoryService.ClearAndDeleteDirectory(extractPath); + directoryService.ClearAndDeleteDirectory(extractPath); } } @@ -231,15 +194,15 @@ public class CacheService : ICacheService var removeNonImages = true; var fileCount = files.Count; var extraPath = string.Empty; - var extractDi = _directoryService.FileSystem.DirectoryInfo.New(extractPath); + var extractDi = directoryService.FileSystem.DirectoryInfo.New(extractPath); if (files[0].Format == MangaFormat.Image) { // Check if all the files are Images. If so, do a directory copy, else do the normal copy if (files.All(f => f.Format == MangaFormat.Image)) { - _directoryService.ExistOrCreate(extractPath); - _directoryService.CopyFilesToDirectory(files.Select(f => f.FilePath), extractPath); + directoryService.ExistOrCreate(extractPath); + directoryService.CopyFilesToDirectory(files.Select(f => f.FilePath), extractPath); } else { @@ -249,9 +212,9 @@ public class CacheService : ICacheService { extraPath = file.Id + string.Empty; } - _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), MangaFormat.Image, files.Count); + readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), MangaFormat.Image, files.Count); } - _directoryService.Flatten(extractDi.FullName); + directoryService.Flatten(extractDi.FullName); } } @@ -266,34 +229,34 @@ public class CacheService : ICacheService switch (file.Format) { case MangaFormat.Archive: - _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); + readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); break; case MangaFormat.Epub: case MangaFormat.Pdf: { - if (!_directoryService.FileSystem.File.Exists(files[0].FilePath)) + if (!directoryService.FileSystem.File.Exists(files[0].FilePath)) { - _logger.LogError("{File} does not exist on disk", files[0].FilePath); + logger.LogError("{File} does not exist on disk", files[0].FilePath); throw new KavitaException($"{files[0].FilePath} does not exist on disk"); } if (extractPdfImages) { - _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); + readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); break; } removeNonImages = false; - _directoryService.ExistOrCreate(extractPath); - _directoryService.CopyFileToDirectory(files[0].FilePath, extractPath); + directoryService.ExistOrCreate(extractPath); + directoryService.CopyFileToDirectory(files[0].FilePath, extractPath); break; } } } - _directoryService.Flatten(extractDi.FullName); + directoryService.Flatten(extractDi.FullName); if (removeNonImages) { - _directoryService.RemoveNonImages(extractDi.FullName); + directoryService.RemoveNonImages(extractDi.FullName); } } @@ -305,7 +268,7 @@ public class CacheService : ICacheService { foreach (var chapter in chapterIds) { - _directoryService.ClearAndDeleteDirectory(GetCachePath(chapter)); + directoryService.ClearAndDeleteDirectory(GetCachePath(chapter)); } } @@ -317,7 +280,7 @@ public class CacheService : ICacheService { foreach (var series in seriesIds) { - _directoryService.ClearAndDeleteDirectory(GetBookmarkCachePath(series)); + directoryService.ClearAndDeleteDirectory(GetBookmarkCachePath(series)); } } @@ -329,7 +292,7 @@ public class CacheService : ICacheService /// public string GetCachePath(int chapterId) { - return _directoryService.FileSystem.Path.GetFullPath(_directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, $"{chapterId}/")); + return directoryService.FileSystem.Path.GetFullPath(directoryService.FileSystem.Path.Join(directoryService.CacheDirectory, $"{chapterId}/")); } /// @@ -339,7 +302,7 @@ public class CacheService : ICacheService /// public string GetBookmarkCachePath(int seriesId) { - return _directoryService.FileSystem.Path.GetFullPath(_directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, $"{seriesId}_bookmarks/")); + return directoryService.FileSystem.Path.GetFullPath(directoryService.FileSystem.Path.Join(directoryService.CacheDirectory, $"{seriesId}_bookmarks/")); } /// @@ -353,20 +316,22 @@ public class CacheService : ICacheService // Calculate what chapter the page belongs to var path = GetCachePath(chapterId); // NOTE: We can optimize this by extracting and renaming, so we don't need to scan for the files and can do a direct access - var files = _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions); + var files = directoryService.GetFilesWithExtension(path, Parser.ImageFileExtensions); return GetPageFromFiles(files, page); } - public async Task CacheBookmarkForSeries(int userId, int seriesId) + public async Task CacheBookmarkForSeries(int userId, int seriesId, CancellationToken ct = default) { - var destDirectory = _directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, seriesId + "_bookmarks"); - if (_directoryService.Exists(destDirectory)) return _directoryService.GetFiles(destDirectory).Count(); + var destDirectory = directoryService.FileSystem.Path.Join(directoryService.CacheDirectory, seriesId + "_bookmarks"); + if (directoryService.Exists(destDirectory)) return directoryService.GetFiles(destDirectory).Count(); - var bookmarkDtos = await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(userId, seriesId); - var files = (await _bookmarkService.GetBookmarkFilesById(bookmarkDtos.Select(b => b.Id))).ToList(); - _directoryService.CopyFilesToDirectory(files, destDirectory, + var bookmarkDtos = await unitOfWork.UserRepository.GetBookmarkDtosForSeries(userId, seriesId, ct); + + var files = (await bookmarkService.GetBookmarkFilesById(bookmarkDtos.Select(b => b.Id), ct)).ToList(); + directoryService.CopyFilesToDirectory(files, destDirectory, Enumerable.Range(1, files.Count).Select(i => i + string.Empty).ToList()); + return files.Count; } @@ -376,10 +341,10 @@ public class CacheService : ICacheService /// public void CleanupBookmarkCache(int seriesId) { - var destDirectory = _directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, seriesId + "_bookmarks"); - if (!_directoryService.Exists(destDirectory)) return; + var destDirectory = directoryService.FileSystem.Path.Join(directoryService.CacheDirectory, seriesId + "_bookmarks"); + if (!directoryService.Exists(destDirectory)) return; - _directoryService.ClearAndDeleteDirectory(destDirectory); + directoryService.ClearAndDeleteDirectory(destDirectory); } /// diff --git a/Kavita.Services/CleanupService.cs b/Kavita.Services/CleanupService.cs new file mode 100644 index 000000000..d1d461127 --- /dev/null +++ b/Kavita.Services/CleanupService.cs @@ -0,0 +1,417 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Scanner; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +/// +/// Cleans up after operations on reoccurring basis +/// +public class CleanupService( + ILogger logger, + IUnitOfWork unitOfWork, + IEventHub eventHub, + IDirectoryService directoryService) + : ICleanupService +{ + /// + /// Cleans up Temp, cache, deleted cover images, and old database backups + /// + /// + [AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail, DelaysInSeconds = [120, 300, 300])] + public async Task Cleanup(CancellationToken ct = default) + { + if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToEncoding", [], + TaskScheduler.DefaultQueue, true) || + TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllBookmarkToEncoding", [], + TaskScheduler.DefaultQueue, true)) + { + logger.LogInformation("Cleanup put on hold as a media conversion in progress"); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ErrorEvent("Cleanup", "Cleanup put on hold as a media conversion in progress"), ct: ct); + return; + } + + logger.LogInformation("Starting Cleanup"); + + // TODO: Why do I have clear temp directory then immediately do it again? + var cleanupSteps = new List<(Func, string)> + { + (innerCt => Task.Run(() => directoryService.ClearDirectory(directoryService.TempDirectory), innerCt), "Cleaning temp directory"), + (CleanupCacheAndTempDirectories, "Cleaning cache and temp directories"), + (CleanupBackups, "Cleaning old database backups"), + (ConsolidateProgress, "Consolidating Progress Events"), + (CleanupMediaErrors, "Consolidating Media Errors"), + (CleanupDbEntries, "Cleaning abandoned database rows"), // Cleanup DB before removing files linked to DB entries + (DeleteSeriesCoverImages, "Cleaning deleted series cover images"), + (DeleteChapterCoverImages, "Cleaning deleted chapter cover images"), + (innerCt => Task.WhenAll(DeleteTagCoverImages(innerCt), DeleteReadingListCoverImages(innerCt), DeletePersonCoverImages(innerCt)), "Cleaning deleted cover images"), + (CleanupLogs, "Cleaning old logs"), + (EnsureChapterProgressIsCapped, "Cleaning progress events that exceed 100%") + }; + + await SendProgress(0F, "Starting cleanup", ct); + + for (var i = 0; i < cleanupSteps.Count; i++) + { + var (method, subtitle) = cleanupSteps[i]; + var progress = (float)(i + 1) / (cleanupSteps.Count + 1); + + logger.LogInformation("{Message}", subtitle); + await method(ct); + await SendProgress(progress, subtitle, ct); + } + + await SendProgress(1F, "Cleanup finished", ct); + logger.LogInformation("Cleanup finished"); + } + + /// + /// Cleans up abandon rows in the DB + /// + public async Task CleanupDbEntries(CancellationToken ct = default) + { + await unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(ct); + await unitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated(ct); + await unitOfWork.GenreRepository.RemoveAllGenreNoLongerAssociated(ct: ct); + await unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(ct); + await unitOfWork.ReadingListRepository.RemoveReadingListsWithoutSeries(ct); + } + + private async Task SendProgress(float progress, string subtitle, CancellationToken ct = default) + { + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.CleanupProgressEvent(progress, subtitle), ct: ct); + } + + /// + /// Removes all series images that are not in the database. They must follow filename pattern. + /// + public async Task DeleteSeriesCoverImages(CancellationToken ct = default) + { + var images = await unitOfWork.SeriesRepository.GetAllCoverImagesAsync(); + var files = directoryService.GetFiles(directoryService.CoverImageDirectory, ImageService.SeriesCoverImageRegex); + directoryService.DeleteFiles(files.Where(file => !images.Contains(directoryService.FileSystem.Path.GetFileName(file)))); + } + + /// + /// Removes all chapter/volume images that are not in the database. They must follow filename pattern. + /// + public async Task DeleteChapterCoverImages(CancellationToken ct = default) + { + var images = await unitOfWork.ChapterRepository.GetAllCoverImagesAsync(ct); + var files = directoryService.GetFiles(directoryService.CoverImageDirectory, ImageService.ChapterCoverImageRegex); + directoryService.DeleteFiles(files.Where(file => !images.Contains(directoryService.FileSystem.Path.GetFileName(file)))); + } + + /// + /// Removes all collection tag images that are not in the database. They must follow filename pattern. + /// + public async Task DeleteTagCoverImages(CancellationToken ct = default) + { + var images = await unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync(ct); + var files = directoryService.GetFiles(directoryService.CoverImageDirectory, ImageService.CollectionTagCoverImageRegex); + directoryService.DeleteFiles(files.Where(file => !images.Contains(directoryService.FileSystem.Path.GetFileName(file)))); + } + + /// + /// Removes all reading list images that are not in the database. They must follow filename pattern. + /// + public async Task DeleteReadingListCoverImages(CancellationToken ct = default) + { + var images = await unitOfWork.ReadingListRepository.GetAllCoverImagesAsync(ct); + var files = directoryService.GetFiles(directoryService.CoverImageDirectory, ImageService.ReadingListCoverImageRegex); + directoryService.DeleteFiles(files.Where(file => !images.Contains(directoryService.FileSystem.Path.GetFileName(file)))); + } + + /// + /// Remove all person cover images no longer associated with a person in the database + /// + public async Task DeletePersonCoverImages(CancellationToken ct = default) + { + var images = await unitOfWork.PersonRepository.GetAllCoverImagesAsync(ct); + var files = directoryService.GetFiles(directoryService.CoverImageDirectory, ImageService.PersonCoverImageRegex); + directoryService.DeleteFiles(files.Where(file => !images.Contains(directoryService.FileSystem.Path.GetFileName(file)))); + } + + /// + /// Removes all files and directories in the cache and temp directory + /// + public Task CleanupCacheAndTempDirectories(CancellationToken ct = default) + { + logger.LogInformation("Performing cleanup of Cache & Temp directories"); + directoryService.ExistOrCreate(directoryService.CacheDirectory); + directoryService.ExistOrCreate(directoryService.TempDirectory); + + try + { + directoryService.ClearDirectory(directoryService.CacheDirectory); + directoryService.ClearDirectory(directoryService.TempDirectory); + } + catch (Exception ex) + { + logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); + } + + logger.LogInformation("Cache and temp directory purged"); + + return Task.CompletedTask; + } + + public void CleanupCacheDirectory() + { + logger.LogInformation("Performing cleanup of Cache directories"); + directoryService.ExistOrCreate(directoryService.CacheDirectory); + + try + { + directoryService.ClearDirectory(directoryService.CacheDirectory); + } + catch (Exception ex) + { + logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); + } + + logger.LogInformation("Cache directory purged"); + } + + /// + /// Removes Database backups older than configured total backups. If all backups are older than total backups days, only the latest is kept. + /// + public async Task CleanupBackups(CancellationToken ct = default) + { + var dayThreshold = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).TotalBackups; + logger.LogInformation("Beginning cleanup of Database backups at {Time}", DateTime.Now); + var backupDirectory = + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory, ct)).Value; + if (!directoryService.Exists(backupDirectory)) return; + + var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold)); + var allBackups = directoryService.GetFiles(backupDirectory).ToList(); + var expiredBackups = allBackups.Select(filename => directoryService.FileSystem.FileInfo.New(filename)) + .Where(f => f.CreationTime < deltaTime) + .ToList(); + + if (expiredBackups.Count == allBackups.Count) + { + logger.LogInformation("All expired backups are older than {Threshold} days. Removing all but last backup", dayThreshold); + var toDelete = expiredBackups.OrderByDescending(f => f.CreationTime).ToList(); + directoryService.DeleteFiles(toDelete.Take(toDelete.Count - 1).Select(f => f.FullName)); + } + else + { + directoryService.DeleteFiles(expiredBackups.Select(f => f.FullName)); + } + logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now); + } + + /// + /// Find any progress events that have duplicate, find the highest page read event, then copy over information from that and delete others, to leave one. + /// + public async Task ConsolidateProgress(CancellationToken ct = default) + { + logger.LogInformation("Consolidating Progress Events"); + // AppUserProgress + var allProgress = await unitOfWork.AppUserProgressRepository.GetAllProgress(ct); + + // Group by the unique identifiers that would make a progress entry unique + var duplicateGroups = allProgress + .GroupBy(p => new + { + p.AppUserId, + p.ChapterId, + }) + .Where(g => g.Count() > 1); + + foreach (var group in duplicateGroups) + { + // Find the entry with the highest pages read + var highestProgress = group + .OrderByDescending(p => p.PagesRead) + .ThenByDescending(p => p.LastModifiedUtc) + .First(); + + // Get the duplicate entries to remove (all except the highest progress) + var duplicatesToRemove = group + .Where(p => p.Id != highestProgress.Id) + .ToList(); + + // Copy over any non-null BookScrollId if the highest progress entry doesn't have one + if (string.IsNullOrEmpty(highestProgress.BookScrollId)) + { + var firstValidScrollId = duplicatesToRemove + .FirstOrDefault(p => !string.IsNullOrEmpty(p.BookScrollId)) + ?.BookScrollId; + + if (firstValidScrollId != null) + { + highestProgress.BookScrollId = firstValidScrollId; + highestProgress.MarkModified(); + } + } + + // Remove the duplicates + foreach (var duplicate in duplicatesToRemove) + { + unitOfWork.AppUserProgressRepository.Remove(duplicate); + } + } + + // Save changes + await unitOfWork.CommitAsync(ct); + } + + /// + /// Scans through Media Error and removes any entries that have been fixed and are within the DB (proper files where wordcount/pagecount > 0) + /// + public async Task CleanupMediaErrors(CancellationToken ct = default) + { + try + { + List errorStrings = ["This archive cannot be read or not supported", "File format not supported"]; + var mediaErrors = await unitOfWork.MediaErrorRepository.GetAllErrorsAsync(errorStrings, ct); + logger.LogInformation("Beginning consolidation of {Count} Media Errors", mediaErrors.Count); + + var pathToErrorMap = mediaErrors + .GroupBy(me => Parser.NormalizePath(me.FilePath)) + .ToDictionary( + group => group.Key, + group => group.ToList() // The same file can be duplicated (rare issue when network drives die out midscan) + ); + + var normalizedPaths = pathToErrorMap.Keys.ToList(); + + // Find all files that are valid + var validFiles = await unitOfWork.DataContext.MangaFile + .Where(f => normalizedPaths.Contains(f.FilePath) && f.Pages > 0) + .Select(f => f.FilePath) + .ToListAsync(cancellationToken: ct); + + var removalCount = 0; + foreach (var validFilePath in validFiles) + { + if (!pathToErrorMap.TryGetValue(validFilePath, out var mediaError)) continue; + + unitOfWork.MediaErrorRepository.Remove(mediaError); + removalCount++; + } + + await unitOfWork.CommitAsync(ct); + + logger.LogInformation("Finished consolidation of {Count} Media Errors, Removed: {RemovalCount}", + mediaErrors.Count, removalCount); + } + catch (Exception ex) + { + logger.LogError(ex, "There was an exception consolidating media errors"); + } + } + + public async Task CleanupLogs(CancellationToken ct = default) + { + logger.LogInformation("Performing cleanup of logs directory"); + var dayThreshold = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).TotalLogs; + var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold)); + var allLogs = directoryService.GetFiles(directoryService.LogDirectory).ToList(); + var expiredLogs = allLogs.Select(filename => directoryService.FileSystem.FileInfo.New(filename)) + .Where(f => f.CreationTime < deltaTime) + .ToList(); + + if (expiredLogs.Count == allLogs.Count) + { + logger.LogInformation("All expired backups are older than {Threshold} days. Removing all but last backup", dayThreshold); + var toDelete = expiredLogs.OrderBy(f => f.CreationTime).ToList(); + directoryService.DeleteFiles(toDelete.Take(toDelete.Count - 1).Select(f => f.FullName)); + } + else + { + directoryService.DeleteFiles(expiredLogs.Select(f => f.FullName)); + } + logger.LogInformation("Finished cleanup of logs at {Time}", DateTime.Now); + } + + public void CleanupTemp() + { + logger.LogInformation("Performing cleanup of Temp directory"); + directoryService.ExistOrCreate(directoryService.TempDirectory); + + try + { + directoryService.ClearDirectory(directoryService.TempDirectory); + } + catch (Exception ex) + { + logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); + } + + logger.LogInformation("Temp directory purged"); + } + + /// + /// Ensures that each chapter's progress (pages read) is capped at the total pages. This can get out of sync when a chapter is replaced after being read with one with lower page count. + /// + /// + public async Task EnsureChapterProgressIsCapped(CancellationToken ct = default) + { + logger.LogInformation("Cleaning up any progress rows that exceed chapter page count"); + await unitOfWork.AppUserProgressRepository.UpdateAllProgressThatAreMoreThanChapterPages(ct); + logger.LogInformation("Cleaning up any progress rows that exceed chapter page count - complete"); + } + + /// + /// This does not cleanup any Series that are not Completed or Cancelled + /// + public async Task CleanupWantToRead(CancellationToken ct = default) + { + logger.LogInformation("Performing cleanup of Series that are Completed and have been fully read that are in Want To Read list"); + + var libraryIds = (await unitOfWork.LibraryRepository.GetLibrariesAsync(ct: ct)).Select(l => l.Id).ToList(); + var filter = new FilterDto() + { + PublicationStatus = new List() + { + PublicationStatus.Completed, + PublicationStatus.Cancelled + }, + Libraries = libraryIds, + ReadStatus = new ReadStatus() + { + Read = true, + InProgress = false, + NotRead = false + } + }; + foreach (var user in await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.WantToRead, ct: ct)) + { + var series = await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(0, user.Id, new UserParams(), filter); + var seriesIds = series.Select(s => s.Id).ToList(); + if (seriesIds.Count == 0) continue; + + user.WantToRead ??= new List(); + user.WantToRead = user.WantToRead.Where(s => !seriesIds.Contains(s.SeriesId)).ToList(); + unitOfWork.UserRepository.Update(user); + } + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(ct); + } + + logger.LogInformation("Performing cleanup of Series that are Completed and have been fully read that are in Want To Read list, completed"); + } +} diff --git a/API/Services/ClientDeviceService.cs b/Kavita.Services/ClientDeviceService.cs similarity index 78% rename from API/Services/ClientDeviceService.cs rename to Kavita.Services/ClientDeviceService.cs index f36289229..938fde728 100644 --- a/API/Services/ClientDeviceService.cs +++ b/Kavita.Services/ClientDeviceService.cs @@ -5,37 +5,20 @@ using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Device.ClientDevice; -using API.DTOs.Progress; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Entities.User; -using API.Extensions.QueryExtensions; -using AutoMapper; -using AutoMapper.QueryableExtensions; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Device.ClientDevice; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface IClientDeviceService -{ - Task IdentifyOrRegisterDeviceAsync(int userId, ClientInfoData clientInfo, string? uiFingerprint, CancellationToken cancellationToken = default); - Task> GetUserDevicesAsync(int userId, bool includeInactive = false); - Task> GetUserDeviceDtosAsync(int userId, bool includeInactive = false); - Task> GetAllUserDeviceDtos(bool includeInactive = false); - Task RenameDeviceAsync(int userId, int deviceId, string newName); - Task DeleteDeviceAsync(int userId, int deviceId); - Task UpdateFriendlyNameAsync(int userId, UpdateClientDeviceNameDto dto); -} - -public class ClientDeviceService(DataContext context, IMapper mapper, ILogger logger) +public class ClientDeviceService(IDataContext context, IUnitOfWork unitOfWork ,ILogger logger) : IClientDeviceService { /// @@ -52,7 +35,7 @@ public class ClientDeviceService(DataContext context, IMapper mapper, ILogger GetClientDeviceByClientFingerprint(int userId, string uiFingerprint, CancellationToken cancellationToken) + public async Task RenameDeviceAsync(int userId, int deviceId, string newName, CancellationToken ct = default) { - return await context.ClientDevice - .Include(d => d.History.OrderByDescending(h => h.CapturedAtUtc).Take(1)) - .FirstOrDefaultAsync(d => - d.AppUserId == userId && - d.UiFingerprint == uiFingerprint && - d.IsActive, cancellationToken: cancellationToken); - } - - public async Task> GetUserDevicesAsync(int userId, bool includeInactive = false) - { - return await context.ClientDevice - .Where(d => d.AppUserId == userId) - .WhereIf(!includeInactive, d => d.IsActive) - .OrderByDescending(d => d.LastSeenUtc) - .ToListAsync(); - } - - public async Task> GetUserDeviceDtosAsync(int userId, bool includeInactive = false) - { - return await context.ClientDevice - .Where(d => d.AppUserId == userId) - .WhereIf(!includeInactive, d => d.IsActive) - .OrderByDescending(d => d.LastSeenUtc) - .ProjectTo(mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task> GetAllUserDeviceDtos(bool includeInactive = false) - { - return await context.ClientDevice - .WhereIf(!includeInactive, d => d.IsActive) - .OrderByDescending(d => d.LastSeenUtc) - .ProjectTo(mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task RenameDeviceAsync(int userId, int deviceId, string newName) - { - var device = await context.ClientDevice - .FirstOrDefaultAsync(d => d.Id == deviceId && d.AppUserId == userId); + var device = await unitOfWork.ClientDeviceRepository.GetClientDeviceById(deviceId, userId, ct); if (device == null) { @@ -152,7 +96,7 @@ public class ClientDeviceService(DataContext context, IMapper mapper, ILogger DeleteDeviceAsync(int userId, int deviceId) + public async Task DeleteDeviceAsync(int userId, int deviceId, CancellationToken ct = default) { - var device = await context.ClientDevice - .FirstOrDefaultAsync(d => d.Id == deviceId && d.AppUserId == userId); + var device = await unitOfWork.ClientDeviceRepository.GetClientDeviceById(deviceId, userId, ct); if (device == null) { @@ -171,7 +114,7 @@ public class ClientDeviceService(DataContext context, IMapper mapper, ILogger d.AppUserId == userId && d.Id == dto.DeviceId) - .FirstOrDefaultAsync() ?? throw new KavitaException("client-device-doesnt-exist"); + var device = await unitOfWork.ClientDeviceRepository.GetClientDeviceById(dto.DeviceId, userId, ct) + ?? throw new KavitaException("client-device-doesnt-exist"); if (!string.IsNullOrWhiteSpace(dto.Name)) { device.FriendlyName = dto.Name; - await context.SaveChangesAsync(); + await unitOfWork.CommitAsync(ct); } } diff --git a/Kavita.Services/CollectionTagService.cs b/Kavita.Services/CollectionTagService.cs new file mode 100644 index 000000000..62dd9b71e --- /dev/null +++ b/Kavita.Services/CollectionTagService.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; + +namespace Kavita.Services; + +public class CollectionTagService(IUnitOfWork unitOfWork, IEventHub eventHub, IDirectoryService directoryService) : ICollectionTagService +{ + public async Task DeleteTag(int tagId, AppUser user, CancellationToken ct = default) + { + var collectionTag = await unitOfWork.CollectionTagRepository.GetCollectionAsync(tagId, ct: ct); + if (collectionTag == null) return true; + + user.Collections.Remove(collectionTag); + + if (!unitOfWork.HasChanges()) return true; + + return await unitOfWork.CommitAsync(ct); + } + + + public async Task UpdateTag(AppUserCollectionDto dto, int userId, CancellationToken ct = default) + { + var existingTag = await unitOfWork.CollectionTagRepository.GetCollectionAsync(dto.Id, ct: ct); + if (existingTag == null) throw new KavitaException("collection-doesnt-exist"); + if (existingTag.AppUserId != userId) throw new KavitaException("access-denied"); + + var title = dto.Title.Trim(); + if (string.IsNullOrEmpty(title)) throw new KavitaException("collection-tag-title-required"); + + // Ensure the title doesn't exist on the user's account already + if (!title.Equals(existingTag.Title) && await unitOfWork.CollectionTagRepository.CollectionExists(dto.Title, userId, ct)) + throw new KavitaException("collection-tag-duplicate"); + + existingTag.Items ??= []; + if (existingTag.Source == ScrobbleProvider.Kavita) + { + existingTag.Title = title; + existingTag.NormalizedTitle = dto.Title.ToNormalized(); + } + + var roles = await unitOfWork.UserRepository.GetRoles(userId, ct); + if (roles.Contains(PolicyConstants.AdminRole) || roles.Contains(PolicyConstants.PromoteRole)) + { + existingTag.Promoted = dto.Promoted; + } + existingTag.CoverImageLocked = dto.CoverImageLocked; + unitOfWork.CollectionTagRepository.Update(existingTag); + + // Check if Tag has updated (Summary) + var summary = (dto.Summary ?? string.Empty).Trim(); + if (existingTag.Summary == null || !existingTag.Summary.Equals(summary)) + { + existingTag.Summary = summary; + unitOfWork.CollectionTagRepository.Update(existingTag); + } + + // If we unlock the cover image, it means reset + if (!dto.CoverImageLocked) + { + existingTag.CoverImageLocked = false; + existingTag.CoverImage = string.Empty; + await eventHub.SendMessageAsync(MessageFactory.CoverUpdate, + MessageFactory.CoverUpdateEvent(existingTag.Id, MessageFactoryEntityTypes.Collection), false, ct); + unitOfWork.CollectionTagRepository.Update(existingTag); + } + + if (!unitOfWork.HasChanges()) return true; + return await unitOfWork.CommitAsync(ct); + } + + public async Task RemoveTagFromSeries(AppUserCollection? tag, IEnumerable seriesIds, CancellationToken ct = default) + { + if (tag == null) return false; + + tag.Items ??= []; + tag.Items = tag.Items.Where(s => !seriesIds.Contains(s.Id)).ToList(); + + if (tag.Items.Count == 0) + { + unitOfWork.CollectionTagRepository.Remove(tag); + } + + if (!unitOfWork.HasChanges()) return true; + + var result = await unitOfWork.CommitAsync(ct); + if (tag.Items.Count > 0) + { + await unitOfWork.CollectionTagRepository.UpdateCollectionAgeRating(tag, ct); + } + + return result; + } + + public async Task GenerateCollectionCoverImage(int collectionId) + { + var covers = await unitOfWork.CollectionTagRepository.GetRandomCoverImagesAsync(collectionId); + var destFile = directoryService.FileSystem.Path.Join(directoryService.TempDirectory, ImageService.GetCollectionTagFormat(collectionId)); + + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + destFile += settings.EncodeMediaAs.GetExtension(); + + if (directoryService.FileSystem.File.Exists(destFile)) return destFile; + + ImageService.CreateMergedImage( + covers.Select(c => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, c)).ToList(), + settings.CoverImageSize, + destFile); + + // TODO: Refactor this so that collections have a dedicated cover image so we can calculate primary/secondary colors + return !directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; + } +} diff --git a/API/Comparators/ChapterSortComparer.cs b/Kavita.Services/Comparators/ChapterSortComparer.cs similarity index 94% rename from API/Comparators/ChapterSortComparer.cs rename to Kavita.Services/Comparators/ChapterSortComparer.cs index f5d566cb1..1b06399bc 100644 --- a/API/Comparators/ChapterSortComparer.cs +++ b/Kavita.Services/Comparators/ChapterSortComparer.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; +using System.Collections.Generic; +using Kavita.Common.Extensions; +using Parser = Kavita.Services.Scanner.Parser; -namespace API.Comparators; - -#nullable enable +namespace Kavita.Services.Comparators; /// /// Sorts chapters based on their Number. Uses natural ordering of doubles. Specials always LAST. diff --git a/Kavita.Services/DeviceService.cs b/Kavita.Services/DeviceService.cs new file mode 100644 index 000000000..3252149c5 --- /dev/null +++ b/Kavita.Services/DeviceService.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.Common; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.DTOs.Email; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.Device; +using Kavita.Models.Entities.User; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class DeviceService( + IUnitOfWork unitOfWork, + ILogger logger, + IEmailService emailService, + IReadingProfileService readingProfileService) + : IDeviceService +{ + public async Task Create(CreateEmailDeviceDto dto, AppUser userWithDevices, CancellationToken ct = default) + { + try + { + userWithDevices.Devices ??= new List(); + var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Name!.Equals(dto.Name)); + if (existingDevice != null) throw new KavitaException("device-duplicate"); + + existingDevice = new DeviceBuilder(dto.Name) + .WithPlatform(dto.Platform) + .WithEmail(dto.EmailAddress) + .Build(); + + + userWithDevices.Devices.Add(existingDevice); + unitOfWork.UserRepository.Update(userWithDevices); + + if (!unitOfWork.HasChanges()) return existingDevice; + if (await unitOfWork.CommitAsync(ct)) return existingDevice; + } + catch (Exception ex) + { + logger.LogError(ex, "There was an error when creating your device"); + await unitOfWork.RollbackAsync(ct); + } + + return null; + } + + public async Task Update(UpdateEmailDeviceDto dto, AppUser userWithDevices, CancellationToken ct = default) + { + try + { + var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Id == dto.Id); + if (existingDevice == null) throw new KavitaException("device-not-created"); + + existingDevice.Name = dto.Name; + existingDevice.Platform = dto.Platform; + existingDevice.EmailAddress = dto.EmailAddress; + + if (!unitOfWork.HasChanges()) return existingDevice; + if (await unitOfWork.CommitAsync(ct)) return existingDevice; + } + catch (Exception ex) + { + logger.LogError(ex, "There was an error when updating your device"); + await unitOfWork.RollbackAsync(ct); + } + + return null; + } + + public async Task Delete(AppUser userWithDevices, int deviceId, CancellationToken ct = default) + { + try + { + userWithDevices.Devices = userWithDevices.Devices.Where(d => d.Id != deviceId).ToList(); + unitOfWork.UserRepository.Update(userWithDevices); + + await readingProfileService.RemoveDeviceLinks(userWithDevices.Id, deviceId); + + if (!unitOfWork.HasChanges()) return true; + if (await unitOfWork.CommitAsync(ct)) return true; + } + catch (Exception ex) + { + logger.LogError(ex, "There was an issue with deleting the device, {DeviceId} for user {UserName}", deviceId, userWithDevices.UserName); + } + + return false; + } + + public async Task SendTo(IReadOnlyList chapterIds, int deviceId, CancellationToken ct = default) + { + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); + if (!settings.IsEmailSetupForSendToDevice()) + throw new KavitaException("send-to-kavita-email"); + + var device = await unitOfWork.DeviceRepository.GetDeviceById(deviceId, ct); + if (device == null) throw new KavitaException("device-doesnt-exist"); + + var files = await unitOfWork.ChapterRepository.GetFilesForChaptersAsync(chapterIds, ct); + if (files.Any(f => f.Format is not (MangaFormat.Epub or MangaFormat.Pdf)) && device.Platform == EmailDevicePlatform.Kindle) + throw new KavitaException("send-to-permission"); + + // If the size of the files is too big + if (files.Sum(f => f.Bytes) >= settings.SmtpConfig.SizeLimit) + throw new KavitaException("send-to-size-limit"); + + + try + { + device.UpdateLastUsed(); + unitOfWork.DeviceRepository.Update(device); + await unitOfWork.CommitAsync(ct); + } + catch (Exception ex) + { + logger.LogError(ex, "There was an issue updating device last used time"); + } + + var success = await emailService.SendFilesToEmail(new SendToDto() + { + DestinationEmail = device.EmailAddress!, + FilePaths = files.Select(m => m.FilePath) + }); + + return success; + } +} diff --git a/API/Services/DeviceTrackingService.cs b/Kavita.Services/DeviceTrackingService.cs similarity index 84% rename from API/Services/DeviceTrackingService.cs rename to Kavita.Services/DeviceTrackingService.cs index b9f99c572..1b58742ce 100644 --- a/API/Services/DeviceTrackingService.cs +++ b/Kavita.Services/DeviceTrackingService.cs @@ -2,23 +2,16 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Entities.Progress; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Models.Entities.Progress; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface IDeviceTrackingService -{ - Task TrackDeviceAsync(int userId, ClientInfoData clientInfo, string? uiFingerprint, CancellationToken ct); - Task ClearDeviceCacheAsync(int deviceId); - Task ClearUserDeviceCachesAsync(int userId); -} - -public class DeviceTrackingService(HybridCache cache, DataContext context, ILogger logger, IClientDeviceService clientDeviceService) : IDeviceTrackingService +public class DeviceTrackingService(HybridCache cache, IDataContext context, ILogger logger, IClientDeviceService clientDeviceService) : IDeviceTrackingService { private static readonly HybridCacheEntryOptions CacheOptions = new() diff --git a/API/Services/DirectoryService.cs b/Kavita.Services/DirectoryService.cs similarity index 88% rename from API/Services/DirectoryService.cs rename to Kavita.Services/DirectoryService.cs index ecce1957a..61e74da9c 100644 --- a/API/Services/DirectoryService.cs +++ b/Kavita.Services/DirectoryService.cs @@ -6,84 +6,15 @@ using System.IO.Abstractions; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.DTOs.System; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Services; +using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Models.DTOs.System; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface IDirectoryService -{ - IFileSystem FileSystem { get; } - string CacheDirectory { get; } - string CoverImageDirectory { get; } - string LogDirectory { get; } - string TempDirectory { get; } - string ConfigDirectory { get; } - string SiteThemeDirectory { get; } - string FaviconDirectory { get; } - string LocalizationDirectory { get; } - string CustomizedTemplateDirectory { get; } - string TemplateDirectory { get; } - string PublisherDirectory { get; } - /// - /// Used for caching documents that may need to stay on disk for more than a day - /// - string LongTermCacheDirectory { get; } - /// - /// Original BookmarkDirectory. Only used for resetting directory. Use for actual path. - /// - string BookmarkDirectory { get; } - /// - /// Used for random files needed, like images to check against, list of countries, etc - /// - string AssetsDirectory { get; } - string EpubFontDirectory { get; } - /// - /// Lists out top-level folders for a given directory. Filters out System and Hidden folders. - /// - /// Absolute path of directory to scan. - /// List of folder names - IEnumerable ListDirectory(string rootPath); - Task ReadFileAsync(string path); - bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, string prepend = ""); - bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, IList newFilenames); - bool Exists(string directory); - void CopyFileToDirectory(string fullFilePath, string targetDirectory); - int TraverseTreeParallelForEach(string root, Action action, string searchPattern, ILogger logger); - bool IsDriveMounted(string path); - bool IsDirectoryEmpty(string path); - long GetTotalSize(IEnumerable paths); - void ClearDirectory(string directoryPath); - void ClearAndDeleteDirectory(string directoryPath); - string[] GetFilesWithExtension(string path, string searchPatternExpression = ""); - bool CopyDirectoryToDirectory(string? sourceDirName, string destDirName, string searchPattern = ""); - Dictionary FindHighestDirectoriesFromFiles(IEnumerable libraryFolders, - IList filePaths); - string? FindLowestDirectoriesFromFiles(IList libraryFolders, - IList filePaths); - IEnumerable GetFoldersTillRoot(string rootPath, string fullPath); - IEnumerable GetFiles(string path, string fileNameRegex = "", SearchOption searchOption = SearchOption.TopDirectoryOnly); - bool ExistOrCreate(string directoryPath); - void DeleteFiles(IEnumerable files); - void CopyFile(string sourcePath, string destinationPath, bool overwrite = true); - void RemoveNonImages(string directoryName); - void Flatten(string directoryName); - Task CheckWriteAccess(string directoryName); - IEnumerable GetFilesWithCertainExtensions(string path, - string searchPatternExpression = "", - SearchOption searchOption = SearchOption.TopDirectoryOnly); - IEnumerable GetDirectories(string folderPath); - IEnumerable GetDirectories(string folderPath, GlobMatcher? matcher); - IEnumerable GetAllDirectories(string folderPath, GlobMatcher? matcher = null); - string GetParentDirectoryName(string fileOrFolder); - IList ScanFiles(string folderPath, string fileTypes, GlobMatcher? matcher = null, SearchOption searchOption = SearchOption.AllDirectories); - DateTime GetLastWriteTime(string folderPath); -} public class DirectoryService : IDirectoryService { public IFileSystem FileSystem { get; } @@ -102,6 +33,7 @@ public class DirectoryService : IDirectoryService public string PublisherDirectory { get; } public string LongTermCacheDirectory { get; } public string EpubFontDirectory { get; } + public string BackupDirectory { get; } private readonly ILogger _logger; private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase; @@ -111,7 +43,6 @@ public class DirectoryService : IDirectoryService MatchOptions, Parser.RegexTimeout); private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)", MatchOptions, Parser.RegexTimeout); - public static readonly string BackupDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "backups"); public DirectoryService(ILogger logger, IFileSystem fileSystem) { @@ -146,12 +77,14 @@ public class DirectoryService : IDirectoryService ExistOrCreate(LongTermCacheDirectory); EpubFontDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "fonts"); ExistOrCreate(EpubFontDirectory); + BackupDirectory = FileSystem.Path.Join(Directory.GetCurrentDirectory(), "config", "backups"); + ExistOrCreate(BackupDirectory); } /// /// Given a set of regex search criteria, get files in the given path. /// - /// This will always exclude patterns + /// This will always exclude patterns /// Directory to search /// Regex version of search pattern (e.g., \.mp3|\.mp4). Defaults to * meaning all files. /// SearchOption to use, defaults to TopDirectoryOnly @@ -253,7 +186,7 @@ public class DirectoryService : IDirectoryService if (!string.IsNullOrEmpty(fileNameRegex)) { // Compile the regex for better performance when used frequently - reSearchPattern = new Regex(fileNameRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled, Tasks.Scanner.Parser.Parser.RegexTimeout); + reSearchPattern = new Regex(fileNameRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled, Parser.RegexTimeout); } // Enumerate files lazily @@ -262,7 +195,7 @@ public class DirectoryService : IDirectoryService var fileName = FileSystem.Path.GetFileName(file); // Exclude macOS metadata files - if (fileName.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith)) + if (fileName.StartsWith(Parser.MacOsMetadataFileStartsWith)) continue; // If a regex is provided, match the file name against it @@ -609,10 +542,10 @@ public class DirectoryService : IDirectoryService { var stopLookingForDirectories = false; var dirs = new Dictionary(); - foreach (var folder in libraryFolders.Select(Tasks.Scanner.Parser.Parser.NormalizePath)) + foreach (var folder in libraryFolders.Select(Parser.NormalizePath)) { if (stopLookingForDirectories) break; - foreach (var file in filePaths.Select(Tasks.Scanner.Parser.Parser.NormalizePath)) + foreach (var file in filePaths.Select(Parser.NormalizePath)) { if (!file.Contains(folder)) continue; @@ -625,7 +558,7 @@ public class DirectoryService : IDirectoryService break; } - var fullPath = Tasks.Scanner.Parser.Parser.NormalizePath(Path.Join(folder, parts[parts.Count - 1])); + var fullPath =Parser.NormalizePath(Path.Join(folder, parts[parts.Count - 1])); dirs.TryAdd(fullPath, string.Empty); } } @@ -747,7 +680,7 @@ public class DirectoryService : IDirectoryService { try { - return Tasks.Scanner.Parser.Parser.NormalizePath(Directory.GetParent(fileOrFolder)?.FullName); + return Parser.NormalizePath(Directory.GetParent(fileOrFolder)?.FullName); } catch (Exception) { @@ -1025,7 +958,7 @@ public class DirectoryService : IDirectoryService /// Fully qualified directory public void RemoveNonImages(string directoryName) { - DeleteFiles(GetFiles(directoryName, searchOption:SearchOption.AllDirectories).Where(file => !Tasks.Scanner.Parser.Parser.IsImage(file))); + DeleteFiles(GetFiles(directoryName, searchOption:SearchOption.AllDirectories).Where(file => !Parser.IsImage(file))); } @@ -1098,9 +1031,9 @@ public class DirectoryService : IDirectoryService foreach (var file in directory.EnumerateFiles().OrderByNatural(file => file.FullName)) { if (file.Directory == null) continue; - var paddedIndex = Tasks.Scanner.Parser.Parser.PadZeros(directoryIndex + string.Empty); + var paddedIndex =Parser.PadZeros(directoryIndex + string.Empty); // We need to rename the files so that after flattening, they are in the order we found them - var newName = $"{paddedIndex}_{Tasks.Scanner.Parser.Parser.PadZeros(fileIndex + string.Empty)}{file.Extension}"; + var newName = $"{paddedIndex}_{Parser.PadZeros(fileIndex + string.Empty)}{file.Extension}"; var newPath = Path.Join(root.FullName, newName); if (!File.Exists(newPath)) file.MoveTo(newPath); fileIndex++; @@ -1112,7 +1045,7 @@ public class DirectoryService : IDirectoryService foreach (var subDirectory in directory.EnumerateDirectories().OrderByNatural(d => d.FullName)) { // We need to check if the directory is not a blacklisted (ie __MACOSX) - if (Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(subDirectory.FullName)) continue; + if (Parser.HasBlacklistedFolderInPath(subDirectory.FullName)) continue; FlattenDirectory(root, subDirectory, ref directoryIndex); } diff --git a/API/Services/DownloadService.cs b/Kavita.Services/DownloadService.cs similarity index 89% rename from API/Services/DownloadService.cs rename to Kavita.Services/DownloadService.cs index b3913e8be..4ca185c08 100644 --- a/API/Services/DownloadService.cs +++ b/Kavita.Services/DownloadService.cs @@ -2,17 +2,13 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using API.Entities; +using Kavita.API.Services; +using Kavita.Models.Entities; using Microsoft.AspNetCore.StaticFiles; using MimeTypes; -namespace API.Services; +namespace Kavita.Services; -public interface IDownloadService -{ - Tuple GetFirstFileDownload(IEnumerable files); - string GetContentTypeFromFile(string filepath); -} public class DownloadService : IDownloadService { private readonly FileExtensionContentTypeProvider _fileTypeProvider = new FileExtensionContentTypeProvider(); diff --git a/API/Services/EmailService.cs b/Kavita.Services/EmailService.cs similarity index 81% rename from API/Services/EmailService.cs rename to Kavita.Services/EmailService.cs index f2988a342..215a7f3c8 100644 --- a/API/Services/EmailService.cs +++ b/Kavita.Services/EmailService.cs @@ -7,15 +7,15 @@ using System.Net; using System.Text; using System.Threading.Tasks; using System.Web; -using API.Data; -using API.DTOs.Account; -using API.DTOs.Email; -using API.Entities; -using API.Entities.User; -using API.Services.Plus; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; using Kavita.Common.EnvironmentInfo; using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Email; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using MailKit.Security; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; @@ -23,8 +23,7 @@ using Microsoft.Extensions.Logging; using MimeKit; using MimeTypes; -namespace API.Services; -#nullable enable +namespace Kavita.Services; internal class EmailOptionsDto { @@ -41,33 +40,14 @@ internal class EmailOptionsDto public required string Template { get; set; } } -public interface IEmailService +public class EmailService( + ILogger logger, + IUnitOfWork unitOfWork, + IDirectoryService directoryService, + IHostEnvironment environment, + ILocalizationService localizationService) + : IEmailService { - Task SendInviteEmail(ConfirmationEmailDto data); - Task SendForgotPasswordEmail(PasswordResetEmailDto dto); - Task SendFilesToEmail(SendToDto data); - Task SendTestEmail(string adminEmail); - Task SendEmailChangeEmail(ConfirmationEmailDto data); - bool IsValidEmail(string email); - - Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, - bool withHost = true); - - Task SendTokenExpiredEmail(int userId, ScrobbleProvider provider); - Task SendTokenExpiringSoonEmail(int userId, ScrobbleProvider provider); - Task SendAuthKeyExpiredEmail(int userId, IList keys); - Task SendAuthKeyExpiringSoonEmail(int userId, IList keys); - Task SendKavitaPlusDebug(); -} - -public class EmailService : IEmailService -{ - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly IHostEnvironment _environment; - private readonly ILocalizationService _localizationService; - private const string TemplatePath = @"{0}.html"; private const string LocalHost = "localhost:4200"; @@ -85,16 +65,6 @@ public class EmailService : IEmailService private const string AuthKeyExpiringFragment = "AuthKeyExpiringFragment"; private const string AuthKeyExpiredFragment = "AuthKeyExpiredFragment"; - public EmailService(ILogger logger, IUnitOfWork unitOfWork, IDirectoryService directoryService, - IHostEnvironment environment, ILocalizationService localizationService) - { - _logger = logger; - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _environment = environment; - _localizationService = localizationService; - } - /// /// Test if the email settings are working. Rejects if user email isn't valid or not all data is setup in server settings. /// @@ -106,19 +76,19 @@ public class EmailService : IEmailService EmailAddress = adminEmail }; - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (!IsValidEmail(adminEmail)) { - var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(); - result.ErrorMessage = await _localizationService.Translate(defaultAdmin.Id, "account-email-invalid"); + var defaultAdmin = await unitOfWork.UserRepository.GetDefaultAdminUser(); + result.ErrorMessage = await localizationService.Translate(defaultAdmin.Id, "account-email-invalid"); result.Successful = false; return result; } if (!settings.IsEmailSetup()) { - var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(); - result.ErrorMessage = await _localizationService.Translate(defaultAdmin.Id, "email-settings-invalid"); + var defaultAdmin = await unitOfWork.UserRepository.GetDefaultAdminUser(); + result.ErrorMessage = await localizationService.Translate(defaultAdmin.Id, "email-settings-invalid"); result.Successful = false; return result; } @@ -193,8 +163,8 @@ public class EmailService : IEmailService public async Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true) { - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); + var serverSettings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var host = environment.IsDevelopment() ? LocalHost : request.Host.ToString(); var basePart = $"{request.Scheme}://{host}{request.PathBase}"; if (!string.IsNullOrEmpty(serverSettings.HostName)) { @@ -213,8 +183,8 @@ public class EmailService : IEmailService public async Task SendTokenExpiredEmail(int userId, ScrobbleProvider provider) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (user == null || !IsValidEmail(user.Email) || !settings.IsEmailSetup()) return false; var placeholders = new List> @@ -243,8 +213,8 @@ public class EmailService : IEmailService public async Task SendTokenExpiringSoonEmail(int userId, ScrobbleProvider provider) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (user == null || !IsValidEmail(user.Email) || !settings.IsEmailSetup()) return false; var placeholders = new List> @@ -273,8 +243,8 @@ public class EmailService : IEmailService public async Task SendAuthKeyExpiredEmail(int userId, IList keys) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (user == null || !IsValidEmail(user.Email) || !settings.IsEmailSetup()) return false; var d = keys.Select(k => new List>() @@ -309,8 +279,8 @@ public class EmailService : IEmailService public async Task SendAuthKeyExpiringSoonEmail(int userId, IList keys) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (user == null || !IsValidEmail(user.Email) || !settings.IsEmailSetup()) return false; var d = keys.Select(k => new List>() @@ -351,7 +321,7 @@ public class EmailService : IEmailService /// public async Task SendKavitaPlusDebug() { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (!settings.IsEmailSetup()) return false; var placeholders = new List> @@ -431,7 +401,7 @@ public class EmailService : IEmailService public async Task SendFilesToEmail(SendToDto data) { - var serverSetting = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var serverSetting = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (!serverSetting.IsEmailSetupForSendToDevice()) return false; var emailOptions = new EmailOptionsDto() @@ -450,7 +420,7 @@ public class EmailService : IEmailService private async Task SendEmail(EmailOptionsDto userEmailOptions) { - var smtpConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig; + var smtpConfig = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig; var email = new MimeMessage() { Subject = userEmailOptions.Subject, @@ -508,11 +478,11 @@ public class EmailService : IEmailService AppUser? user; if (userEmailOptions.Template == SendToDeviceTemplate) { - user = await _unitOfWork.UserRepository.GetUserByDeviceEmail(emailAddress); + user = await unitOfWork.UserRepository.GetUserByDeviceEmail(emailAddress); } else { - user = await _unitOfWork.UserRepository.GetUserByEmailAsync(emailAddress); + user = await unitOfWork.UserRepository.GetUserByEmailAsync(emailAddress); } @@ -532,13 +502,13 @@ public class EmailService : IEmailService } catch (Exception ex) { - _logger.LogError(ex, "There was an issue sending the email"); + logger.LogError(ex, "There was an issue sending the email"); if (user != null) { await LogEmailHistory(user.Id, userEmailOptions.Template, userEmailOptions.Subject, userEmailOptions.Body, "Failed", ex.Message); } - _logger.LogError("Could not find user on file for email, {Template} email was not sent and not recorded into history table", userEmailOptions.Template); + logger.LogError("Could not find user on file for email, {Template} email was not sent and not recorded into history table", userEmailOptions.Template); throw; } @@ -566,21 +536,21 @@ public class EmailService : IEmailService ErrorMessage = errorMessage }; - _unitOfWork.DataContext.EmailHistory.Add(emailHistory); - await _unitOfWork.CommitAsync(); + unitOfWork.DataContext.EmailHistory.Add(emailHistory); + await unitOfWork.CommitAsync(); } private async Task GetTemplatePath(string templateName) { - if ((await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig.CustomizedTemplates) + if ((await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig.CustomizedTemplates) { - var templateDirectory = Path.Join(_directoryService.CustomizedTemplateDirectory, TemplatePath); + var templateDirectory = Path.Join(directoryService.CustomizedTemplateDirectory, TemplatePath); var fullName = string.Format(templateDirectory, templateName); - if (_directoryService.FileSystem.File.Exists(fullName)) return fullName; - _logger.LogError("Customized Templates is on, but template {TemplatePath} is missing", fullName); + if (directoryService.FileSystem.File.Exists(fullName)) return fullName; + logger.LogError("Customized Templates is on, but template {TemplatePath} is missing", fullName); } - return string.Format(Path.Join(_directoryService.TemplateDirectory, TemplatePath), templateName); + return string.Format(Path.Join(directoryService.TemplateDirectory, TemplatePath), templateName); } private async Task GetEmailBody(string templateName) diff --git a/API/Services/EntityNamingService.cs b/Kavita.Services/EntityNamingService.cs similarity index 85% rename from API/Services/EntityNamingService.cs rename to Kavita.Services/EntityNamingService.cs index ead40e7d3..85c4b92d0 100644 --- a/API/Services/EntityNamingService.cs +++ b/Kavita.Services/EntityNamingService.cs @@ -2,55 +2,14 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; -using API.DTOs; -using API.DTOs.ReadingLists; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Services; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; -namespace API.Services; -#nullable enable - -/// -/// Provides consistent, testable naming for series, volumes, and chapters across the application. -/// All methods are pure functions with no side effects. -/// -public interface IEntityNamingService -{ - /// - /// Formats a chapter title based on library type and chapter metadata. - /// - string FormatChapterTitle(LibraryType libraryType, ChapterDto chapter, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); - - /// - /// Formats a chapter title from raw values. - /// - string FormatChapterTitle(LibraryType libraryType, bool isSpecial, string range, string? title, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null, bool withHash = true); - - /// - /// Formats a volume name based on library type and volume metadata. - /// - string? FormatVolumeName(LibraryType libraryType, VolumeDto volume, string? volumeLabel = null); - /// - /// Builds a full display title for a chapter within a series/volume context. - /// Used for OPDS feeds, reading lists, etc. - /// - string BuildFullTitle(LibraryType libraryType, SeriesDto series, VolumeDto? volume, ChapterDto chapter, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); - /// - /// Builds a display title for a chapter within its volume context. - /// Used when series context is not needed (e.g., reading history within a series grouping). - /// - string BuildChapterTitle(LibraryType libraryType, VolumeDto volume, ChapterDto chapter, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); - /// - /// Formats a reading list item title based on the item's metadata. - /// Handles the unique naming conventions for reading list display. - /// - string FormatReadingListItemTitle(ReadingListItemDto item, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); - - /// - /// Formats a reading list item title from raw values. - /// - string FormatReadingListItemTitle( LibraryType libraryType, MangaFormat format, string? chapterNumber, string? volumeNumber, string? chapterTitleName, bool isSpecial, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); -} +namespace Kavita.Services; public partial class EntityNamingService : IEntityNamingService { diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/Kavita.Services/Extensions/ApplicationServiceExtensions.cs similarity index 56% rename from API/Extensions/ApplicationServiceExtensions.cs rename to Kavita.Services/Extensions/ApplicationServiceExtensions.cs index 67d59022e..38f7c4001 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/Kavita.Services/Extensions/ApplicationServiceExtensions.cs @@ -1,41 +1,27 @@ -using System.IO.Abstractions; -using API.Constants; -using API.Data; -using API.Data.AutoMapper; -using API.Helpers; -using API.Services; -using API.Services.Caching; -using API.Services.Plus; -using API.Services.Reading; -using API.Services.Store; -using API.Services.Tasks; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner; -using API.SignalR; -using API.SignalR.Presence; -using Kavita.Common; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.Extensions.Configuration; +using System.IO.Abstractions; +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.Scanner; +using Kavita.API.Services.SignalR; +using Kavita.Services.Helpers; +using Kavita.Services.HostedServices; +using Kavita.Services.Metadata; +using Kavita.Services.Plus; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; +using Kavita.Services.SignalR; using Microsoft.Extensions.DependencyInjection; -using NeoSmart.Caching.Sqlite; - -namespace API.Extensions; +namespace Kavita.Services.Extensions; public static class ApplicationServiceExtensions { - public static void AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env) + + public static void AddKavitaServices(this IServiceCollection services) { - services.AddAutoMapper(typeof(Program).Assembly); - - services.AddScoped(); - services.AddScoped(sp => sp.GetRequiredService()); - - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -87,7 +73,6 @@ public static class ApplicationServiceExtensions services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); @@ -107,56 +92,9 @@ public static class ApplicationServiceExtensions services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); - services.AddSqLite(); - services.AddSignalR(opt => opt.EnableDetailedErrors = true); - - services.AddEasyCaching(options => - { - options.UseInMemory(EasyCacheProfiles.Favicon); - options.UseInMemory(EasyCacheProfiles.Publisher); - options.UseInMemory(EasyCacheProfiles.Library); - options.UseInMemory(EasyCacheProfiles.RevokedJwt); - options.UseInMemory(EasyCacheProfiles.LocaleOptions); - - // KavitaPlus stuff - options.UseInMemory(EasyCacheProfiles.KavitaPlusExternalSeries); - options.UseInMemory(EasyCacheProfiles.License); - options.UseInMemory(EasyCacheProfiles.LicenseInfo); - options.UseInMemory(EasyCacheProfiles.KavitaPlusMatchSeries); - }); - - services.AddMemoryCache(options => - { - options.SizeLimit = Configuration.CacheSize * 1024 * 1024; // 75 MB - options.CompactionPercentage = 0.1; // LRU compaction, Evict 10% when limit reached - }); - - services.AddSingleton(); - services.AddSingleton(); - - services.AddSwaggerGen(g => - { - g.UseInlineDefinitionsForEnums(); - }); + services.AddHostedService(); } - private static void AddSqLite(this IServiceCollection services) - { - services.AddSqliteCache("config/cache.db"); - - services.AddDbContextPool(options => - { - options.UseSqlite("Data source=config/kavita.db", builder => - { - builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); - }); - options.EnableDetailedErrors(); - options.EnableSensitiveDataLogging(); - options.ConfigureWarnings(warnings => - warnings.Ignore(RelationalEventId.PendingModelChangesWarning)); - }); - } } diff --git a/Kavita.Services/Extensions/ChapterExtensions.cs b/Kavita.Services/Extensions/ChapterExtensions.cs new file mode 100644 index 000000000..1323c1183 --- /dev/null +++ b/Kavita.Services/Extensions/ChapterExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; + +namespace Kavita.Services.Extensions; + +public static class ChapterExtensions +{ + extension(Chapter chapter) + { + public void UpdateFrom(ParserInfo info) + { + chapter.Files ??= new List(); + chapter.IsSpecial = info.IsSpecialInfo(); + if (chapter.IsSpecial) + { + chapter.Number = Scanner.Parser.DefaultChapter; + chapter.MinNumber = Scanner.Parser.DefaultChapterNumber; + chapter.MaxNumber = Scanner.Parser.DefaultChapterNumber; + } + chapter.Title = (chapter.IsSpecial && info.Format is MangaFormat.Epub or MangaFormat.Pdf) + ? info.Title + : Scanner.Parser.RemoveExtensionIfSupported(chapter.Range); + + var specialTreatment = info.IsSpecialInfo(); + chapter.Range = specialTreatment ? info.Filename : info.Chapters; + } + + /// + /// Returns the Chapter Number. If the chapter is a range, returns that, formatted. + /// + /// + public string GetNumberTitle() + { + try + { + if (chapter.MinNumber.Is(chapter.MaxNumber)) + { + if (chapter.MinNumber.Is(Scanner.Parser.DefaultChapterNumber) && chapter.IsSpecial) + { + return Scanner.Parser.RemoveExtensionIfSupported(chapter.Title) ?? string.Empty; + } + + if (chapter.MinNumber.Is(0f) && !float.TryParse(chapter.Range, CultureInfo.InvariantCulture, out _)) + { + return $"{chapter.Range.ToString(CultureInfo.InvariantCulture)}"; + } + + return $"{chapter.MinNumber.ToString(CultureInfo.InvariantCulture)}"; + + } + + return $"{chapter.MinNumber.ToString(CultureInfo.InvariantCulture)}-{chapter.MaxNumber.ToString(CultureInfo.InvariantCulture)}"; + } + catch (Exception) + { + return chapter.MinNumber.ToString(CultureInfo.InvariantCulture); + } + } + } +} diff --git a/API/Extensions/ChapterListExtensions.cs b/Kavita.Services/Extensions/ChapterListExtensions.cs similarity index 80% rename from API/Extensions/ChapterListExtensions.cs rename to Kavita.Services/Extensions/ChapterListExtensions.cs index 0b6db61dc..0de71ea28 100644 --- a/API/Extensions/ChapterListExtensions.cs +++ b/Kavita.Services/Extensions/ChapterListExtensions.cs @@ -1,12 +1,11 @@ using System.Collections.Generic; using System.Linq; -using API.Entities; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Common.Helpers; +using Kavita.Models.Entities; +using Kavita.Models.Parser; +using Kavita.Services.Builders; -namespace API.Extensions; -#nullable enable +namespace Kavita.Services.Extensions; public static class ChapterListExtensions { @@ -30,13 +29,17 @@ public static class ChapterListExtensions /// public static Chapter? GetChapterByRange(this IEnumerable chapters, ParserInfo info) { - var normalizedPath = Parser.NormalizePath(info.FullFilePath); + var normalizedPath = Scanner.Parser.NormalizePath(info.FullFilePath); var specialTreatment = info.IsSpecialInfo(); + // NOTE: This can fail to find the chapter when Range is "1.0" as the chapter will store it as "1" hence why we need to emulate a Chapter var fakeChapter = new ChapterBuilder(info.Chapters, info.Chapters).Build(); fakeChapter.UpdateFrom(info); + return specialTreatment - ? chapters.FirstOrDefault(c => c.Range == Parser.RemoveExtensionIfSupported(info.Filename) || c.Files.Select(f => Parser.NormalizePath(f.FilePath)).Contains(normalizedPath)) + ? chapters.FirstOrDefault(c => + c.Range == Scanner.Parser.RemoveExtensionIfSupported(info.Filename) + || c.Files.Select(f => Scanner.Parser.NormalizePath(f.FilePath)).Contains(normalizedPath)) : chapters.FirstOrDefault(c => c.Range == fakeChapter.GetNumberTitle()); } diff --git a/Kavita.Services/Extensions/ComicInfoExtensions.cs b/Kavita.Services/Extensions/ComicInfoExtensions.cs new file mode 100644 index 000000000..1fc2e5de7 --- /dev/null +++ b/Kavita.Services/Extensions/ComicInfoExtensions.cs @@ -0,0 +1,77 @@ +using Kavita.Models.Metadata; +using Nager.ArticleNumber; + +namespace Kavita.Services.Extensions; + +public static class ComicInfoExtensions +{ + + extension(ComicInfo? info) + { + public void CleanComicInfo() + { + if (info == null) return; + + info.Series = info.Series.Trim(); + info.SeriesSort = info.SeriesSort.Trim(); + info.LocalizedSeries = info.LocalizedSeries.Trim(); + + info.Writer = Scanner.Parser.CleanAuthor(info.Writer); + info.Colorist = Scanner.Parser.CleanAuthor(info.Colorist); + info.Editor = Scanner.Parser.CleanAuthor(info.Editor); + info.Inker = Scanner.Parser.CleanAuthor(info.Inker); + info.Letterer = Scanner.Parser.CleanAuthor(info.Letterer); + info.Penciller = Scanner.Parser.CleanAuthor(info.Penciller); + info.Publisher = Scanner.Parser.CleanAuthor(info.Publisher); + info.Imprint = Scanner.Parser.CleanAuthor(info.Imprint); + info.Characters = Scanner.Parser.CleanAuthor(info.Characters); + info.Translator = Scanner.Parser.CleanAuthor(info.Translator); + info.CoverArtist = Scanner.Parser.CleanAuthor(info.CoverArtist); + info.Teams = Scanner.Parser.CleanAuthor(info.Teams); + info.Locations = Scanner.Parser.CleanAuthor(info.Locations); + + // We need to convert GTIN to ISBN + info.Isbn = ParseGtin(info.GTIN); + + if (!string.IsNullOrEmpty(info.Number)) + { + info.Number = info.Number.Trim().Replace(",", "."); // Corrective measure for non English OSes + } + + if (!string.IsNullOrEmpty(info.Volume)) + { + info.Volume = info.Volume.Trim(); + } + } + } + + /// + /// For a given GTIN, attempts to parse out an ISBN and set the Isbn property. + /// + /// + /// + public static string ParseGtin(string? gtin) + { + if (string.IsNullOrEmpty(gtin)) return string.Empty; + + + // This is likely a valid ISBN + if (gtin[0] == '0') + { + var offset = gtin[1] == '-' ? 0 : 1; + var potentialIsbn = gtin[offset..]; + if (ArticleNumberHelper.IsValidIsbn13(potentialIsbn)) + { + return potentialIsbn; + } + } + + if (ArticleNumberHelper.IsValidIsbn10(gtin) || ArticleNumberHelper.IsValidIsbn13(gtin)) + { + return gtin; + } + + return string.Empty; + } + +} diff --git a/API/Extensions/FileTypeGroupExtensions.cs b/Kavita.Services/Extensions/FileTypeGroupExtensions.cs similarity index 58% rename from API/Extensions/FileTypeGroupExtensions.cs rename to Kavita.Services/Extensions/FileTypeGroupExtensions.cs index 24073f642..1b0db59f0 100644 --- a/API/Extensions/FileTypeGroupExtensions.cs +++ b/Kavita.Services/Extensions/FileTypeGroupExtensions.cs @@ -1,8 +1,7 @@ -using System; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; +using System; +using Kavita.Models.Entities.Enums; -namespace API.Extensions; +namespace Kavita.Services.Extensions; public static class FileTypeGroupExtensions { @@ -11,13 +10,13 @@ public static class FileTypeGroupExtensions switch (fileTypeGroup) { case FileTypeGroup.Archive: - return Parser.ArchiveFileExtensions; + return Scanner.Parser.ArchiveFileExtensions; case FileTypeGroup.Epub: - return Parser.EpubFileExtension; + return Scanner.Parser.EpubFileExtension; case FileTypeGroup.Pdf: - return Parser.PdfFileExtension; + return Scanner.Parser.PdfFileExtension; case FileTypeGroup.Images: - return Parser.ImageFileExtensions; + return Scanner.Parser.ImageFileExtensions; default: throw new ArgumentOutOfRangeException(nameof(fileTypeGroup), fileTypeGroup, null); } diff --git a/API/Extensions/IHasKPlusMetadataExtensions.cs b/Kavita.Services/Extensions/IHasKPlusMetadataExtensions.cs similarity index 79% rename from API/Extensions/IHasKPlusMetadataExtensions.cs rename to Kavita.Services/Extensions/IHasKPlusMetadataExtensions.cs index 84e35adc4..0e2a57164 100644 --- a/API/Extensions/IHasKPlusMetadataExtensions.cs +++ b/Kavita.Services/Extensions/IHasKPlusMetadataExtensions.cs @@ -1,7 +1,7 @@ -using API.Entities.Interfaces; -using API.Entities.MetadataMatching; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.MetadataMatching; -namespace API.Extensions; +namespace Kavita.Services.Extensions; public static class IHasKPlusMetadataExtensions { diff --git a/Kavita.Services/Extensions/ParserExtensions.cs b/Kavita.Services/Extensions/ParserExtensions.cs new file mode 100644 index 000000000..1b094878f --- /dev/null +++ b/Kavita.Services/Extensions/ParserExtensions.cs @@ -0,0 +1,36 @@ +using Kavita.Models.Parser; + +namespace Kavita.Services.Extensions; + +public static class ParserExtensions +{ + + extension(ParserInfo info) + { + /// + /// Merges non-empty/null properties from info2 into this entity. + /// + /// This does not merge ComicInfo as they should always be the same + /// + public void Merge(ParserInfo? info2) + { + if (info2 == null) return; + info.Chapters = Scanner.Parser.IsDefaultChapter(info.Chapters) ? info2.Chapters: info.Chapters; + info.Volumes = Scanner.Parser.IsLooseLeafVolume(info.Volumes) ? info2.Volumes : info.Volumes; + info.Edition = string.IsNullOrEmpty(info.Edition) ? info2.Edition : info.Edition; + info.Title = string.IsNullOrEmpty(info.Title) ? info2.Title : info.Title; + info.Series = string.IsNullOrEmpty(info.Series) ? info2.Series : info.Series; + info.IsSpecial = info.IsSpecial || info2.IsSpecial; + } + + /// + /// If the ParserInfo has the IsSpecial tag or both volumes and chapters are default aka 0 + /// + /// + public bool IsSpecialInfo() + { + return info.IsSpecial || (Scanner.Parser.IsLooseLeafVolume(info.Volumes) && Scanner.Parser.IsDefaultChapter(info.Chapters)); + } + } + +} diff --git a/API/Extensions/ParserInfoListExtensions.cs b/Kavita.Services/Extensions/ParserInfoListExtensions.cs similarity index 73% rename from API/Extensions/ParserInfoListExtensions.cs rename to Kavita.Services/Extensions/ParserInfoListExtensions.cs index 38a8ecc30..740d1b446 100644 --- a/API/Extensions/ParserInfoListExtensions.cs +++ b/Kavita.Services/Extensions/ParserInfoListExtensions.cs @@ -1,10 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using API.Entities; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities; +using Kavita.Models.Parser; -namespace API.Extensions; -#nullable enable +namespace Kavita.Services.Extensions; public static class ParserInfoListExtensions { @@ -27,8 +26,8 @@ public static class ParserInfoListExtensions /// public static bool HasInfo(this IList infos, Chapter chapter) { - var chapterFiles = chapter.Files.Select(x => Parser.NormalizePath(x.FilePath)).ToList(); - var infoFiles = infos.Select(x => Parser.NormalizePath(x.FullFilePath)).ToList(); + var chapterFiles = chapter.Files.Select(x => Scanner.Parser.NormalizePath(x.FilePath)).ToList(); + var infoFiles = infos.Select(x => Scanner.Parser.NormalizePath(x.FullFilePath)).ToList(); return infoFiles.Intersect(chapterFiles).Any(); } diff --git a/API/Extensions/SeriesExtensions.cs b/Kavita.Services/Extensions/SeriesExtensions.cs similarity index 81% rename from API/Extensions/SeriesExtensions.cs rename to Kavita.Services/Extensions/SeriesExtensions.cs index 01ae718c7..78de92330 100644 --- a/API/Extensions/SeriesExtensions.cs +++ b/Kavita.Services/Extensions/SeriesExtensions.cs @@ -1,10 +1,9 @@ -using System.Linq; -using API.Comparators; -using API.Entities; -using API.Services.Tasks.Scanner.Parser; +using System.Linq; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Services.Comparators; -namespace API.Extensions; -#nullable enable +namespace Kavita.Services.Extensions; public static class SeriesExtensions { @@ -23,7 +22,7 @@ public static class SeriesExtensions if (firstVolume == null) return null; // If first volume here is specials, move to the next as specials should almost always be last. - if (firstVolume.MinNumber.Is(Parser.SpecialVolumeNumber) && volumes.Count > 1) + if (firstVolume.MinNumber.Is(Scanner.Parser.SpecialVolumeNumber) && volumes.Count > 1) { firstVolume = volumes[1]; } @@ -44,16 +43,16 @@ public static class SeriesExtensions } // just volumes - if (volumes.TrueForAll(v => v.MinNumber.IsNot(Parser.LooseLeafVolumeNumber))) + if (volumes.TrueForAll(v => v.MinNumber.IsNot(Scanner.Parser.LooseLeafVolumeNumber))) { return firstVolume.CoverImage; } // If we have loose leaf chapters // if loose leaf chapters AND volumes, just return first volume - if (volumes.Count >= 1 && volumes[0].MinNumber.IsNot(Parser.LooseLeafVolumeNumber)) + if (volumes.Count >= 1 && volumes[0].MinNumber.IsNot(Scanner.Parser.LooseLeafVolumeNumber)) { - var looseLeafChapters = volumes.Where(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber)) + var looseLeafChapters = volumes.Where(v => v.MinNumber.Is(Scanner.Parser.LooseLeafVolumeNumber)) .SelectMany(c => c.Chapters.Where(c2 => !c2.IsSpecial)) .OrderBy(c => c.SortOrder) .ToList(); @@ -68,7 +67,7 @@ public static class SeriesExtensions } var chpts = volumes - .First(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber)) + .First(v => v.MinNumber.Is(Scanner.Parser.LooseLeafVolumeNumber)) .Chapters .Where(c => !c.IsSpecial) .OrderBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default) diff --git a/Kavita.Services/Extensions/VolumeExtensions.cs b/Kavita.Services/Extensions/VolumeExtensions.cs new file mode 100644 index 000000000..f508e871b --- /dev/null +++ b/Kavita.Services/Extensions/VolumeExtensions.cs @@ -0,0 +1,30 @@ +using Kavita.Common.Extensions; +using Kavita.Models.DTOs; + +namespace Kavita.Services.Extensions; + +public static class VolumeExtensions +{ + + extension(VolumeDto volumeDto) + { + /// + /// Is this a loose leaf volume + /// + /// + public bool IsLooseLeaf() + { + return volumeDto.MinNumber.Is(Scanner.Parser.LooseLeafVolumeNumber); + } + + /// + /// Does this volume hold only specials + /// + /// + public bool IsSpecial() + { + return volumeDto.MinNumber.Is(Scanner.Parser.SpecialVolumeNumber); + } + } + +} diff --git a/API/Extensions/VolumeListExtensions.cs b/Kavita.Services/Extensions/VolumeListExtensions.cs similarity index 77% rename from API/Extensions/VolumeListExtensions.cs rename to Kavita.Services/Extensions/VolumeListExtensions.cs index 2fa0446b4..218b5a3a0 100644 --- a/API/Extensions/VolumeListExtensions.cs +++ b/Kavita.Services/Extensions/VolumeListExtensions.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using API.Comparators; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Comparators; -namespace API.Extensions; -#nullable enable +namespace Kavita.Services.Extensions; public static class VolumeListExtensions { @@ -44,7 +43,7 @@ public static class VolumeListExtensions /// public static bool HasAnyNonLooseLeafVolumes(this IEnumerable volumes) { - return volumes.Any(v => v.MinNumber.IsNot(Parser.DefaultChapterNumber)); + return volumes.Any(v => v.MinNumber.IsNot(Scanner.Parser.DefaultChapterNumber)); } /// @@ -55,7 +54,7 @@ public static class VolumeListExtensions public static Volume? FirstNonLooseLeafOrDefault(this IEnumerable volumes) { return volumes.OrderBy(x => x.MinNumber, ChapterSortComparerDefaultLast.Default) - .FirstOrDefault(v => v.MinNumber.IsNot(Parser.DefaultChapterNumber)); + .FirstOrDefault(v => v.MinNumber.IsNot(Scanner.Parser.DefaultChapterNumber)); } /// @@ -65,7 +64,7 @@ public static class VolumeListExtensions /// public static Volume? GetLooseLeafVolumeOrDefault(this IEnumerable volumes) { - return volumes.FirstOrDefault(v => v.MinNumber.Is(Parser.DefaultChapterNumber)); + return volumes.FirstOrDefault(v => v.MinNumber.Is(Scanner.Parser.DefaultChapterNumber)); } /// @@ -75,16 +74,16 @@ public static class VolumeListExtensions /// public static Volume? GetSpecialVolumeOrDefault(this IEnumerable volumes) { - return volumes.FirstOrDefault(v => v.MinNumber.Is(Parser.SpecialVolumeNumber)); + return volumes.FirstOrDefault(v => v.MinNumber.Is(Scanner.Parser.SpecialVolumeNumber)); } public static IEnumerable WhereNotLooseLeaf(this IEnumerable volumes) { - return volumes.Where(v => v.MinNumber.Is(Parser.DefaultChapterNumber)); + return volumes.Where(v => v.MinNumber.Is(Scanner.Parser.DefaultChapterNumber)); } public static IEnumerable WhereLooseLeaf(this IEnumerable volumes) { - return volumes.Where(v => v.MinNumber.Is(Parser.DefaultChapterNumber)); + return volumes.Where(v => v.MinNumber.Is(Scanner.Parser.DefaultChapterNumber)); } } diff --git a/API/Extensions/ZipArchiveExtensions.cs b/Kavita.Services/Extensions/ZipArchiveExtensions.cs similarity index 70% rename from API/Extensions/ZipArchiveExtensions.cs rename to Kavita.Services/Extensions/ZipArchiveExtensions.cs index 8ed338e57..68763c4f6 100644 --- a/API/Extensions/ZipArchiveExtensions.cs +++ b/Kavita.Services/Extensions/ZipArchiveExtensions.cs @@ -1,14 +1,13 @@ -using System.IO; +using System.IO; using System.IO.Compression; using System.Linq; -namespace API.Extensions; -#nullable enable +namespace Kavita.Services.Extensions; public static class ZipArchiveExtensions { /// - /// Checks if archive has one or more files. Excludes directory entries. + /// Checks if the archive has one or more files. Excludes directory entries. /// /// /// diff --git a/API/Services/FileService.cs b/Kavita.Services/FileService.cs similarity index 87% rename from API/Services/FileService.cs rename to Kavita.Services/FileService.cs index 19a2952e1..8f19484fd 100644 --- a/API/Services/FileService.cs +++ b/Kavita.Services/FileService.cs @@ -3,17 +3,10 @@ using System.IO; using System.IO.Abstractions; using System.Security.Cryptography; using System.Text; -using API.Extensions; +using Kavita.API.Services; +using Kavita.Common.Extensions; -namespace API.Services; - -public interface IFileService -{ - IFileSystem GetFileSystem(); - bool HasFileBeenModifiedSince(string filePath, DateTime time); - bool Exists(string filePath); - bool ValidateSha(string filepath, string sha); -} +namespace Kavita.Services; public class FileService : IFileService { diff --git a/API/Services/FontService.cs b/Kavita.Services/FontService.cs similarity index 61% rename from API/Services/FontService.cs rename to Kavita.Services/FontService.cs index 7b8c9f230..489bd2727 100644 --- a/API/Services/FontService.cs +++ b/Kavita.Services/FontService.cs @@ -2,18 +2,21 @@ using System; using System.IO; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Entities; -using API.Entities.Enums.Font; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; using Flurl.Http; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.Font; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace API.Services.Tasks; +namespace Kavita.Services; // Although we don't use all the fields, just including them all for completeness internal class GoogleFontsMetadata @@ -79,54 +82,33 @@ internal class GoogleFontsData public required int nanos { get; init; } } -public interface IFontService +public class FontService(IDirectoryService directoryService, IUnitOfWork unitOfWork, ILogger logger) + : IFontService { - Task CreateFontFromFileAsync(string path); - Task Delete(int fontId); - Task CreateFontFromUrl(string url); - Task IsFontInUse(int fontId); -} - -public class FontService: IFontService -{ - - public static readonly string DefaultFont = "Default"; - - private readonly IDirectoryService _directoryService; - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private const string SupportedFontUrlPrefix = "https://fonts.google.com/"; private const string DownloadFontUrlPrefix = "https://fonts.google.com/download/list?family="; private const string GoogleFontsInvalidJsonPrefix = ")]}'"; - public FontService(IDirectoryService directoryService, IUnitOfWork unitOfWork, ILogger logger) + public async Task CreateFontFromFileAsync(string path, CancellationToken ct = default) { - _directoryService = directoryService; - _unitOfWork = unitOfWork; - _logger = logger; - } - - public async Task CreateFontFromFileAsync(string path) - { - if (!_directoryService.FileSystem.File.Exists(path)) + if (!directoryService.FileSystem.File.Exists(path)) { - _logger.LogInformation("Unable to create font from manual upload as font not in temp"); + logger.LogInformation("Unable to create font from manual upload as font not in temp"); throw new KavitaException("errors.font-manual-upload"); } - var fileName = _directoryService.FileSystem.FileInfo.New(path).Name; - var nakedFileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(fileName); + var fileName = directoryService.FileSystem.FileInfo.New(path).Name; + var nakedFileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(fileName); var fontName = Parser.PrettifyFileName(nakedFileName); var normalizedName = Parser.Normalize(nakedFileName); - if (await _unitOfWork.EpubFontRepository.GetFontDtoByNameAsync(fontName) != null) + if (await unitOfWork.EpubFontRepository.GetFontDtoByNameAsync(fontName, ct) != null) { throw new KavitaException("errors.font-already-in-use"); } - _directoryService.CopyFileToDirectory(path, _directoryService.EpubFontDirectory); - var finalLocation = _directoryService.FileSystem.Path.Join(_directoryService.EpubFontDirectory, fileName); + directoryService.CopyFileToDirectory(path, directoryService.EpubFontDirectory); + var finalLocation = directoryService.FileSystem.Path.Join(directoryService.EpubFontDirectory, fileName); var font = new EpubFont() { @@ -135,10 +117,10 @@ public class FontService: IFontService FileName = Path.GetFileName(finalLocation), Provider = FontProvider.User }; - _unitOfWork.EpubFontRepository.Add(font); - await _unitOfWork.CommitAsync(); + unitOfWork.EpubFontRepository.Add(font); + await unitOfWork.CommitAsync(ct); - // TODO: Send update to UI + // default: Send update to UI return font; } @@ -146,15 +128,16 @@ public class FontService: IFontService /// This does not check if in use, use /// /// - public async Task Delete(int fontId) + /// + public async Task Delete(int fontId, CancellationToken ct = default) { - var font = await _unitOfWork.EpubFontRepository.GetFontAsync(fontId); + var font = await unitOfWork.EpubFontRepository.GetFontAsync(fontId, ct); if (font == null) return; await RemoveFont(font); } - public async Task CreateFontFromUrl(string url) + public async Task CreateFontFromUrl(string url, CancellationToken ct = default) { if (!url.StartsWith(SupportedFontUrlPrefix)) { @@ -163,69 +146,70 @@ public class FontService: IFontService // Extract Font name from url var fontFamily = url.Split(SupportedFontUrlPrefix)[1].Split("?")[0].Split("/").Last(); - _logger.LogInformation("Preparing to download {FontName} font", fontFamily.Sanitize()); + logger.LogInformation("Preparing to download {FontName} font", fontFamily.Sanitize()); var metaData = await GetGoogleFontsMetadataAsync(fontFamily); if (metaData == null) { - _logger.LogError("Unable to find metadata for {FontName}", fontFamily.Sanitize()); + logger.LogError("Unable to find metadata for {FontName}", fontFamily.Sanitize()); throw new KavitaException("errors.font-not-found"); } var googleFontRef = metaData.VariableFont(); if (googleFontRef == null) { - _logger.LogError("Unable to find variable font for {FontName} with metadata {MetaData}", fontFamily.Sanitize(), metaData); + logger.LogError("Unable to find variable font for {FontName} with metadata {MetaData}", fontFamily.Sanitize(), metaData); throw new KavitaException("errors.font-not-found"); } var fontExt = Path.GetExtension(googleFontRef.filename); var fileName = $"{fontFamily}{fontExt}"; - _logger.LogDebug("Downloading font {FontFamily} to {FileName} from {Url}", fontFamily.Sanitize(), fileName, googleFontRef.url); - var path = await googleFontRef.url.DownloadFileAsync(_directoryService.TempDirectory, fileName); + logger.LogDebug("Downloading font {FontFamily} to {FileName} from {Url}", fontFamily.Sanitize(), fileName.Sanitize(), googleFontRef.url); + var path = await googleFontRef.url.DownloadFileAsync(directoryService.TempDirectory, fileName, cancellationToken: ct); - return await CreateFontFromFileAsync(path); + return await CreateFontFromFileAsync(path, ct); } /// /// Returns if the given font is in use by any other user. System provided fonts will always return true. /// /// + /// /// - public async Task IsFontInUse(int fontId) + public async Task IsFontInUse(int fontId, CancellationToken ct = default) { - var font = await _unitOfWork.EpubFontRepository.GetFontAsync(fontId); + var font = await unitOfWork.EpubFontRepository.GetFontAsync(fontId, ct); if (font == null || font.Provider == FontProvider.System) return true; - return await _unitOfWork.EpubFontRepository.IsFontInUseAsync(fontId); + return await unitOfWork.EpubFontRepository.IsFontInUseAsync(fontId, ct); } - public async Task RemoveFont(EpubFont font) + private async Task RemoveFont(EpubFont font) { if (font.Provider == FontProvider.System) return; - var prefs = await _unitOfWork.UserRepository.GetAllPreferencesByFontAsync(font.Name); + var prefs = await unitOfWork.UserRepository.GetAllPreferencesByFontAsync(font.Name); foreach (var pref in prefs) { - pref.BookReaderFontFamily = DefaultFont; - _unitOfWork.UserRepository.Update(pref); + pref.BookReaderFontFamily = Defaults.DefaultFont; + unitOfWork.UserRepository.Update(pref); } try { // Copy the font file to temp for nightly removal (to give user time to reclaim if made a mistake) var existingLocation = - _directoryService.FileSystem.Path.Join(_directoryService.EpubFontDirectory, font.FileName); + directoryService.FileSystem.Path.Join(directoryService.EpubFontDirectory, font.FileName); var newLocation = - _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, font.FileName); - _directoryService.CopyFileToDirectory(existingLocation, newLocation); - _directoryService.DeleteFiles([existingLocation]); + directoryService.FileSystem.Path.Join(directoryService.TempDirectory, font.FileName); + directoryService.CopyFileToDirectory(existingLocation, newLocation); + directoryService.DeleteFiles([existingLocation]); } catch (Exception) { /* Swallow */ } - _unitOfWork.EpubFontRepository.Remove(font); - await _unitOfWork.CommitAsync(); + unitOfWork.EpubFontRepository.Remove(font); + await unitOfWork.CommitAsync(); } private async Task GetGoogleFontsMetadataAsync(string fontName) @@ -243,7 +227,7 @@ public class FontService: IFontService .GetStringAsync(); } catch (Exception ex) { - _logger.LogError(ex, "Unable to get metadata for {FontName} from {Url}", fontName.Sanitize(), url); + logger.LogError(ex, "Unable to get metadata for {FontName} from {Url}", fontName.Sanitize(), url.Sanitize()); return null; } diff --git a/API/Helpers/AnnotationHelper.cs b/Kavita.Services/Helpers/AnnotationHelper.cs similarity index 99% rename from API/Helpers/AnnotationHelper.cs rename to Kavita.Services/Helpers/AnnotationHelper.cs index 3b2fb4277..caddc9938 100644 --- a/API/Helpers/AnnotationHelper.cs +++ b/Kavita.Services/Helpers/AnnotationHelper.cs @@ -1,12 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using API.DTOs.Reader; using HtmlAgilityPack; +using Kavita.Models.DTOs.Reader; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; public static partial class AnnotationHelper { diff --git a/API/Helpers/BookChapterItemHelper.cs b/Kavita.Services/Helpers/BookChapterItemHelper.cs similarity index 96% rename from API/Helpers/BookChapterItemHelper.cs rename to Kavita.Services/Helpers/BookChapterItemHelper.cs index 05ac09e85..1da6f5367 100644 --- a/API/Helpers/BookChapterItemHelper.cs +++ b/Kavita.Services/Helpers/BookChapterItemHelper.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using API.DTOs.Reader; +using Kavita.Models.DTOs.Reader; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; public static class BookChapterItemHelper { diff --git a/API/Helpers/BookSortTitlePrefixHelper.cs b/Kavita.Services/Helpers/BookSortTitlePrefixHelper.cs similarity index 98% rename from API/Helpers/BookSortTitlePrefixHelper.cs rename to Kavita.Services/Helpers/BookSortTitlePrefixHelper.cs index c92df5d65..41a5f7774 100644 --- a/API/Helpers/BookSortTitlePrefixHelper.cs +++ b/Kavita.Services/Helpers/BookSortTitlePrefixHelper.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -namespace API.Helpers; +namespace Kavita.Services.Helpers; /// /// Responsible for parsing book titles "The man on the street" and removing the prefix -> "man on the street". diff --git a/API/Helpers/CacheHelper.cs b/Kavita.Services/Helpers/CacheHelper.cs similarity index 84% rename from API/Helpers/CacheHelper.cs rename to Kavita.Services/Helpers/CacheHelper.cs index ede5caaef..79b340219 100644 --- a/API/Helpers/CacheHelper.cs +++ b/Kavita.Services/Helpers/CacheHelper.cs @@ -1,23 +1,10 @@ -using System; -using API.Entities; -using API.Entities.Interfaces; -using API.Services; +using System; +using Kavita.API.Services; +using Kavita.API.Services.Helpers; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Interfaces; -namespace API.Helpers; -#nullable enable - -public interface ICacheHelper -{ - bool ShouldUpdateCoverImage(string coverPath, MangaFile? firstFile, DateTime chapterCreated, - bool forceUpdate = false, - bool isCoverLocked = false); - - bool CoverImageExists(string path); - - bool IsFileUnmodifiedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile? firstFile); - bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile? firstFile); - -} +namespace Kavita.Services.Helpers; public class CacheHelper : ICacheHelper { diff --git a/API/Helpers/GenreHelper.cs b/Kavita.Services/Helpers/GenreHelper.cs similarity index 96% rename from API/Helpers/GenreHelper.cs rename to Kavita.Services/Helpers/GenreHelper.cs index 8580178d9..3d8654a3c 100644 --- a/API/Helpers/GenreHelper.cs +++ b/Kavita.Services/Helpers/GenreHelper.cs @@ -2,15 +2,14 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Metadata; -using API.Entities; -using API.Extensions; -using API.Helpers.Builders; +using Kavita.API.Database; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.Entities; using Microsoft.EntityFrameworkCore; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; public static class GenreHelper diff --git a/API/Helpers/KoreaderHelper.cs b/Kavita.Services/Helpers/KoreaderHelper.cs similarity index 98% rename from API/Helpers/KoreaderHelper.cs rename to Kavita.Services/Helpers/KoreaderHelper.cs index e5bbba5f3..a6d9ca343 100644 --- a/API/Helpers/KoreaderHelper.cs +++ b/Kavita.Services/Helpers/KoreaderHelper.cs @@ -1,12 +1,12 @@ -using API.DTOs.Progress; using System; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; +using Kavita.Models.DTOs.Progress; -namespace API.Helpers; +namespace Kavita.Services.Helpers; /// /// All things related to Koreader diff --git a/API/Helpers/PdfComicInfoExtractor.cs b/Kavita.Services/Helpers/PdfComicInfoExtractor.cs similarity index 79% rename from API/Helpers/PdfComicInfoExtractor.cs rename to Kavita.Services/Helpers/PdfComicInfoExtractor.cs index f0e0b1832..9663da4d2 100644 --- a/API/Helpers/PdfComicInfoExtractor.cs +++ b/Kavita.Services/Helpers/PdfComicInfoExtractor.cs @@ -5,31 +5,24 @@ * PDF 1.7 Specification a.k.a. PDF32000-1:2008 * https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf */ + using System; -using API.Data.Metadata; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Microsoft.Extensions.Logging; -using Nager.ArticleNumber; using System.Collections.Generic; using System.Globalization; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Services.Extensions; +using Microsoft.Extensions.Logging; +using Nager.ArticleNumber; -namespace API.Helpers; -#nullable enable - -public interface IPdfComicInfoExtractor -{ - ComicInfo? GetComicInfo(string filePath); -} +namespace Kavita.Services.Helpers; /// /// Translate PDF metadata (See PdfMetadataExtractor.cs) into ComicInfo structure. /// -public class PdfComicInfoExtractor : IPdfComicInfoExtractor +public class PdfComicInfoExtractor(ILogger logger, IMediaErrorService mediaErrorService) { - private readonly ILogger _logger; - private readonly IMediaErrorService _mediaErrorService; private readonly string[] _pdfDateFormats = [ // PDF Spec 7.9.4 "D:yyyyMMddHHmmsszzz:", "D:yyyyMMddHHmmss+", "D:yyyyMMddHHmmss", "D:yyyyMMddHHmmzzz:", "D:yyyyMMddHHmm+", "D:yyyyMMddHHmm", @@ -37,12 +30,6 @@ public class PdfComicInfoExtractor : IPdfComicInfoExtractor "D:yyyyMMdd", "D:yyyyMM", "D:yyyy" ]; - public PdfComicInfoExtractor(ILogger logger, IMediaErrorService mediaErrorService) - { - _logger = logger; - _mediaErrorService = mediaErrorService; - } - private static float? GetFloatFromText(string? text) { if (string.IsNullOrEmpty(text)) return null; @@ -106,7 +93,7 @@ public class PdfComicInfoExtractor : IPdfComicInfoExtractor if (info.Isbn != string.Empty && !ArticleNumberHelper.IsValidIsbn10(info.Isbn) && !ArticleNumberHelper.IsValidIsbn13(info.Isbn)) { - _logger.LogDebug("[BookService] {File} has an invalid ISBN number", filePath); + logger.LogDebug("[BookService] {File} has an invalid ISBN number", filePath); info.Isbn = string.Empty; } @@ -116,12 +103,12 @@ public class PdfComicInfoExtractor : IPdfComicInfoExtractor info.Volume = MaybeGetMetadata(metadata, "Volume") ?? string.Empty; // If this is a single book and not a collection, set publication status to Completed - if (string.IsNullOrEmpty(info.Volume) && Parser.IsLooseLeafVolume(Parser.ParseVolume(filePath, LibraryType.Manga))) + if (string.IsNullOrEmpty(info.Volume) && Scanner.Parser.IsLooseLeafVolume(Scanner.Parser.ParseVolume(filePath, LibraryType.Manga))) { info.Count = 1; } - ComicInfo.CleanComicInfo(info); + info.CleanComicInfo(); return info; } @@ -130,14 +117,14 @@ public class PdfComicInfoExtractor : IPdfComicInfoExtractor { try { - using var extractor = new PdfMetadataExtractor(_logger, filePath); + using var extractor = new PdfMetadataExtractor(logger, filePath); return GetComicInfoFromMetadata(extractor.GetMetadata(), filePath); } catch (Exception ex) { - _logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing PDF metadata for {File}", filePath); - _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, + logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing PDF metadata for {File}", filePath); + mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, ex.Message == "Encryption not supported" ? "Encrypted PDFs are not supported" : "There was an exception parsing PDF metadata", ex); diff --git a/API/Helpers/PdfMetadataExtractor.cs b/Kavita.Services/Helpers/PdfMetadataExtractor.cs similarity index 99% rename from API/Helpers/PdfMetadataExtractor.cs rename to Kavita.Services/Helpers/PdfMetadataExtractor.cs index ef959896f..09f2420ea 100644 --- a/API/Helpers/PdfMetadataExtractor.cs +++ b/Kavita.Services/Helpers/PdfMetadataExtractor.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.IO.Compression; using System.Text; using System.Xml; -using System.IO; -using API.Services; +using Kavita.API.Services; using Microsoft.Extensions.Logging; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; // Contributed by https://github.com/microtherion @@ -804,7 +803,7 @@ internal class PdfLexer(Stream stream) internal class PdfMetadataExtractor : IPdfMetadataExtractor { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly PdfLexer _lexer; private readonly FileStream _stream; private readonly Dictionary _objectOffsets = []; @@ -824,7 +823,7 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor public readonly long Count = count; } - public PdfMetadataExtractor(ILogger logger, string filename) + public PdfMetadataExtractor(ILogger logger, string filename) { _logger = logger; _stream = File.OpenRead(filename); diff --git a/API/Helpers/PersonHelper.cs b/Kavita.Services/Helpers/PersonHelper.cs similarity index 96% rename from API/Helpers/PersonHelper.cs rename to Kavita.Services/Helpers/PersonHelper.cs index a23050800..efa0ea169 100644 --- a/API/Helpers/PersonHelper.cs +++ b/Kavita.Services/Helpers/PersonHelper.cs @@ -1,18 +1,16 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.Metadata; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.Person; -using API.Extensions; -using API.Helpers.Builders; +using Kavita.API.Database; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Person; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; // This isn't needed in the new person architecture public static class PersonHelper @@ -223,7 +221,7 @@ public static class PersonHelper } - public static async Task UpdateChapterPeopleAsync(Chapter chapter, IList people, PersonRole role, IUnitOfWork unitOfWork) + public static async Task UpdateChapterPeopleAsync(Chapter chapter, IList people, PersonRole role, IUnitOfWork unitOfWork) { var modification = false; @@ -303,6 +301,8 @@ public static class PersonHelper { await unitOfWork.CommitAsync(); } + + return modification; } diff --git a/Kavita.Services/Helpers/ReviewHelper.cs b/Kavita.Services/Helpers/ReviewHelper.cs new file mode 100644 index 000000000..6de9ea988 --- /dev/null +++ b/Kavita.Services/Helpers/ReviewHelper.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using HtmlAgilityPack; +using Kavita.Models.DTOs.SeriesDetail; + +namespace Kavita.Services.Helpers; + +public static class ReviewHelper +{ + private const int BodyTextLimit = 175; + public static IEnumerable SelectSpectrumOfReviews(IList reviews) + { + IList externalReviews; + var totalReviews = reviews.Count; + + if (totalReviews > 10) + { + var stepSize = Math.Max((totalReviews - 4) / 8, 1); + + var selectedReviews = new List() + { + reviews[0], + reviews[1], + }; + for (var i = 2; i < totalReviews - 2; i += stepSize) + { + selectedReviews.Add(reviews[i]); + + if (selectedReviews.Count >= 8) + break; + } + + selectedReviews.Add(reviews[totalReviews - 2]); + selectedReviews.Add(reviews[totalReviews - 1]); + + externalReviews = selectedReviews; + } + else + { + externalReviews = reviews; + } + + return externalReviews.OrderByDescending(r => r.Score); + } + +} diff --git a/API/Helpers/SeriesHelper.cs b/Kavita.Services/Helpers/SeriesHelper.cs similarity index 89% rename from API/Helpers/SeriesHelper.cs rename to Kavita.Services/Helpers/SeriesHelper.cs index 231575b0e..f19440267 100644 --- a/API/Helpers/SeriesHelper.cs +++ b/Kavita.Services/Helpers/SeriesHelper.cs @@ -1,12 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; public static class SeriesHelper { @@ -21,7 +20,7 @@ public static class SeriesHelper return (series.NormalizedName.Equals(parsedInfoKey.NormalizedName) || (series.LocalizedName != null && series.LocalizedName.ToNormalized().Equals(parsedInfoKey.NormalizedName)) || (series.OriginalName != null && series.OriginalName.ToNormalized().Equals(parsedInfoKey.NormalizedName)) - ) + ) && (series.Format == parsedInfoKey.Format || series.Format == MangaFormat.Unknown); } diff --git a/API/Helpers/SmartFilterHelper.cs b/Kavita.Services/Helpers/SmartFilterHelper.cs similarity index 98% rename from API/Helpers/SmartFilterHelper.cs rename to Kavita.Services/Helpers/SmartFilterHelper.cs index 8f61fde21..697d56fd6 100644 --- a/API/Helpers/SmartFilterHelper.cs +++ b/Kavita.Services/Helpers/SmartFilterHelper.cs @@ -2,12 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Web; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; -#nullable enable - -namespace API.Helpers; +namespace Kavita.Services.Helpers; public static class SmartFilterHelper { diff --git a/API/Helpers/TagHelper.cs b/Kavita.Services/Helpers/TagHelper.cs similarity index 85% rename from API/Helpers/TagHelper.cs rename to Kavita.Services/Helpers/TagHelper.cs index c00d6ee8f..22dfe2176 100644 --- a/API/Helpers/TagHelper.cs +++ b/Kavita.Services/Helpers/TagHelper.cs @@ -3,16 +3,14 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Metadata; -using API.Entities; -using API.Extensions; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.Entities; using Microsoft.EntityFrameworkCore; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; public static class TagHelper { @@ -87,24 +85,6 @@ public static class TagHelper } } - /// - /// Returns a list of strings separated by ',', distinct by normalized names, already trimmed and empty entries removed. - /// - /// - /// - public static IList GetTagValues(string comicInfoTagSeparatedByComma) - { - // TODO: Refactor this into an Extension - if (string.IsNullOrEmpty(comicInfoTagSeparatedByComma)) - { - return ImmutableList.Empty; - } - - return comicInfoTagSeparatedByComma.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) - .DistinctBy(Parser.Normalize) - .ToList(); - } - public static void UpdateTagList(ICollection? existingDbTags, Series series, IReadOnlyCollection newTags, Action handleAdd, Action onModified) { diff --git a/API/Services/HostedServices/ReadingSessionInitializer.cs b/Kavita.Services/HostedServices/ReadingSessionInitializer.cs similarity index 96% rename from API/Services/HostedServices/ReadingSessionInitializer.cs rename to Kavita.Services/HostedServices/ReadingSessionInitializer.cs index bd4a652f3..2992337c5 100644 --- a/API/Services/HostedServices/ReadingSessionInitializer.cs +++ b/Kavita.Services/HostedServices/ReadingSessionInitializer.cs @@ -2,13 +2,13 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace API.Services.HostedServices; +namespace Kavita.Services.HostedServices; public class ReadingSessionInitializer : IHostedService { diff --git a/API/Services/HostedServices/StartupTasksHostedService.cs b/Kavita.Services/HostedServices/StartupTasksHostedService.cs similarity index 83% rename from API/Services/HostedServices/StartupTasksHostedService.cs rename to Kavita.Services/HostedServices/StartupTasksHostedService.cs index 44a60849f..ec9697c20 100644 --- a/API/Services/HostedServices/StartupTasksHostedService.cs +++ b/Kavita.Services/HostedServices/StartupTasksHostedService.cs @@ -1,14 +1,14 @@ using System; using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Services.Tasks.Scanner; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace API.Services.HostedServices; -#nullable enable +namespace Kavita.Services.HostedServices; public class StartupTasksHostedService(IServiceProvider serviceProvider) : IHostedService { @@ -17,7 +17,7 @@ public class StartupTasksHostedService(IServiceProvider serviceProvider) : IHost using var scope = serviceProvider.CreateScope(); var taskScheduler = scope.ServiceProvider.GetRequiredService(); - await taskScheduler.ScheduleTasks(); + await taskScheduler.ScheduleTasks(cancellationToken); taskScheduler.ScheduleUpdaterTasks(); @@ -25,7 +25,7 @@ public class StartupTasksHostedService(IServiceProvider serviceProvider) : IHost { // These methods will automatically check if stat collection is disabled to prevent sending any data regardless // of when setting was changed - await taskScheduler.ScheduleStatsTasks(); + await taskScheduler.ScheduleStatsTasks(cancellationToken); await taskScheduler.RunStatCollection(); } catch (Exception) @@ -36,7 +36,7 @@ public class StartupTasksHostedService(IServiceProvider serviceProvider) : IHost try { var unitOfWork = scope.ServiceProvider.GetRequiredService(); - if ((await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableFolderWatching) + if ((await unitOfWork.SettingsRepository.GetSettingsDtoAsync(cancellationToken)).EnableFolderWatching) { var libraryWatcher = scope.ServiceProvider.GetRequiredService(); // Push this off for a bit for people with massive libraries, as it can take up to 45 mins and blocks the thread diff --git a/API/Services/ImageService.cs b/Kavita.Services/ImageService.cs similarity index 82% rename from API/Services/ImageService.cs rename to Kavita.Services/ImageService.cs index 37f315384..b85567fe4 100644 --- a/API/Services/ImageService.cs +++ b/Kavita.Services/ImageService.cs @@ -3,11 +3,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Extensions; +using Kavita.API.Services; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NetVips; using SixLabors.ImageSharp.PixelFormats; @@ -15,60 +18,12 @@ using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; using Image = NetVips.Image; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface IImageService -{ - void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1); - string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size); - - /// - /// Creates a Thumbnail version of a base64 image - /// - /// base64 encoded image - /// - /// Convert and save as encoding format - /// Width of thumbnail - /// If null, will write to - /// File name with extension of the file. - string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320, string? targetDirectory = null); - /// - /// Writes out a thumbnail by stream input - /// - /// - /// - /// - /// - /// - string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); - /// - /// Writes out a thumbnail by file path input - /// - /// - /// - /// - /// - /// - string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); - - /// - /// Converts the passed image to encoding and outputs it in the same directory - /// - /// Full path to the image to convert - /// Where to output the file - /// Encoding Format - /// File of written encoded image - Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat); - Task IsImage(string filePath); - void UpdateColorScape(IHasCoverImage entity); -} - -public class ImageService : IImageService +public class ImageService(ILogger logger, IDirectoryService directoryService) + : IImageService { public const string Name = "ImageService"; - private readonly ILogger _logger; - private readonly IDirectoryService _directoryService; public const string ChapterCoverImageRegex = @"v\d+_c\d+"; public const string SeriesCoverImageRegex = @"series\d+"; @@ -94,24 +49,18 @@ public class ImageService : IImageService public const int LibraryThumbnailWidth = 32; - public ImageService(ILogger logger, IDirectoryService directoryService) - { - _logger = logger; - _directoryService = directoryService; - } - public void ExtractImages(string? fileFilePath, string targetDirectory, int fileCount = 1) { if (string.IsNullOrEmpty(fileFilePath)) return; - _directoryService.ExistOrCreate(targetDirectory); + directoryService.ExistOrCreate(targetDirectory); if (fileCount == 1) { - _directoryService.CopyFileToDirectory(fileFilePath, targetDirectory); + directoryService.CopyFileToDirectory(fileFilePath, targetDirectory); } else { - _directoryService.CopyDirectoryToDirectory(_directoryService.FileSystem.Path.GetDirectoryName(fileFilePath), targetDirectory, - Tasks.Scanner.Parser.Parser.ImageFileExtensions); + directoryService.CopyDirectoryToDirectory(directoryService.FileSystem.Path.GetDirectoryName(fileFilePath), targetDirectory, + Parser.ImageFileExtensions); } } @@ -200,12 +149,12 @@ public class ImageService : IImageService size: GetSizeForDimensions(sourceImage, width, height), crop: GetCropForDimensions(sourceImage, width, height)); var filename = fileName + encodeFormat.GetExtension(); - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + thumbnail.WriteToFile(directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } catch (Exception ex) { - _logger.LogWarning(ex, "[GetCoverImage] There was an error and prevented thumbnail generation on {ImageFile}. Defaulting to no cover image", path); + logger.LogWarning(ex, "[GetCoverImage] There was an error and prevented thumbnail generation on {ImageFile}. Defaulting to no cover image", path); } return string.Empty; @@ -234,16 +183,16 @@ public class ImageService : IImageService crop: scalingCrop); var filename = fileName + encodeFormat.GetExtension(); - _directoryService.ExistOrCreate(outputDirectory); + directoryService.ExistOrCreate(outputDirectory); try { - _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + directoryService.FileSystem.File.Delete(directoryService.FileSystem.Path.Join(outputDirectory, filename)); } catch (Exception) {/* Swallow exception */} try { - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + thumbnail.WriteToFile(directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } @@ -256,7 +205,7 @@ public class ImageService : IImageService using var thumbnail2 = Image.ThumbnailStream(stream, targetWidth, height: targetHeight, size: scalingSize, crop: scalingCrop); - thumbnail2.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + thumbnail2.WriteToFile(directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } @@ -271,18 +220,19 @@ public class ImageService : IImageService size: GetSizeForDimensions(sourceImage, width, height), crop: GetCropForDimensions(sourceImage, width, height)); var filename = fileName + encodeFormat.GetExtension(); - _directoryService.ExistOrCreate(outputDirectory); + directoryService.ExistOrCreate(outputDirectory); try { - _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + directoryService.FileSystem.File.Delete(directoryService.FileSystem.Path.Join(outputDirectory, filename)); } catch (Exception) {/* Swallow exception */} - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + thumbnail.WriteToFile(directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } - public Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat) + public Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat, + CancellationToken ct = default) { - var file = _directoryService.FileSystem.FileInfo.New(filePath); + var file = directoryService.FileSystem.FileInfo.New(filePath); var fileName = file.Name.Replace(file.Extension, string.Empty); var outputFile = Path.Join(outputPath, fileName + encodeFormat.GetExtension()); @@ -291,16 +241,11 @@ public class ImageService : IImageService return Task.FromResult(outputFile); } - /// - /// Performs I/O to determine if the file is a valid Image - /// - /// - /// - public async Task IsImage(string filePath) + public async Task IsImage(string filePath, CancellationToken ct = default) { try { - var info = await SixLabors.ImageSharp.Image.IdentifyAsync(filePath); + var info = await SixLabors.ImageSharp.Image.IdentifyAsync(filePath, ct); if (info == null) return false; return true; @@ -583,17 +528,17 @@ public class ImageService : IImageService // TODO: This code has no concept of cropping nor Thumbnail Size try { - targetDirectory ??= _directoryService.CoverImageDirectory; + targetDirectory ??= directoryService.CoverImageDirectory; using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), thumbnailWidth); fileName += encodeFormat.GetExtension(); - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(targetDirectory, fileName)); + thumbnail.WriteToFile(directoryService.FileSystem.Path.Join(targetDirectory, fileName)); return fileName; } catch (Exception e) { - _logger.LogError(e, "Error creating thumbnail from url"); + logger.LogError(e, "Error creating thumbnail from url"); } return string.Empty; @@ -638,7 +583,7 @@ public class ImageService : IImageService /// public static string GetSeriesFormat(int seriesId) { - return $"series{seriesId}"; + return $"series{seriesId}"; // If this ever changes, also needs to update in SeriesRepository#GetAllWithCoversInDifferentEncoding } /// @@ -757,7 +702,7 @@ public class ImageService : IImageService public void UpdateColorScape(IHasCoverImage entity) { var colors = CalculateColorScape( - _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, entity.CoverImage)); + directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, entity.CoverImage)); entity.PrimaryColor = colors.Primary; entity.SecondaryColor = colors.Secondary; } diff --git a/Kavita.Services/Kavita.Services.csproj b/Kavita.Services/Kavita.Services.csproj new file mode 100644 index 000000000..9affb3d48 --- /dev/null +++ b/Kavita.Services/Kavita.Services.csproj @@ -0,0 +1,70 @@ + + + + net10.0 + disable + enable + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/Kavita.Services/KoreaderService.cs b/Kavita.Services/KoreaderService.cs new file mode 100644 index 000000000..e8a0ff037 --- /dev/null +++ b/Kavita.Services/KoreaderService.cs @@ -0,0 +1,105 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Koreader; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Helpers; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class KoreaderService( + IReaderService readerService, + IUnitOfWork unitOfWork, + ILocalizationService localizationService, + ILogger logger) + : IKoreaderService +{ + /// + /// Given a Koreader hash, locate the underlying file and generate/update a progress event. + /// + /// + /// + /// + public async Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId, CancellationToken ct = default) + { + logger.LogDebug("Saving Koreader progress for User ({UserId}): {KoreaderProgress}", userId, koreaderBookDto.progress.Sanitize()); + var file = await unitOfWork.MangaFileRepository.GetByKoreaderHash(koreaderBookDto.document, ct); + if (file == null) throw new KavitaException(await localizationService.Translate(userId, "file-missing")); + + var userProgressDto = await unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId, ct); + if (userProgressDto == null) + { + var chapterDto = await unitOfWork.ChapterRepository.GetChapterDtoAsync(file.ChapterId, userId, ct); + if (chapterDto == null) throw new KavitaException(await localizationService.Translate(userId, "chapter-doesnt-exist")); + + var volumeDto = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapterDto.VolumeId, ct: ct); + if (volumeDto == null) throw new KavitaException(await localizationService.Translate(userId, "volume-doesnt-exist")); + + var seriesDto = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(volumeDto.SeriesId, userId); + if (seriesDto == null) throw new KavitaException(await localizationService.Translate(userId, "series-doesnt-exist")); + + userProgressDto = new ProgressDto() + { + PageNum = 0, // This is updated in KoreaderHelper.UpdateProgressDto + ChapterId = file.ChapterId, + VolumeId = chapterDto.VolumeId, + SeriesId = seriesDto.Id, + LibraryId = seriesDto.LibraryId + }; + } + + // Update the bookScrollId if possible + var reportedProgress = koreaderBookDto.progress; + KoreaderHelper.UpdateProgressDto(userProgressDto, koreaderBookDto.progress); + + logger.LogDebug("Converted KOReader progress from {ProgressEncoding} to Page {PageNum} with ScrollId: {ScrollId}", reportedProgress.Sanitize(), + userProgressDto.PageNum, userProgressDto.BookScrollId?.Sanitize() ?? string.Empty); + + // Normal saving from kavita will be //body/h2[1] + await readerService.SaveReadingProgress(userProgressDto, userId); + } + + /// + /// Returns a Koreader Dto representing the current book and the progress within + /// + /// + /// + /// + /// + public async Task GetProgress(string bookHash, int userId, CancellationToken ct = default) + { + var settingsDto = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); + + var file = await unitOfWork.MangaFileRepository.GetByKoreaderHash(bookHash, ct); + if (file == null) throw new KavitaException(await localizationService.Translate(userId, "file-missing")); + + var progressDto = await unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId, ct); + + // Non-epubs use the pageNum as the progress. KOReader is 1-index based + var koreaderProgress = $"{progressDto?.PageNum + 1 ?? 0}"; + if (file.Format == MangaFormat.Epub) + { + koreaderProgress = KoreaderHelper.GetKoreaderPosition(progressDto); + } + + var response = new KoreaderBookDtoBuilder(bookHash) + .WithProgress(koreaderProgress) + .WithPercentage(progressDto?.PageNum, file.Pages) + .WithDeviceId(settingsDto.InstallId, userId) + .WithTimestamp(progressDto?.LastModifiedUtc) + .Build(); + + logger.LogDebug("Responding to KOReader with Page {PageNum}, Scroll Id: {ScrollId}, and Progress: {Progress}", + progressDto?.PageNum, response.progress.Sanitize(), response.percentage); + + + return response; + } +} diff --git a/API/Services/LocalizationService.cs b/Kavita.Services/LocalizationService.cs similarity index 97% rename from API/Services/LocalizationService.cs rename to Kavita.Services/LocalizationService.cs index 31c9a2d0c..4a50868cd 100644 --- a/API/Services/LocalizationService.cs +++ b/Kavita.Services/LocalizationService.cs @@ -3,20 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using API.Data; -using API.DTOs; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Models.DTOs; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Hosting; -namespace API.Services; -#nullable enable - -public interface ILocalizationService -{ - Task Get(string locale, string key, params object[] args); - Task Translate(int userId, string key, params object[] args); - IEnumerable GetLocales(); -} +namespace Kavita.Services; public class LocalizationService : ILocalizationService { diff --git a/Kavita.Services/MediaConversionService.cs b/Kavita.Services/MediaConversionService.cs new file mode 100644 index 000000000..7a0769fbe --- /dev/null +++ b/Kavita.Services/MediaConversionService.cs @@ -0,0 +1,327 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; +using Kavita.Services.Comparators; +using Kavita.Services.Extensions; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class MediaConversionService( + IUnitOfWork unitOfWork, + IImageService imageService, + IEventHub eventHub, + IDirectoryService directoryService, + ILogger logger) + : IMediaConversionService +{ + public const string Name = "MediaConversionService"; + public static readonly string[] ConversionMethods = ["ConvertAllBookmarkToEncoding", "ConvertAllCoversToEncoding", "ConvertAllManagedMediaToEncodingFormat"]; + + /// + /// Converts all Kavita managed media (bookmarks, covers, favicons, etc) to the saved target encoding. + /// Do not invoke anyway except via Hangfire. + /// + /// + /// This is a long-running job + /// + [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] + public async Task ConvertAllManagedMediaToEncodingFormat(CancellationToken ct = default) + { + await ConvertAllBookmarkToEncoding(ct); + await ConvertAllCoversToEncoding(ct); + await CoverAllFaviconsToEncoding(ct); + + } + + /// + /// This is a long-running job that will convert all bookmarks into a format that is not PNG. Do not invoke anyway except via Hangfire. + /// + /// + [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] + public async Task ConvertAllBookmarkToEncoding(CancellationToken ct = default) + { + var bookmarkDirectory = + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory, ct)).Value; + var encodeFormat = + (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).EncodeMediaAs; + + if (encodeFormat == EncodeFormat.PNG) + { + logger.LogError("Cannot convert media to PNG"); + return; + } + + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started), ct: ct); + + var bookmarks = (await unitOfWork.UserRepository.GetAllBookmarksAsync(ct)) + .Where(b => !b.FileName.EndsWith(encodeFormat.GetExtension())).ToList(); + + var count = 1F; + foreach (var bookmark in bookmarks) + { + bookmark.FileName = await SaveAsEncodingFormat(bookmarkDirectory, bookmark.FileName, + BookmarkService.BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId), encodeFormat); + + unitOfWork.UserRepository.Update(bookmark); + + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertBookmarksProgressEvent(count / bookmarks.Count, ProgressEventType.Updated), ct: ct); + + count++; + } + + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended), ct: ct); + + logger.LogInformation("[MediaConversionService] Converted bookmarks to {Format}", encodeFormat); + } + + /// + /// This is a long-running job that will convert all covers into WebP. Do not invoke anyway except via Hangfire. + /// + /// + [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] + public async Task ConvertAllCoversToEncoding(CancellationToken ct = default) + { + var coverDirectory = directoryService.CoverImageDirectory; + var encodeFormat = + (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).EncodeMediaAs; + + if (encodeFormat == EncodeFormat.PNG) + { + logger.LogError("Cannot convert media to PNG"); + return; + } + + logger.LogInformation("[MediaConversionService] Starting conversion of all covers to {Format}", encodeFormat); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(0F, ProgressEventType.Started), ct: ct); + + var chapterCovers = await unitOfWork.ChapterRepository.GetAllChaptersWithCoversInDifferentEncoding(encodeFormat, ct); + var customSeriesCovers = await unitOfWork.SeriesRepository.GetAllWithCoversInDifferentEncoding(encodeFormat); + var seriesCovers = await unitOfWork.SeriesRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, false); + var nonCustomOrConvertedVolumeCovers = await unitOfWork.VolumeRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, ct); + + var readingListCovers = await unitOfWork.ReadingListRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, ct); + var libraryCovers = await unitOfWork.LibraryRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, ct); + var collectionCovers = await unitOfWork.CollectionTagRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, ct); + + var totalCount = chapterCovers.Count + seriesCovers.Count + readingListCovers.Count + + libraryCovers.Count + collectionCovers.Count + nonCustomOrConvertedVolumeCovers.Count + customSeriesCovers.Count; + + var count = 1F; + logger.LogInformation("[MediaConversionService] Starting conversion of chapters"); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(0, ProgressEventType.Started), ct: ct); + logger.LogInformation("[MediaConversionService] Starting conversion of libraries"); + foreach (var library in libraryCovers) + { + if (string.IsNullOrEmpty(library.CoverImage)) continue; + + var newFile = await SaveAsEncodingFormat(coverDirectory, library.CoverImage, coverDirectory, encodeFormat); + library.CoverImage = Path.GetFileName(newFile); + + unitOfWork.LibraryRepository.Update(library); + + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated), ct: ct); + + count++; + } + + logger.LogInformation("[MediaConversionService] Starting conversion of reading lists"); + foreach (var readingList in readingListCovers) + { + if (string.IsNullOrEmpty(readingList.CoverImage)) continue; + + var newFile = await SaveAsEncodingFormat(coverDirectory, readingList.CoverImage, coverDirectory, encodeFormat); + readingList.CoverImage = Path.GetFileName(newFile); + + unitOfWork.ReadingListRepository.Update(readingList); + + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated), ct: ct); + + count++; + } + + logger.LogInformation("[MediaConversionService] Starting conversion of collections"); + foreach (var collection in collectionCovers) + { + if (string.IsNullOrEmpty(collection.CoverImage)) continue; + + var newFile = await SaveAsEncodingFormat(coverDirectory, collection.CoverImage, coverDirectory, encodeFormat); + collection.CoverImage = Path.GetFileName(newFile); + + unitOfWork.CollectionTagRepository.Update(collection); + + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated), ct: ct); + + count++; + } + + logger.LogInformation("[MediaConversionService] Starting conversion of chapters"); + foreach (var chapter in chapterCovers) + { + if (string.IsNullOrEmpty(chapter.CoverImage)) continue; + + var newFile = await SaveAsEncodingFormat(coverDirectory, chapter.CoverImage, coverDirectory, encodeFormat); + chapter.CoverImage = Path.GetFileName(newFile); + + unitOfWork.ChapterRepository.Update(chapter); + + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated), ct: ct); + + count++; + } + + // Now null out all series and volumes that aren't webp or custom + logger.LogInformation("[MediaConversionService] Starting conversion of volumes"); + foreach (var volume in nonCustomOrConvertedVolumeCovers) + { + if (string.IsNullOrEmpty(volume.CoverImage)) continue; + volume.CoverImage = volume.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; + unitOfWork.VolumeRepository.Update(volume); + await unitOfWork.CommitAsync(ct); + } + + logger.LogInformation("[MediaConversionService] Starting conversion of series"); + foreach (var series in customSeriesCovers) + { + if (string.IsNullOrEmpty(series.CoverImage)) continue; + + var newFile = await SaveAsEncodingFormat(coverDirectory, series.CoverImage, coverDirectory, encodeFormat); + series.CoverImage = string.IsNullOrEmpty(newFile) ? + series.CoverImage.Replace(Path.GetExtension(series.CoverImage), encodeFormat.GetExtension()) : Path.GetFileName(newFile); + + unitOfWork.SeriesRepository.Update(series); + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated), ct: ct); + count++; + } + + foreach (var series in seriesCovers) + { + if (string.IsNullOrEmpty(series.CoverImage)) continue; + series.CoverImage = series.GetCoverImage(); + if (series.CoverImage == null) + { + logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); + } + unitOfWork.SeriesRepository.Update(series); + await unitOfWork.CommitAsync(ct); + } + + // Get all volumes and remap their covers + + // Get all series and remap their covers + + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(1F, ProgressEventType.Ended), ct: ct); + + logger.LogInformation("[MediaConversionService] Converted covers to {Format}", encodeFormat); + } + + private async Task CoverAllFaviconsToEncoding(CancellationToken ct = default) + { + var encodeFormat = + (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).EncodeMediaAs; + + if (encodeFormat == EncodeFormat.PNG) + { + logger.LogError("Cannot convert media to PNG"); + return; + } + + logger.LogInformation("[MediaConversionService] Starting conversion of favicons to {Format}", encodeFormat); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started), ct: ct); + var pngFavicons = directoryService.GetFiles(directoryService.FaviconDirectory) + .Where(b => !b.EndsWith(encodeFormat.GetExtension())). + ToList(); + + var count = 1F; + foreach (var file in pngFavicons) + { + await SaveAsEncodingFormat(directoryService.FaviconDirectory, directoryService.FileSystem.FileInfo.New(file).Name, directoryService.FaviconDirectory, + encodeFormat); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertBookmarksProgressEvent(count / pngFavicons.Count, ProgressEventType.Updated), ct: ct); + count++; + } + + + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended), ct: ct); + + logger.LogInformation("[MediaConversionService] Converted favicons to {Format}", encodeFormat); + } + + + /// + /// Converts an image file, deletes original and returns the new path back + /// + /// Full Path to where files are stored + /// The file to convert + /// Full path to where files should be stored or any stem + /// Encoding Format + /// + public async Task SaveAsEncodingFormat(string imageDirectory, string filename, string targetFolder, EncodeFormat encodeFormat) + { + // This must be Public as it's used in via Hangfire as a background task + var fullSourcePath = directoryService.FileSystem.Path.Join(imageDirectory, filename); + var fullTargetDirectory = fullSourcePath.Replace(new FileInfo(filename).Name, string.Empty); + + var newFilename = string.Empty; + logger.LogDebug("Converting {Source} image into {Encoding} at {Target}", fullSourcePath, encodeFormat, fullTargetDirectory); + + if (!File.Exists(fullSourcePath)) + { + logger.LogError("Requested to convert {File} but it doesn't exist", fullSourcePath); + return newFilename; + } + + try + { + // Convert target file to format then delete original target file + try + { + var targetFile = await imageService.ConvertToEncodingFormat(fullSourcePath, fullTargetDirectory, encodeFormat); + var targetName = new FileInfo(targetFile).Name; + newFilename = Path.Join(targetFolder, targetName); + directoryService.DeleteFiles([fullSourcePath]); + } + catch (Exception ex) + { + logger.LogError(ex, "Could not convert image {FilePath} to {Format}", filename, encodeFormat); + newFilename = filename; + } + } + catch (Exception ex) + { + logger.LogError(ex, "Could not convert image to {Format}", encodeFormat); + } + + return newFilename; + } + +} diff --git a/Kavita.Services/MediaErrorService.cs b/Kavita.Services/MediaErrorService.cs new file mode 100644 index 000000000..2ec0ad235 --- /dev/null +++ b/Kavita.Services/MediaErrorService.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; + +namespace Kavita.Services; + + + +public class MediaErrorService(IUnitOfWork unitOfWork) : IMediaErrorService +{ + public void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, Exception ex) + { + // TODO: Localize all these messages + // To avoid overhead on commits, do async. We don't need to wait. + BackgroundJob.Enqueue(() => ReportMediaIssueAsync(filename, producer, errorMessage, ex.Message, CancellationToken.None)); + } + + public void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, string details) + { + // To avoid overhead on commits, do async. We don't need to wait. + BackgroundJob.Enqueue(() => ReportMediaIssueAsync(filename, producer, errorMessage, details, CancellationToken.None)); + } + + public async Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, Exception ex, CancellationToken ct = default) + { + await ReportMediaIssueAsync(filename, producer, errorMessage, ex.Message, ct); + } + + public async Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, + string details, CancellationToken ct = default) + { + var error = new MediaErrorBuilder(filename) + .WithComment(errorMessage) + .WithDetails(details) + .Build(); + + if (await unitOfWork.MediaErrorRepository.ExistsAsync(error, ct)) + { + return; + } + + + unitOfWork.MediaErrorRepository.Attach(error); + await unitOfWork.CommitAsync(ct); + } + +} diff --git a/API/Services/Tasks/Metadata/CoverDbService.cs b/Kavita.Services/Metadata/CoverDbService.cs similarity index 92% rename from API/Services/Tasks/Metadata/CoverDbService.cs rename to Kavita.Services/Metadata/CoverDbService.cs index af4310116..caed47c08 100644 --- a/API/Services/Tasks/Metadata/CoverDbService.cs +++ b/Kavita.Services/Metadata/CoverDbService.cs @@ -2,41 +2,32 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Extensions; -using API.SignalR; using EasyCaching.Core; using Flurl; using Flurl.Http; using HtmlAgilityPack; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Services.Repositories; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NetVips; - -namespace API.Services.Tasks.Metadata; -#nullable enable - -public interface ICoverDbService -{ - Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat); - Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat); - Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat); - Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url); - Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false, bool chooseBetterImage = true); - Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, bool chooseBetterImage = false); - Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false); - Task SetUserCoverByUrl(int userId, string url, bool fromBase64 = true, bool chooseBetterImage = false); - Task SetUserCoverByUrl(AppUser user, string url, bool fromBase64 = true, bool chooseBetterImage = false); -} - +namespace Kavita.Services.Metadata; public class CoverDbService : ICoverDbService { @@ -88,6 +79,7 @@ public class CoverDbService : ICoverDbService /// /// The full URL of the website to extract the favicon from. /// The desired image encoding format for saving the favicon (e.g., WebP, PNG). + /// /// /// A string representing the filename of the downloaded favicon image, saved to the configured favicon directory. /// @@ -99,7 +91,7 @@ public class CoverDbService : ICoverDbService /// It then attempts to parse HTML for `link` tags pointing to `.png` favicons and /// falls back to an internal fallback method if needed. Valid results are saved to disk. /// - public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat) + public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat, CancellationToken ct = default) { // Parse the URL to get the domain (including subdomain) var uri = new Uri(url); @@ -108,7 +100,7 @@ public class CoverDbService : ICoverDbService var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Favicon); - var res = await provider.GetAsync(baseUrl); + var res = await provider.GetAsync(baseUrl, ct); if (res.HasValue) { var sanitizedBaseUrl = baseUrl.Sanitize(); @@ -116,7 +108,7 @@ public class CoverDbService : ICoverDbService throw new KavitaException($"Kavita has already tried to fetch from {sanitizedBaseUrl} and failed. Skipping duplicate check"); } - await provider.SetAsync(baseUrl, string.Empty, _cacheTime); + await provider.SetAsync(baseUrl, string.Empty, _cacheTime, ct); if (FaviconUrlMapper.TryGetValue(baseUrl, out var value)) { url = value; @@ -126,7 +118,7 @@ public class CoverDbService : ICoverDbService try { - var htmlContent = url.GetStringAsync().Result; + var htmlContent = url.GetStringAsync(cancellationToken: ct).Result; var htmlDocument = new HtmlDocument(); htmlDocument.LoadHtml(htmlContent); @@ -170,7 +162,7 @@ public class CoverDbService : ICoverDbService // Download the favicon.ico file using Flurl var faviconStream = await finalUrl .AllowHttpStatus("2xx,304") - .GetStreamAsync(); + .GetStreamAsync(cancellationToken: ct); // Create the destination file path using var image = Image.PngloadStream(faviconStream); @@ -187,21 +179,22 @@ public class CoverDbService : ICoverDbService } } - public async Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat) + public async Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat, + CancellationToken ct = default) { try { // Sanitize user input publisherName = publisherName.Replace(Environment.NewLine, string.Empty).Replace("\r", string.Empty).Replace("\n", string.Empty); var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Publisher); - var res = await provider.GetAsync(publisherName); + var res = await provider.GetAsync(publisherName, ct); if (res.HasValue) { _logger.LogInformation("Kavita has already tried to fetch Publisher: {PublisherName} and failed. Skipping duplicate check", publisherName); throw new KavitaException($"Kavita has already tried to fetch Publisher: {publisherName} and failed. Skipping duplicate check"); } - await provider.SetAsync(publisherName, string.Empty, _cacheTime); + await provider.SetAsync(publisherName, string.Empty, _cacheTime, ct); var publisherLink = await FallbackToKavitaReaderPublisher(publisherName); if (string.IsNullOrEmpty(publisherLink)) { @@ -229,8 +222,10 @@ public class CoverDbService : ICoverDbService /// /// /// + /// /// Person image (in correct directory) or null if not found/error - public async Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat) + public async Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, + CancellationToken ct = default) { try { @@ -239,7 +234,7 @@ public class CoverDbService : ICoverDbService { throw new KavitaException($"Could not grab person image for {person.Name}"); } - return await DownloadPersonImageAsync(person, encodeFormat, personImageLink); + return await DownloadPersonImageAsync(person, encodeFormat, personImageLink, ct); } catch (Exception ex) { _logger.LogError(ex, "Error downloading image for {PersonName}", person.Name); @@ -254,10 +249,12 @@ public class CoverDbService : ICoverDbService /// /// /// + /// /// /// /// - public async Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url) + public async Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url, + CancellationToken ct = default) { try { @@ -283,7 +280,7 @@ public class CoverDbService : ICoverDbService private async Task DownloadImageFromUrl(string filenameWithoutExtension, EncodeFormat encodeFormat, string url, string? targetDirectory = null) { - // TODO: I need to unit test this to ensure it works when overwriting, etc + // default: I need to unit test this to ensure it works when overwriting, etc // Target Directory defaults to CoverImageDirectory, but can be temp for when comparison between images is used targetDirectory ??= _directoryService.CoverImageDirectory; @@ -475,7 +472,9 @@ public class CoverDbService : ICoverDbService /// /// Will check against all known null image placeholders to avoid writing it /// If we check cross-reference the current cover for the better option - public async Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false, bool chooseBetterImage = true) + /// + public async Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, + bool checkNoImagePlaceholder = false, bool chooseBetterImage = true, CancellationToken ct = default) { if (!string.IsNullOrEmpty(url)) { @@ -555,9 +554,9 @@ public class CoverDbService : ICoverDbService if (_unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(person.Id, MessageFactoryEntityTypes.Person), false); + MessageFactory.CoverUpdateEvent(person.Id, MessageFactoryEntityTypes.Person), false, ct); } } @@ -568,7 +567,9 @@ public class CoverDbService : ICoverDbService /// /// /// If images are similar, will choose the higher quality image - public async Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, bool chooseBetterImage = false) + /// + public async Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, + bool chooseBetterImage = false, CancellationToken ct = default) { if (!string.IsNullOrEmpty(url)) { @@ -638,14 +639,15 @@ public class CoverDbService : ICoverDbService if (_unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false); + MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false, ct); } } - // TODO: Refactor this to IHasCoverImage instead of a hard entity type - public async Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false) + // default: Refactor this to IHasCoverImage instead of a hard entity type + public async Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, + bool chooseBetterImage = false, CancellationToken ct = default) { if (!string.IsNullOrEmpty(url)) { @@ -711,24 +713,25 @@ public class CoverDbService : ICoverDbService if (_unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); await _eventHub.SendMessageAsync( MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), - false - ); + false, ct); } } - public async Task SetUserCoverByUrl(int userId, string url, bool fromBase64 = true, bool chooseBetterImage = false) + public async Task SetUserCoverByUrl(int userId, string url, bool fromBase64 = true, bool chooseBetterImage = false, + CancellationToken ct = default) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences, ct); if (user == null) return; - await SetUserCoverByUrl(user, url, fromBase64, chooseBetterImage); + await SetUserCoverByUrl(user, url, fromBase64, chooseBetterImage, ct); } - public async Task SetUserCoverByUrl(AppUser user, string url, bool fromBase64 = true, bool chooseBetterImage = false) + public async Task SetUserCoverByUrl(AppUser user, string url, bool fromBase64 = true, + bool chooseBetterImage = false, CancellationToken ct = default) { if (!string.IsNullOrEmpty(url)) { @@ -763,9 +766,9 @@ public class CoverDbService : ICoverDbService if (_unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(user.Id, MessageFactoryEntityTypes.User), false); + MessageFactory.CoverUpdateEvent(user.Id, MessageFactoryEntityTypes.User), false, ct); } } diff --git a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs b/Kavita.Services/Metadata/WordCountAnalyzerService.cs similarity index 61% rename from API/Services/Tasks/Metadata/WordCountAnalyzerService.cs rename to Kavita.Services/Metadata/WordCountAnalyzerService.cs index 1a6bb5ab3..6470002d2 100644 --- a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs +++ b/Kavita.Services/Metadata/WordCountAnalyzerService.cs @@ -1,86 +1,72 @@ using System; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Services.Reading; -using API.SignalR; using Hangfire; using HtmlAgilityPack; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Helpers; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.SignalR; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Reading; using Microsoft.Extensions.Logging; using VersOne.Epub; -namespace API.Services.Tasks.Metadata; -#nullable enable - -public interface IWordCountAnalyzerService -{ - [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] - [AutomaticRetry(Attempts = 2, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task ScanLibrary(int libraryId, bool forceUpdate = false); - Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true); -} +namespace Kavita.Services.Metadata; /// /// This service is a metadata task that generates information around time to read /// -public class WordCountAnalyzerService : IWordCountAnalyzerService +public class WordCountAnalyzerService( + ILogger logger, + IUnitOfWork unitOfWork, + IEventHub eventHub, + ICacheHelper cacheHelper, + IMediaErrorService mediaErrorService) + : IWordCountAnalyzerService { - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - private readonly ICacheHelper _cacheHelper; - private readonly IMediaErrorService _mediaErrorService; - public const int AverageCharactersPerWord = 5; - public WordCountAnalyzerService(ILogger logger, IUnitOfWork unitOfWork, IEventHub eventHub, - ICacheHelper cacheHelper, IMediaErrorService mediaErrorService) - { - _logger = logger; - _unitOfWork = unitOfWork; - _eventHub = eventHub; - _cacheHelper = cacheHelper; - _mediaErrorService = mediaErrorService; - } - [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] [AutomaticRetry(Attempts = 2, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ScanLibrary(int libraryId, bool forceUpdate = false) + public async Task ScanLibrary(int libraryId, bool forceUpdate = false, CancellationToken ct = default) { var sw = Stopwatch.StartNew(); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, ct: ct); if (library == null) return; - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 0F, ProgressEventType.Started, string.Empty)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 0F, ProgressEventType.Started, string.Empty), ct: ct); - var chunkInfo = await _unitOfWork.SeriesRepository.GetChunkInfo(library.Id); + var chunkInfo = await unitOfWork.SeriesRepository.GetChunkInfo(library.Id, ct); var stopwatch = Stopwatch.StartNew(); - _logger.LogInformation("[MetadataService] Refreshing Library {LibraryName}. Total Items: {TotalSize}. Total Chunks: {TotalChunks} with {ChunkSize} size", library.Name, chunkInfo.TotalSize, chunkInfo.TotalChunks, chunkInfo.ChunkSize); + logger.LogInformation("[MetadataService] Refreshing Library {LibraryName}. Total Items: {TotalSize}. Total Chunks: {TotalChunks} with {ChunkSize} size", library.Name, chunkInfo.TotalSize, chunkInfo.TotalChunks, chunkInfo.ChunkSize); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.WordCountAnalyzerProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}")); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.WordCountAnalyzerProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}"), ct: ct); for (var chunk = 1; chunk <= chunkInfo.TotalChunks; chunk++) { if (chunkInfo.TotalChunks == 0) continue; stopwatch.Restart(); - _logger.LogInformation("[MetadataService] Processing chunk {ChunkNumber} / {TotalChunks} with size {ChunkSize}. Series ({SeriesStart} - {SeriesEnd}", + logger.LogInformation("[MetadataService] Processing chunk {ChunkNumber} / {TotalChunks} with size {ChunkSize}. Series ({SeriesStart} - {SeriesEnd}", chunk, chunkInfo.TotalChunks, chunkInfo.ChunkSize, chunk * chunkInfo.ChunkSize, (chunk + 1) * chunkInfo.ChunkSize); - var nonLibrarySeries = await _unitOfWork.SeriesRepository.GetFullSeriesForLibraryIdAsync(library.Id, + var nonLibrarySeries = await unitOfWork.SeriesRepository.GetFullSeriesForLibraryIdAsync(library.Id, new UserParams() { PageNumber = chunk, PageSize = chunkInfo.ChunkSize - }); - _logger.LogDebug("[MetadataService] Fetched {SeriesCount} series for refresh", nonLibrarySeries.Count); + }, ct); + logger.LogDebug("[MetadataService] Fetched {SeriesCount} series for refresh", nonLibrarySeries.Count); var seriesIndex = 0; foreach (var series in nonLibrarySeries) @@ -88,8 +74,8 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService var index = chunk * seriesIndex; var progress = Math.Max(0F, Math.Min(1F, index * 1F / chunkInfo.TotalSize)); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.WordCountAnalyzerProgressEvent(library.Id, progress, ProgressEventType.Updated, series.Name)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.WordCountAnalyzerProgressEvent(library.Id, progress, ProgressEventType.Updated, series.Name), ct: ct); try { @@ -97,53 +83,53 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService } catch (Exception ex) { - _logger.LogError(ex, "[MetadataService] There was an exception during metadata refresh for {SeriesName}", series.Name); + logger.LogError(ex, "[MetadataService] There was an exception during metadata refresh for {SeriesName}", series.Name); } seriesIndex++; } - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); } - _logger.LogInformation( + logger.LogInformation( "[MetadataService] Processed {SeriesStart} - {SeriesEnd} out of {TotalSeries} series in {ElapsedScanTime} milliseconds for {LibraryName}", chunk * chunkInfo.ChunkSize, (chunk * chunkInfo.ChunkSize) + nonLibrarySeries.Count, chunkInfo.TotalSize, stopwatch.ElapsedMilliseconds, library.Name); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.WordCountAnalyzerProgressEvent(library.Id, 1F, ProgressEventType.Ended, $"Complete")); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.WordCountAnalyzerProgressEvent(library.Id, 1F, ProgressEventType.Ended, $"Complete"), ct: ct); - _logger.LogInformation("[WordCountAnalyzerService] Updated metadata for {LibraryName} in {ElapsedMilliseconds} milliseconds", library.Name, sw.ElapsedMilliseconds); + logger.LogInformation("[WordCountAnalyzerService] Updated metadata for {LibraryName} in {ElapsedMilliseconds} milliseconds", library.Name, sw.ElapsedMilliseconds); } - public async Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true) + public async Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true, CancellationToken ct = default) { var sw = Stopwatch.StartNew(); - var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); + var series = await unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId, ct); if (series == null) { - _logger.LogError("[WordCountAnalyzerService] Series {SeriesId} was not found on Library {LibraryId}", seriesId, libraryId); + logger.LogError("[WordCountAnalyzerService] Series {SeriesId} was not found on Library {LibraryId}", seriesId, libraryId); return; } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 0F, ProgressEventType.Started, series.Name)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 0F, ProgressEventType.Started, series.Name), ct: ct); await ProcessSeries(series, forceUpdate); - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 1F, ProgressEventType.Ended, series.Name)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 1F, ProgressEventType.Ended, series.Name), ct: ct); - _logger.LogInformation("[WordCountAnalyzerService] Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds); + logger.LogInformation("[WordCountAnalyzerService] Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds); } @@ -159,7 +145,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService { // This compares if it's changed since a file scan only var firstFile = chapter.Files.FirstOrDefault(); - if (firstFile == null || !_cacheHelper.HasFileChangedSinceLastScan(firstFile.LastFileAnalysis, + if (firstFile == null || !cacheHelper.HasFileChangedSinceLastScan(firstFile.LastFileAnalysis, forceUpdate, firstFile)) { @@ -178,7 +164,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService var pageCounter = 1; try { - // TODO: Replace with BookService method, we will loose progress but these tasks are usually fast + // default: Replace with BookService method, we will loose progress but these tasks are usually fast using var book = await EpubReader.OpenBookAsync(filePath, BookService.LenientBookReaderOptions); var totalPages = book.Content.Html.Local; @@ -187,7 +173,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService var progress = Math.Max(0F, Math.Min(1F, (fileCounter * pageCounter) * 1F / (chapter.Files.Count * totalPages.Count))); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.WordCountAnalyzerProgressEvent(series.LibraryId, progress, ProgressEventType.Updated, useFileName ? filePath : series.Name)); sum += await GetWordCountFromHtml(bookPage, filePath); @@ -198,8 +184,8 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService } catch (Exception ex) { - _logger.LogError(ex, "There was an error reading an epub file for word count, series skipped"); - await _eventHub.SendMessageAsync(MessageFactory.Error, + logger.LogError(ex, "There was an error reading an epub file for word count, series skipped"); + await eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent("There was an issue counting words on an epub", $"{series.Name} - {file.FilePath}")); return; @@ -222,14 +208,14 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService { UpdateFileAnalysis(file); } - _unitOfWork.ChapterRepository.Update(chapter); + unitOfWork.ChapterRepository.Update(chapter); } var volumeEst = ReaderService.GetTimeEstimate(volume.WordCount, volume.Pages, isEpub); volume.MinHoursToRead = volumeEst.MinHours; volume.MaxHoursToRead = volumeEst.MaxHours; volume.AvgHoursToRead = volumeEst.AvgHours; - _unitOfWork.VolumeRepository.Update(volume); + unitOfWork.VolumeRepository.Update(volume); } @@ -238,13 +224,13 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService series.MinHoursToRead = seriesEstimate.MinHours; series.MaxHoursToRead = seriesEstimate.MaxHours; series.AvgHoursToRead = seriesEstimate.AvgHours; - _unitOfWork.SeriesRepository.Update(series); + unitOfWork.SeriesRepository.Update(series); } private void UpdateFileAnalysis(MangaFile file) { file.UpdateLastFileAnalysis(); - _unitOfWork.MangaFileRepository.Update(file); + unitOfWork.MangaFileRepository.Update(file); } private async Task GetWordCountFromHtml(EpubLocalTextContentFileRef bookFile, string filePath) @@ -260,8 +246,8 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService } catch (EpubContentException ex) { - _logger.LogError(ex, "Error when counting words in epub {EpubPath}", filePath); - await _mediaErrorService.ReportMediaIssueAsync(filePath, MediaErrorProducer.BookService, + logger.LogError(ex, "Error when counting words in epub {EpubPath}", filePath); + await mediaErrorService.ReportMediaIssueAsync(filePath, MediaErrorProducer.BookService, $"Invalid Epub Metadata, {bookFile.FilePath} does not exist", ex.Message); return 0; } diff --git a/API/Services/MetadataService.cs b/Kavita.Services/MetadataService.cs similarity index 58% rename from API/Services/MetadataService.cs rename to Kavita.Services/MetadataService.cs index f9b07d4e7..15e2acce6 100644 --- a/API/Services/MetadataService.cs +++ b/Kavita.Services/MetadataService.cs @@ -2,73 +2,42 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Comparators; -using API.Data; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Extensions; -using API.Helpers; -using API.SignalR; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Helpers; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Services.Comparators; +using Kavita.Services.Extensions; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable - -public interface IMetadataService -{ - /// - /// Recalculates cover images for all entities in a library. - /// - /// - /// - [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false, bool forceColorScape = false); - /// - /// Performs a forced refresh of cover images just for a series, and it's nested entities - /// - /// - /// - /// Overrides any cache logic and forces execution - - Task GenerateCoversForSeries(ServerSettingDto serverSetting, int libraryId, int seriesId, bool forceUpdate = true, bool forceColorScape = true); - Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false, bool forceColorScape = true); - Task RemoveAbandonedMetadataKeys(); -} +namespace Kavita.Services; /// /// Handles everything around Cover/ColorScape management /// -public class MetadataService : IMetadataService +public class MetadataService( + IUnitOfWork unitOfWork, + ILogger logger, + IEventHub eventHub, + ICacheHelper cacheHelper, + IReadingItemService readingItemService, + IDirectoryService directoryService, + IImageService imageService) + : IMetadataService { public const string Name = "MetadataService"; - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IEventHub _eventHub; - private readonly ICacheHelper _cacheHelper; - private readonly IReadingItemService _readingItemService; - private readonly IDirectoryService _directoryService; - private readonly IImageService _imageService; private readonly IList _updateEvents = new List(); - public MetadataService(IUnitOfWork unitOfWork, ILogger logger, - IEventHub eventHub, ICacheHelper cacheHelper, - IReadingItemService readingItemService, IDirectoryService directoryService, - IImageService imageService) - { - _unitOfWork = unitOfWork; - _logger = logger; - _eventHub = eventHub; - _cacheHelper = cacheHelper; - _readingItemService = readingItemService; - _directoryService = directoryService; - _imageService = imageService; - } - /// /// Updates the metadata for a Chapter /// @@ -83,14 +52,14 @@ public class MetadataService : IMetadataService var firstFile = chapter.Files.MinBy(x => x.Chapter); if (firstFile == null) return false; - if (!_cacheHelper.ShouldUpdateCoverImage( - _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), + if (!cacheHelper.ShouldUpdateCoverImage( + directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, chapter.CoverImage), firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked)) { if (NeedsColorSpace(chapter, forceColorScape)) { - _imageService.UpdateColorScape(chapter); - _unitOfWork.ChapterRepository.Update(chapter); + imageService.UpdateColorScape(chapter); + unitOfWork.ChapterRepository.Update(chapter); _updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter)); } @@ -98,14 +67,14 @@ public class MetadataService : IMetadataService } - _logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath); + logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath); - chapter.CoverImage = _readingItemService.GetCoverImage(firstFile.FilePath, + chapter.CoverImage = readingItemService.GetCoverImage(firstFile.FilePath, ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format, encodeFormat, coverImageSize); - _imageService.UpdateColorScape(chapter); + imageService.UpdateColorScape(chapter); - _unitOfWork.ChapterRepository.Update(chapter); + unitOfWork.ChapterRepository.Update(chapter); _updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter)); return true; @@ -114,7 +83,7 @@ public class MetadataService : IMetadataService private void UpdateChapterLastModified(Chapter chapter, bool forceUpdate) { var firstFile = chapter.Files.MinBy(x => x.Chapter); - if (firstFile == null || _cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, forceUpdate, firstFile)) return; + if (firstFile == null || cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, forceUpdate, firstFile)) return; firstFile.UpdateLastModified(); } @@ -141,14 +110,14 @@ public class MetadataService : IMetadataService // We need to check if Volume coverImage matches first chapters if forceUpdate is false if (volume == null) return false; - if (!_cacheHelper.ShouldUpdateCoverImage( - _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, volume.CoverImage), + if (!cacheHelper.ShouldUpdateCoverImage( + directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, volume.CoverImage), null, volume.Created, forceUpdate)) { if (NeedsColorSpace(volume, forceColorScape)) { - _imageService.UpdateColorScape(volume); - _unitOfWork.VolumeRepository.Update(volume); + imageService.UpdateColorScape(volume); + unitOfWork.VolumeRepository.Update(volume); _updateEvents.Add(MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume)); } return false; @@ -168,7 +137,7 @@ public class MetadataService : IMetadataService volume.CoverImage = firstChapter.CoverImage; } - _imageService.UpdateColorScape(volume); + imageService.UpdateColorScape(volume); _updateEvents.Add(MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume)); @@ -184,14 +153,14 @@ public class MetadataService : IMetadataService { if (series == null) return; - if (!_cacheHelper.ShouldUpdateCoverImage( - _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, series.CoverImage), + if (!cacheHelper.ShouldUpdateCoverImage( + directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, series.CoverImage), null, series.Created, forceUpdate, series.CoverImageLocked)) { // Check if we don't have a primary/seconary color if (NeedsColorSpace(series, forceColorScape)) { - _imageService.UpdateColorScape(series); + imageService.UpdateColorScape(series); _updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series)); } @@ -202,10 +171,10 @@ public class MetadataService : IMetadataService series.CoverImage = series.GetCoverImage(); if (series.CoverImage == null) { - _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); + logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); } - _imageService.UpdateColorScape(series); + imageService.UpdateColorScape(series); _updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series)); } @@ -219,7 +188,7 @@ public class MetadataService : IMetadataService /// private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceColorScape = false) { - _logger.LogDebug("[MetadataService] Processing cover image generation for series: {SeriesName}", series.OriginalName); + logger.LogDebug("[MetadataService] Processing cover image generation for series: {SeriesName}", series.OriginalName); try { var totalVolumes = series.Volumes.Count; @@ -248,7 +217,7 @@ public class MetadataService : IMetadataService firstVolumeUpdated = true; } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.CoverUpdateProgressEvent(series.LibraryId, volumeIndex / (float) totalVolumes, ProgressEventType.Started, series.Name)); volumeIndex++; @@ -258,7 +227,7 @@ public class MetadataService : IMetadataService } catch (Exception ex) { - _logger.LogError(ex, "[MetadataService] There was an exception during cover generation for {SeriesName} ", series.Name); + logger.LogError(ex, "[MetadataService] There was an exception during cover generation for {SeriesName} ", series.Name); } } @@ -270,25 +239,27 @@ public class MetadataService : IMetadataService /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image /// Force updating colorscape + /// [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false, bool forceColorScape = false) + public async Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false, bool forceColorScape = false, + CancellationToken ct = default) { - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, ct: ct); if (library == null) return; - _logger.LogInformation("[MetadataService] Beginning cover generation refresh of {LibraryName}", library.Name); + logger.LogInformation("[MetadataService] Beginning cover generation refresh of {LibraryName}", library.Name); _updateEvents.Clear(); - var chunkInfo = await _unitOfWork.SeriesRepository.GetChunkInfo(library.Id); + var chunkInfo = await unitOfWork.SeriesRepository.GetChunkInfo(library.Id, ct); var stopwatch = Stopwatch.StartNew(); var totalTime = 0L; - _logger.LogInformation("[MetadataService] Refreshing Library {LibraryName} for cover generation. Total Items: {TotalSize}. Total Chunks: {TotalChunks} with {ChunkSize} size", library.Name, chunkInfo.TotalSize, chunkInfo.TotalChunks, chunkInfo.ChunkSize); + logger.LogInformation("[MetadataService] Refreshing Library {LibraryName} for cover generation. Total Items: {TotalSize}. Total Chunks: {TotalChunks} with {ChunkSize} size", library.Name, chunkInfo.TotalSize, chunkInfo.TotalChunks, chunkInfo.ChunkSize); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.CoverUpdateProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}")); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.CoverUpdateProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}"), ct: ct); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); var encodeFormat = settings.EncodeMediaAs; var coverImageSize = settings.CoverImageSize; @@ -298,16 +269,16 @@ public class MetadataService : IMetadataService totalTime += stopwatch.ElapsedMilliseconds; stopwatch.Restart(); - _logger.LogDebug("[MetadataService] Processing chunk {ChunkNumber} / {TotalChunks} with size {ChunkSize}. Series ({SeriesStart} - {SeriesEnd})", + logger.LogDebug("[MetadataService] Processing chunk {ChunkNumber} / {TotalChunks} with size {ChunkSize}. Series ({SeriesStart} - {SeriesEnd})", chunk, chunkInfo.TotalChunks, chunkInfo.ChunkSize, chunk * chunkInfo.ChunkSize, (chunk + 1) * chunkInfo.ChunkSize); - var nonLibrarySeries = await _unitOfWork.SeriesRepository.GetFullSeriesForLibraryIdAsync(library.Id, + var nonLibrarySeries = await unitOfWork.SeriesRepository.GetFullSeriesForLibraryIdAsync(library.Id, new UserParams() { PageNumber = chunk, PageSize = chunkInfo.ChunkSize - }); - _logger.LogDebug("[MetadataService] Fetched {SeriesCount} series for refresh", nonLibrarySeries.Count); + }, ct); + logger.LogDebug("[MetadataService] Fetched {SeriesCount} series for refresh", nonLibrarySeries.Count); var seriesIndex = 0; foreach (var series in nonLibrarySeries) @@ -315,8 +286,8 @@ public class MetadataService : IMetadataService var index = chunk * seriesIndex; var progress = Math.Max(0F, Math.Min(1F, index * 1F / chunkInfo.TotalSize)); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.CoverUpdateProgressEvent(library.Id, progress, ProgressEventType.Updated, series.Name)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.CoverUpdateProgressEvent(library.Id, progress, ProgressEventType.Updated, series.Name), ct: ct); try { @@ -324,57 +295,60 @@ public class MetadataService : IMetadataService } catch (Exception ex) { - _logger.LogError(ex, "[MetadataService] There was an exception during cover generation refresh for {SeriesName}", series.Name); + logger.LogError(ex, "[MetadataService] There was an exception during cover generation refresh for {SeriesName}", series.Name); } seriesIndex++; } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); await FlushEvents(); - _logger.LogInformation( + logger.LogInformation( "[MetadataService] Processed {SeriesStart} - {SeriesEnd} out of {TotalSeries} series in {ElapsedScanTime} milliseconds for {LibraryName}", chunk * chunkInfo.ChunkSize, (chunk * chunkInfo.ChunkSize) + nonLibrarySeries.Count, chunkInfo.TotalSize, stopwatch.ElapsedMilliseconds, library.Name); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.CoverUpdateProgressEvent(library.Id, 1F, ProgressEventType.Ended, $"Complete")); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.CoverUpdateProgressEvent(library.Id, 1F, ProgressEventType.Ended, $"Complete"), ct: ct); - _logger.LogInformation("[MetadataService] Updated covers for {SeriesNumber} series in library {LibraryName} in {ElapsedMilliseconds} milliseconds total", chunkInfo.TotalSize, library.Name, totalTime); + logger.LogInformation("[MetadataService] Updated covers for {SeriesNumber} series in library {LibraryName} in {ElapsedMilliseconds} milliseconds total", chunkInfo.TotalSize, library.Name, totalTime); } - public async Task RemoveAbandonedMetadataKeys() + public async Task RemoveAbandonedMetadataKeys(CancellationToken ct = default) { - await _unitOfWork.TagRepository.RemoveAllTagNoLongerAssociated(); - await _unitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated(); - await _unitOfWork.GenreRepository.RemoveAllGenreNoLongerAssociated(); - await _unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(); - await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); + await unitOfWork.TagRepository.RemoveAllTagNoLongerAssociated(ct); + await unitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated(ct); + await unitOfWork.GenreRepository.RemoveAllGenreNoLongerAssociated(ct: ct); + await unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(ct); + await unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(ct); } /// /// Refreshes Metadata for a Series. Will always force updates. /// + /// /// /// /// Overrides any cache logic and forces execution /// Will ensure that the colorscape is regenerated - public async Task GenerateCoversForSeries(ServerSettingDto serverSetting, int libraryId, int seriesId, bool forceUpdate = true, bool forceColorScape = true) + /// + public async Task GenerateCoversForSeries(ServerSettingDto serverSetting, int libraryId, int seriesId, + bool forceUpdate = true, bool forceColorScape = true, CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); + var series = await unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId, ct); if (series == null) { - _logger.LogError("[MetadataService] Series {SeriesId} was not found on Library {LibraryId}", seriesId, libraryId); + logger.LogError("[MetadataService] Series {SeriesId} was not found on Library {LibraryId}", seriesId, libraryId); return; } var encodeFormat = serverSetting.EncodeMediaAs; var coverImageSize = serverSetting.CoverImageSize; - await GenerateCoversForSeries(series, encodeFormat, coverImageSize, forceUpdate, forceColorScape); + await GenerateCoversForSeries(series, encodeFormat, coverImageSize, forceUpdate, forceColorScape, ct); } /// @@ -382,37 +356,40 @@ public class MetadataService : IMetadataService /// /// A full Series, with metadata, chapters, etc /// When saving the file, what encoding should be used + /// /// /// Forces just colorscape generation - public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false, bool forceColorScape = true) + /// + public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, + bool forceUpdate = false, bool forceColorScape = true, CancellationToken ct = default) { var sw = Stopwatch.StartNew(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 0F, ProgressEventType.Started, series.Name)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 0F, ProgressEventType.Started, series.Name), ct: ct); await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize, forceColorScape); - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - _logger.LogInformation("[MetadataService] Updated covers for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds); + await unitOfWork.CommitAsync(ct); + logger.LogInformation("[MetadataService] Updated covers for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 1F, ProgressEventType.Ended, series.Name)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 1F, ProgressEventType.Ended, series.Name), ct: ct); - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false); + await eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false, ct); await FlushEvents(); } private async Task FlushEvents() { // Send all events out now that entities are saved - _logger.LogDebug("Dispatching {Count} update events", _updateEvents.Count); + logger.LogDebug("Dispatching {Count} update events", _updateEvents.Count); foreach (var updateEvent in _updateEvents) { - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, updateEvent, false); + await eventHub.SendMessageAsync(MessageFactory.CoverUpdate, updateEvent, false); } _updateEvents.Clear(); } diff --git a/API/Services/OidcService.cs b/Kavita.Services/OidcService.cs similarity index 93% rename from API/Services/OidcService.cs rename to Kavita.Services/OidcService.cs index b9207b100..b2ae26735 100644 --- a/API/Services/OidcService.cs +++ b/Kavita.Services/OidcService.cs @@ -1,27 +1,29 @@ -#nullable enable -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; +using System.Threading; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Email; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Extensions; -using API.Helpers.Builders; -using API.Services.Tasks.Metadata; using Hangfire; using Flurl.Http; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Email; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Services.Extensions; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; @@ -33,34 +35,10 @@ using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; -namespace API.Services; - -public interface IOidcService -{ - /// - /// Returns the user authenticated with OpenID Connect - /// - /// - /// - /// - /// if any requirements aren't met - Task LoginOrCreate(HttpRequest request, ClaimsPrincipal principal); - /// - /// Refresh the token inside the cookie when it's close to expiring. And sync the user - /// - /// - /// - /// If the token is refreshed successfully, updates the last active time of the suer - Task RefreshCookieToken(CookieValidatePrincipalContext ctx); - /// - /// Remove from all users - /// - /// - Task ClearOidcIds(); -} +namespace Kavita.Services; /// -/// The ConfigurationManager will refresh the configuration periodically to ensure the data stays up to date +/// The ConfigurationManager will refresh the configuration periodically to ensure the data stays up to date. /// We can store the same one indefinitely as the authority does not change unless Kavita is restarted /// /// The ConfigurationManager has its own lock, it loads data thread safe @@ -84,9 +62,10 @@ public class OidcService(ILogger logger, UserManager userM private static readonly ConcurrentDictionary RefreshInProgress = new(); private static readonly ConcurrentDictionary LastFailedRefresh = new(); - public async Task LoginOrCreate(HttpRequest request, ClaimsPrincipal principal) + public async Task LoginOrCreate(HttpRequest request, ClaimsPrincipal principal, + CancellationToken ct = default) { - var settings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var settings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).OidcConfig; var oidcId = principal.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(oidcId)) @@ -94,7 +73,7 @@ public class OidcService(ILogger logger, UserManager userM throw new KavitaException("errors.oidc.missing-external-id"); } - var user = await unitOfWork.UserRepository.GetByOidcId(oidcId, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams); + var user = await unitOfWork.UserRepository.GetByOidcId(oidcId, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams, ct); if (user != null) { await SyncUserSettings(request, settings, principal, user); @@ -114,7 +93,7 @@ public class OidcService(ILogger logger, UserManager userM } - user = await unitOfWork.UserRepository.GetUserByEmailAsync(email, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams); + user = await unitOfWork.UserRepository.GetUserByEmailAsync(email, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams, ct); if (user != null) { // Don't allow taking over accounts @@ -126,7 +105,7 @@ public class OidcService(ILogger logger, UserManager userM logger.LogDebug("User {UserName} has matched on email to {OidcId}", user.Id, oidcId); user.OidcId = oidcId; - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); await SyncUserSettings(request, settings, principal, user); @@ -136,11 +115,11 @@ public class OidcService(ILogger logger, UserManager userM return await CreateNewAccount(request, principal, settings, oidcId); } - public async Task RefreshCookieToken(CookieValidatePrincipalContext ctx) + public async Task RefreshCookieToken(CookieValidatePrincipalContext ctx, CancellationToken ct = default) { if (ctx.Principal == null) return null; - var user = await unitOfWork.UserRepository.GetUserByIdAsync(ctx.Principal.GetUserId()) ?? throw new UnauthorizedAccessException(); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(ctx.Principal.GetUserId(), ct: ct) ?? throw new UnauthorizedAccessException(); var key = ctx.Principal.GetUsername(); var refreshToken = ctx.Properties.GetTokenValue(RefreshToken); @@ -160,7 +139,7 @@ public class OidcService(ILogger logger, UserManager userM try { - var settings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var settings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).OidcConfig; var tokenResponse = await RefreshTokenAsync(settings, refreshToken); if (tokenResponse == null || !string.IsNullOrEmpty(tokenResponse.Error)) @@ -194,19 +173,19 @@ public class OidcService(ILogger logger, UserManager userM return user; } - public async Task ClearOidcIds() + public async Task ClearOidcIds(CancellationToken ct = default) { - var users = await unitOfWork.UserRepository.GetAllUsersAsync(); + var users = await unitOfWork.UserRepository.GetAllUsersAsync(ct: ct); foreach (var user in users) { user.OidcId = null; } - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); } /// - /// Tries to construct a new account from the OIDC Principal, may fail if required conditions aren't met + /// Tries to construct a new account from the OIDC Principal may fail if required conditions aren't met /// /// /// @@ -423,7 +402,7 @@ public class OidcService(ILogger logger, UserManager userM // Will just need to be documented on the wiki. if (!string.IsNullOrEmpty(picture) && string.IsNullOrEmpty(user.CoverImage)) { - // Run in background to not block http thread, pass id to Hangfire doesn't kill itself + // Run in the background to not block http thread, pass id to Hangfire doesn't kill itself BackgroundJob.Enqueue(() => coverDbService.SetUserCoverByUrl(user.Id, picture, false)); } } diff --git a/API/Services/OpdsService.cs b/Kavita.Services/OpdsService.cs similarity index 74% rename from API/Services/OpdsService.cs rename to Kavita.Services/OpdsService.cs index a24d71ace..97a95be25 100644 --- a/API/Services/OpdsService.cs +++ b/Kavita.Services/OpdsService.cs @@ -2,68 +2,41 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; -using API.DTOs.OPDS; -using API.DTOs.OPDS.Requests; -using API.DTOs.Person; -using API.DTOs.ReadingLists; -using API.DTOs.Search; -using API.Entities; -using API.Entities.Enums; -using API.Exceptions; -using API.Helpers; -using API.Helpers.Formatting; -using API.Services.Reading; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; +using Kavita.API.Errors; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.OPDS; +using Kavita.Models.DTOs.OPDS.Requests; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.Search; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Helpers; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface IOpdsService +public class OpdsService( + IUnitOfWork unitOfWork, + ILocalizationService localizationService, + ISeriesService seriesService, + IDownloadService downloadService, + IDirectoryService directoryService, + IReaderService readerService, + IEntityNamingService namingService, + IReadingListService readingListService) + : IOpdsService { - Task GetCatalogue(OpdsCatalogueRequest request); - Task GetSmartFilters(OpdsPaginatedCatalogueRequest request); - Task GetLibraries(OpdsPaginatedCatalogueRequest request); - Task GetWantToRead(OpdsPaginatedCatalogueRequest request); - Task GetCollections(OpdsPaginatedCatalogueRequest request); - Task GetReadingLists(OpdsPaginatedCatalogueRequest request); - Task GetRecentlyAdded(OpdsPaginatedCatalogueRequest request); - Task GetRecentlyUpdated(OpdsPaginatedCatalogueRequest request); - Task GetOnDeck(OpdsPaginatedCatalogueRequest request); - - Task GetMoreInGenre(OpdsItemsFromEntityIdRequest request); - Task GetSeriesFromSmartFilter(OpdsItemsFromEntityIdRequest request); - Task GetSeriesFromCollection(OpdsItemsFromEntityIdRequest request); - Task GetSeriesFromLibrary(OpdsItemsFromEntityIdRequest request); - Task GetReadingListItems(OpdsItemsFromEntityIdRequest request); - Task GetSeriesDetail(OpdsItemsFromEntityIdRequest request); - Task GetItemsFromVolume(OpdsItemsFromCompoundEntityIdsRequest request); - Task GetItemsFromChapter(OpdsItemsFromCompoundEntityIdsRequest request); - - Task Search(OpdsSearchRequest request); - - string SerializeXml(Feed? feed); -} - -public class OpdsService : IOpdsService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly ISeriesService _seriesService; - private readonly IDownloadService _downloadService; - private readonly IDirectoryService _directoryService; - private readonly IReaderService _readerService; - private readonly IEntityNamingService _namingService; - private readonly IReadingListService _readingListService; - - private readonly XmlSerializer _xmlSerializer; + private readonly XmlSerializer _xmlSerializer = new(typeof(Feed)); public const int PageSize = 20; public const int FirstPageNumber = 1; @@ -101,29 +74,13 @@ public class OpdsService : IOpdsService PublicationStatus = [] }; - public OpdsService(IUnitOfWork unitOfWork, ILocalizationService localizationService, ISeriesService seriesService, - IDownloadService downloadService, IDirectoryService directoryService, IReaderService readerService, - IEntityNamingService namingService, IReadingListService readingListService) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _seriesService = seriesService; - _downloadService = downloadService; - _directoryService = directoryService; - _readerService = readerService; - _namingService = namingService; - _readingListService = readingListService; - - _xmlSerializer = new XmlSerializer(typeof(Feed)); - } - - public async Task GetCatalogue(OpdsCatalogueRequest request) + public async Task GetCatalogue(OpdsCatalogueRequest request, CancellationToken ct = default) { var feed = CreateFeed("Kavita", string.Empty, request.ApiKey, request.Prefix); SetFeedId(feed, "root"); // Get the user's customized dashboard - var streams = await _unitOfWork.UserRepository.GetDashboardStreams(request.UserId, true); + var streams = await unitOfWork.UserRepository.GetDashboardStreams(request.UserId, true, ct); foreach (var stream in streams) { switch (stream.StreamType) @@ -132,10 +89,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "onDeck", - Title = await _localizationService.Translate(request.UserId, "on-deck"), + Title = await localizationService.Translate(request.UserId, "on-deck"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-on-deck") + Text = await localizationService.Translate(request.UserId, "browse-on-deck") }, Links = [ @@ -147,10 +104,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "recentlyAdded", - Title = await _localizationService.Translate(request.UserId, "recently-added"), + Title = await localizationService.Translate(request.UserId, "recently-added"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-recently-added") + Text = await localizationService.Translate(request.UserId, "browse-recently-added") }, Links = [ @@ -162,10 +119,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "recentlyUpdated", - Title = await _localizationService.Translate(request.UserId, "recently-updated"), + Title = await localizationService.Translate(request.UserId, "recently-updated"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-recently-updated") + Text = await localizationService.Translate(request.UserId, "browse-recently-updated") }, Links = [ @@ -174,16 +131,16 @@ public class OpdsService : IOpdsService }); break; case DashboardStreamType.MoreInGenre: - var randomGenre = await _unitOfWork.GenreRepository.GetRandomGenre(); + var randomGenre = await unitOfWork.GenreRepository.GetRandomGenre(ct); if (randomGenre == null) break; feed.Entries.Add(new FeedEntry() { Id = "moreInGenre", - Title = await _localizationService.Translate(request.UserId, "more-in-genre", randomGenre.Title), + Title = await localizationService.Translate(request.UserId, "more-in-genre", randomGenre.Title), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-more-in-genre", randomGenre.Title) + Text = await localizationService.Translate(request.UserId, "browse-more-in-genre", randomGenre.Title) }, Links = [ @@ -214,10 +171,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "readingList", - Title = await _localizationService.Translate(request.UserId, "reading-lists"), + Title = await localizationService.Translate(request.UserId, "reading-lists"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-reading-lists") + Text = await localizationService.Translate(request.UserId, "browse-reading-lists") }, Links = [ @@ -227,10 +184,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "wantToRead", - Title = await _localizationService.Translate(request.UserId, "want-to-read"), + Title = await localizationService.Translate(request.UserId, "want-to-read"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-want-to-read") + Text = await localizationService.Translate(request.UserId, "browse-want-to-read") }, Links = [ @@ -240,10 +197,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "allLibraries", - Title = await _localizationService.Translate(request.UserId, "libraries"), + Title = await localizationService.Translate(request.UserId, "libraries"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-libraries") + Text = await localizationService.Translate(request.UserId, "browse-libraries") }, Links = [ @@ -253,10 +210,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "allCollections", - Title = await _localizationService.Translate(request.UserId, "collections"), + Title = await localizationService.Translate(request.UserId, "collections"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-collections") + Text = await localizationService.Translate(request.UserId, "browse-collections") }, Links = [ @@ -264,15 +221,15 @@ public class OpdsService : IOpdsService ] }); - if ((_unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(request.UserId)).Any()) + if ((await unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(request.UserId, ct)).Any()) { feed.Entries.Add(new FeedEntry() { Id = "allSmartFilters", - Title = await _localizationService.Translate(request.UserId, "smart-filters"), + Title = await localizationService.Translate(request.UserId, "smart-filters"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-smart-filters") + Text = await localizationService.Translate(request.UserId, "browse-smart-filters") }, Links = [ @@ -284,12 +241,12 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetSmartFilters(OpdsPaginatedCatalogueRequest request) + public async Task GetSmartFilters(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var filters = await _unitOfWork.AppUserSmartFilterRepository.GetPagedDtosByUserIdAsync(userId, GetUserParams(request.PageNumber)); - var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilters"), $"{apiKey}/smart-filters", apiKey, prefix); + var filters = await unitOfWork.AppUserSmartFilterRepository.GetPagedDtosByUserIdAsync(userId, GetUserParams(request.PageNumber), ct); + var feed = CreateFeed(await localizationService.Translate(userId, "smartFilters"), $"{apiKey}/smart-filters", apiKey, prefix); SetFeedId(feed, "smartFilters"); foreach (var filter in filters) @@ -311,16 +268,16 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetLibraries(OpdsPaginatedCatalogueRequest request) + public async Task GetLibraries(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var feed = CreateFeed(await _localizationService.Translate(userId, "libraries"), $"{apiKey}/libraries", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "libraries"), $"{apiKey}/libraries", apiKey, prefix); SetFeedId(feed, "libraries"); - // TODO: This needs pagination and the query can be optimized + // default: This needs pagination and the query can be optimized // Ensure libraries follow SideNav order - var userSideNavStreams = await _unitOfWork.UserRepository.GetSideNavStreams(userId); + var userSideNavStreams = await unitOfWork.UserRepository.GetSideNavStreams(userId, ct: ct); var libraries = userSideNavStreams.Where(s => s.StreamType == SideNavStreamType.Library) .Select(sideNavStream => sideNavStream.Library); @@ -347,14 +304,14 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetWantToRead(OpdsPaginatedCatalogueRequest request) + public async Task GetWantToRead(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var wantToReadSeries = await _unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(userId, GetUserParams(request.PageNumber), _filterV2Dto); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(wantToReadSeries.Select(s => s.Id)); + var wantToReadSeries = await unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(userId, GetUserParams(request.PageNumber), _filterV2Dto, ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(wantToReadSeries.Select(s => s.Id), ct); - var feed = CreateFeed(await _localizationService.Translate(userId, "want-to-read"), $"{apiKey}/want-to-read", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "want-to-read"), $"{apiKey}/want-to-read", apiKey, prefix); SetFeedId(feed, "want-to-read"); AddPagination(feed, wantToReadSeries, $"{prefix}{apiKey}/want-to-read"); @@ -364,12 +321,12 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetCollections(OpdsPaginatedCatalogueRequest request) + public async Task GetCollections(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var tags = await _unitOfWork.CollectionTagRepository.GetCollectionDtosPagedAsync(userId, GetUserParams(request.PageNumber), true); + var tags = await unitOfWork.CollectionTagRepository.GetCollectionDtosPagedAsync(userId, GetUserParams(request.PageNumber), true, ct); - var feed = CreateFeed(await _localizationService.Translate(userId, "collections"), $"{apiKey}/collections", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "collections"), $"{apiKey}/collections", apiKey, prefix); SetFeedId(feed, "collections"); @@ -394,14 +351,14 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetRecentlyAdded(OpdsPaginatedCatalogueRequest request) + public async Task GetRecentlyAdded(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var recentlyAdded = await _unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, GetUserParams(request.PageNumber), _filterV2Dto); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(recentlyAdded.Select(s => s.Id)); + var recentlyAdded = await unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, GetUserParams(request.PageNumber), _filterV2Dto, ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(recentlyAdded.Select(s => s.Id), ct); - var feed = CreateFeed(await _localizationService.Translate(userId, "recently-added"), $"{apiKey}/recently-added", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "recently-added"), $"{apiKey}/recently-added", apiKey, prefix); SetFeedId(feed, "recently-added"); AddPagination(feed, recentlyAdded, $"{prefix}{apiKey}/recently-added"); @@ -413,14 +370,14 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetRecentlyUpdated(OpdsPaginatedCatalogueRequest request) + public async Task GetRecentlyUpdated(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var seriesDtos = (await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId, GetUserParams(request.PageNumber))).ToList(); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.SeriesId)); + var seriesDtos = (await unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId, GetUserParams(request.PageNumber), ct)).ToList(); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.SeriesId), ct); - var feed = CreateFeed(await _localizationService.Translate(userId, "recently-updated"), $"{apiKey}/recently-updated", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "recently-updated"), $"{apiKey}/recently-updated", apiKey, prefix); SetFeedId(feed, "recently-updated"); foreach (var groupedSeries in seriesDtos) @@ -441,14 +398,14 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetOnDeck(OpdsPaginatedCatalogueRequest request) + public async Task GetOnDeck(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, 0, GetUserParams(request.PageNumber), _filterDto); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(pagedList.Select(s => s.Id)); + var pagedList = await unitOfWork.SeriesRepository.GetOnDeck(userId, 0, GetUserParams(request.PageNumber), _filterDto, ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(pagedList.Select(s => s.Id), ct); - var feed = CreateFeed(await _localizationService.Translate(userId, "on-deck"), $"{apiKey}/on-deck", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "on-deck"), $"{apiKey}/on-deck", apiKey, prefix); SetFeedId(feed, "on-deck"); AddPagination(feed, pagedList, $"{prefix}{apiKey}/on-deck"); @@ -460,20 +417,20 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetMoreInGenre(OpdsItemsFromEntityIdRequest request) + public async Task GetMoreInGenre(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); var genreId = request.EntityId; - var genre = await _unitOfWork.GenreRepository.GetGenreById(genreId); + var genre = await unitOfWork.GenreRepository.GetGenreById(genreId, ct); if (genre == null) { - throw new OpdsException(await _localizationService.Translate(userId, "genre-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "genre-doesnt-exist")); } - var seriesDtos = await _unitOfWork.SeriesRepository.GetMoreIn(userId, 0, genreId, GetUserParams(request.PageNumber)); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.Id)); + var seriesDtos = await unitOfWork.SeriesRepository.GetMoreIn(userId, 0, genreId, GetUserParams(request.PageNumber), ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.Id), ct); - var feed = CreateFeed(await _localizationService.Translate(userId, "more-in-genre", genre.Title), $"{apiKey}/more-in-genre", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "more-in-genre", genre.Title), $"{apiKey}/more-in-genre", apiKey, prefix); SetFeedId(feed, "more-in-genre"); AddPagination(feed, seriesDtos, $"{prefix}{apiKey}/more-in-genre"); @@ -489,24 +446,25 @@ public class OpdsService : IOpdsService /// Returns the Series matching this smart filter. /// /// + /// /// - public async Task GetSeriesFromSmartFilter(OpdsItemsFromEntityIdRequest request) + public async Task GetSeriesFromSmartFilter(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var filter = await _unitOfWork.AppUserSmartFilterRepository.GetById(request.EntityId); + var filter = await unitOfWork.AppUserSmartFilterRepository.GetById(request.EntityId, ct); if (filter == null) { - throw new OpdsException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "smart-filter-doesnt-exist")); } - var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilters-" + filter.Id), $"{apiKey}/smart-filters/{filter.Id}/", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "smartFilters-" + filter.Id), $"{apiKey}/smart-filters/{filter.Id}/", apiKey, prefix); SetFeedId(feed, "smartFilters-" + filter.Id); var decodedFilter = SmartFilterHelper.Decode(filter.Filter); - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, GetUserParams(request.PageNumber), - decodedFilter); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id)); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, GetUserParams(request.PageNumber), + decodedFilter, ct: ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id), ct); foreach (var seriesDto in series) { @@ -518,19 +476,19 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetSeriesFromCollection(OpdsItemsFromEntityIdRequest request) + public async Task GetSeriesFromCollection(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); var collectionId = request.EntityId; - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(collectionId); + var tag = await unitOfWork.CollectionTagRepository.GetCollectionAsync(collectionId, ct: ct); if (tag == null || (tag.AppUserId != userId && !tag.Promoted)) { - throw new OpdsException(await _localizationService.Translate(userId, "collection-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "collection-doesnt-exist")); } - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, GetUserParams(request.PageNumber)); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id)); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, GetUserParams(request.PageNumber), ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id), ct); var feed = CreateFeed(tag.Title + " Collection", $"{apiKey}/collections/{collectionId}", apiKey, prefix); SetFeedId(feed, $"collections-{collectionId}"); @@ -544,17 +502,17 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetSeriesFromLibrary(OpdsItemsFromEntityIdRequest request) + public async Task GetSeriesFromLibrary(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); var libraryId = request.EntityId; - var library = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)) + var library = (await unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId, ct)) .SingleOrDefault(l => l.Id == libraryId); if (library == null) { - throw new OpdsException(await _localizationService.Translate(userId, "no-library-access")); + throw new OpdsException(await localizationService.Translate(userId, "no-library-access")); } var filter = new FilterV2Dto @@ -569,8 +527,8 @@ public class OpdsService : IOpdsService ] }; - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, GetUserParams(request.PageNumber), filter); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id)); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, GetUserParams(request.PageNumber), filter, ct: ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id), ct); var feed = CreateFeed(library.Name, $"{apiKey}/libraries/{libraryId}", apiKey, prefix); SetFeedId(feed, $"library-{library.Name}"); @@ -583,25 +541,25 @@ public class OpdsService : IOpdsService } - public async Task GetReadingListItems(OpdsItemsFromEntityIdRequest request) + public async Task GetReadingListItems(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out _); var readingListId = request.EntityId; - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId, ct); if (readingList == null) { - throw new OpdsException(await _localizationService.Translate(request.UserId, "reading-list-restricted")); + throw new OpdsException(await localizationService.Translate(request.UserId, "reading-list-restricted")); } - var feed = CreateFeed(readingList.Title + " " + await _localizationService.Translate(userId, "reading-list"), $"{apiKey}/reading-list/{readingListId}", apiKey, prefix); + var feed = CreateFeed(readingList.Title + " " + await localizationService.Translate(userId, "reading-list"), $"{apiKey}/reading-list/{readingListId}", apiKey, prefix); SetFeedId(feed, $"reading-list-{readingListId}"); - var items = await _readingListService.GetReadingListItems(readingListId, userId, GetUserParams(request.PageNumber)); - var totalItems = await _unitOfWork.ReadingListRepository .GetReadingListItemCountAsync(readingListId, userId); + var items = await readingListService.GetReadingListItems(readingListId, userId, GetUserParams(request.PageNumber)); + var totalItems = await unitOfWork.ReadingListRepository .GetReadingListItemCountAsync(readingListId, userId, ct); var chapterIds = items.Select(i => i.ChapterId).Distinct().ToList(); - var chapters = (await _unitOfWork.ChapterRepository .GetChapterDtosAsync(chapterIds, userId)) + var chapters = (await unitOfWork.ChapterRepository .GetChapterDtosAsync(chapterIds, userId, ct)) .ToDictionary(c => c.Id); // Build naming contexts per library type (usually just 1-2) @@ -613,15 +571,15 @@ public class OpdsService : IOpdsService if (request.Preferences.IncludeContinueFrom && request.PageNumber == FirstPageNumber) { - var anyProgress = await _unitOfWork.ReadingListRepository.AnyUserReadingProgressAsync(readingListId, userId); + var anyProgress = await unitOfWork.ReadingListRepository.AnyUserReadingProgressAsync(readingListId, userId, ct); if (anyProgress) { - var continuePoint = await _unitOfWork.ReadingListRepository.GetContinueReadingPoint(readingListId, userId); + var continuePoint = await unitOfWork.ReadingListRepository.GetContinueReadingPoint(readingListId, userId, ct); if (continuePoint != null) { var continueChapter = - await _unitOfWork.ChapterRepository.GetChapterDtoAsync(continuePoint.ChapterId, request.UserId); + await unitOfWork.ChapterRepository.GetChapterDtoAsync(continuePoint.ChapterId, request.UserId, ct); if (continueChapter is {Files.Count: 1}) { feed.Entries.Add(await CreateContinueReadingEntryAsync(continuePoint, continueChapter, request)); @@ -669,32 +627,32 @@ public class OpdsService : IOpdsService foreach (var libraryType in libraryTypes.Distinct()) { contexts[libraryType] = await LocalizedNamingContext.CreateAsync( - _namingService, _localizationService, userId, libraryType); + namingService, localizationService, userId, libraryType); } return contexts; } - public async Task GetSeriesDetail(OpdsItemsFromEntityIdRequest request) + public async Task GetSeriesDetail(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); var seriesId = request.EntityId; - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct); if (series == null) { - throw new OpdsException(await _localizationService.Translate(userId, "series-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "series-doesnt-exist")); } - var seriesDetailTask = _seriesService.GetSeriesDetail(seriesId, userId); - var libraryTypeTask = _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); + var seriesDetailTask = seriesService.GetSeriesDetail(seriesId, userId); + var libraryTypeTask = unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct); await Task.WhenAll(seriesDetailTask, libraryTypeTask); var seriesDetail = await seriesDetailTask; var libraryType = await libraryTypeTask; - var namingContext = await LocalizedNamingContext.CreateAsync(_namingService, _localizationService, userId, libraryType); + var namingContext = await LocalizedNamingContext.CreateAsync(namingService, localizationService, userId, libraryType); var volumesById = seriesDetail.Volumes.ToDictionary(v => v.Id); var feed = CreateFeed(series.Name + " - Storyline", $"{apiKey}/series/{series.Id}", apiKey, prefix); @@ -705,11 +663,11 @@ public class OpdsService : IOpdsService // Check if there is reading progress or not, if so, inject a "continue-reading" item if (request.Preferences.IncludeContinueFrom) { - var anyUserProgress = await _unitOfWork.AppUserProgressRepository - .AnyUserProgressForSeriesAsync(seriesId, userId); + var anyUserProgress = await unitOfWork.AppUserProgressRepository + .AnyUserProgressForSeriesAsync(seriesId, userId, ct); if (anyUserProgress) { - var continueChapter = await _readerService.GetContinuePoint(seriesId, userId); + var continueChapter = await readerService.GetContinuePoint(seriesId, userId); if (continueChapter is { Files.Count: 1 }) { volumesById.TryGetValue(continueChapter.VolumeId, out var continueVolume); @@ -770,26 +728,27 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetItemsFromVolume(OpdsItemsFromCompoundEntityIdsRequest request) + public async Task GetItemsFromVolume(OpdsItemsFromCompoundEntityIdsRequest request, + CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out _); var seriesId = request.SeriesId; var volumeId = request.VolumeId; - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct); if (series == null) { - throw new OpdsException(await _localizationService.Translate(userId, "series-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "series-doesnt-exist")); } - var volume = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, request.UserId); + var volume = await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, request.UserId, ct); if (volume == null) { - throw new OpdsException(await _localizationService.Translate(userId, "volume-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "volume-doesnt-exist")); } - var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); - var namingContext = await LocalizedNamingContext.CreateAsync( _namingService, _localizationService, userId, libraryType); + var libraryType = await unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct); + var namingContext = await LocalizedNamingContext.CreateAsync( namingService, localizationService, userId, libraryType); var feed = CreateFeed($"{series.Name} - Volume {volume.Name}", $"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix); @@ -818,7 +777,8 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetItemsFromChapter(OpdsItemsFromCompoundEntityIdsRequest request) + public async Task GetItemsFromChapter(OpdsItemsFromCompoundEntityIdsRequest request, + CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out _); @@ -826,26 +786,26 @@ public class OpdsService : IOpdsService var volumeId = request.VolumeId; var chapterId = request.ChapterId; - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct); if (series == null) { - throw new OpdsException(await _localizationService.Translate(userId, "series-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "series-doesnt-exist")); } - var volume = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId); + var volume = await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId, ct); if (volume == null) { - throw new OpdsException(await _localizationService.Translate(userId, "volume-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "volume-doesnt-exist")); } - var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); + var libraryType = await unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct); var chapter = volume.Chapters.FirstOrDefault(c => c.Id == chapterId); if (chapter == null) { - throw new OpdsException(await _localizationService.Translate(userId, "chapter-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "chapter-doesnt-exist")); } - var namingContext = await LocalizedNamingContext.CreateAsync(_namingService, _localizationService, userId, libraryType); + var namingContext = await LocalizedNamingContext.CreateAsync(namingService, localizationService, userId, libraryType); var chapterName = namingContext.FormatChapterTitle(chapter); var feed = CreateFeed( $"{series.Name} - Volume {volume.Name} - {chapterName} {chapterId}", @@ -861,29 +821,29 @@ public class OpdsService : IOpdsService return feed; } - public async Task Search(OpdsSearchRequest request) + public async Task Search(OpdsSearchRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); var query = request.Query; - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, ct: ct); if (string.IsNullOrEmpty(query)) { - throw new OpdsException(await _localizationService.Translate(userId, "query-required")); + throw new OpdsException(await localizationService.Translate(userId, "query-required")); } query = query.Replace("%", string.Empty); - var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList(); + var libraries = (await unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId, ct)).ToList(); if (libraries.Count == 0) { - throw new OpdsException(await _localizationService.Translate(userId, "libraries-restricted")); + throw new OpdsException(await localizationService.Translate(userId, "libraries-restricted")); } - var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); + var isAdmin = await unitOfWork.UserRepository.IsUserAdminAsync(user, ct); - var searchResults = await _unitOfWork.SeriesRepository.SearchSeries(userId, isAdmin, - libraries.Select(l => l.Id).ToArray(), query, includeChapterAndFiles: false); + var searchResults = await unitOfWork.SeriesRepository.SearchSeries(userId, isAdmin, + libraries.Select(l => l.Id).ToArray(), query, includeChapterAndFiles: false, ct: ct); var feed = CreateFeed(query, $"{apiKey}/series?query=" + query, apiKey, prefix); SetFeedId(feed, "search-series"); @@ -930,12 +890,12 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetReadingLists(OpdsPaginatedCatalogueRequest request) + public async Task GetReadingLists(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, - true, GetUserParams(request.PageNumber), false); + var readingLists = await unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, + true, GetUserParams(request.PageNumber), false, ct); var feed = CreateFeed("All Reading Lists", $"{apiKey}/reading-list", apiKey, prefix); @@ -1086,7 +1046,7 @@ public class OpdsService : IOpdsService feed.Links.Add(CreateLink(FeedLinkRelation.Next, FeedLinkType.AtomNavigation, url + "pageNumber=" + (pageNumber + 1))); } - // Update self to point to current page + // Update self to point to the current page var selfLink = feed.Links.SingleOrDefault(l => l.Rel == FeedLinkRelation.Self); if (selfLink != null) { @@ -1124,7 +1084,7 @@ public class OpdsService : IOpdsService feed.Links.Add(CreateLink(FeedLinkRelation.Next, FeedLinkType.AtomNavigation, url + "pageNumber=" + (pageNumber + 1))); } - // Update self to point to current page + // Update self to point to the current page var selfLink = feed.Links.SingleOrDefault(l => l.Rel == FeedLinkRelation.Self); if (selfLink != null) { @@ -1219,7 +1179,7 @@ public class OpdsService : IOpdsService { var mangaFile = chapter.Files.First(); var fileSize = GetFileSize(mangaFile); - var fileType = _downloadService.GetContentTypeFromFile(mangaFile.FilePath); + var fileType = downloadService.GetContentTypeFromFile(mangaFile.FilePath); var filename = Uri.EscapeDataString(Path.GetFileName(mangaFile.FilePath)); @@ -1273,7 +1233,7 @@ public class OpdsService : IOpdsService { var fileSize = mangaFile.Bytes > 0 ? DirectoryService.GetHumanReadableBytes(mangaFile.Bytes) : - DirectoryService.GetHumanReadableBytes(_directoryService.GetTotalSize((List) [mangaFile.FilePath])); + DirectoryService.GetHumanReadableBytes(directoryService.GetTotalSize((List) [mangaFile.FilePath])); return fileSize; } @@ -1292,10 +1252,10 @@ public class OpdsService : IOpdsService { var mangaFile = chapter.Files.First(); var fileSize = GetFileSize(mangaFile); - var fileType = _downloadService.GetContentTypeFromFile(mangaFile.FilePath); + var fileType = downloadService.GetContentTypeFromFile(mangaFile.FilePath); var filename = Uri.EscapeDataString(Path.GetFileName(mangaFile.FilePath)); - var title = _namingService.FormatReadingListItemTitle(item); + var title = namingService.FormatReadingListItemTitle(item); var displayTitle = $"{item.Order} - {item.SeriesName}: {title}"; var accLink = CreateLink( @@ -1391,13 +1351,13 @@ public class OpdsService : IOpdsService } /// - /// Creates a continue reading feed entry from a chapter. + /// Creates a continued reading feed entry from a chapter. /// private async Task CreateContinueReadingEntryAsync( SeriesDto series, VolumeDto? volume, ChapterDto chapter, LocalizedNamingContext namingContext, IOpdsRequest request) { var entry = CreateChapterWithFile(series, volume, chapter, namingContext, request); - entry.Title = await _localizationService.Translate( + entry.Title = await localizationService.Translate( request.UserId, "opds-continue-reading-title", entry.Title); return entry; @@ -1414,7 +1374,7 @@ public class OpdsService : IOpdsService ? entry.Title[2..] : entry.Title; - entry.Title = await _localizationService.Translate( + entry.Title = await localizationService.Translate( request.UserId, "opds-continue-reading-title", titleWithoutIcon); return entry; diff --git a/API/Services/PersonService.cs b/Kavita.Services/PersonService.cs similarity index 75% rename from API/Services/PersonService.cs rename to Kavita.Services/PersonService.cs index ff0049cbe..63ae7cb38 100644 --- a/API/Services/PersonService.cs +++ b/Kavita.Services/PersonService.cs @@ -1,38 +1,19 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Entities.Person; -using API.Extensions; -using API.Helpers.Builders; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Person; -namespace API.Services; - -public interface IPersonService -{ - /// - /// Adds src as an alias to dst, this is a destructive operation - /// - /// Merged person - /// Remaining person - /// The entities passed as arguments **must** include all relations - /// - Task MergePeopleAsync(Person src, Person dst); - - /// - /// Adds the alias to the person, requires that the aliases are not shared with anyone else - /// - /// This method does NOT commit changes - /// - /// - /// - Task UpdatePersonAliasesAsync(Person person, IList aliases); -} +namespace Kavita.Services; public class PersonService(IUnitOfWork unitOfWork): IPersonService { - public async Task MergePeopleAsync(Person src, Person dst) + public async Task MergePeopleAsync(Person src, Person dst, CancellationToken ct = default) { if (dst.Id == src.Id) return; @@ -78,7 +59,7 @@ public class PersonService(IUnitOfWork unitOfWork): IPersonService unitOfWork.PersonRepository.Remove(src); unitOfWork.PersonRepository.Update(dst); - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); } private static void MergeChapterPeople(Person dst, Person src) @@ -122,7 +103,7 @@ public class PersonService(IUnitOfWork unitOfWork): IPersonService } } - public async Task UpdatePersonAliasesAsync(Person person, IList aliases) + public async Task UpdatePersonAliasesAsync(Person person, IList aliases, CancellationToken ct = default) { var normalizedAliases = aliases .Select(a => a.ToNormalized()) @@ -135,7 +116,7 @@ public class PersonService(IUnitOfWork unitOfWork): IPersonService return true; } - var others = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedAliases); + var others = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedAliases, ct: ct); others = others.Where(p => p.Id != person.Id).ToList(); if (others.Count != 0) return false; diff --git a/API/Services/Plus/ExternalMetadataService.cs b/Kavita.Services/Plus/ExternalMetadataService.cs similarity index 90% rename from API/Services/Plus/ExternalMetadataService.cs rename to Kavita.Services/Plus/ExternalMetadataService.cs index 0010bca82..2c841e206 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/Kavita.Services/Plus/ExternalMetadataService.cs @@ -2,61 +2,43 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Collection; -using API.DTOs.KavitaPlus.ExternalMetadata; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Metadata.Matching; -using API.DTOs.Person; -using API.DTOs.Recommendation; -using API.DTOs.Scrobbling; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using AutoMapper; using Flurl.Http; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Plus; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Metadata.Matching; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.Recommendation; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Services.Extensions; +using Kavita.Services.Helpers; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services.Plus; -#nullable enable - - - -public interface IExternalMetadataService -{ - Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId); - Task GetSeriesDetailPlus(int seriesId, LibraryType libraryType); - Task FetchExternalDataTask(); - /// - /// This is an entry point and provides a level of protection against calling upstream API. Will only allow 100 new - /// series to fetch data within a day and enqueues background jobs at certain times to fetch that data. - /// - /// - /// - /// If the fetch was made - Task FetchSeriesMetadata(int seriesId, LibraryType libraryType); - - Task> GetStacksForUser(int userId); - Task> MatchSeries(MatchSeriesDto dto); - Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId); - Task UpdateSeriesDontMatch(int seriesId, bool dontMatch); - Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId); -} +namespace Kavita.Services.Plus; public class ExternalMetadataService : IExternalMetadataService { @@ -108,21 +90,15 @@ public class ExternalMetadataService : IExternalMetadataService return !NonEligibleLibraryTypes.Contains(type); } - /// - /// This is a task that runs on a schedule and slowly fetches data from Kavita+ to keep - /// data in the DB non-stale and fetched. - /// - /// To avoid blasting Kavita+ API, this only processes 25 records. The goal is to slowly build out/refresh the data - /// [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task FetchExternalDataTask() + public async Task FetchExternalDataTask(CancellationToken ct = default) { // Find all Series that are eligible and limit - var ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25); + var ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25, ct: ct); if (ids.Count == 0) { - ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25, true); + ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25, true, ct); } if (ids.Count == 0) @@ -135,32 +111,26 @@ public class ExternalMetadataService : IExternalMetadataService _logger.LogInformation("[Kavita+ Data Refresh] Started Refreshing {Count} series data from Kavita+: {Ids}", ids.Count, string.Join(',', ids)); var count = 0; var successfulMatches = new List(); - var libTypes = await _unitOfWork.LibraryRepository.GetLibraryTypesBySeriesIdsAsync(ids); + var libTypes = await _unitOfWork.LibraryRepository.GetLibraryTypesBySeriesIdsAsync(ids, ct); foreach (var seriesId in ids) { var libraryType = libTypes[seriesId]; - var success = await FetchSeriesMetadata(seriesId, libraryType); + var success = await FetchSeriesMetadata(seriesId, libraryType, ct); if (success) { count++; successfulMatches.Add(seriesId); } - await Task.Delay(10000); // Currently AL is degraded and has 30 requests/min, give a little padding since this is a background request + await Task.Delay(10000, ct); // Currently AL is degraded and has 30 requests/min, give a little padding since this is a background request } _logger.LogInformation("[Kavita+ Data Refresh] Finished Refreshing {Count} / {Total} series data from Kavita+: {Ids}", count, ids.Count, string.Join(',', successfulMatches)); } - /// - /// Fetches data from Kavita+ - /// - /// - /// - /// If a successful match was made - public async Task FetchSeriesMetadata(int seriesId, LibraryType libraryType) + public async Task FetchSeriesMetadata(int seriesId, LibraryType libraryType, CancellationToken ct = default) { if (!IsPlusEligible(libraryType)) return false; - if (!await _licenseService.HasActiveLicense()) return false; + if (!await _licenseService.HasActiveLicense(ct: ct)) return false; // Generate key based on seriesId and libraryType or any unique identifier for the request // Check if the request is allowed based on the rate limit @@ -172,15 +142,15 @@ public class ExternalMetadataService : IExternalMetadataService } // Prefetch SeriesDetail data - return await GetSeriesDetailPlus(seriesId, libraryType) != null; + return await GetSeriesDetailPlus(seriesId, libraryType, ct) != null; } - public async Task> GetStacksForUser(int userId) + public async Task> GetStacksForUser(int userId, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return ArraySegment.Empty; + if (!await _licenseService.HasActiveLicense(ct: ct)) return ArraySegment.Empty; // See if this user has Mal account on record - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, ct: ct); if (user == null || string.IsNullOrEmpty(user.MalUserName) || string.IsNullOrEmpty(user.MalAccessToken)) { _logger.LogInformation("User is attempting to fetch MAL Stacks, but missing information on their account"); @@ -190,8 +160,8 @@ public class ExternalMetadataService : IExternalMetadataService { _logger.LogDebug("Fetching Kavita+ for MAL Stacks for user {UserName}", user.MalUserName); - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - return await _kavitaPlusApiService.GetMalStacks(user.MalUserName, license); + var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct)).Value; + return await _kavitaPlusApiService.GetMalStacks(user.MalUserName, license, ct); } catch (Exception ex) { @@ -200,23 +170,15 @@ public class ExternalMetadataService : IExternalMetadataService } } - /// - /// Returns the match results for a Series from UI Flow - /// - /// - /// Will extract alternative names like Localized name, year will send as ReleaseYear but fallback to Comic Vine syntax if applicable - /// - /// - /// - public async Task> MatchSeries(MatchSeriesDto dto) + public async Task> MatchSeries(MatchSeriesDto dto, CancellationToken ct = default) { var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, - SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata | SeriesIncludes.Library); + SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata | SeriesIncludes.Library, ct); if (series == null) return []; - var potentialAnilistId = ScrobblingService.ExtractId(dto.Query, ScrobblingService.AniListWeblinkWebsite); - var potentialMalId = ScrobblingService.ExtractId(dto.Query, ScrobblingService.MalWeblinkWebsite); + var potentialAnilistId = ScrobblingHelper.ExtractId(dto.Query, ScrobblingService.AniListWeblinkWebsite); + var potentialMalId = ScrobblingHelper.ExtractId(dto.Query, ScrobblingService.MalWeblinkWebsite); var format = series.Library.Type.ConvertToPlusMediaFormat(series.Format); var otherNames = ExtractAlternativeNames(series); @@ -238,13 +200,13 @@ public class ExternalMetadataService : IExternalMetadataService SeriesName = series.Name, AlternativeNames = otherNames, Year = year, - AniListId = potentialAnilistId ?? ScrobblingService.GetAniListId(series), - MalId = potentialMalId ?? ScrobblingService.GetMalId(series) + AniListId = potentialAnilistId ?? ScrobblingHelper.GetAniListId(series), + MalId = potentialMalId ?? ScrobblingHelper.GetMalId(series) }; try { - var results = await _kavitaPlusApiService.MatchSeries(matchRequest); + var results = await _kavitaPlusApiService.MatchSeries(matchRequest, ct); // Some summaries can contain multiple
    s, we need to ensure it's only 1 foreach (var result in results) @@ -269,15 +231,7 @@ public class ExternalMetadataService : IExternalMetadataService } - /// - /// Retrieves Metadata about a Recommended External Series - /// - /// - /// - /// - /// - /// - public async Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId) + public async Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId, CancellationToken ct = default) { if (!aniListId.HasValue && !malId.HasValue) { @@ -285,42 +239,34 @@ public class ExternalMetadataService : IExternalMetadataService } // This is for the Series drawer. We can get this extra information during the initial SeriesDetail call so it's all coming from the DB - var details = await GetSeriesDetail(aniListId, malId, seriesId); - - return details; + return await GetSeriesDetail(aniListId, malId, seriesId, ct); } - /// - /// Returns Series Detail data from Kavita+ - Review, Recs, Ratings - /// - /// - /// - /// - public async Task GetSeriesDetailPlus(int seriesId, LibraryType libraryType) + public async Task GetSeriesDetailPlus(int seriesId, LibraryType libraryType, CancellationToken ct = default) { - if (!IsPlusEligible(libraryType) || !await _licenseService.HasActiveLicense()) return _defaultReturn; + if (!IsPlusEligible(libraryType) || !await _licenseService.HasActiveLicense(ct: ct)) return _defaultReturn; // Check blacklist (bad matches) or if there is a don't match - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, ct: ct); if (series == null || !series.WillScrobble()) return _defaultReturn; var needsRefresh = - await _unitOfWork.ExternalSeriesMetadataRepository.NeedsDataRefresh(seriesId); + await _unitOfWork.ExternalSeriesMetadataRepository.NeedsDataRefresh(seriesId, ct); if (!needsRefresh) { // Convert into DTOs and return - return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId); + return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId, ct); } - var data = await _unitOfWork.SeriesRepository.GetPlusSeriesDto(seriesId); + var data = await _unitOfWork.SeriesRepository.GetPlusSeriesDto(seriesId, ct); if (data == null) return _defaultReturn; // Get from Kavita+ API the Full Series metadata with rec/rev and cache to ExternalMetadata tables try { - return await FetchExternalMetadataForSeries(seriesId, libraryType, data); + return await FetchExternalMetadataForSeries(seriesId, libraryType, data, ct); } catch (KavitaException ex) { @@ -330,16 +276,9 @@ public class ExternalMetadataService : IExternalMetadataService } } - /// - /// This will override any sort of matching that was done prior and force it to be what the user Selected - /// - /// - /// - /// - /// - public async Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId) + public async Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId, CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library, ct); if (series == null) return; // Remove from Blacklist @@ -358,7 +297,7 @@ public class ExternalMetadataService : IExternalMetadataService CbrId = cbrId, MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format), SeriesName = series.Name // Required field, not used since AniList/Mal Id are passed - }); + }, ct); if (metadata.Series == null) { @@ -368,14 +307,14 @@ public class ExternalMetadataService : IExternalMetadataService } // Find all scrobble events and rewrite them to be the correct - var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); + var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId, ct); _unitOfWork.ScrobbleRepository.Remove(events); // Find all scrobble errors and remove them - var errors = await _unitOfWork.ScrobbleRepository.GetAllScrobbleErrorsForSeries(seriesId); + var errors = await _unitOfWork.ScrobbleRepository.GetAllScrobbleErrorsForSeries(seriesId, ct); _unitOfWork.ScrobbleRepository.Remove(errors); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); // Regenerate all events for the series for all users BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistoryForSeries(seriesId)); @@ -388,20 +327,14 @@ public class ExternalMetadataService : IExternalMetadataService { // We can't rethrow because Fix match is done in a background thread and Hangfire will requeue multiple times _logger.LogInformation(ex, "Rate limit hit for matching {SeriesName} with Kavita+", series.Name); - // Fire SignalR event about this await _eventHub.SendMessageAsync(MessageFactory.ExternalMatchRateLimitError, - MessageFactory.ExternalMatchRateLimitErrorEvent(series.Id, series.Name)); + MessageFactory.ExternalMatchRateLimitErrorEvent(series.Id, series.Name), ct: ct); } } - /// - /// Sets a series to Don't Match and removes all previously cached - /// - /// - /// - public async Task UpdateSeriesDontMatch(int seriesId, bool dontMatch) + public async Task UpdateSeriesDontMatch(int seriesId, bool dontMatch, CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.ExternalMetadata); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.ExternalMetadata, ct); if (series == null) return; _logger.LogInformation("User has asked Kavita to stop matching/scrobbling on {SeriesName}", series.Name); @@ -420,7 +353,7 @@ public class ExternalMetadataService : IExternalMetadataService _unitOfWork.SeriesRepository.Update(series); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); } /// @@ -430,10 +363,10 @@ public class ExternalMetadataService : IExternalMetadataService /// /// /// - private async Task FetchExternalMetadataForSeries(int seriesId, LibraryType libraryType, PlusSeriesRequestDto data) + private async Task FetchExternalMetadataForSeries(int seriesId, LibraryType libraryType, PlusSeriesRequestDto data, CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library, ct); if (series == null) { return _defaultReturn; @@ -447,7 +380,7 @@ public class ExternalMetadataService : IExternalMetadataService try { // This returns an AniListSeries and Match returns ExternalSeriesDto - result = await _kavitaPlusApiService.GetSeriesDetail(data); + result = await _kavitaPlusApiService.GetSeriesDetail(data, ct); } catch (FlurlHttpException ex) { @@ -460,14 +393,14 @@ public class ExternalMetadataService : IExternalMetadataService if (errorMessage.Contains("Too many Requests")) { _logger.LogDebug("Hit rate limit, will retry in 3 seconds"); - await Task.Delay(3000); + await Task.Delay(3000, ct); - result = await _kavitaPlusApiService.GetSeriesDetail(data); + result = await _kavitaPlusApiService.GetSeriesDetail(data, ct); } else if (errorMessage.Contains("Unknown Series")) { series.IsBlacklisted = true; - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); } } } @@ -522,11 +455,11 @@ public class ExternalMetadataService : IExternalMetadataService var madeMetadataModification = false; if (result.Series != null && series.Library.AllowMetadataMatching) { - externalSeriesMetadata.Series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); + externalSeriesMetadata.Series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, ct: ct); try { - madeMetadataModification = await WriteExternalMetadataToSeries(result.Series, seriesId); + madeMetadataModification = await WriteExternalMetadataToSeries(result.Series, seriesId, ct); if (madeMetadataModification) { _unitOfWork.SeriesRepository.Update(series); @@ -543,13 +476,13 @@ public class ExternalMetadataService : IExternalMetadataService // WriteExternalMetadataToSeries will commit but not always if (_unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); } if (madeMetadataModification) { // Inform the UI of the update - await _eventHub.SendMessageAsync(MessageFactory.ScanSeries, MessageFactory.ScanSeriesEvent(series.LibraryId, series.Id, series.Name), false); + await _eventHub.SendMessageAsync(MessageFactory.ScanSeries, MessageFactory.ScanSeriesEvent(series.LibraryId, series.Id, series.Name), false, ct); } return new SeriesDetailPlusDto() @@ -588,26 +521,20 @@ public class ExternalMetadataService : IExternalMetadataService // Blacklist the series as it wasn't found in Kavita+ series.IsBlacklisted = true; - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); return _defaultReturn; } - /// - /// Given external metadata from Kavita+, write as much as possible to the Kavita series as possible - /// - /// - /// - /// - public async Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId) + public async Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId, CancellationToken ct = default) { - var settings = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); + var settings = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(ct); if (!settings.Enabled) return false; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Related); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Related, ct); if (series == null) return false; - var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(); + var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(ct: ct); _logger.LogInformation("Writing External metadata to Series {SeriesName}", series.Name); @@ -848,7 +775,7 @@ public class ExternalMetadataService : IExternalMetadataService .Select(w => new PersonDto() { Name = w.Name.Trim(), - AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListCharacterWebsite), + AniListId = ScrobblingHelper.ExtractId(w.Url, ScrobblingService.AniListCharacterWebsite), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) .Concat(series.Metadata.People @@ -890,7 +817,7 @@ public class ExternalMetadataService : IExternalMetadataService foreach (var character in externalCharacters) { - var aniListId = ScrobblingService.ExtractId(character.Url, ScrobblingService.AniListCharacterWebsite); + var aniListId = ScrobblingHelper.ExtractId(character.Url, ScrobblingService.AniListCharacterWebsite); if (aniListId <= 0) continue; var person = await _unitOfWork.PersonRepository.GetPersonByAniListId(aniListId); if (person != null && !string.IsNullOrEmpty(character.ImageUrl) && string.IsNullOrEmpty(person.CoverImage)) @@ -929,7 +856,7 @@ public class ExternalMetadataService : IExternalMetadataService .Select(w => new PersonDto() { Name = w.Name.Trim(), - AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), + AniListId = ScrobblingHelper.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) .Concat(series.Metadata.People @@ -986,7 +913,7 @@ public class ExternalMetadataService : IExternalMetadataService .Select(w => new PersonDto() { Name = w.Name.Trim(), - AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), + AniListId = ScrobblingHelper.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) .Concat(series.Metadata.People @@ -1612,7 +1539,7 @@ public class ExternalMetadataService : IExternalMetadataService { foreach (var staff in people) { - var aniListId = ScrobblingService.ExtractId(staff.Url, ScrobblingService.AniListStaffWebsite); + var aniListId = ScrobblingHelper.ExtractId(staff.Url, ScrobblingService.AniListStaffWebsite); if (aniListId is null or <= 0) continue; var person = await _unitOfWork.PersonRepository.GetPersonByAniListId(aniListId.Value); if (person == null || string.IsNullOrEmpty(staff.ImageUrl) || @@ -1858,8 +1785,8 @@ public class ExternalMetadataService : IExternalMetadataService { // Find the series based on name and type and that the user has access too var seriesForRec = await _unitOfWork.SeriesRepository.GetSeriesDtoByNamesAndMetadataIds(rec.RecommendationNames, - libraryType, ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, rec.AniListId), - ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, rec.MalId)); + libraryType, ScrobblingHelper.CreateUrl(ScrobblingService.AniListWeblinkWebsite, rec.AniListId), + ScrobblingHelper.CreateUrl(ScrobblingService.MalWeblinkWebsite, rec.MalId)); if (seriesForRec != null) { @@ -1916,8 +1843,9 @@ public class ExternalMetadataService : IExternalMetadataService /// /// /// + /// /// - private async Task GetSeriesDetail(int? aniListId, long? malId, int? seriesId) + private async Task GetSeriesDetail(int? aniListId, long? malId, int? seriesId, CancellationToken ct = default) { var payload = new ExternalMetadataIdsDto() { @@ -1930,16 +1858,16 @@ public class ExternalMetadataService : IExternalMetadataService if (seriesId is > 0) { var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value, - SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalReviews); + SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalReviews, ct); if (series != null) { if (payload.AniListId <= 0) { - payload.AniListId = ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.AniListWeblinkWebsite); + payload.AniListId = ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingService.AniListWeblinkWebsite); } if (payload.MalId <= 0) { - payload.MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.MalWeblinkWebsite); + payload.MalId = ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingService.MalWeblinkWebsite); } payload.SeriesName = series.Name; payload.LocalizedSeriesName = series.LocalizedName; @@ -1949,7 +1877,7 @@ public class ExternalMetadataService : IExternalMetadataService } try { - var ret = await _kavitaPlusApiService.GetSeriesDetailById(payload); + var ret = await _kavitaPlusApiService.GetSeriesDetailById(payload, ct); ret.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(ret.Summary)); diff --git a/API/Services/Plus/KavitaPlusApiService.cs b/Kavita.Services/Plus/KavitaPlusApiService.cs similarity index 69% rename from API/Services/Plus/KavitaPlusApiService.cs rename to Kavita.Services/Plus/KavitaPlusApiService.cs index ec4f414c3..6aa3f64db 100644 --- a/API/Services/Plus/KavitaPlusApiService.cs +++ b/Kavita.Services/Plus/KavitaPlusApiService.cs @@ -1,95 +1,86 @@ #nullable enable using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Collection; -using API.DTOs.KavitaPlus.ExternalMetadata; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Metadata.Matching; -using API.DTOs.Scrobbling; -using API.Entities.Enums; -using API.Extensions; using Flurl.Http; +using Kavita.API.Database; +using Kavita.API.Services.Plus; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Metadata.Matching; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Enums; using Microsoft.Extensions.Logging; -namespace API.Services.Plus; - -/// -/// All Http requests to K+ should be contained in this service, the service will not handle any errors. -/// This is expected from the caller. -/// -public interface IKavitaPlusApiService -{ - Task HasTokenExpired(string license, string token, ScrobbleProvider provider); - Task GetRateLimit(string license, string token); - Task PostScrobbleUpdate(ScrobbleDto data, string license); - Task> GetMalStacks(string malUsername, string license); - Task> MatchSeries(MatchSeriesRequestDto request); - Task GetSeriesDetail(PlusSeriesRequestDto request); - Task GetSeriesDetailById(ExternalMetadataIdsDto request); -} +namespace Kavita.Services.Plus; public class KavitaPlusApiService(ILogger logger, IUnitOfWork unitOfWork): IKavitaPlusApiService { private const string ScrobblingPath = "/api/scrobbling/"; - public async Task HasTokenExpired(string license, string token, ScrobbleProvider provider) + public async Task HasTokenExpired(string license, string token, ScrobbleProvider provider, + CancellationToken ct = default) { var res = await Get(ScrobblingPath + "valid-key?provider=" + provider + "&key=" + token, license, token); var str = await res.GetStringAsync(); return bool.Parse(str); } - public async Task GetRateLimit(string license, string token) + public async Task GetRateLimit(string license, string token, CancellationToken ct = default) { var res = await Get(ScrobblingPath + "rate-limit?accessToken=" + token, license, token); var str = await res.GetStringAsync(); return int.Parse(str); } - public async Task PostScrobbleUpdate(ScrobbleDto data, string license) + public async Task PostScrobbleUpdate(ScrobbleDto data, string license, + CancellationToken ct = default) { return await PostAndReceive(ScrobblingPath + "update", data, license); } - public async Task> GetMalStacks(string malUsername, string license) + public async Task> GetMalStacks(string malUsername, string license, CancellationToken ct = default) { return await $"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/stacks?username={malUsername}" .WithKavitaPlusHeaders(license) - .GetJsonAsync>(); + .GetJsonAsync>(cancellationToken: ct); } - public async Task> MatchSeries(MatchSeriesRequestDto request) + public async Task> MatchSeries(MatchSeriesRequestDto request, + CancellationToken ct = default) { - var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; + var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct)).Value; + var token = (await unitOfWork.UserRepository.GetDefaultAdminUser(ct: ct)).AniListAccessToken; return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/match-series") .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(request) + .PostJsonAsync(request, cancellationToken: ct) .ReceiveJson>(); } - public async Task GetSeriesDetail(PlusSeriesRequestDto request) + public async Task GetSeriesDetail(PlusSeriesRequestDto request, CancellationToken ct = default) { - var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; + var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct)).Value; + var token = (await unitOfWork.UserRepository.GetDefaultAdminUser(ct: ct)).AniListAccessToken; return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(request) + .PostJsonAsync(request, cancellationToken: ct) .ReceiveJson(); } - public async Task GetSeriesDetailById(ExternalMetadataIdsDto request) + public async Task GetSeriesDetailById(ExternalMetadataIdsDto request, + CancellationToken ct = default) { - var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; + var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct)).Value; + var token = (await unitOfWork.UserRepository.GetDefaultAdminUser(ct: ct)).AniListAccessToken; return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids") .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(request) + .PostJsonAsync(request, cancellationToken: ct) .ReceiveJson(); } diff --git a/API/Services/Plus/LicenseService.cs b/Kavita.Services/Plus/LicenseService.cs similarity index 84% rename from API/Services/Plus/LicenseService.cs rename to Kavita.Services/Plus/LicenseService.cs index c3d184f0a..c593c4d0b 100644 --- a/API/Services/Plus/LicenseService.cs +++ b/Kavita.Services/Plus/LicenseService.cs @@ -1,20 +1,21 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.KavitaPlus.License; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks; using EasyCaching.Core; using Flurl.Http; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Plus; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.KavitaPlus.License; +using Kavita.Models.Entities.Enums; using Microsoft.Extensions.Logging; -namespace API.Services.Plus; -#nullable enable +namespace Kavita.Services.Plus; internal class RegisterLicenseResponseDto { @@ -23,18 +24,6 @@ internal class RegisterLicenseResponseDto public string ErrorMessage { get; set; } } -public interface ILicenseService -{ - //Task ValidateLicenseStatus(); - Task RemoveLicense(); - Task AddLicense(string license, string email, string? discordId); - Task HasActiveLicense(bool forceCheck = false); - Task HasActiveSubscription(string? license); - Task ResetLicense(string license, string email); - Task GetLicenseInfo(bool forceCheck = false); - Task ResendWelcomeEmail(); -} - public class LicenseService( IEasyCachingProviderFactory cachingProviderFactory, IUnitOfWork unitOfWork, @@ -120,20 +109,21 @@ public class LicenseService( /// Checks licenses and updates cache /// /// Skip what's in cache + /// /// - public async Task HasActiveLicense(bool forceCheck = false) + public async Task HasActiveLicense(bool forceCheck = false, CancellationToken ct = default) { var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); if (!forceCheck) { - var cacheValue = await provider.GetAsync(CacheKey); + var cacheValue = await provider.GetAsync(CacheKey, ct); if (cacheValue.HasValue) return cacheValue.Value; } var result = false; try { - var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct); result = await IsLicenseValid(serverSetting.Value); } catch (Exception ex) @@ -142,8 +132,8 @@ public class LicenseService( } finally { - await provider.FlushAsync(); - await provider.SetAsync(CacheKey, result, _licenseCacheTimeout); + await provider.FlushAsync(ct); + await provider.SetAsync(CacheKey, result, _licenseCacheTimeout, ct); } return result; @@ -153,8 +143,9 @@ public class LicenseService( /// Checks if the sub is active and caches the result. This should not be used too much over cache as it will skip backend caching. ///
    /// + /// /// - public async Task HasActiveSubscription(string? license) + public async Task HasActiveSubscription(string? license, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(license)) return false; try @@ -165,14 +156,14 @@ public class LicenseService( { License = license, InstallId = HashUtil.ServerToken() - }) + }, cancellationToken: ct) .ReceiveString(); var result = bool.Parse(response); var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - await provider.FlushAsync(); - await provider.SetAsync(CacheKey, result, _licenseCacheTimeout); + await provider.FlushAsync(ct); + await provider.SetAsync(CacheKey, result, _licenseCacheTimeout, ct); return result; } @@ -183,37 +174,37 @@ public class LicenseService( } } - public async Task RemoveLicense() + public async Task RemoveLicense(CancellationToken ct = default) { - var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct); serverSetting.Value = string.Empty; unitOfWork.SettingsRepository.Update(serverSetting); - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - await provider.RemoveAsync(CacheKey); + await provider.RemoveAsync(CacheKey, ct); } - public async Task AddLicense(string license, string email, string? discordId) + public async Task AddLicense(string license, string email, string? discordId, CancellationToken ct = default) { - var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct); var lic = await RegisterLicense(license, email, discordId); if (string.IsNullOrWhiteSpace(lic)) throw new KavitaException("unable-to-register-k+"); serverSetting.Value = lic; unitOfWork.SettingsRepository.Update(serverSetting); - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); } - public async Task ResetLicense(string license, string email) + public async Task ResetLicense(string license, string email, CancellationToken ct = default) { try { - var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct); var response = await (Configuration.KavitaPlusApiUrl + "/api/license/reset") .WithKavitaPlusHeaders(encryptedLicense.Value) .PostJsonAsync(new ResetLicenseDto() @@ -221,13 +212,13 @@ public class LicenseService( License = license.Trim(), InstallId = HashUtil.ServerToken(), EmailId = email - }) + }, cancellationToken: ct) .ReceiveString(); if (string.IsNullOrEmpty(response)) { var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - await provider.RemoveAsync(CacheKey); + await provider.RemoveAsync(CacheKey, ct); return true; } @@ -246,12 +237,13 @@ public class LicenseService( /// Fetches information about the license from Kavita+. If there is no license or an exception, will return null and can be assumed it is not active ///
    /// + /// /// - public async Task GetLicenseInfo(bool forceCheck = false) + public async Task GetLicenseInfo(bool forceCheck = false, CancellationToken ct = default) { // Check if there is a license var hasLicense = - !string.IsNullOrEmpty((await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)) + !string.IsNullOrEmpty((await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct)) .Value); if (!hasLicense) return null; @@ -260,22 +252,22 @@ public class LicenseService( var licenseInfoProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.LicenseInfo); if (!forceCheck) { - var cacheValue = await licenseInfoProvider.GetAsync(LicenseInfoCacheKey); + var cacheValue = await licenseInfoProvider.GetAsync(LicenseInfoCacheKey, ct); if (cacheValue.HasValue) return cacheValue.Value; } try { - var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct); var response = await (Configuration.KavitaPlusApiUrl + "/api/license/info") .WithKavitaPlusHeaders(encryptedLicense.Value) - .GetJsonAsync(); + .GetJsonAsync(cancellationToken: ct); // This indicates a mismatch on installId or no active subscription if (response == null) return null; // Ensure that current version is within the 3 version limit. Don't count Nightly releases or Hotfixes - var releases = await versionUpdaterService.GetAllReleases(); + var releases = await versionUpdaterService.GetAllReleases(ct: ct); response.IsValidVersion = releases .Where(r => !r.UpdateTitle.Contains("Hotfix")) // We don't care about Hotfix releases .Where(r => !r.IsPrerelease) // Ensure we don't take current nightlies within the current/last stable @@ -287,9 +279,9 @@ public class LicenseService( // Cache if the license is valid here as well var licenseProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - await licenseProvider.SetAsync(CacheKey, response.IsActive, _licenseCacheTimeout); + await licenseProvider.SetAsync(CacheKey, response.IsActive, _licenseCacheTimeout, ct); - // TODO: If info.IsCancelled && notActive, let's remove the license so we aren't constantly checking + // default: If info.IsCancelled && notActive, let's remove the license so we aren't constantly checking if (response is {IsCancelled: true, IsActive: false}) { //logger.LogWarning("Kavita+ License is no longer active, removing Server registration"); @@ -298,7 +290,7 @@ public class LicenseService( // Cache the license info if IsActive and ExpirationDate > DateTime.UtcNow + 2 if (response.IsActive && response.ExpirationDate > DateTime.UtcNow.AddDays(2)) { - await licenseInfoProvider.SetAsync(LicenseInfoCacheKey, response, _licenseCacheTimeout); + await licenseInfoProvider.SetAsync(LicenseInfoCacheKey, response, _licenseCacheTimeout, ct); } @@ -315,17 +307,18 @@ public class LicenseService( /// /// Attempts to resend a welcome email to the registered user. The sub does not need to be active. /// + /// /// - public async Task ResendWelcomeEmail() + public async Task ResendWelcomeEmail(CancellationToken ct = default) { try { - var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct); if (string.IsNullOrEmpty(encryptedLicense.Value)) return false; var httpResponse = await (Configuration.KavitaPlusApiUrl + "/api/license/resend-welcome-email") .WithKavitaPlusHeaders(encryptedLicense.Value) - .PostAsync(); + .PostAsync(cancellationToken: ct); var response = await httpResponse.GetStringAsync(); diff --git a/API/Services/Plus/ScrobblingService.cs b/Kavita.Services/Plus/ScrobblingService.cs similarity index 82% rename from API/Services/Plus/ScrobblingService.cs rename to Kavita.Services/Plus/ScrobblingService.cs index 00c7a9c84..5ff3581e7 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/Kavita.Services/Plus/ScrobblingService.cs @@ -1,121 +1,36 @@ -using System; + + +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Filtering; -using API.DTOs.Scrobbling; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.Scrobble; -using API.Extensions; -using API.Helpers; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using Flurl.Http; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.SignalR; using Kavita.Common; using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Scrobble; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Services.Plus; -#nullable enable - -/// -/// Misleading name but is the source of data (like a review coming from AniList) -/// -public enum ScrobbleProvider -{ - /// - /// For now, this means data comes from within this instance of Kavita - /// - Kavita = 0, - AniList = 1, - Mal = 2, - [Obsolete("No longer supported")] - GoogleBooks = 3, - Cbr = 4 -} - -public interface IScrobblingService -{ - /// - /// An automated job that will run against all user's tokens and validate if they are still active - /// - /// This service can validate without license check as the task which calls will be guarded - /// - Task CheckExternalAccessTokens(); - - /// - /// Checks if the token has expired with , if it has double checks with K+, - /// otherwise return false. - /// - /// - /// - /// - /// Returns true if there is no license present - Task HasTokenExpired(int userId, ScrobbleProvider provider); - /// - /// Create, or update a non-processed, event, for the given series - /// - /// - /// - /// - /// - Task ScrobbleRatingUpdate(int userId, int seriesId, float rating); - /// - /// NOP, until hardcover support has been worked out - /// - /// - /// - /// - /// - /// - Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody); - /// - /// Create, or update a non-processed, event, for the given series - /// - /// - /// - /// - Task ScrobbleReadingUpdate(int userId, int seriesId); - /// - /// Creates an or for - /// the given series - /// - /// - /// - /// - /// - /// Only the result of both WantToRead types is send to K+ - Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead); - - /// - /// Removed all processed events that are at least 7 days old - /// - /// - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public Task ClearProcessedEvents(); - - /// - /// Makes K+ requests for all non-processed events until rate limits are reached - /// - /// - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task ProcessUpdatesSinceLastSync(); - - Task CreateEventsFromExistingHistory(int userId = 0); - Task CreateEventsFromExistingHistoryForSeries(int seriesId); - Task ClearEventsForSeries(int userId, int seriesId); -} +namespace Kavita.Services.Plus; /// /// Context used when syncing scrobble events. Do NOT reuse between syncs @@ -166,26 +81,15 @@ public class ScrobblingService : IScrobblingService private readonly IEmailService _emailService; private readonly IKavitaPlusApiService _kavitaPlusApiService; - public const string AniListWeblinkWebsite = "https://anilist.co/manga/"; - public const string MalWeblinkWebsite = "https://myanimelist.net/manga/"; - public const string MalStaffWebsite = "https://myanimelist.net/people/"; - public const string MalCharacterWebsite = "https://myanimelist.net/character/"; - public const string GoogleBooksWeblinkWebsite = "https://books.google.com/books?id="; - public const string MangaDexWeblinkWebsite = "https://mangadex.org/title/"; - public const string AniListStaffWebsite = "https://anilist.co/staff/"; - public const string AniListCharacterWebsite = "https://anilist.co/character/"; - public const string HardcoverStaffWebsite = "https://hardcover.app/authors/"; - - - private static readonly Dictionary WeblinkExtractionMap = new() - { - {AniListWeblinkWebsite, 0}, - {MalWeblinkWebsite, 0}, - {GoogleBooksWeblinkWebsite, 0}, - {MangaDexWeblinkWebsite, 0}, - {AniListStaffWebsite, 0}, - {AniListCharacterWebsite, 0}, - }; + public const string AniListWeblinkWebsite = ScrobblingHelper.AniListWeblinkWebsite; + public const string MalWeblinkWebsite = ScrobblingHelper.MalWeblinkWebsite; + public const string MalStaffWebsite = ScrobblingHelper.MalStaffWebsite; + public const string MalCharacterWebsite = ScrobblingHelper.MalCharacterWebsite; + public const string GoogleBooksWeblinkWebsite = ScrobblingHelper.GoogleBooksWeblinkWebsite; + public const string MangaDexWeblinkWebsite = ScrobblingHelper.MangaDexWeblinkWebsite; + public const string AniListStaffWebsite = ScrobblingHelper.AniListStaffWebsite; + public const string AniListCharacterWebsite = ScrobblingHelper.AniListCharacterWebsite; + public const string HardcoverStaffWebsite = ScrobblingHelper.HardcoverStaffWebsite; private const int ScrobbleSleepTime = 1000; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90) @@ -226,12 +130,13 @@ public class ScrobblingService : IScrobblingService /// /// An automated job that will run against all user's tokens and validate if they are still active /// + /// /// This service can validate without license check as the task which calls will be guarded /// - public async Task CheckExternalAccessTokens() + public async Task CheckExternalAccessTokens(CancellationToken ct = default) { // Validate AniList - var users = await _unitOfWork.UserRepository.GetAllUsersAsync(); + var users = await _unitOfWork.UserRepository.GetAllUsersAsync(ct: ct); foreach (var user in users) { if (string.IsNullOrEmpty(user.AniListAccessToken)) continue; @@ -261,7 +166,7 @@ public class ScrobblingService : IScrobblingService await _eventHub.SendMessageToAsync( MessageFactory.ScrobblingKeyExpired, MessageFactory.ScrobblingKeyExpiredEvent(ScrobbleProvider.AniList), - user.Id); + user.Id, ct); } } @@ -298,7 +203,7 @@ public class ScrobblingService : IScrobblingService return !hasAlreadySentExpirationEmail; } - public async Task HasTokenExpired(int userId, ScrobbleProvider provider) + public async Task HasTokenExpired(int userId, ScrobbleProvider provider, CancellationToken ct = default) { var token = await GetTokenForProvider(userId, provider); @@ -306,7 +211,7 @@ public class ScrobblingService : IScrobblingService { // NOTE: Should this side effect be here? await _eventHub.SendMessageToAsync(MessageFactory.ScrobblingKeyExpired, - MessageFactory.ScrobblingKeyExpiredEvent(ScrobbleProvider.AniList), userId); + MessageFactory.ScrobblingKeyExpiredEvent(ScrobbleProvider.AniList), userId, ct); return true; } @@ -352,27 +257,28 @@ public class ScrobblingService : IScrobblingService #region Scrobble ingest - public Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody) + public Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody, + CancellationToken ct = default) { // Currently disabled until at least hardcover is implemented return Task.CompletedTask; } - public async Task ScrobbleRatingUpdate(int userId, int seriesId, float rating) + public async Task ScrobbleRatingUpdate(int userId, int seriesId, float rating, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; + if (!await _licenseService.HasActiveLicense(ct: ct)) return; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata, ct); if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences, ct); if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; _logger.LogInformation("Processing Scrobbling rating event for {AppUserId} on {SeriesName}", userId, series.Name); if (await CheckIfCannotScrobble(userId, seriesId, series)) return; var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, - ScrobbleEventType.ScoreUpdated, true); + ScrobbleEventType.ScoreUpdated, true, ct); if (existingEvt is {IsProcessed: false}) { // We need to just update Volume/Chapter number @@ -380,7 +286,7 @@ public class ScrobblingService : IScrobblingService existingEvt.Series.Name, existingEvt.Rating, rating); existingEvt.Rating = rating; _unitOfWork.ScrobbleRepository.Update(existingEvt); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); return; } @@ -389,45 +295,45 @@ public class ScrobblingService : IScrobblingService SeriesId = series.Id, LibraryId = series.LibraryId, ScrobbleEventType = ScrobbleEventType.ScoreUpdated, - AniListId = GetAniListId(series), - MalId = GetMalId(series), + AniListId = ScrobblingHelper.GetAniListId(series), + MalId = ScrobblingHelper.GetMalId(series), AppUserId = userId, Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), Rating = rating }; _unitOfWork.ScrobbleRepository.Attach(evt); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); _logger.LogDebug("Added Scrobbling Rating update on {SeriesName} with Userid {AppUserId}", series.Name, userId); } - public async Task ScrobbleReadingUpdate(int userId, int seriesId) + public async Task ScrobbleReadingUpdate(int userId, int seriesId, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; + if (!await _licenseService.HasActiveLicense(ct: ct)) return; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata, ct); if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences, ct); if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; _logger.LogInformation("Processing Scrobbling reading event for {AppUserId} on {SeriesName}", userId, series.Name); if (await CheckIfCannotScrobble(userId, seriesId, series)) return; - var isAnyProgressOnSeries = await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, userId); + var isAnyProgressOnSeries = await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, userId, ct); - var volumeNumber = (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId); - var chapterNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId); + var volumeNumber = (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId, ct); + var chapterNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId, ct); // Check if there is an existing not yet processed event, if so update it var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, - ScrobbleEventType.ChapterRead, true); + ScrobbleEventType.ChapterRead, true, ct); if (existingEvt is {IsProcessed: false}) { if (!isAnyProgressOnSeries) { _unitOfWork.ScrobbleRepository.Remove(existingEvt); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); _logger.LogDebug("Removed scrobble event for {Series} as there is no reading progress", series.Name); return; } @@ -440,7 +346,7 @@ public class ScrobblingService : IScrobblingService existingEvt.ChapterNumber = chapterNumber; _unitOfWork.ScrobbleRepository.Update(existingEvt); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); _logger.LogDebug("Overriding scrobble event for {Series} from vol {PrevVol} ch {PrevChap} -> vol {UpdatedVol} ch {UpdatedChap}", existingEvt.Series.Name, prevVol, prevChapter, existingEvt.VolumeNumber, existingEvt.ChapterNumber); @@ -460,8 +366,8 @@ public class ScrobblingService : IScrobblingService SeriesId = series.Id, LibraryId = series.LibraryId, ScrobbleEventType = ScrobbleEventType.ChapterRead, - AniListId = GetAniListId(series), - MalId = GetMalId(series), + AniListId = ScrobblingHelper.GetAniListId(series), + MalId = ScrobblingHelper.GetMalId(series), AppUserId = userId, VolumeNumber = volumeNumber, ChapterNumber = chapterNumber, @@ -475,7 +381,7 @@ public class ScrobblingService : IScrobblingService } _unitOfWork.ScrobbleRepository.Attach(evt); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); _logger.LogDebug("Added Scrobbling Read update on {SeriesName} - Volume: {VolumeNumber} Chapter: {ChapterNumber} for User: {AppUserId}", series.Name, evt.VolumeNumber, evt.ChapterNumber, userId); } catch (Exception ex) @@ -484,23 +390,23 @@ public class ScrobblingService : IScrobblingService } } - public async Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead) + public async Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; + if (!await _licenseService.HasActiveLicense(ct: ct)) return; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata, ct); if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); if (!series.Library.AllowScrobbling) return; - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences, ct); if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; if (await CheckIfCannotScrobble(userId, seriesId, series)) return; _logger.LogInformation("Processing Scrobbling want-to-read event for {AppUserId} on {SeriesName}", userId, series.Name); // Get existing events for this series/user - var existingEvents = (await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId)) + var existingEvents = (await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId, ct)) .Where(e => new[] { ScrobbleEventType.AddWantToRead, ScrobbleEventType.RemoveWantToRead }.Contains(e.ScrobbleEventType)); // Remove all existing want-to-read events for this series/user @@ -512,114 +418,19 @@ public class ScrobblingService : IScrobblingService SeriesId = series.Id, LibraryId = series.LibraryId, ScrobbleEventType = onWantToRead ? ScrobbleEventType.AddWantToRead : ScrobbleEventType.RemoveWantToRead, - AniListId = GetAniListId(series), - MalId = GetMalId(series), + AniListId = ScrobblingHelper.GetAniListId(series), + MalId = ScrobblingHelper.GetMalId(series), AppUserId = userId, Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), }; _unitOfWork.ScrobbleRepository.Attach(evt); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); _logger.LogDebug("Added Scrobbling WantToRead update on {SeriesName} with Userid {AppUserId} ", series.Name, userId); } #endregion - #region Scrobble provider methods - - private static bool IsAniListReviewValid(string reviewTitle, string reviewBody) - { - return string.IsNullOrEmpty(reviewTitle) || string.IsNullOrEmpty(reviewBody) || (reviewTitle.Length < 2200 || - reviewTitle.Length > 120 || - reviewTitle.Length < 20); - } - - public static long? GetMalId(Series series) - { - var malId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite); - return malId ?? series.ExternalSeriesMetadata?.MalId; - } - - public static int? GetAniListId(Series seriesWithExternalMetadata) - { - var aniListId = ExtractId(seriesWithExternalMetadata.Metadata.WebLinks, AniListWeblinkWebsite); - return aniListId ?? seriesWithExternalMetadata.ExternalSeriesMetadata?.AniListId; - } - - /// - /// 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) - { - return id is null or 0 ? string.Empty : $"{url}{id}/"; - } - - #endregion - /// /// Returns false if, the series is on hold or Don't Match, or when the library has scrobbling disable or not eligible /// @@ -761,9 +572,10 @@ public class ScrobblingService : IScrobblingService /// This is a task that is run on a fixed schedule (every few hours or every day) that clears out the scrobble event table /// and offloads the data to the API server which performs the syncing to the providers. /// + /// [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ProcessUpdatesSinceLastSync() + public async Task ProcessUpdatesSinceLastSync(CancellationToken ct = default) { var ctx = await PrepareScrobbleContext(); if (ctx.TotalCount == 0) return; @@ -1210,18 +1022,18 @@ public class ScrobblingService : IScrobblingService #region BackFill - /// /// This will backfill events from existing progress history, ratings, and want to read for users that have a valid license /// /// Defaults to 0 meaning all users. Allows a userId to be set if a scrobble key is added to a user - public async Task CreateEventsFromExistingHistory(int userId = 0) + /// + public async Task CreateEventsFromExistingHistory(int userId = 0, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; + if (!await _licenseService.HasActiveLicense(ct: ct)) return; if (userId != 0) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, ct: ct); if (user == null || string.IsNullOrEmpty(user.AniListAccessToken)) return; if (user.HasRunScrobbleEventGeneration) { @@ -1230,10 +1042,10 @@ public class ScrobblingService : IScrobblingService } } - var libAllowsScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) + var libAllowsScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync(ct: ct)) .ToDictionary(lib => lib.Id, lib => lib.AllowScrobbling); - var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()) + var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync(ct: ct)) .Where(l => userId == 0 || userId == l.Id) .Where(u => !u.HasRunScrobbleEventGeneration) .Select(u => u.Id); @@ -1299,42 +1111,42 @@ public class ScrobblingService : IScrobblingService } } - public async Task CreateEventsFromExistingHistoryForSeries(int seriesId) + public async Task CreateEventsFromExistingHistoryForSeries(int seriesId, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; + if (!await _licenseService.HasActiveLicense(ct: ct)) return; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library, ct); if (series == null || !series.Library.AllowScrobbling) return; _logger.LogInformation("Creating Scrobbling events for Series {SeriesName}", series.Name); - var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(u => u.Id); + var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync(ct: ct)).Select(u => u.Id); foreach (var uId in userIds) { // Handle "Want to Read" updates specific to the series - var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(uId); + var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(uId, ct); foreach (var wtr in wantToRead.Where(wtr => wtr.Id == seriesId)) { - await ScrobbleWantToReadUpdate(uId, wtr.Id, true); + await ScrobbleWantToReadUpdate(uId, wtr.Id, true, ct); } // Handle ratings specific to the series - var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(uId); + var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(uId, ct); foreach (var rating in ratings.Where(rating => rating.SeriesId == seriesId)) { - await ScrobbleRatingUpdate(uId, rating.SeriesId, rating.Rating); + await ScrobbleRatingUpdate(uId, rating.SeriesId, rating.Rating, ct); } // Handle review specific to the series - var reviews = await _unitOfWork.UserRepository.GetSeriesWithReviews(uId); + var reviews = await _unitOfWork.UserRepository.GetSeriesWithReviews(uId, ct); foreach (var review in reviews.Where(r => r.SeriesId == seriesId && !string.IsNullOrEmpty(r.Review))) { - await ScrobbleReviewUpdate(uId, review.SeriesId, string.Empty, review.Review!); + await ScrobbleReviewUpdate(uId, review.SeriesId, string.Empty, review.Review!, ct); } // Handle progress updates for the specific series - await ScrobbleReadingUpdate(uId, seriesId); + await ScrobbleReadingUpdate(uId, seriesId, ct); } } @@ -1345,27 +1157,29 @@ public class ScrobblingService : IScrobblingService ///
    /// /// - public async Task ClearEventsForSeries(int userId, int seriesId) + /// + public async Task ClearEventsForSeries(int userId, int seriesId, CancellationToken ct = default) { _logger.LogInformation("Clearing Pre-existing Scrobble events for Series {SeriesId} by User {AppUserId} as Series is now on hold list", seriesId, userId); - var events = await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId); + var events = await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId, ct); _unitOfWork.ScrobbleRepository.Remove(events); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); } /// /// Removes all events that have been processed that are 7 days old /// + /// [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ClearProcessedEvents() + public async Task ClearProcessedEvents(CancellationToken ct = default) { const int daysAgo = 7; - var events = await _unitOfWork.ScrobbleRepository.GetProcessedEvents(daysAgo); + var events = await _unitOfWork.ScrobbleRepository.GetProcessedEvents(daysAgo, ct); _unitOfWork.ScrobbleRepository.Remove(events); _logger.LogInformation("Removing {Count} scrobble events that have been processed {DaysAgo}+ days ago", events.Count, daysAgo); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); } private static bool CanProcessScrobbleEvent(ScrobbleEvent readEvent) diff --git a/API/Services/Plus/SmartCollectionSyncService.cs b/Kavita.Services/Plus/SmartCollectionSyncService.cs similarity index 61% rename from API/Services/Plus/SmartCollectionSyncService.cs rename to Kavita.Services/Plus/SmartCollectionSyncService.cs index c56054d3d..df5e70f8b 100644 --- a/API/Services/Plus/SmartCollectionSyncService.cs +++ b/Kavita.Services/Plus/SmartCollectionSyncService.cs @@ -3,21 +3,25 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.ExternalMetadata; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.SignalR; using Flurl.Http; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services.Plus; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; using Microsoft.Extensions.Logging; -namespace API.Services.Plus; -#nullable enable +namespace Kavita.Services.Plus; internal sealed class SeriesCollection { @@ -30,63 +34,38 @@ internal sealed class SeriesCollection public int TotalItems { get; set; } } -/// -/// Responsible to synchronize Collection series from non-Kavita sources -/// -public interface ISmartCollectionSyncService +public class SmartCollectionSyncService( + IUnitOfWork unitOfWork, + ILogger logger, + IEventHub eventHub, + ILicenseService licenseService) + : ISmartCollectionSyncService { - /// - /// Synchronize all collections - /// - /// - Task Sync(); - /// - /// Synchronize a collection - /// - /// - /// - Task Sync(int collectionId); -} - -public class SmartCollectionSyncService : ISmartCollectionSyncService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IEventHub _eventHub; - private readonly ILicenseService _licenseService; - private const int SyncDelta = -2; // Allow 50 requests per 24 hours private static readonly RateLimiter RateLimiter = new RateLimiter(50, TimeSpan.FromHours(24), false); - public SmartCollectionSyncService(IUnitOfWork unitOfWork, ILogger logger, - IEventHub eventHub, ILicenseService licenseService) - { - _unitOfWork = unitOfWork; - _logger = logger; - _eventHub = eventHub; - _licenseService = licenseService; - } - /// /// For every Sync-eligible collection, synchronize with upstream /// + /// /// - public async Task Sync() + public async Task Sync(CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; + if (!await licenseService.HasActiveLicense(ct: ct)) return; + var expirationTime = DateTime.UtcNow.AddDays(SyncDelta).Truncate(TimeSpan.TicksPerHour); - var collections = (await _unitOfWork.CollectionTagRepository.GetAllCollectionsForSyncing(expirationTime)) + var collections = (await unitOfWork.CollectionTagRepository.GetAllCollectionsForSyncing(expirationTime, ct)) .Where(CanSync) .ToList(); - _logger.LogInformation("Found {Count} collections to synchronize", collections.Count); + logger.LogInformation("Found {Count} collections to synchronize", collections.Count); foreach (var collection in collections) { try { - await SyncCollection(collection); + await SyncCollection(collection, ct); } catch (RateLimitException) { @@ -94,22 +73,23 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService } } - _logger.LogInformation("Synchronization complete"); + logger.LogInformation("Synchronization complete"); } - public async Task Sync(int collectionId) + public async Task Sync(int collectionId, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; - var collection = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(collectionId, CollectionIncludes.Series); + if (!await licenseService.HasActiveLicense(ct: ct)) return; + + var collection = await unitOfWork.CollectionTagRepository.GetCollectionAsync(collectionId, CollectionIncludes.Series, ct); if (!CanSync(collection)) { - _logger.LogInformation("Requested to sync {CollectionName} but not applicable to sync", collection!.Title); + logger.LogInformation("Requested to sync {CollectionName} but not applicable to sync", collection!.Title); return; } try { - await SyncCollection(collection!); + await SyncCollection(collection!, ct); } catch (RateLimitException) {/* Swallow */} } @@ -121,28 +101,28 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService return true; } - private async Task SyncCollection(AppUserCollection collection) + private async Task SyncCollection(AppUserCollection collection, CancellationToken ct = default) { if (!RateLimiter.TryAcquire(string.Empty)) { // Request not allowed due to rate limit - _logger.LogDebug("Rate Limit hit for Smart Collection Sync"); + logger.LogDebug("Rate Limit hit for Smart Collection Sync"); throw new RateLimitException(); } var info = await GetStackInfo(GetStackId(collection.SourceUrl!)); if (info == null) { - _logger.LogInformation("Unable to find collection through Kavita+"); + logger.LogInformation("Unable to find collection through Kavita+"); return; } // Check each series in the collection against what's in the target // For everything that's not there, link it up for this user. - _logger.LogInformation("Starting Sync on {CollectionName} with {SeriesCount} Series", info.Title, info.TotalItems); + logger.LogInformation("Starting Sync on {CollectionName} with {SeriesCount} Series", info.Title, info.TotalItems); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SmartCollectionProgressEvent(info.Title, string.Empty, 0, info.TotalItems, ProgressEventType.Started)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SmartCollectionProgressEvent(info.Title, string.Empty, 0, info.TotalItems, ProgressEventType.Started), ct: ct); var missingCount = 0; var missingSeries = new StringBuilder(); @@ -168,20 +148,20 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService s.NormalizedLocalizedName == normalizedSeriesName) && formats.Contains(s.Format)); - _logger.LogDebug("Trying to find {SeriesName} with formats ({Formats}) within Kavita for linking. Found: {ExistingSeriesName} ({ExistingSeriesId})", + logger.LogDebug("Trying to find {SeriesName} with formats ({Formats}) within Kavita for linking. Found: {ExistingSeriesName} ({ExistingSeriesId})", seriesInfo.SeriesName, formats, existingSeries?.Name, existingSeries?.Id); if (existingSeries != null) { - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SmartCollectionProgressEvent(info.Title, seriesInfo.SeriesName, counter, info.TotalItems, ProgressEventType.Updated)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SmartCollectionProgressEvent(info.Title, seriesInfo.SeriesName, counter, info.TotalItems, ProgressEventType.Updated), ct: ct); continue; } // Series not found in the collection, try to find it in the server - var newSeries = await _unitOfWork.SeriesRepository.GetSeriesByAnyName(seriesInfo.SeriesName, + var newSeries = await unitOfWork.SeriesRepository.GetSeriesByAnyName(seriesInfo.SeriesName, seriesInfo.LocalizedSeriesName, - formats, collection.AppUserId); + formats, collection.AppUserId, ct: ct); collection.Items ??= new List(); if (newSeries != null) @@ -192,7 +172,7 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService } else { - _logger.LogDebug("{Series} not found in the server", seriesInfo.SeriesName); + logger.LogDebug("{Series} not found in the server", seriesInfo.SeriesName); missingCount++; missingSeries.Append( $"
    {seriesInfo.SeriesName}"); @@ -201,15 +181,15 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService } catch (Exception ex) { - _logger.LogError(ex, "An exception occured when linking up a series to the collection. Skipping"); + logger.LogError(ex, "An exception occured when linking up a series to the collection. Skipping"); missingCount++; missingSeries.Append( $"{seriesInfo.SeriesName}"); missingSeries.Append("
    "); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SmartCollectionProgressEvent(info.Title, seriesInfo.SeriesName, counter, info.TotalItems, ProgressEventType.Updated)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SmartCollectionProgressEvent(info.Title, seriesInfo.SeriesName, counter, info.TotalItems, ProgressEventType.Updated), ct: ct); } // At this point, all series in the info have been checked and added if necessary @@ -218,26 +198,26 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService collection.Summary = info.Summary; collection.MissingSeriesFromSource = missingSeries.ToString(); - _unitOfWork.CollectionTagRepository.Update(collection); + unitOfWork.CollectionTagRepository.Update(collection); try { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); - await _unitOfWork.CollectionTagRepository.UpdateCollectionAgeRating(collection); + await unitOfWork.CollectionTagRepository.UpdateCollectionAgeRating(collection, ct); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SmartCollectionProgressEvent(info.Title, string.Empty, info.TotalItems, info.TotalItems, ProgressEventType.Ended)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SmartCollectionProgressEvent(info.Title, string.Empty, info.TotalItems, info.TotalItems, ProgressEventType.Ended), ct: ct); - await _eventHub.SendMessageAsync(MessageFactory.CollectionUpdated, - MessageFactory.CollectionUpdatedEvent(collection.Id), false); + await eventHub.SendMessageAsync(MessageFactory.CollectionUpdated, + MessageFactory.CollectionUpdatedEvent(collection.Id), false, ct); - _logger.LogInformation("Finished Syncing Collection {CollectionName} - Missing {MissingCount} series", + logger.LogInformation("Finished Syncing Collection {CollectionName} - Missing {MissingCount} series", collection.Title, missingCount); } catch (Exception ex) { - _logger.LogError(ex, "There was an error during saving the collection"); + logger.LogError(ex, "There was an error during saving the collection"); } } @@ -251,9 +231,9 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService private async Task GetStackInfo(long stackId) { - _logger.LogDebug("Fetching Kavita+ for MAL Stack"); + logger.LogDebug("Fetching Kavita+ for MAL Stack"); - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; + var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; var seriesForStack = await ($"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/stack?stackId=" + stackId) .WithKavitaPlusHeaders(license) diff --git a/API/Services/Plus/WantToReadSyncService.cs b/Kavita.Services/Plus/WantToReadSyncService.cs similarity index 62% rename from API/Services/Plus/WantToReadSyncService.cs rename to Kavita.Services/Plus/WantToReadSyncService.cs index 7eefa44d4..26cd595af 100644 --- a/API/Services/Plus/WantToReadSyncService.cs +++ b/Kavita.Services/Plus/WantToReadSyncService.cs @@ -1,68 +1,58 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; using Flurl.Http; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services.Plus; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using Microsoft.Extensions.Logging; -namespace API.Services.Plus; +namespace Kavita.Services.Plus; -public interface IWantToReadSyncService -{ - Task Sync(); -} - /// /// Responsible for syncing Want To Read from upstream providers with Kavita /// -public class WantToReadSyncService : IWantToReadSyncService +public class WantToReadSyncService( + IUnitOfWork unitOfWork, + ILogger logger, + ILicenseService licenseService) + : IWantToReadSyncService { - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly ILicenseService _licenseService; - - public WantToReadSyncService(IUnitOfWork unitOfWork, ILogger logger, ILicenseService licenseService) + public async Task Sync(CancellationToken ct = default) { - _unitOfWork = unitOfWork; - _logger = logger; - _licenseService = licenseService; - } + if (!await licenseService.HasActiveLicense(ct: ct)) return; - public async Task Sync() - { - if (!await _licenseService.HasActiveLicense()) return; + var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct)).Value; - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - - var users = await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.WantToRead | AppUserIncludes.UserPreferences); + var users = await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.WantToRead | AppUserIncludes.UserPreferences, ct: ct); foreach (var user in users.Where(u => u.UserPreferences.WantToReadSync)) { if (string.IsNullOrEmpty(user.MalUserName) && string.IsNullOrEmpty(user.AniListAccessToken)) continue; try { - _logger.LogInformation("Syncing want to read for user: {UserName}", user.UserName); + logger.LogInformation("Syncing want to read for user: {UserName}", user.UserName); var wantToReadSeries = await ( $"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/want-to-read?malUsername={user.MalUserName}&aniListToken={user.AniListAccessToken}") .WithKavitaPlusHeaders(license) .WithTimeout( TimeSpan.FromSeconds(120)) // Give extra time as MAL + AniList can result in a lot of data - .GetJsonAsync>(); + .GetJsonAsync>(cancellationToken: ct); // Match the series (note: There may be duplicates in the final result) foreach (var unmatchedSeries in wantToReadSeries) { - var match = await _unitOfWork.SeriesRepository.MatchSeries(unmatchedSeries); + var match = await unitOfWork.SeriesRepository.MatchSeries(unmatchedSeries, ct); if (match == null) { continue; @@ -73,7 +63,7 @@ public class WantToReadSyncService : IWantToReadSyncService { SeriesId = match.Id, }); - _logger.LogDebug("Added {MatchName} ({Format}) to Want to Read", match.Name, match.Format); + logger.LogDebug("Added {MatchName} ({Format}) to Want to Read", match.Name, match.Format); } // Remove existing Want to Read that are duplicates @@ -82,15 +72,15 @@ public class WantToReadSyncService : IWantToReadSyncService // TODO: Need to write in the history table the last sync time // Save the left over entities - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); // Trigger CleanupService to cleanup any series in WantToRead that don't belong RecurringJob.TriggerJob(TaskScheduler.RemoveFromWantToReadTaskId); } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when processing want to read series sync for {User}", user.UserName); + logger.LogError(ex, "There was an exception when processing want to read series sync for {User}", user.UserName); } } diff --git a/Kavita.Services/RatingService.cs b/Kavita.Services/RatingService.cs new file mode 100644 index 000000000..31e167632 --- /dev/null +++ b/Kavita.Services/RatingService.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.User; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class RatingService(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, ILogger logger) + : IRatingService +{ + public async Task UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto, + CancellationToken ct = default) + { + var userRating = + await unitOfWork.UserRepository.GetUserRatingAsync(updateRatingDto.SeriesId, user.Id, ct) ?? + new AppUserRating(); + + try + { + userRating.Rating = Math.Clamp(updateRatingDto.UserRating, 0f, 5f); + userRating.HasBeenRated = true; + userRating.SeriesId = updateRatingDto.SeriesId; + + if (userRating.Id == 0) + { + user.Ratings ??= new List(); + user.Ratings.Add(userRating); + } + + unitOfWork.UserRepository.Update(user); + + if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync(ct)) + { + BackgroundJob.Enqueue(() => + scrobblingService.ScrobbleRatingUpdate(user.Id, updateRatingDto.SeriesId, + userRating.Rating)); + return true; + } + } + catch (Exception ex) + { + logger.LogError(ex, "There was an exception saving rating"); + } + + await unitOfWork.RollbackAsync(ct); + user.Ratings?.Remove(userRating); + + return false; + } + + public async Task UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto, + CancellationToken ct = default) + { + if (updateRatingDto.ChapterId == null) + { + return false; + } + + var userRating = + await unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, updateRatingDto.ChapterId.Value, ct) ?? + new AppUserChapterRating(); + + try + { + userRating.Rating = Math.Clamp(updateRatingDto.UserRating, 0f, 5f); + userRating.HasBeenRated = true; + userRating.SeriesId = updateRatingDto.SeriesId; + userRating.ChapterId = updateRatingDto.ChapterId.Value; + + if (userRating.Id == 0) + { + user.ChapterRatings ??= new List(); + user.ChapterRatings.Add(userRating); + } + + unitOfWork.UserRepository.Update(user); + + await unitOfWork.CommitAsync(ct); + return true; + } + catch (Exception ex) + { + logger.LogError(ex, "There was an exception saving rating"); + } + + await unitOfWork.RollbackAsync(ct); + user.ChapterRatings?.Remove(userRating); + + return false; + } + +} diff --git a/API/Services/Reading/ReaderService.cs b/Kavita.Services/Reading/ReaderService.cs similarity index 95% rename from API/Services/Reading/ReaderService.cs rename to Kavita.Services/Reading/ReaderService.cs index 34b9769a6..bb729fb27 100644 --- a/API/Services/Reading/ReaderService.cs +++ b/Kavita.Services/Reading/ReaderService.cs @@ -3,47 +3,29 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Comparators; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Progress; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Entities.User; -using API.Extensions; -using API.Helpers.Formatting; -using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Services.Comparators; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services.Reading; -#nullable enable - -public interface IReaderService -{ - Task MarkSeriesAsRead(AppUser user, int seriesId); - Task MarkSeriesAsUnread(AppUser user, int seriesId); - Task MarkChaptersAsRead(AppUser user, int seriesId, IList chapters); - Task MarkChaptersAsUnread(AppUser user, int seriesId, IList chapters); - Task SaveReadingProgress(ProgressDto progressDto, int userId); - int CapPageToChapter(Chapter chapter, int page); - Task GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId); - Task GetPrevChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId); - Task GetContinuePoint(int seriesId, int userId); - Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber); - Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber); - IDictionary GetPairs(IEnumerable dimensions); - Task GetThumbnail(Chapter chapter, int pageNum, IEnumerable cachedImages); - Task CheckSeriesForReRead(int userId, int seriesId, int libraryId); - Task CheckVolumeForReRead(int userId, int volumeId, int seriesId, int libraryId); - Task CheckChapterForReRead(int userId, int chapterId, int seriesId, int libraryId); -} +namespace Kavita.Services.Reading; public class ReaderService(IUnitOfWork unitOfWork, ILogger logger, IEventHub eventHub, IImageService imageService, IDirectoryService directoryService, IScrobblingService scrobblingService, IReadingSessionService readingSessionService, @@ -55,12 +37,12 @@ public class ReaderService(IUnitOfWork unitOfWork, ILogger logger private readonly ChapterSortComparerDefaultFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerDefaultFirst.Default; private readonly ChapterSortComparerSpecialsLast _chapterSortComparerSpecialsLast = ChapterSortComparerSpecialsLast.Default; - private const float MinWordsPerHour = 10260F; - private const float MaxWordsPerHour = 30000F; - public const float AvgWordsPerHour = (MaxWordsPerHour + MinWordsPerHour) / 2F; - private const float MinPagesPerMinute = 3.33F; - private const float MaxPagesPerMinute = 2.75F; - public const float AvgPagesPerMinute = (MaxPagesPerMinute + MinPagesPerMinute) / 2F; //3.04 + private const float MinWordsPerHour = IReaderService.MinWordsPerHour; + private const float MaxWordsPerHour = IReaderService.MaxWordsPerHour; + private const float MinPagesPerMinute = IReaderService.MinPagesPerMinute; + private const float MaxPagesPerMinute = IReaderService.MaxPagesPerMinute; + public const float AvgWordsPerHour = IReaderService.AvgWordsPerHour; + public const float AvgPagesPerMinute = IReaderService.AvgWordsPerHour; public static string FormatBookmarkFolderPath(string baseDirectory, int userId, int seriesId, int chapterId) diff --git a/API/Services/Reading/ReadingHistoryService.cs b/Kavita.Services/Reading/ReadingHistoryService.cs similarity index 84% rename from API/Services/Reading/ReadingHistoryService.cs rename to Kavita.Services/Reading/ReadingHistoryService.cs index 6b7330ca9..f44a8ee31 100644 --- a/API/Services/Reading/ReadingHistoryService.cs +++ b/Kavita.Services/Reading/ReadingHistoryService.cs @@ -1,77 +1,66 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Progress; -using API.Entities.Enums; -using API.Entities.Progress; +using Kavita.API.Database; +using Kavita.API.Services.Reading; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Services.Reading; -#nullable enable +namespace Kavita.Services.Reading; -public interface IReadingHistoryService +public class ReadingHistoryService(IDataContext context, ILogger logger) + : IReadingHistoryService { - Task AggregateYesterdaysActivity(); -} - -public class ReadingHistoryService : IReadingHistoryService -{ - private readonly DataContext _context; - private readonly ILogger _logger; private sealed record ChapterMetadata(int Id, string? Range, float VolumeNumber, string SeriesName, string? LocalizedSeriesName, string LibraryName, LibraryType LibraryType); private sealed record SeriesMetadata(int Id, string Name, string? LocalizedName, string LibraryName, LibraryType LibraryType); - public ReadingHistoryService(DataContext context, ILogger logger) - { - _context = context; - _logger = logger; - } - - public async Task AggregateYesterdaysActivity() + public async Task AggregateYesterdaysActivity(CancellationToken ct = default) { var yesterdayUtc = DateTime.UtcNow.Date.AddDays(-1); var startUtc = yesterdayUtc; var endUtc = yesterdayUtc.AddDays(1).AddTicks(-1); - var usersToProcess = await GetUsersPendingAggregation(startUtc, endUtc, yesterdayUtc); + var usersToProcess = await GetUsersPendingAggregation(startUtc, endUtc, yesterdayUtc, ct); foreach (var userId in usersToProcess) { - await AggregateUserActivity(userId, startUtc, endUtc, yesterdayUtc); + await AggregateUserActivity(userId, startUtc, endUtc, yesterdayUtc, ct); } - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(ct); } - private async Task> GetUsersPendingAggregation(DateTime start, DateTime end, DateTime reportDate) + private async Task> GetUsersPendingAggregation(DateTime start, DateTime end, DateTime reportDate, CancellationToken ct = default) { - var needAggregationUserIds = await _context.AppUserReadingSession + var needAggregationUserIds = await context.AppUserReadingSession .Where(s => s.StartTime >= start && s.StartTime <= end) .Where(s => !s.IsActive && s.EndTime != null) .Select(s => s.AppUserId) .Distinct() - .ToListAsync(); + .ToListAsync(ct); - var alreadyHasHistoryUserIds = await _context.AppUserReadingHistory + var alreadyHasHistoryUserIds = await context.AppUserReadingHistory .Where(h => h.DateUtc == reportDate) .Select(h => h.AppUserId) - .ToListAsync(); + .ToListAsync(ct); return needAggregationUserIds.Except(alreadyHasHistoryUserIds).ToList(); } - private async Task AggregateUserActivity(int userId, DateTime start, DateTime end, DateTime reportDate) + private async Task AggregateUserActivity(int userId, DateTime start, DateTime end, DateTime reportDate, CancellationToken ct = default) { - var sessions = await _context.AppUserReadingSession + var sessions = await context.AppUserReadingSession .Include(s => s.ActivityData) .Where(s => s.AppUserId == userId && s.StartTime >= start && s.StartTime <= end && !s.IsActive && s.EndTime != null) - .ToListAsync(); + .ToListAsync(ct); if (sessions.Count == 0) return; @@ -80,7 +69,7 @@ public class ReadingHistoryService : IReadingHistoryService var dailyData = CalculateDailyData(sessions, chapterMeta, seriesMeta); - _context.AppUserReadingHistory.Add(new AppUserReadingHistory + context.AppUserReadingHistory.Add(new AppUserReadingHistory { AppUserId = userId, DateUtc = reportDate, @@ -92,7 +81,7 @@ public class ReadingHistoryService : IReadingHistoryService private async Task> GetChapterMetadata(List sessions) { var ids = sessions.SelectMany(s => s.ActivityData.Select(ad => ad.ChapterId)).Distinct().ToList(); - return await _context.Chapter + return await context.Chapter .Where(c => ids.Contains(c.Id)) .Select(c => new ChapterMetadata( c.Id, c.Range, c.Volume.MinNumber, c.Volume.Series.Name, @@ -104,7 +93,7 @@ public class ReadingHistoryService : IReadingHistoryService private async Task> GetSeriesMetadata(List sessions) { var ids = sessions.SelectMany(s => s.ActivityData.Select(ad => ad.SeriesId)).Distinct().ToList(); - return await _context.Series + return await context.Series .Where(s => ids.Contains(s.Id)) .Select(s => new SeriesMetadata(s.Id, s.Name, s.LocalizedName, s.Library.Name, s.Library.Type)) .ToDictionaryAsync(s => s.Id); diff --git a/API/Services/ReadingItemService.cs b/Kavita.Services/Reading/ReadingItemService.cs similarity index 92% rename from API/Services/ReadingItemService.cs rename to Kavita.Services/Reading/ReadingItemService.cs index 543fbaedb..ce7cc0f63 100644 --- a/API/Services/ReadingItemService.cs +++ b/Kavita.Services/Reading/ReadingItemService.cs @@ -1,19 +1,12 @@ using System; -using API.Data.Metadata; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable - -public interface IReadingItemService -{ - int GetNumberOfPages(string filePath, MangaFormat format); - string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); - void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1); - ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata); -} +namespace Kavita.Services.Reading; public class ReadingItemService : IReadingItemService { diff --git a/API/Services/ReadingListService.cs b/Kavita.Services/Reading/ReadingListService.cs similarity index 75% rename from API/Services/ReadingListService.cs rename to Kavita.Services/Reading/ReadingListService.cs index 0ba617b98..b4fd5dbcb 100644 --- a/API/Services/ReadingListService.cs +++ b/Kavita.Services/Reading/ReadingListService.cs @@ -6,91 +6,45 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml.Serialization; -using API.Data; -using API.Data.Repositories; -using API.DTOs.ReadingLists; -using API.DTOs.ReadingLists.CBL; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Reading; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.ReadingLists.CBL; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Models.Helpers; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable - -public interface IReadingListService -{ - Task CreateReadingListForUser(AppUser userWithReadingList, string title); - Task UpdateReadingList(ReadingList readingList, UpdateReadingListDto dto); - Task RemoveFullyReadItems(int readingListId, AppUser user); - Task UpdateReadingListItemPosition(UpdateReadingListPosition dto); - Task DeleteReadingListItem(UpdateReadingListPosition dto); - 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 ValidateCblFile(int userId, CblReadingList cblReading, bool useComicLibraryMatching = false); - Task CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false, bool useComicLibraryMatching = false); - Task CalculateStartAndEndDates(ReadingList readingListWithItems); - /// - /// This is expected to be called from ProcessSeries and has the Full Series present. Will generate on the default admin user. - /// - /// - /// - /// - Task CreateReadingListsFromSeries(Series series, Library library); - - Task CreateReadingListsFromSeries(int libraryId, int seriesId); - Task GenerateReadingListCoverImage(int readingListId); - /// - /// Check, and update if needed, all reading lists' AgeRating who contain the passed series - /// - /// The series whose age rating is being updated - /// The new (uncommited) age rating of the series - /// - /// This method does not commit changes - Task UpdateReadingListAgeRatingForSeries(int seriesId, AgeRating ageRating); - - Task> GetReadingListItems(int readingListId, int userId, UserParams? userParams = null); - Task GetContinueReadingPoint(int readingListId, int userId); -} +namespace Kavita.Services.Reading; /// /// Methods responsible for management of Reading Lists /// /// If called from API layer, expected for to be called beforehand -public class ReadingListService : IReadingListService +public class ReadingListService( + IUnitOfWork unitOfWork, + ILogger logger, + IEventHub eventHub, + IImageService imageService, + IDirectoryService directoryService, + IEntityNamingService namingService) + : IReadingListService { - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IEventHub _eventHub; - private readonly IImageService _imageService; - private readonly IDirectoryService _directoryService; - private readonly IEntityNamingService _namingService; - private static readonly Regex JustNumbers = new Regex(@"^\d+$", RegexOptions.Compiled | RegexOptions.IgnoreCase, Parser.RegexTimeout); - public ReadingListService(IUnitOfWork unitOfWork, ILogger logger, - IEventHub eventHub, IImageService imageService, IDirectoryService directoryService, - IEntityNamingService namingService) - { - _unitOfWork = unitOfWork; - _logger = logger; - _eventHub = eventHub; - _imageService = imageService; - _directoryService = directoryService; - _namingService = namingService; - } - public static string FormatTitle(ReadingListItemDto item) { var title = string.Empty; @@ -166,8 +120,8 @@ public class ReadingListService : IReadingListService public async Task CreateReadingListForUser(AppUser userWithReadingList, string title) { // When creating, we need to make sure Title is unique - // TODO: Perform normalization - var hasExisting = userWithReadingList.ReadingLists.Any(l => l.Title.Equals(title)); + var normalizedTitle = title.ToNormalized(); + var hasExisting = userWithReadingList.ReadingLists.Any(l => l.NormalizedTitle == normalizedTitle); if (hasExisting) { throw new KavitaException("reading-list-name-exists"); @@ -176,8 +130,8 @@ public class ReadingListService : IReadingListService var readingList = new ReadingListBuilder(title).Build(); userWithReadingList.ReadingLists.Add(readingList); - if (!_unitOfWork.HasChanges()) throw new KavitaException("generic-reading-list-create"); - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) throw new KavitaException("generic-reading-list-create"); + await unitOfWork.CommitAsync(); return readingList; } @@ -192,7 +146,7 @@ public class ReadingListService : IReadingListService dto.Title = dto.Title.Trim(); if (string.IsNullOrEmpty(dto.Title)) throw new KavitaException("reading-list-title-required"); - if (!dto.Title.Equals(readingList.Title) && await _unitOfWork.ReadingListRepository.ReadingListExists(dto.Title)) + if (!dto.Title.Equals(readingList.Title) && await unitOfWork.ReadingListRepository.ReadingListExists(dto.Title, readingList.Id)) throw new KavitaException("reading-list-name-exists"); readingList.Summary = dto.Summary; @@ -224,15 +178,15 @@ public class ReadingListService : IReadingListService { readingList.CoverImageLocked = false; readingList.CoverImage = string.Empty; - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, + await eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false); - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); - if (!_unitOfWork.HasChanges()) return; - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return; + await unitOfWork.CommitAsync(); } /// @@ -244,7 +198,7 @@ public class ReadingListService : IReadingListService /// public async Task RemoveFullyReadItems(int readingListId, AppUser user) { - var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, user.Id); + var items = await unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, user.Id); // Collect all Ids to remove var itemIdsToRemove = items.Where(item => item.PagesRead == item.PagesTotal).Select(item => item.Id).ToList(); @@ -253,21 +207,21 @@ public class ReadingListService : IReadingListService try { var listItems = - (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).Where(r => + (await unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).Where(r => itemIdsToRemove.Contains(r.Id)); - _unitOfWork.ReadingListRepository.BulkRemove(listItems); + unitOfWork.ReadingListRepository.BulkRemove(listItems); - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId); if (readingList == null) return true; await CalculateReadingListAgeRating(readingList); await CalculateStartAndEndDates(readingList); - if (!_unitOfWork.HasChanges()) return true; - return await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return true; + return await unitOfWork.CommitAsync(); } catch { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } return false; @@ -280,12 +234,12 @@ public class ReadingListService : IReadingListService /// public async Task UpdateReadingListItemPosition(UpdateReadingListPosition dto) { - var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList(); + var items = (await unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList(); OrderableHelper.ReorderItems(items, dto.ReadingListItemId, dto.ToPosition); - if (!_unitOfWork.HasChanges()) return true; + if (!unitOfWork.HasChanges()) return true; - return await _unitOfWork.CommitAsync(); + return await unitOfWork.CommitAsync(); } /// @@ -295,7 +249,7 @@ public class ReadingListService : IReadingListService /// public async Task DeleteReadingListItem(UpdateReadingListPosition dto) { - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId); if (readingList == null) return false; readingList.Items = readingList.Items.Where(r => r.Id != dto.ReadingListItemId).OrderBy(r => r.Order).ToList(); @@ -309,9 +263,9 @@ public class ReadingListService : IReadingListService await CalculateReadingListAgeRating(readingList); await CalculateStartAndEndDates(readingList); - if (!_unitOfWork.HasChanges()) return true; + if (!unitOfWork.HasChanges()) return true; - return await _unitOfWork.CommitAsync(); + return await unitOfWork.CommitAsync(); } /// @@ -333,13 +287,13 @@ public class ReadingListService : IReadingListService if (readingListWithItems.Items.All(i => i.Chapter == null)) { items = - (await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListWithItems.Id, ReadingListIncludes.ItemChapter))?.Items; + (await unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListWithItems.Id, ReadingListIncludes.ItemChapter))?.Items; } if (items == null || items.Count == 0) return; if (items.First().Chapter == null) { - _logger.LogError("Tried to calculate release dates for Reading List, but missing Chapter entities"); + logger.LogError("Tried to calculate release dates for Reading List, but missing Chapter entities"); return; } var maxReleaseDate = items.Where(item => item.Chapter != null).Max(item => item.Chapter.ReleaseDate); @@ -364,7 +318,7 @@ public class ReadingListService : IReadingListService /// The series ids of all the reading list items private async Task CalculateReadingListAgeRating(ReadingList readingList, IEnumerable seriesIds) { - var ageRating = await _unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(seriesIds); + var ageRating = await unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(seriesIds); readingList.AgeRating = ageRating; } @@ -377,7 +331,7 @@ public class ReadingListService : IReadingListService public async Task UserHasReadingListAccess(int readingListId, string username) { // We need full reading list with items as this is used by many areas that manipulate items - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username, + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(username, AppUserIncludes.ReadingListsWithItems); if (user == null || !await UserHasReadingListAccess(readingListId, user)) { @@ -395,7 +349,7 @@ public class ReadingListService : IReadingListService /// private async Task UserHasReadingListAccess(int readingListId, AppUser user) { - return user.ReadingLists.Any(rl => rl.Id == readingListId) || await _unitOfWork.UserRepository.IsUserAdminAsync(user); + return user.ReadingLists.Any(rl => rl.Id == readingListId) || await unitOfWork.UserRepository.IsUserAdminAsync(user); } /// @@ -406,13 +360,13 @@ public class ReadingListService : IReadingListService /// public async Task DeleteReadingList(int readingListId, AppUser user) { - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId); if (readingList == null) return true; user.ReadingLists.Remove(readingList); - if (!_unitOfWork.HasChanges()) return true; + if (!unitOfWork.HasChanges()) return true; - return await _unitOfWork.CommitAsync(); + return await unitOfWork.CommitAsync(); } /// @@ -432,7 +386,7 @@ public class ReadingListService : IReadingListService } var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet(); - var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes)) + var chaptersForSeries = (await unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes)) .OrderBy(c => c.Volume.MinNumber) .ThenBy(x => x.SortOrder) .ToList(); @@ -457,8 +411,8 @@ public class ReadingListService : IReadingListService /// public async Task CreateReadingListsFromSeries(int libraryId, int seriesId) { - var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + var series = await unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); if (series == null || library == null) return; await CreateReadingListsFromSeries(series, library); @@ -474,8 +428,8 @@ public class ReadingListService : IReadingListService if (!hasReadingListMarkers) return; - _logger.LogInformation("Processing Reading Lists for {SeriesName}", series.Name); - var user = await _unitOfWork.UserRepository.GetDefaultAdminUser(); + logger.LogInformation("Processing Reading Lists for {SeriesName}", series.Name); + var user = await unitOfWork.UserRepository.GetDefaultAdminUser(); series.Metadata ??= new SeriesMetadataBuilder().Build(); foreach (var chapter in series.Volumes.SelectMany(v => v.Chapters)) @@ -492,13 +446,13 @@ public class ReadingListService : IReadingListService foreach (var arcPair in pairs) { - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByTitleAsync(arcPair.Item1, user.Id); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListByTitleAsync(arcPair.Item1, user.Id); if (readingList == null) { readingList = new ReadingListBuilder(arcPair.Item1) .WithAppUserId(user.Id) .Build(); - _unitOfWork.ReadingListRepository.Add(readingList); + unitOfWork.ReadingListRepository.Add(readingList); } @@ -518,7 +472,7 @@ public class ReadingListService : IReadingListService { if (order == int.MaxValue) { - _logger.LogWarning("{Filename} has a missing StoryArcNumber/AlternativeNumber but list already exists with this item. Skipping item", chapter.Files.FirstOrDefault()?.FilePath); + logger.LogWarning("{Filename} has a missing StoryArcNumber/AlternativeNumber but list already exists with this item. Skipping item", chapter.Files.FirstOrDefault()?.FilePath); } else { @@ -528,17 +482,17 @@ public class ReadingListService : IReadingListService readingList.Items = items; - if (!_unitOfWork.HasChanges()) continue; + if (!unitOfWork.HasChanges()) continue; - _imageService.UpdateColorScape(readingList); + imageService.UpdateColorScape(readingList); await CalculateReadingListAgeRating(readingList); - await _unitOfWork.CommitAsync(); // TODO: See if we can avoid this extra commit by reworking bottom logic + await unitOfWork.CommitAsync(); // TODO: See if we can avoid this extra commit by reworking bottom logic - await CalculateStartAndEndDates(await _unitOfWork.ReadingListRepository.GetReadingListByTitleAsync(arcPair.Item1, - user.Id, ReadingListIncludes.Items | ReadingListIncludes.ItemChapter)); - await _unitOfWork.CommitAsync(); + await CalculateStartAndEndDates((await unitOfWork.ReadingListRepository.GetReadingListByTitleAsync( + arcPair.Item1, user.Id, ReadingListIncludes.Items | ReadingListIncludes.ItemChapter))!); + await unitOfWork.CommitAsync(); } } } @@ -552,7 +506,7 @@ public class ReadingListService : IReadingListService var arcNumbers = storyArcNumbers.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (arcNumbers.Count(s => !string.IsNullOrEmpty(s)) != arcs.Length) { - _logger.LogWarning("There is a mismatch on StoryArc and StoryArcNumber for {FileName}", filename); + logger.LogWarning("There is a mismatch on StoryArc and StoryArcNumber for {FileName}", filename); } var maxPairs = Math.Max(arcs.Length, arcNumbers.Length); @@ -590,7 +544,7 @@ public class ReadingListService : IReadingListService if (IsCblEmpty(cblReading, importSummary, out var readingListFromCbl)) return readingListFromCbl; // Is there another reading list with the same name on the user's account? - if (await _unitOfWork.ReadingListRepository.ReadingListExistsForUser(cblReading.Name, userId)) + if (await unitOfWork.ReadingListRepository.ReadingListExistsForUser(cblReading.Name, userId)) { importSummary.Success = CblImportResult.Fail; importSummary.Results.Add(new CblBookResult @@ -603,7 +557,7 @@ public class ReadingListService : IReadingListService var uniqueSeries = GetUniqueSeries(cblReading, useComicLibraryMatching); var userSeries = - (await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList(); + (await unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList(); if (userSeries.Count == 0) { @@ -655,8 +609,8 @@ public class ReadingListService : IReadingListService /// public async Task CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false, bool useComicLibraryMatching = false) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.ReadingListsWithItems); - _logger.LogDebug("Importing {ReadingListName} CBL for User {UserName}", cblReading.Name, user!.UserName); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.ReadingListsWithItems); + logger.LogDebug("Importing {ReadingListName} CBL for User {UserName}", cblReading.Name, user!.UserName); var importSummary = new CblImportSummaryDto { CblName = cblReading.Name, @@ -667,7 +621,7 @@ public class ReadingListService : IReadingListService var uniqueSeries = GetUniqueSeries(cblReading, useComicLibraryMatching); var userSeries = - (await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList(); + (await unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList(); var allSeries = userSeries.ToDictionary(s => s.NormalizedName); var allSeriesLocalized = userSeries.ToDictionary(s => s.NormalizedLocalizedName); @@ -777,11 +731,11 @@ public class ReadingListService : IReadingListService } // If there are no items, don't create a blank list - if (!_unitOfWork.HasChanges() || readingList.Items.Count == 0) return importSummary; + if (!unitOfWork.HasChanges() || readingList.Items.Count == 0) return importSummary; - _imageService.UpdateColorScape(readingList); - await _unitOfWork.CommitAsync(); + imageService.UpdateColorScape(readingList); + await unitOfWork.CommitAsync(); return importSummary; @@ -845,31 +799,31 @@ public class ReadingListService : IReadingListService // // } - var covers = await _unitOfWork.ReadingListRepository.GetRandomCoverImagesAsync(readingListId); - var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, + var covers = await unitOfWork.ReadingListRepository.GetRandomCoverImagesAsync(readingListId); + var destFile = directoryService.FileSystem.Path.Join(directoryService.TempDirectory, ImageService.GetReadingListFormat(readingListId)); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); destFile += settings.EncodeMediaAs.GetExtension(); - if (_directoryService.FileSystem.File.Exists(destFile)) return destFile; + if (directoryService.FileSystem.File.Exists(destFile)) return destFile; ImageService.CreateMergedImage( - covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(), + covers.Select(c => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, c)).ToList(), settings.CoverImageSize, destFile); // TODO: Refactor this so that reading lists have a dedicated cover image so we can calculate primary/secondary colors - return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; + return !directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; } public async Task UpdateReadingListAgeRatingForSeries(int seriesId, AgeRating ageRating) { - var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListsBySeriesId(seriesId); + var readingLists = await unitOfWork.ReadingListRepository.GetReadingListsBySeriesId(seriesId); foreach (var readingList in readingLists) { var seriesIds = readingList.Items.Select(item => item.SeriesId).ToList(); seriesIds.Remove(seriesId); // Don't get AgeRating from database - var maxAgeRating = await _unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(seriesIds); + var maxAgeRating = await unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(seriesIds); if (ageRating > maxAgeRating) { maxAgeRating = ageRating; @@ -881,12 +835,12 @@ public class ReadingListService : IReadingListService public async Task> GetReadingListItems(int readingListId, int userId, UserParams? userParams = null) { - var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId, userParams); + var items = await unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId, userParams); // Add the title foreach (var item in items) { - item.Title = _namingService.FormatReadingListItemTitle(item); + item.Title = namingService.FormatReadingListItemTitle(item); } return items; @@ -894,8 +848,8 @@ public class ReadingListService : IReadingListService public async Task GetContinueReadingPoint(int readingListId, int userId) { - var item = await _unitOfWork.ReadingListRepository.GetContinueReadingPoint(readingListId, userId); - item?.Title = _namingService.FormatReadingListItemTitle(item); + var item = await unitOfWork.ReadingListRepository.GetContinueReadingPoint(readingListId, userId); + item?.Title = namingService.FormatReadingListItemTitle(item); return item; } diff --git a/API/Services/ReadingProfileService.cs b/Kavita.Services/Reading/ReadingProfileService.cs similarity index 74% rename from API/Services/ReadingProfileService.cs rename to Kavita.Services/Reading/ReadingProfileService.cs index 6726dbfb4..09ae8b597 100644 --- a/API/Services/ReadingProfileService.cs +++ b/Kavita.Services/Reading/ReadingProfileService.cs @@ -2,171 +2,21 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Reading; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Services; -#nullable enable - -public interface IReadingProfileService -{ - /// - /// Returns the ReadingProfile that should be applied to the given series, walks up the tree. - /// Series (Implicit) -> Series (User) -> Library (User) -> Default - /// - /// - /// - /// - /// - /// - /// - Task GetReadingProfileDtoForSeries(int userId, int libraryId, int seriesId, int? activeDeviceId, bool skipImplicit = false); - - /// - /// Creates a new reading profile for a user. Name must be unique per user - /// - /// - /// - /// - Task CreateReadingProfile(int userId, UserReadingProfileDto dto); - /// - /// Given an implicit profile, promotes it to a profile of kind , then removes - /// all links to the series this implicit profile was created for from other reading profiles (if the device id matches - /// if given) - /// - /// - /// - /// - /// - Task PromoteImplicitProfile(int userId, int profileId, int? activeDeviceId); - - /// - /// Updates the implicit reading profile for a series, creates one if none exists - /// - /// - /// - /// - /// - /// - /// - Task UpdateImplicitReadingProfile(int userId, int libraryId, int seriesId, UserReadingProfileDto dto, int? activeDeviceId); - - /// - /// Updates the non-implicit reading profile for the given series, and removes implicit profiles - /// - /// - /// - /// - /// - /// - /// - Task UpdateParent(int userId, int libraryId, int seriesId, UserReadingProfileDto dto, int? activeDeviceId); - - /// - /// Updates a given reading profile for a user - /// - /// - /// - /// - /// Does not update connected series and libraries - Task UpdateReadingProfile(int userId, UserReadingProfileDto dto); - - /// - /// Deletes a given profile for a user - /// - /// - /// - /// - /// - /// The default profile for the user cannot be deleted - Task DeleteReadingProfile(int userId, int profileId); - - /// - /// Binds the reading profile to the series, and remove the implicit RP from the series if it exists - /// - /// - /// - /// - /// - Task SetSeriesProfiles(int userId, List profileIds, int seriesId); - - /// - /// Binds the reading profile to many series, and remove the implicit RP from the series if it exists - /// - /// - /// - /// - /// - Task BulkSetSeriesProfiles(int userId, List profileIds, List seriesIds); - - /// - /// Remove all reading profiles bound to the series - /// - /// - /// - /// - Task ClearSeriesProfile(int userId, int seriesId); - - /// - /// Bind the reading profile to the library - /// - /// - /// - /// - /// - Task SetLibraryProfiles(int userId, List profileIds, int libraryId); - - /// - /// Remove the reading profile bound to the library, if it exists - /// - /// - /// - /// - Task ClearLibraryProfile(int userId, int libraryId); - - /// - /// Returns the all bound Reading Profile to a Library - /// - /// - /// - /// - Task> GetReadingProfileDtosForLibrary(int userId, int libraryId); - - /// - /// Returns the all bound Reading Profile to a Series - /// - /// - /// - /// - Task> GetReadingProfileDtosForSeries(int userId, int seriesId); - - /// - /// Set the assigned devices for the given reading profile. Then removes all duplicate links, ensuring each series - /// and library only has one profile per device - /// - /// - /// - /// - /// - Task SetProfileDevices(int userId, int profileId, List deviceIds); - - /// - /// Remove device ids from all profiles, does **NOT** commit - /// - /// - /// - /// - Task RemoveDeviceLinks(int userId, int deviceId); -} +namespace Kavita.Services.Reading; public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper): IReadingProfileService { diff --git a/API/Services/Reading/ReadingSessionService.cs b/Kavita.Services/Reading/ReadingSessionService.cs similarity index 96% rename from API/Services/Reading/ReadingSessionService.cs rename to Kavita.Services/Reading/ReadingSessionService.cs index b921b8658..d257b74de 100644 --- a/API/Services/Reading/ReadingSessionService.cs +++ b/Kavita.Services/Reading/ReadingSessionService.cs @@ -4,25 +4,21 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Progress; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace API.Services.Reading; - -#nullable enable - -public interface IReadingSessionService -{ - Task UpdateProgress(int userId, ProgressDto progressDto, ClientInfoData? clientInfo, int? deviceId); -} +namespace Kavita.Services.Reading; public sealed class ReadingSessionService : IReadingSessionService, IDisposable, IAsyncDisposable { @@ -74,7 +70,7 @@ public sealed class ReadingSessionService : IReadingSessionService, IDisposable, _logger.LogDebug("Updating Reading Session for {UserId} on {ChapterId}", userId, progressDto.ChapterId); using var scope = _serviceScopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); + var context = scope.ServiceProvider.GetRequiredService(); var eventHub = scope.ServiceProvider.GetRequiredService(); var session = await GetOrCreateSessionAsync(userId, progressDto, context); @@ -95,7 +91,7 @@ public sealed class ReadingSessionService : IReadingSessionService, IDisposable, } } - private async Task GetOrCreateSessionAsync(int userId, ProgressDto dto, DataContext context) + private async Task GetOrCreateSessionAsync(int userId, ProgressDto dto, IDataContext context) { var cutoffUtc = DateTime.UtcNow - _sessionTimeout; var midnightToday = DateTime.Today; @@ -133,7 +129,7 @@ public sealed class ReadingSessionService : IReadingSessionService, IDisposable, } private async Task UpdateActivityDataAsync(AppUserReadingSession session, ProgressDto progressDto, ClientInfoData? clientInfo, - int? deviceId, IServiceScope scope, DataContext context) + int? deviceId, IServiceScope scope, IDataContext context) { var cutoffUtc = DateTime.UtcNow - _sessionTimeout; @@ -254,7 +250,7 @@ public sealed class ReadingSessionService : IReadingSessionService, IDisposable, var midnightToday = DateTime.Today; using var scope = _serviceScopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); + var context = scope.ServiceProvider.GetRequiredService(); var eventHub = scope.ServiceProvider.GetRequiredService(); var expiredSessions = await context.AppUserReadingSession @@ -349,7 +345,7 @@ public sealed class ReadingSessionService : IReadingSessionService, IDisposable, return completedChapterIds; } - private async Task GetChapterFormatAsync(int chapterId, DataContext context) + private async Task GetChapterFormatAsync(int chapterId, IDataContext context) { var cacheKey = GetChapterFormatCacheKey(chapterId); diff --git a/Kavita.Services/Repositories/CoverDbRepository.cs b/Kavita.Services/Repositories/CoverDbRepository.cs new file mode 100644 index 000000000..a7b436565 --- /dev/null +++ b/Kavita.Services/Repositories/CoverDbRepository.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Kavita.Models.DTOs.CoverDb; +using Kavita.Models.Entities.Person; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Kavita.Services.Repositories; + +/// +/// This is a manual repository, not a DB repo +/// +public class CoverDbRepository +{ + private readonly List _authors; + + public CoverDbRepository(string filePath) + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + // Read and deserialize YAML file + var yamlContent = File.ReadAllText(filePath); + var peopleData = deserializer.Deserialize(yamlContent); + _authors = peopleData.People; + } + + public CoverDbAuthor? FindAuthorByNameOrAlias(string name) + { + return _authors.Find(author => + author.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || + author.Aliases.Contains(name, StringComparer.OrdinalIgnoreCase)); + } + + public CoverDbAuthor? FindBestAuthorMatch(Person person) + { + var aniListId = person.AniListId > 0 ? $"{person.AniListId}" : string.Empty; + var highestScore = 0; + CoverDbAuthor? bestMatch = null; + + foreach (var author in _authors) + { + var score = 0; + + // Check metadata IDs and add points if they match + if (!string.IsNullOrEmpty(author.Ids.AmazonId) && author.Ids.AmazonId == person.Asin) + { + score += 10; + } + if (!string.IsNullOrEmpty(author.Ids.AnilistId) && author.Ids.AnilistId == aniListId) + { + score += 10; + } + if (!string.IsNullOrEmpty(author.Ids.HardcoverId) && author.Ids.HardcoverId == person.HardcoverId) + { + score += 10; + } + + // Check for exact name match + if (author.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase)) + { + score += 7; + } + + // Check for alias match + if (author.Aliases.Contains(person.Name, StringComparer.OrdinalIgnoreCase)) + { + score += 5; + } + + // Update the best match if current score is higher + if (score <= highestScore) continue; + + highestScore = score; + bestMatch = author; + } + + return bestMatch; + } + +} diff --git a/API/Services/Tasks/Scanner/Parser/BasicParser.cs b/Kavita.Services/Scanner/BasicParser.cs similarity index 67% rename from API/Services/Tasks/Scanner/Parser/BasicParser.cs rename to Kavita.Services/Scanner/BasicParser.cs index 11cb51bcd..c747c1b67 100644 --- a/API/Services/Tasks/Scanner/Parser/BasicParser.cs +++ b/Kavita.Services/Scanner/BasicParser.cs @@ -1,10 +1,11 @@ using System; using System.IO; -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; -namespace API.Services.Tasks.Scanner.Parser; -#nullable enable +namespace Kavita.Services.Scanner; /// /// This is the basic parser for handling Manga/Comic/Book libraries. This was previously DefaultParser before splitting each parser @@ -16,9 +17,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 && Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null; + if (type != LibraryType.Image && Scanner.Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null; - if (Parser.IsImage(filePath)) + if (Scanner.Parser.IsImage(filePath)) { return imageParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Image, enableMetadata, comicInfo); } @@ -26,44 +27,44 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag var ret = new ParserInfo() { Filename = Path.GetFileName(filePath), - Format = Parser.ParseFormat(filePath), - Title = Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Parser.NormalizePath(filePath), - Series = Parser.ParseSeries(fileName, type), + Format = Scanner.Parser.ParseFormat(filePath), + Title = Scanner.Parser.RemoveExtensionIfSupported(fileName)!, + FullFilePath = Scanner.Parser.NormalizePath(filePath), + Series = Scanner.Parser.ParseSeries(fileName, type), ComicInfo = comicInfo, - Chapters = Parser.ParseChapter(fileName, type), - Volumes = Parser.ParseVolume(fileName, type), + Chapters = Scanner.Parser.ParseChapter(fileName, type), + Volumes = Scanner.Parser.ParseVolume(fileName, type), }; - if (ret.Series == string.Empty || Parser.IsImage(filePath)) + if (ret.Series == string.Empty || Scanner.Parser.IsImage(filePath)) { // Try to parse information out of each folder all the way to rootPath ParseFromFallbackFolders(filePath, rootPath, type, ref ret); } - var edition = Parser.ParseEdition(fileName); + var edition = Scanner.Parser.ParseEdition(fileName); if (!string.IsNullOrEmpty(edition)) { - ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); + ret.Series = Scanner.Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); ret.Edition = edition; } - var isSpecial = Parser.IsSpecial(fileName, type); + var isSpecial = Scanner.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 (Parser.IsDefaultChapter(ret.Chapters) && Parser.IsLooseLeafVolume(ret.Volumes) && isSpecial) + if (Scanner.Parser.IsDefaultChapter(ret.Chapters) && Scanner.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 (Parser.HasSpecialMarker(fileName)) + if (Scanner.Parser.HasSpecialMarker(fileName)) { ret.IsSpecial = true; - ret.SpecialIndex = Parser.ParseSpecialIndex(fileName); - ret.Chapters = Parser.DefaultChapter; - ret.Volumes = Parser.SpecialVolume; + ret.SpecialIndex = Scanner.Parser.ParseSpecialIndex(fileName); + ret.Chapters = Scanner.Parser.DefaultChapter; + ret.Volumes = Scanner.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/ @@ -80,22 +81,22 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag (fileDirectory.EndsWith("Specials", StringComparison.OrdinalIgnoreCase) || fileDirectory.EndsWith("Specials/", StringComparison.OrdinalIgnoreCase))) { - ret.Series = Parser.CleanTitle(Directory.GetParent(fileDirectory)?.Name ?? string.Empty); + ret.Series = Scanner.Parser.CleanTitle(Directory.GetParent(fileDirectory)?.Name ?? string.Empty); } else { ParseFromFallbackFolders(filePath, tempRootPath, type, ref ret); } - ret.Title = Parser.CleanSpecialTitle(fileName); + ret.Title = Scanner.Parser.CleanSpecialTitle(fileName); } if (string.IsNullOrEmpty(ret.Series)) { - ret.Series = Parser.CleanTitle(fileName, type is LibraryType.Comic); + ret.Series = Scanner.Parser.CleanTitle(fileName, type is LibraryType.Comic); } // Pdfs may have .pdf in the series name, remove that - if (Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) + if (Scanner.Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) { ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length); } @@ -108,7 +109,7 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag - if (Parser.IsLooseLeafVolume(ret.Volumes) && Parser.IsDefaultChapter(ret.Chapters)) + if (Scanner.Parser.IsLooseLeafVolume(ret.Volumes) && Scanner.Parser.IsDefaultChapter(ret.Chapters)) { ret.IsSpecial = true; } @@ -116,7 +117,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 = Parser.SpecialVolume; + ret.Volumes = Scanner.Parser.SpecialVolume; } return ret.Series == string.Empty ? null : ret; diff --git a/API/Services/Tasks/Scanner/Parser/BookParser.cs b/Kavita.Services/Scanner/BookParser.cs similarity index 65% rename from API/Services/Tasks/Scanner/Parser/BookParser.cs rename to Kavita.Services/Scanner/BookParser.cs index 89b142faa..edc9344fb 100644 --- a/API/Services/Tasks/Scanner/Parser/BookParser.cs +++ b/Kavita.Services/Scanner/BookParser.cs @@ -1,8 +1,11 @@ using System.IO; -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using Kavita.Services.Extensions; -namespace API.Services.Tasks.Scanner.Parser; +namespace Kavita.Services.Scanner; public class BookParser(IDirectoryService directoryService, IBookService bookService, BasicParser basicParser) : DefaultParser(directoryService) { @@ -21,11 +24,11 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer { Filename = Path.GetFileName(filePath), Format = MangaFormat.Epub, - Title = Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Parser.NormalizePath(filePath), - Series = Parser.ParseSeries(fileName, type), - Chapters = Parser.ParseChapter(fileName, type), - Volumes = Parser.ParseVolume(fileName, type), + 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), }; } @@ -38,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 (!Parser.IsLooseLeafVolume(Parser.ParseVolume(info.Series, type))) + if (!Scanner.Parser.IsLooseLeafVolume(Scanner.Parser.ParseVolume(info.Series, type))) { - var parsedVolumeFromTitle = Parser.ParseVolume(info.Title, type); - var parsedVolumeFromSeries = Parser.ParseVolume(info.Series, type); + var parsedVolumeFromTitle = Scanner.Parser.ParseVolume(info.Title, type); + var parsedVolumeFromSeries = Scanner.Parser.ParseVolume(info.Series, type); - var hasVolumeInTitle = !Parser.IsLooseLeafVolume(parsedVolumeFromTitle); - var hasVolumeInSeries = !Parser.IsLooseLeafVolume(parsedVolumeFromSeries); + var hasVolumeInTitle = !Scanner.Parser.IsLooseLeafVolume(parsedVolumeFromTitle); + var hasVolumeInSeries = !Scanner.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 = Parser.ParseSeries(info.Title, type); + info.Series = Scanner.Parser.ParseSeries(info.Title, type); info.Volumes = parsedVolumeFromTitle; } else @@ -58,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 && Parser.IsLooseLeafVolume(Parser.ParseVolume(info2.Series, type))) + if (hasVolumeInSeries && info2 != null && Scanner.Parser.IsLooseLeafVolume(Scanner.Parser.ParseVolume(info2.Series, type))) { // Override the Series name so it groups appropriately info.Series = info2.Series; @@ -77,6 +80,6 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer /// public override bool IsApplicable(string filePath, LibraryType type) { - return Parser.IsEpub(filePath); + return Scanner.Parser.IsEpub(filePath); } } diff --git a/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs b/Kavita.Services/Scanner/ComicVineParser.cs similarity index 73% rename from API/Services/Tasks/Scanner/Parser/ComicVineParser.cs rename to Kavita.Services/Scanner/ComicVineParser.cs index 4b0878504..a10ad7363 100644 --- a/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs +++ b/Kavita.Services/Scanner/ComicVineParser.cs @@ -1,9 +1,11 @@ using System.IO; using System.Linq; -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; -namespace API.Services.Tasks.Scanner.Parser; +namespace Kavita.Services.Scanner; #nullable enable /// @@ -25,20 +27,20 @@ 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) || Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null; + if (string.IsNullOrEmpty(fileName) || Scanner.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 = Parser.ParseFormat(filePath), - Title = Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Parser.NormalizePath(filePath), + Format = Scanner.Parser.ParseFormat(filePath), + Title = Scanner.Parser.RemoveExtensionIfSupported(fileName)!, + FullFilePath = Scanner.Parser.NormalizePath(filePath), Series = string.Empty, ComicInfo = comicInfo, - Chapters = Parser.ParseChapter(fileName, type), - Volumes = Parser.ParseVolume(fileName, type) + Chapters = Scanner.Parser.ParseChapter(fileName, type), + Volumes = Scanner.Parser.ParseVolume(fileName, type) }; // See if we can formulate the name from the ComicInfo @@ -55,30 +57,30 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser { foreach (var directory in directories) { - if (!Parser.IsSeriesAndYear(directory)) continue; + if (!Scanner.Parser.IsSeriesAndYear(directory)) continue; info.Series = directory; - info.Volumes = Parser.ParseYear(directory); + info.Volumes = Scanner.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 = Parser.CleanTitle(directories[0], true); + info.Series = Scanner.Parser.CleanTitle(directories[0], true); } } else { - if (Parser.IsSeriesAndYear(directoryName)) + if (Scanner.Parser.IsSeriesAndYear(directoryName)) { info.Series = directoryName; - info.Volumes = Parser.ParseYear(directoryName); + info.Volumes = Scanner.Parser.ParseYear(directoryName); } } } // Check if this is a Special/Annual - info.IsSpecial = Parser.IsSpecial(info.Filename, type) || Parser.IsSpecial(info.ComicInfo?.Format, type); + info.IsSpecial = Scanner.Parser.IsSpecial(info.Filename, type) || Scanner.Parser.IsSpecial(info.ComicInfo?.Format, type); // Patch in other information from ComicInfo if (enableMetadata) @@ -88,7 +90,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser if (string.IsNullOrEmpty(info.Series)) { - info.Series = Parser.CleanTitle(directoryName, true); + info.Series = Scanner.Parser.CleanTitle(directoryName, true); } @@ -121,10 +123,10 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser if (!string.IsNullOrEmpty(info.ComicInfo.Number)) { info.Chapters = info.ComicInfo.Number; - if (info.IsSpecial && !Parser.IsDefaultChapter(info.Chapters)) + if (info.IsSpecial && !Scanner.Parser.IsDefaultChapter(info.Chapters)) { info.IsSpecial = false; - info.Volumes = $"{Parser.SpecialVolumeNumber}"; + info.Volumes = $"{Scanner.Parser.SpecialVolumeNumber}"; } } diff --git a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs b/Kavita.Services/Scanner/DefaultParser.cs similarity index 75% rename from API/Services/Tasks/Scanner/Parser/DefaultParser.cs rename to Kavita.Services/Scanner/DefaultParser.cs index 20b48271c..0d3a1c7bd 100644 --- a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs +++ b/Kavita.Services/Scanner/DefaultParser.cs @@ -1,9 +1,10 @@ using System.Linq; -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; -namespace API.Services.Tasks.Scanner.Parser; -#nullable enable +namespace Kavita.Services.Scanner; public interface IDefaultParser { @@ -39,17 +40,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 => !Parser.IsSpecial(f, type)) + .Where(f => !Scanner.Parser.IsSpecial(f, type)) .ToList(); if (fallbackFolders.Count == 0) { var rootFolderName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name; - var series = Parser.ParseSeries(rootFolderName, type); + var series = Scanner.Parser.ParseSeries(rootFolderName, type); if (string.IsNullOrEmpty(series)) { - ret.Series = Parser.CleanTitle(rootFolderName, type is LibraryType.Comic); + ret.Series = Scanner.Parser.CleanTitle(rootFolderName, type is LibraryType.Comic); return; } @@ -64,18 +65,18 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau { var folder = fallbackFolders[i]; - var parsedVolume = Parser.ParseVolume(folder, type); - var parsedChapter = Parser.ParseChapter(folder, type); + var parsedVolume = Scanner.Parser.ParseVolume(folder, type); + var parsedChapter = Scanner.Parser.ParseChapter(folder, type); - var isLooseLeafVolume = Parser.IsLooseLeafVolume(parsedVolume); - var isDefaultChapter = Parser.IsDefaultChapter(parsedChapter); + var isLooseLeafVolume = Scanner.Parser.IsLooseLeafVolume(parsedVolume); + var isDefaultChapter = Scanner.Parser.IsDefaultChapter(parsedChapter); - if ((string.IsNullOrEmpty(ret.Volumes) || Parser.IsLooseLeafVolume(ret.Volumes)) + if ((string.IsNullOrEmpty(ret.Volumes) || Scanner.Parser.IsLooseLeafVolume(ret.Volumes)) && !string.IsNullOrEmpty(parsedVolume) && !isLooseLeafVolume) { ret.Volumes = parsedVolume; } - if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters.Equals(Parser.DefaultChapter)) + if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters.Equals(Scanner.Parser.DefaultChapter)) && !string.IsNullOrEmpty(parsedChapter) && !isDefaultChapter) { ret.Chapters = parsedChapter; @@ -84,11 +85,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 = Parser.ParseSeries(folder, type); + var series = Scanner.Parser.ParseSeries(folder, type); if (string.IsNullOrEmpty(series)) { - ret.Series = Parser.CleanTitle(folder, type is LibraryType.Comic); + ret.Series = Scanner.Parser.CleanTitle(folder, type is LibraryType.Comic); break; } @@ -122,11 +123,11 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim(); } - if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Parser.HasComicInfoSpecial(info.ComicInfo.Format)) + if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Scanner.Parser.HasComicInfoSpecial(info.ComicInfo.Format)) { info.IsSpecial = true; - info.Chapters = Parser.DefaultChapter; - info.Volumes = Parser.SpecialVolume; + info.Chapters = Scanner.Parser.DefaultChapter; + info.Volumes = Scanner.Parser.SpecialVolume; } // Patch is SeriesSort from ComicInfo @@ -141,7 +142,7 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau protected static bool IsEmptyOrDefault(string volumes, string chapters) { - return (string.IsNullOrEmpty(chapters) || Parser.IsDefaultChapter(chapters)) && - (string.IsNullOrEmpty(volumes) || Parser.IsLooseLeafVolume(volumes)); + return (string.IsNullOrEmpty(chapters) || Scanner.Parser.IsDefaultChapter(chapters)) && + (string.IsNullOrEmpty(volumes) || Scanner.Parser.IsLooseLeafVolume(volumes)); } } diff --git a/API/Services/Tasks/Scanner/Parser/ImageParser.cs b/Kavita.Services/Scanner/ImageParser.cs similarity index 74% rename from API/Services/Tasks/Scanner/Parser/ImageParser.cs rename to Kavita.Services/Scanner/ImageParser.cs index 12f9f4d50..ff92749bf 100644 --- a/API/Services/Tasks/Scanner/Parser/ImageParser.cs +++ b/Kavita.Services/Scanner/ImageParser.cs @@ -1,8 +1,10 @@ using System.IO; -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; -namespace API.Services.Tasks.Scanner.Parser; +namespace Kavita.Services.Scanner; #nullable enable public class ImageParser(IDirectoryService directoryService) : DefaultParser(directoryService) @@ -16,12 +18,12 @@ public class ImageParser(IDirectoryService directoryService) : DefaultParser(dir var ret = new ParserInfo { Series = directoryName, - Volumes = Parser.LooseLeafVolume, - Chapters = Parser.DefaultChapter, + Volumes = Scanner.Parser.LooseLeafVolume, + Chapters = Scanner.Parser.DefaultChapter, ComicInfo = comicInfo, Format = MangaFormat.Image, Filename = Path.GetFileName(filePath), - FullFilePath = Parser.NormalizePath(filePath), + FullFilePath = Scanner.Parser.NormalizePath(filePath), Title = fileName, }; ParseFromFallbackFolders(filePath, libraryRoot, LibraryType.Image, ref ret); @@ -29,13 +31,13 @@ public class ImageParser(IDirectoryService directoryService) : DefaultParser(dir if (IsEmptyOrDefault(ret.Volumes, ret.Chapters)) { ret.IsSpecial = true; - ret.Volumes = Parser.SpecialVolume; + ret.Volumes = Scanner.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 = Parser.CleanTitle(directoryName); + ret.Series = Scanner.Parser.CleanTitle(directoryName); } @@ -50,6 +52,6 @@ public class ImageParser(IDirectoryService directoryService) : DefaultParser(dir /// public override bool IsApplicable(string filePath, LibraryType type) { - return type == LibraryType.Image && Parser.IsImage(filePath); + return type == LibraryType.Image && Scanner.Parser.IsImage(filePath); } } diff --git a/API/Services/Tasks/Scanner/LibraryWatcher.cs b/Kavita.Services/Scanner/LibraryWatcher.cs similarity index 93% rename from API/Services/Tasks/Scanner/LibraryWatcher.cs rename to Kavita.Services/Scanner/LibraryWatcher.cs index fec0304a8..e8146b134 100644 --- a/API/Services/Tasks/Scanner/LibraryWatcher.cs +++ b/Kavita.Services/Scanner/LibraryWatcher.cs @@ -1,35 +1,18 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Entities.Enums; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; +using Kavita.Models.Entities.Enums; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace API.Services.Tasks.Scanner; -#nullable enable - -public interface ILibraryWatcher -{ - /// - /// Start watching all library folders - /// - /// - Task StartWatching(); - /// - /// Stop watching all folders - /// - void StopWatching(); - /// - /// Essentially stops then starts watching. Useful if there is a change in folders or libraries - /// - /// - Task RestartWatching(); -} +namespace Kavita.Services.Scanner; /// /// Responsible for watching the file system and processing change events. This is mainly responsible for invoking @@ -91,7 +74,7 @@ public class LibraryWatcher : ILibraryWatcher .Where(l => l.FolderWatching) .SelectMany(l => l.Folders) .Distinct() - .Select(Parser.Parser.NormalizePath) + .Select(Parser.NormalizePath) .Where(_directoryService.Exists) .ToList(); @@ -254,14 +237,14 @@ public class LibraryWatcher : ILibraryWatcher try { // If the change occurs in a blacklisted folder path, then abort processing - if (Parser.Parser.HasBlacklistedFolderInPath(filePath)) + if (Parser.HasBlacklistedFolderInPath(filePath)) { return; } // If not a directory change AND file is not an archive or book, ignore if (!isDirectoryChange && - !(Parser.Parser.IsArchive(filePath) || Parser.Parser.IsBook(filePath))) + !(Parser.IsArchive(filePath) || Parser.IsBook(filePath))) { _logger.LogTrace("[LibraryWatcher] Change from {FilePath} is not an archive or book, ignoring change", filePath); return; @@ -270,7 +253,7 @@ public class LibraryWatcher : ILibraryWatcher var libraryFolders = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync()) .SelectMany(l => l.Folders) .Distinct() - .Select(Parser.Parser.NormalizePath) + .Select(Parser.NormalizePath) .Where(_directoryService.Exists) .ToList(); @@ -310,7 +293,7 @@ public class LibraryWatcher : ILibraryWatcher if (rootFolder.Count == 0) return string.Empty; // Select the first folder and join with library folder, this should give us the folder to scan. - return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[^1])); + return Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[^1])); } diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/Kavita.Services/Scanner/ParseScannedFiles.cs similarity index 92% rename from API/Services/Tasks/Scanner/ParseScannedFiles.cs rename to Kavita.Services/Scanner/ParseScannedFiles.cs index f846831e6..7a70914fc 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/Kavita.Services/Scanner/ParseScannedFiles.cs @@ -1,93 +1,22 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Extensions; using Microsoft.Extensions.Logging; -namespace API.Services.Tasks.Scanner; -#nullable enable - -public class ParsedSeries -{ - /// - /// Name of the Series - /// - public required string Name { get; init; } - /// - /// Normalized Name of the Series - /// - public required string NormalizedName { get; init; } - /// - /// Format of the Series - /// - public required MangaFormat Format { get; init; } - /// - /// Has this Series changed or not aka do we need to process it or not. - /// - public bool HasChanged { get; set; } -} - -public class ScanResult -{ - /// - /// A list of files in the Folder. Empty if HasChanged = false - /// - public IList Files { get; set; } - /// - /// A nested folder from Library Root (at any level) - /// - public string Folder { get; set; } - /// - /// The library root - /// - public string LibraryRoot { get; set; } - /// - /// Was the Folder scanned or not. If not modified since last scan, this will be false and Files empty - /// - public bool HasChanged { get; set; } - /// - /// Set in Stage 2: Parsed Info from the Files - /// - public IList ParserInfos { get; set; } -} - -/// -/// The final product of ParseScannedFiles. This has all the processed parserInfo and is ready for tracking/processing into entities -/// -public class ScannedSeriesResult -{ - /// - /// Was the Folder scanned or not. If not modified since last scan, this will be false and indicates that upstream should count this as skipped - /// - public bool HasChanged { get; set; } - /// - /// The Parsed Series information used for tracking - /// - public ParsedSeries ParsedSeries { get; set; } - /// - /// Parsed files - /// - public IList ParsedInfos { get; set; } -} - -public class SeriesModified -{ - public required string? FolderPath { get; set; } - public required string? LowestFolderPath { get; set; } - public required string SeriesName { get; set; } - public DateTime LastScanned { get; set; } - public MangaFormat Format { get; set; } - public IEnumerable LibraryRoots { get; set; } = ArraySegment.Empty; -} +namespace Kavita.Services.Scanner; /// /// Responsible for taking parsed info from ReadingItemService and DirectoryService and combining them to emit DB work @@ -152,7 +81,7 @@ public class ParseScannedFiles Library library, bool forceCheck, GlobMatcher matcher, List result, string fileExtensions) { var allDirectories = _directoryService.GetAllDirectories(folderPath, matcher) - .Select(Parser.Parser.NormalizePath) + .Select(Scanner.Parser.NormalizePath) .OrderByDescending(d => d.Length) .ToList(); @@ -318,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 = Parser.Parser.NormalizePath(folderPath); + var normalizedPath = Scanner.Parser.NormalizePath(folderPath); var libraryRoot = library.Folders.FirstOrDefault(f => - normalizedPath.Contains(Parser.Parser.NormalizePath(f.Path)))?.Path ?? + normalizedPath.Contains(Scanner.Parser.NormalizePath(f.Path)))?.Path ?? folderPath; await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, @@ -357,7 +286,7 @@ public class ParseScannedFiles return new ScanResult() { Files = files, - Folder = Parser.Parser.NormalizePath(folderPath), + Folder = Scanner.Parser.NormalizePath(folderPath), LibraryRoot = libraryRoot, HasChanged = hasChanged }; @@ -708,7 +637,7 @@ public class ParseScannedFiles case 1: return seriesForLocalized[0]; case <= 2: - return seriesForLocalized.FirstOrDefault(s => !s.Equals(Parser.Parser.Normalize(localizedSeries))); + return seriesForLocalized.FirstOrDefault(s => !s.Equals(Scanner.Parser.Normalize(localizedSeries))); default: _logger.LogError( "[ScannerService] Multiple series detected across scan results that contain localized series. " + @@ -763,7 +692,7 @@ public class ParseScannedFiles /// private async Task ParseFiles(ScanResult result, IDictionary> seriesPaths, Library library) { - var normalizedFolder = Parser.Parser.NormalizePath(result.Folder); + var normalizedFolder = Scanner.Parser.NormalizePath(result.Folder); // If folder hasn't changed, generate fake ParserInfos if (!result.HasChanged) @@ -849,7 +778,7 @@ public class ParseScannedFiles if (specialTreatment) { chapters = infos - .OrderByNatural(info => Parser.Parser.RemoveExtensionIfSupported(info.Filename)!) + .OrderByNatural(info => Scanner.Parser.RemoveExtensionIfSupported(info.Filename)!) .ToList(); foreach (var chapter in chapters) @@ -871,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 = - $"{Parser.Parser.MinNumberFromRange(chapter.Chapters).ToString(CultureInfo.InvariantCulture)}"; + $"{Scanner.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/API/Services/Tasks/Scanner/Parser/Parser.cs b/Kavita.Services/Scanner/Parser.cs similarity index 98% rename from API/Services/Tasks/Scanner/Parser/Parser.cs rename to Kavita.Services/Scanner/Parser.cs index f0b16c8d1..5444aac28 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/Kavita.Services/Scanner/Parser.cs @@ -1,26 +1,28 @@ using System; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using API.Entities.Enums; -using API.Extensions; +using Kavita.Common.Constants; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.Entities.Enums; -namespace API.Services.Tasks.Scanner.Parser; -#nullable enable +namespace Kavita.Services.Scanner; public static partial class Parser { // NOTE: If you change this, don't forget to change in the UI (see Series Detail) - public const string DefaultChapter = "-100000"; - public const string LooseLeafVolume = "-100000"; - public const int DefaultChapterNumber = -100_000; - public const int LooseLeafVolumeNumber = -100_000; + public const string DefaultChapter = ParserConstants.DefaultChapter; + public const string LooseLeafVolume = ParserConstants.LooseLeafVolume; + public const int DefaultChapterNumber = ParserConstants.DefaultChapterNumber; + public const int LooseLeafVolumeNumber = ParserConstants.LooseLeafVolumeNumber; /// /// The Volume Number of Specials to reside in /// - public const int SpecialVolumeNumber = 100_000; - public const string SpecialVolume = "100000"; + public const int SpecialVolumeNumber = ParserConstants.SpecialVolumeNumber; + public const string SpecialVolume = ParserConstants.SpecialVolume; public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); @@ -105,13 +107,6 @@ public static partial class Parser private static readonly Regex CoverImageRegex = new(@"(? - /// Normalize everything within Kavita. Some characters don't fall under Unicode, like full-width characters and need to be - /// added on a case-by-case basis. - /// - private static readonly Regex NormalizeRegex = new(@"[^\p{L}0-9\+!*!+]", - MatchOptions, RegexTimeout); - /// /// Supports Batman (2020) or Batman (2) /// @@ -1076,7 +1071,7 @@ public static partial class Parser public static string Normalize(string name) { - return NormalizeRegex.Replace(name, string.Empty).Trim().ToLower(); + return name.ToNormalized(); } /// @@ -1160,8 +1155,7 @@ public static partial class Parser /// public static string NormalizePath(string? path) { - return string.IsNullOrEmpty(path) ? string.Empty : path.Replace('\\', Path.AltDirectorySeparatorChar) - .Replace(@"//", Path.AltDirectorySeparatorChar + string.Empty); + return path.NormalizePath(); } /// @@ -1219,6 +1213,7 @@ public static partial class Parser return match.Groups["Year"].Value; } + [return: NotNullIfNotNull(nameof(filename))] public static string? RemoveExtensionIfSupported(string? filename) { if (string.IsNullOrEmpty(filename)) return filename; diff --git a/API/Services/Tasks/Scanner/Parser/PdfParser.cs b/Kavita.Services/Scanner/PdfParser.cs similarity index 66% rename from API/Services/Tasks/Scanner/Parser/PdfParser.cs rename to Kavita.Services/Scanner/PdfParser.cs index 1e43f3bb4..4bde04bd5 100644 --- a/API/Services/Tasks/Scanner/Parser/PdfParser.cs +++ b/Kavita.Services/Scanner/PdfParser.cs @@ -1,8 +1,10 @@ using System.IO; -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; -namespace API.Services.Tasks.Scanner.Parser; +namespace Kavita.Services.Scanner; public class PdfParser(IDirectoryService directoryService) : DefaultParser(directoryService) { @@ -12,21 +14,21 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc var ret = new ParserInfo { Filename = Path.GetFileName(filePath), - Format = Parser.ParseFormat(filePath), - Title = Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Parser.NormalizePath(filePath), + Format = Scanner.Parser.ParseFormat(filePath), + Title = Scanner.Parser.RemoveExtensionIfSupported(fileName)!, + FullFilePath = Scanner.Parser.NormalizePath(filePath), Series = string.Empty, ComicInfo = comicInfo, - Chapters = Parser.ParseChapter(fileName, type) + Chapters = Scanner.Parser.ParseChapter(fileName, type) }; if (type == LibraryType.Book) { - ret.Chapters = Parser.DefaultChapter; + ret.Chapters = Scanner.Parser.DefaultChapter; } - ret.Series = Parser.ParseSeries(fileName, type); - ret.Volumes = Parser.ParseVolume(fileName, type); + ret.Series = Scanner.Parser.ParseSeries(fileName, type); + ret.Volumes = Scanner.Parser.ParseVolume(fileName, type); if (ret.Series == string.Empty) { @@ -34,17 +36,17 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc ParseFromFallbackFolders(filePath, rootPath, type, ref ret); } - var edition = Parser.ParseEdition(fileName); + var edition = Scanner.Parser.ParseEdition(fileName); if (!string.IsNullOrEmpty(edition)) { - ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); + ret.Series = Scanner.Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); ret.Edition = edition; } - var isSpecial = Parser.IsSpecial(fileName, type); + var isSpecial = Scanner.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 (Parser.IsDefaultChapter(ret.Chapters) && Parser.IsLooseLeafVolume(ret.Volumes) && isSpecial) + if (Scanner.Parser.IsDefaultChapter(ret.Chapters) && Scanner.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 @@ -52,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 (Parser.HasSpecialMarker(fileName)) + if (Scanner.Parser.HasSpecialMarker(fileName)) { ret.IsSpecial = true; - ret.SpecialIndex = Parser.ParseSpecialIndex(fileName); - ret.Chapters = Parser.DefaultChapter; - ret.Volumes = Parser.SpecialVolume; + ret.SpecialIndex = Scanner.Parser.ParseSpecialIndex(fileName); + ret.Chapters = Scanner.Parser.DefaultChapter; + ret.Volumes = Scanner.Parser.SpecialVolume; var tempRootPath = rootPath; if (rootPath.EndsWith("Specials") || rootPath.EndsWith("Specials/")) @@ -80,11 +82,11 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc } - if (Parser.IsDefaultChapter(ret.Chapters) && Parser.IsLooseLeafVolume(ret.Volumes) && type == LibraryType.Book) + if (Scanner.Parser.IsDefaultChapter(ret.Chapters) && Scanner.Parser.IsLooseLeafVolume(ret.Volumes) && type == LibraryType.Book) { ret.IsSpecial = true; - ret.Chapters = Parser.DefaultChapter; - ret.Volumes = Parser.SpecialVolume; + ret.Chapters = Scanner.Parser.DefaultChapter; + ret.Volumes = Scanner.Parser.SpecialVolume; ParseFromFallbackFolders(filePath, rootPath, type, ref ret); } @@ -103,11 +105,11 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc if (string.IsNullOrEmpty(ret.Series)) { - ret.Series = Parser.CleanTitle(fileName, type is LibraryType.Comic); + ret.Series = Scanner.Parser.CleanTitle(fileName, type is LibraryType.Comic); } // Pdfs may have .pdf in the series name, remove that - if (Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) + if (Scanner.Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) { ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length); } @@ -115,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 = $"{Parser.SpecialVolumeNumber}"; + ret.Volumes = $"{Scanner.Parser.SpecialVolumeNumber}"; } return string.IsNullOrEmpty(ret.Series) ? null : ret; @@ -129,6 +131,6 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc /// public override bool IsApplicable(string filePath, LibraryType type) { - return Parser.IsPdf(filePath); + return Scanner.Parser.IsPdf(filePath); } } diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/Kavita.Services/Scanner/ProcessSeries.cs similarity index 94% rename from API/Services/Tasks/Scanner/ProcessSeries.cs rename to Kavita.Services/Scanner/ProcessSeries.cs index 390071e95..4bb6790da 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/Kavita.Services/Scanner/ProcessSeries.cs @@ -1,42 +1,36 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.Metadata; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; +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.Scanner; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using Kavita.Services.Builders; +using Kavita.Services.Extensions; +using Kavita.Services.Helpers; +using Kavita.Services.Plus; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Services.Tasks.Scanner; -#nullable enable - -public interface IProcessSeries -{ - Task ProcessSeriesAsync(MetadataSettingsDto settings, IList parsedInfos, ProcessSeriesArgs args); -} - -public sealed record ProcessSeriesArgs -{ - public required Library Library { get; init; } - public required int TotalToProcess { get; init; } - public required int LeftToProcess { get; init; } - public bool ForceUpdate { get; init; } = false; -} +namespace Kavita.Services.Scanner; internal sealed record UpdateChapterArgs { @@ -249,7 +243,7 @@ public class ProcessSeries( var tableRows = $"Name: {firstCollision.Name}Name: {secondCollision.Name}" + $"Localized: {firstCollision.LocalizedName}Localized: {secondCollision.LocalizedName}" + - $"Filename: {Parser.Parser.NormalizePath(firstCollision.FolderPath)}Filename: {Parser.Parser.NormalizePath(secondCollision.FolderPath)}"; + $"Filename: {Parser.NormalizePath(firstCollision.FolderPath)}Filename: {Parser.NormalizePath(secondCollision.FolderPath)}"; var htmlTable = $"{string.Join(string.Empty, tableRows)}
    Series 1Series 2
    "; @@ -265,8 +259,8 @@ public class ProcessSeries( private async Task UpdateSeriesFolderPath(IEnumerable parsedInfos, Library library, Series series) { - var libraryFolders = library.Folders.Select(l => Parser.Parser.NormalizePath(l.Path)).ToList(); - var seriesFiles = parsedInfos.Select(f => Parser.Parser.NormalizePath(f.FullFilePath)).ToList(); + var libraryFolders = library.Folders.Select(l => Parser.NormalizePath(l.Path)).ToList(); + var seriesFiles = parsedInfos.Select(f => Parser.NormalizePath(f.FullFilePath)).ToList(); var seriesDirs = directoryService.FindHighestDirectoriesFromFiles(libraryFolders, seriesFiles); if (seriesDirs.Keys.Count == 0) { @@ -283,7 +277,7 @@ public class ProcessSeries( { // BUG: FolderPath can be a level higher than it needs to be. I'm not sure why it's like this, but I thought it should be one level lower. // I think it's like this because higher level is checked or not checked. But i think we can do both - series.FolderPath = Parser.Parser.NormalizePath(seriesDirs.Keys.First()); + series.FolderPath = Parser.NormalizePath(seriesDirs.Keys.First()); logger.LogDebug("Updating {Series} FolderPath to {FolderPath}", series.Name, series.FolderPath); } } @@ -527,7 +521,7 @@ public class ProcessSeries( series.Metadata.MaxCount = chapters.Max(chapter => chapter.Count); var nonSpecialVolumes = series.Volumes - .Where(v => v.MaxNumber.IsNot(Parser.Parser.SpecialVolumeNumber)) + .Where(v => v.MaxNumber.IsNot(Parser.SpecialVolumeNumber)) .ToList(); var maxVolume = (int)(nonSpecialVolumes.Any() ? nonSpecialVolumes.Max(v => v.MaxNumber) : 0); @@ -543,7 +537,7 @@ public class ProcessSeries( // If a series has a TotalCount of 1 (or no total count) and there is only a Special, mark it as Complete series.Metadata.MaxCount = series.Metadata.TotalCount; } - else if ((maxChapter == Parser.Parser.DefaultChapterNumber || maxChapter > series.Metadata.TotalCount) && + else if ((maxChapter == Parser.DefaultChapterNumber || maxChapter > series.Metadata.TotalCount) && maxVolume <= series.Metadata.TotalCount) { series.Metadata.MaxCount = maxVolume; @@ -690,9 +684,9 @@ public class ProcessSeries( // Add files AddOrUpdateFileForChapter(chapter, info, args.ForceUpdate); - chapter.Number = Parser.Parser.MinNumberFromRange(info.Chapters).ToString(CultureInfo.InvariantCulture); - chapter.MinNumber = Parser.Parser.MinNumberFromRange(info.Chapters); - chapter.MaxNumber = Parser.Parser.MaxNumberFromRange(info.Chapters); + chapter.Number = Parser.MinNumberFromRange(info.Chapters).ToString(CultureInfo.InvariantCulture); + chapter.MinNumber = Parser.MinNumberFromRange(info.Chapters); + chapter.MaxNumber = Parser.MaxNumberFromRange(info.Chapters); chapter.Range = chapter.GetNumberTitle(); if (!chapter.SortOrderLocked) @@ -752,7 +746,7 @@ public class ProcessSeries( if (hasMatchingDirectory) { existingChapter.Files = existingChapter.Files - .Where(f => parsedInfos.Any(p => Parser.Parser.NormalizePath(p.FullFilePath) == Parser.Parser.NormalizePath(f.FilePath))) + .Where(f => parsedInfos.Any(p => Parser.NormalizePath(p.FullFilePath) == Parser.NormalizePath(f.FilePath))) .OrderByNatural(f => f.FilePath) .ToList(); @@ -796,8 +790,8 @@ public class ProcessSeries( existingFile.Pages = readingItemService.GetNumberOfPages(info.FullFilePath, info.Format); existingFile.Extension = fileInfo.Extension.ToLowerInvariant(); - existingFile.FileName = Parser.Parser.RemoveExtensionIfSupported(existingFile.FilePath); - existingFile.FilePath = Parser.Parser.NormalizePath(existingFile.FilePath); + existingFile.FileName = Parser.RemoveExtensionIfSupported(existingFile.FilePath); + existingFile.FilePath = Parser.NormalizePath(existingFile.FilePath); existingFile.Bytes = fileInfo.Length; existingFile.KoreaderHash = KoreaderHelper.HashContents(existingFile.FilePath); @@ -878,11 +872,11 @@ public class ProcessSeries( if (!string.IsNullOrEmpty(comicInfo.Web)) { - chapter.WebLinks = string.Join(",", comicInfo.Web - .Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - ); + 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)) @@ -916,8 +910,8 @@ public class ProcessSeries( if (!chapter.GenresLocked || !chapter.TagsLocked) { - var genres = TagHelper.GetTagValues(comicInfo.Genre); - var tags = TagHelper.GetTagValues(comicInfo.Tags); + var genres = comicInfo.Genre.SplitBy(','); + var tags = comicInfo.Tags.SplitBy(','); ExternalMetadataService.GenerateExternalGenreAndTagsList(genres, tags, args.Settings, out var finalTags, out var finalGenres); diff --git a/API/Services/Tasks/ScannerService.cs b/Kavita.Services/Scanner/ScannerService.cs similarity index 65% rename from API/Services/Tasks/ScannerService.cs rename to Kavita.Services/Scanner/ScannerService.cs index d633da92d..224ec1406 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/Kavita.Services/Scanner/ScannerService.cs @@ -6,53 +6,26 @@ using System.IO; using System.Linq; using System.Threading.Channels; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Settings; -using API.Entities; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Plus; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Scanner; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Parser; +using Kavita.Services.Helpers; +using Kavita.Services.Plus; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace API.Services.Tasks; -#nullable enable - -public interface IScannerService -{ - /// - /// Given a library id, scans folders for said library. Parses files and generates DB updates. Will overwrite - /// cover images if forceUpdate is true. - /// - /// Library to scan against - /// Don't perform optimization checks, defaults to false - [Queue(TaskScheduler.ScanQueue)] - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task ScanLibrary(int libraryId, bool forceUpdate = false, bool isSingleScan = true); - - [Queue(TaskScheduler.ScanQueue)] - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task ScanLibraries(bool forceUpdate = false); - - [Queue(TaskScheduler.ScanQueue)] - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task ScanSeries(int seriesId, bool bypassFolderOptimizationChecks = true); - - Task ScanFolder(string folder, string originalPath, bool abortOnNoSeriesMatch = false); - Task AnalyzeFiles(); - -} +namespace Kavita.Services.Scanner; public enum ScanCancelReason { @@ -77,46 +50,31 @@ public enum ScanCancelReason /** * Responsible for Scanning the disk and importing/updating/deleting files -> DB entities. */ -public class ScannerService : IScannerService +public class ScannerService( + IUnitOfWork unitOfWork, + ILogger logger, + IMetadataService metadataService, + ICacheService cacheService, + IEventHub eventHub, + IDirectoryService directoryService, + IReadingItemService readingItemService, + IServiceScopeFactory scopeFactory, + IWordCountAnalyzerService wordCountAnalyzerService) + : IScannerService { public const string Name = "ScannerService"; private const int Timeout = 60 * 60 * 60; // 2.5 days - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IMetadataService _metadataService; - private readonly ICacheService _cacheService; - private readonly IEventHub _eventHub; - private readonly IDirectoryService _directoryService; - private readonly IReadingItemService _readingItemService; - private readonly IWordCountAnalyzerService _wordCountAnalyzerService; - private readonly IServiceScopeFactory _scopeFactory; - - public ScannerService(IUnitOfWork unitOfWork, ILogger logger, - IMetadataService metadataService, ICacheService cacheService, IEventHub eventHub, - IDirectoryService directoryService, IReadingItemService readingItemService, - IServiceScopeFactory scopeFactory, IWordCountAnalyzerService wordCountAnalyzerService) - { - _unitOfWork = unitOfWork; - _logger = logger; - _metadataService = metadataService; - _cacheService = cacheService; - _eventHub = eventHub; - _directoryService = directoryService; - _readingItemService = readingItemService; - _scopeFactory = scopeFactory; - _wordCountAnalyzerService = wordCountAnalyzerService; - } /// /// This is only used for v0.7 to get files analyzed /// public async Task AnalyzeFiles() { - _logger.LogInformation("Starting Analyze Files task"); - var missingExtensions = await _unitOfWork.MangaFileRepository.GetAllWithMissingExtension(); + logger.LogInformation("Starting Analyze Files task"); + var missingExtensions = await unitOfWork.MangaFileRepository.GetAllWithMissingExtension(); if (missingExtensions.Count == 0) { - _logger.LogInformation("Nothing to do"); + logger.LogInformation("Nothing to do"); return; } @@ -124,16 +82,16 @@ public class ScannerService : IScannerService foreach (var file in missingExtensions) { - var fileInfo = _directoryService.FileSystem.FileInfo.New(file.FilePath); + var fileInfo = directoryService.FileSystem.FileInfo.New(file.FilePath); if (!fileInfo.Exists)continue; file.Extension = fileInfo.Extension.ToLowerInvariant(); file.Bytes = fileInfo.Length; - _unitOfWork.MangaFileRepository.Update(file); + unitOfWork.MangaFileRepository.Update(file); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); - _logger.LogInformation("Completed Analyze Files task in {ElapsedTime}", sw.Elapsed); + logger.LogInformation("Completed Analyze Files task in {ElapsedTime}", sw.Elapsed); } /// @@ -148,16 +106,16 @@ public class ScannerService : IScannerService Series? series = null; try { - series = await _unitOfWork.SeriesRepository.GetSeriesThatContainsLowestFolderPath(originalPath, + series = await unitOfWork.SeriesRepository.GetSeriesThatContainsLowestFolderPath(originalPath, SeriesIncludes.Library) ?? - await _unitOfWork.SeriesRepository.GetSeriesByFolderPath(originalPath, SeriesIncludes.Library) ?? - await _unitOfWork.SeriesRepository.GetSeriesByFolderPath(folder, SeriesIncludes.Library); + await unitOfWork.SeriesRepository.GetSeriesByFolderPath(originalPath, SeriesIncludes.Library) ?? + await unitOfWork.SeriesRepository.GetSeriesByFolderPath(folder, SeriesIncludes.Library); } catch (InvalidOperationException ex) { if (ex.Message.Equals("Sequence contains more than one element.")) { - _logger.LogCritical(ex, "[ScannerService] Multiple series map to this folder or folder is at library root. Library scan will be used for ScanFolder"); + logger.LogCritical(ex, "[ScannerService] Multiple series map to this folder or folder is at library root. Library scan will be used for ScanFolder"); } } @@ -165,11 +123,11 @@ public class ScannerService : IScannerService { if (TaskScheduler.HasScanTaskRunningForSeries(series.Id)) { - _logger.LogTrace("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this series. Dropping request", folder); + logger.LogTrace("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this series. Dropping request", folder); return; } - _logger.LogInformation("[ScannerService] Scan folder invoked for {Folder}, Series matched to folder and ScanSeries enqueued for 1 minute", folder); + logger.LogInformation("[ScannerService] Scan folder invoked for {Folder}, Series matched to folder and ScanSeries enqueued for 1 minute", folder); BackgroundJob.Schedule(() => ScanSeries(series.Id, false), TimeSpan.FromMinutes(1)); return; } @@ -178,12 +136,12 @@ public class ScannerService : IScannerService // This is basically rework of what's already done in Library Watcher but is needed if invoked via API - var parentDirectory = _directoryService.GetParentDirectoryName(folder); + var parentDirectory = directoryService.GetParentDirectoryName(folder); if (string.IsNullOrEmpty(parentDirectory)) return; - var libraries = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync()).ToList(); + var libraries = (await unitOfWork.LibraryRepository.GetLibraryDtosAsync()).ToList(); var libraryFolders = libraries.SelectMany(l => l.Folders); - var libraryFolder = libraryFolders.Select(Parser.NormalizePath).FirstOrDefault(f => f.Contains(parentDirectory)); + var libraryFolder = libraryFolders.Select(Parser.Normalize).FirstOrDefault(f => f.Contains(parentDirectory)); if (string.IsNullOrEmpty(libraryFolder)) return; var library = libraries.Find(l => l.Folders.Select(Parser.NormalizePath).Contains(libraryFolder)); @@ -192,7 +150,7 @@ public class ScannerService : IScannerService { if (TaskScheduler.HasScanTaskRunningForLibrary(library.Id)) { - _logger.LogTrace("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this library. Dropping request", folder); + logger.LogTrace("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this library. Dropping request", folder); return; } BackgroundJob.Schedule(() => ScanLibrary(library.Id, false, true), TimeSpan.FromMinutes(1)); @@ -211,44 +169,44 @@ public class ScannerService : IScannerService { if (TaskScheduler.HasAlreadyEnqueuedTask(Name, "ScanSeries", [seriesId, bypassFolderOptimizationChecks], TaskScheduler.ScanQueue)) { - _logger.LogInformation("[ScannerService] Scan series invoked but a task is already running/enqueued. Dropping request"); + logger.LogInformation("[ScannerService] Scan series invoked but a task is already running/enqueued. Dropping request"); return; } var sw = Stopwatch.StartNew(); - var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); + var series = await unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); if (series == null) return; // This can occur when UI deletes a series but doesn't update and user re-requests update - var settings = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetMetadataSettingDto(); + var serverSettings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var existingChapterIdsToClean = await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new[] {seriesId}); + var existingChapterIdsToClean = await unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new[] {seriesId}); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); if (library == null) return; var libraryPaths = library.Folders.Select(f => f.Path).ToList(); if (await ShouldScanSeries(seriesId, library, libraryPaths, series, true) != ScanCancelReason.NoCancel) { - BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(serverSettings, series.LibraryId, seriesId, false, false)); - BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(library.Id, seriesId, bypassFolderOptimizationChecks)); + BackgroundJob.Enqueue(() => metadataService.GenerateCoversForSeries(serverSettings, series.LibraryId, seriesId, false, false)); + BackgroundJob.Enqueue(() => wordCountAnalyzerService.ScanSeries(library.Id, seriesId, bypassFolderOptimizationChecks)); return; } // TODO: We need to refactor this to handle the path changes better var folderPath = series.LowestFolderPath ?? series.FolderPath; - if (string.IsNullOrEmpty(folderPath) || !_directoryService.Exists(folderPath)) + if (string.IsNullOrEmpty(folderPath) || !directoryService.Exists(folderPath)) { // We don't care if it's multiple due to new scan loop enforcing all in one root directory - var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId); - var seriesDirs = _directoryService.FindHighestDirectoriesFromFiles(libraryPaths, + var files = await unitOfWork.SeriesRepository.GetFilesForSeries(seriesId); + var seriesDirs = directoryService.FindHighestDirectoriesFromFiles(libraryPaths, files.Select(f => f.FilePath).ToList()); if (seriesDirs.Keys.Count == 0) { - _logger.LogCritical("Scan Series has files spread outside a main series folder. Defaulting to library folder (this is expensive)"); - await _eventHub.SendMessageAsync(MessageFactory.Info, MessageFactory.InfoEvent($"{series.Name} is not organized well and scan series will be expensive!", "Scan Series has files spread outside a main series folder. Defaulting to library folder (this is expensive)")); - seriesDirs = _directoryService.FindHighestDirectoriesFromFiles(libraryPaths, files.Select(f => f.FilePath).ToList()); + logger.LogCritical("Scan Series has files spread outside a main series folder. Defaulting to library folder (this is expensive)"); + await eventHub.SendMessageAsync(MessageFactory.Info, MessageFactory.InfoEvent($"{series.Name} is not organized well and scan series will be expensive!", "Scan Series has files spread outside a main series folder. Defaulting to library folder (this is expensive)")); + seriesDirs = directoryService.FindHighestDirectoriesFromFiles(libraryPaths, files.Select(f => f.FilePath).ToList()); } folderPath = seriesDirs.Keys.FirstOrDefault(); @@ -256,27 +214,27 @@ public class ScannerService : IScannerService // We should check if folderPath is a library folder path and if so, return early and tell user to correct their setup. if (!string.IsNullOrEmpty(folderPath) && libraryPaths.Contains(folderPath)) { - _logger.LogCritical("[ScannerSeries] {SeriesName} scan aborted. Files for series are not in a nested folder under library path. Correct this and rescan", series.Name); - await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"{series.Name} scan aborted", "Files for series are not in a nested folder under library path. Correct this and rescan.")); + logger.LogCritical("[ScannerSeries] {SeriesName} scan aborted. Files for series are not in a nested folder under library path. Correct this and rescan", series.Name); + await eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"{series.Name} scan aborted", "Files for series are not in a nested folder under library path. Correct this and rescan.")); return; } } if (string.IsNullOrEmpty(folderPath)) { - _logger.LogCritical("[ScannerSeries] Scan Series could not find a single, valid folder root for files"); - await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"{series.Name} scan aborted", "Scan Series could not find a single, valid folder root for files")); + logger.LogCritical("[ScannerSeries] Scan Series could not find a single, valid folder root for files"); + await eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"{series.Name} scan aborted", "Scan Series could not find a single, valid folder root for files")); return; } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Started, series.Name, 1)); - _logger.LogInformation("Beginning file scan on {SeriesName}", series.Name); + logger.LogInformation("Beginning file scan on {SeriesName}", series.Name); var (scanElapsedTime, parsedSeries) = await ScanFiles(library, [folderPath], false, true); - _logger.LogInformation("ScanFiles for {Series} took {Time} milliseconds", series.Name, scanElapsedTime); + logger.LogInformation("ScanFiles for {Series} took {Time} milliseconds", series.Name, scanElapsedTime); // Remove any parsedSeries keys that don't belong to our series. This can occur when users store 2 series in the same folder RemoveParsedInfosNotForSeries(parsedSeries, series); @@ -284,31 +242,31 @@ public class ScannerService : IScannerService // If nothing was found, first validate any of the files still exist. If they don't then we have a deletion and can skip the rest of the logic flow if (parsedSeries.Count == 0) { - var seriesFiles = (await _unitOfWork.SeriesRepository.GetFilesForSeries(series.Id)); + var seriesFiles = (await unitOfWork.SeriesRepository.GetFilesForSeries(series.Id)); if (!string.IsNullOrEmpty(series.FolderPath) && !seriesFiles.Where(f => f.FilePath.Contains(series.FolderPath)).Any(m => File.Exists(m.FilePath))) { try { - _unitOfWork.SeriesRepository.Remove(series); + unitOfWork.SeriesRepository.Remove(series); await CommitAndSend(1, sw, scanElapsedTime, series); - await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, + await eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, MessageFactory.SeriesRemovedEvent(seriesId, string.Empty, series.LibraryId), false); } catch (Exception ex) { - _logger.LogCritical(ex, "There was an error during ScanSeries to delete the series as no files could be found. Aborting scan"); - await _unitOfWork.RollbackAsync(); + logger.LogCritical(ex, "There was an error during ScanSeries to delete the series as no files could be found. Aborting scan"); + await unitOfWork.RollbackAsync(); return; } } else { // I think we should just fail and tell user to fix their setup. This is extremely expensive for an edge case - _logger.LogCritical("We weren't able to find any files in the series scan, but there should be. Please correct your naming convention or put Series in a dedicated folder. Aborting scan"); - await _eventHub.SendMessageAsync(MessageFactory.Error, + logger.LogCritical("We weren't able to find any files in the series scan, but there should be. Please correct your naming convention or put Series in a dedicated folder. Aborting scan"); + await eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"Error scanning {series.Name}", "We weren't able to find any files in the series scan, but there should be. Please correct your naming convention or put Series in a dedicated folder. Aborting scan")); - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); return; } } @@ -328,7 +286,7 @@ public class ScannerService : IScannerService { current++; - using var scope = _scopeFactory.CreateScope(); + using var scope = scopeFactory.CreateScope(); var processSeries = scope.ServiceProvider.GetRequiredService(); var unitOfWork = scope.ServiceProvider.GetRequiredService(); @@ -353,13 +311,13 @@ public class ScannerService : IScannerService } // Tell UI that this series is done - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name)); - await _metadataService.RemoveAbandonedMetadataKeys(); + await metadataService.RemoveAbandonedMetadataKeys(); - BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(existingChapterIdsToClean)); - BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.CacheDirectory)); + BackgroundJob.Enqueue(() => cacheService.CleanupChapters(existingChapterIdsToClean)); + BackgroundJob.Enqueue(() => directoryService.ClearDirectory(directoryService.CacheDirectory)); } private static Dictionary> TrackFoundSeriesAndFiles(IList seenSeries) @@ -386,22 +344,22 @@ public class ScannerService : IScannerService private async Task ShouldScanSeries(int seriesId, Library library, IList libraryPaths, Series series, bool bypassFolderChecks = false) { - var seriesFolderPaths = (await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId)) - .Select(f => _directoryService.FileSystem.FileInfo.New(f.FilePath).Directory?.FullName ?? string.Empty) + var seriesFolderPaths = (await unitOfWork.SeriesRepository.GetFilesForSeries(seriesId)) + .Select(f => directoryService.FileSystem.FileInfo.New(f.FilePath).Directory?.FullName ?? string.Empty) .Where(f => !string.IsNullOrEmpty(f)) .Distinct() .ToList(); if (!await CheckMounts(library.Name, seriesFolderPaths)) { - _logger.LogCritical( + logger.LogCritical( "Some of the root folders for library are not accessible. Please check that drives are connected and rescan. Scan will be aborted"); return ScanCancelReason.FolderMount; } if (!await CheckMounts(library.Name, libraryPaths)) { - _logger.LogCritical( + logger.LogCritical( "Some of the root folders for library are not accessible. Please check that drives are connected and rescan. Scan will be aborted"); return ScanCancelReason.FolderMount; } @@ -410,17 +368,17 @@ public class ScannerService : IScannerService if (!bypassFolderChecks) { - var allFolders = seriesFolderPaths.SelectMany(path => _directoryService.GetDirectories(path)).ToList(); + var allFolders = seriesFolderPaths.SelectMany(path => directoryService.GetDirectories(path)).ToList(); allFolders.AddRange(seriesFolderPaths); try { - if (allFolders.TrueForAll(folder => _directoryService.GetLastWriteTime(folder) <= series.LastFolderScanned)) + if (allFolders.TrueForAll(folder => directoryService.GetLastWriteTime(folder) <= series.LastFolderScanned)) { - _logger.LogInformation( + logger.LogInformation( "[ScannerService] {SeriesName} scan has no work to do. All folders have not been changed since last scan", series.Name); - await _eventHub.SendMessageAsync(MessageFactory.Info, + await eventHub.SendMessageAsync(MessageFactory.Info, MessageFactory.InfoEvent($"{series.Name} scan has no work to do", $"All folders have not been changed since last scan ({series.LastFolderScanned.ToString(CultureInfo.CurrentCulture)}). Scan will be aborted.")); return ScanCancelReason.NoChange; @@ -429,9 +387,9 @@ public class ScannerService : IScannerService catch (IOException ex) { // If there is an exception it means that the folder doesn't exist. So we should delete the series - _logger.LogError(ex, "[ScannerService] Scan series for {SeriesName} found the folder path no longer exists", + logger.LogError(ex, "[ScannerService] Scan series for {SeriesName} found the folder path no longer exists", series.Name); - await _eventHub.SendMessageAsync(MessageFactory.Info, + await eventHub.SendMessageAsync(MessageFactory.Info, MessageFactory.ErrorEvent($"{series.Name} scan has no work to do", "The folder the series was in is missing. Delete series manually or perform a library scan.")); return ScanCancelReason.NoCancel; @@ -453,10 +411,10 @@ public class ScannerService : IScannerService private async Task CommitAndSend(int seriesCount, Stopwatch sw, long scanElapsedTime, Series series) { - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - _logger.LogInformation( + await unitOfWork.CommitAsync(); + logger.LogInformation( "Processed files and {SeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}", seriesCount, sw.ElapsedMilliseconds + scanElapsedTime, series.Name); } @@ -471,28 +429,28 @@ public class ScannerService : IScannerService private async Task CheckMounts(string libraryName, IList folders) { // Check if any of the folder roots are not available (ie disconnected from network, etc) and fail if any of them are - if (folders.Any(f => !_directoryService.IsDriveMounted(f))) + if (folders.Any(f => !directoryService.IsDriveMounted(f))) { - _logger.LogCritical("[ScannerService] Some of the root folders for library ({LibraryName} are not accessible. Please check that drives are connected and rescan. Scan will be aborted", libraryName); + logger.LogCritical("[ScannerService] Some of the root folders for library ({LibraryName} are not accessible. Please check that drives are connected and rescan. Scan will be aborted", libraryName); - await _eventHub.SendMessageAsync(MessageFactory.Error, + await eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent("Some of the root folders for library are not accessible. Please check that drives are connected and rescan. Scan will be aborted", - string.Join(", ", folders.Where(f => !_directoryService.IsDriveMounted(f))))); + string.Join(", ", folders.Where(f => !directoryService.IsDriveMounted(f))))); return false; } // For Docker instances check if any of the folder roots are not available (ie disconnected volumes, etc) and fail if any of them are - if (folders.Any(f => _directoryService.IsDirectoryEmpty(f))) + if (folders.Any(f => directoryService.IsDirectoryEmpty(f))) { // That way logging and UI informing is all in one place with full context - _logger.LogError("[ScannerService] Some of the root folders for the library are empty. " + + logger.LogError("[ScannerService] Some of the root folders for the library are empty. " + "Either your mount has been disconnected or you are trying to delete all series in the library. " + "Scan has been aborted. " + "Check that your mount is connected or change the library's root folder and rescan"); - await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent( $"Some of the root folders for the library, {libraryName}, are empty.", + await eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent( $"Some of the root folders for the library, {libraryName}, are empty.", "Either your mount has been disconnected or you are trying to delete all series in the library. " + "Scan has been aborted. " + "Check that your mount is connected or change the library's root folder and rescan")); @@ -508,21 +466,21 @@ public class ScannerService : IScannerService [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public async Task ScanLibraries(bool forceUpdate = false) { - _logger.LogInformation("[ScannerService] Starting Scan of All Libraries, Forced: {Forced}", forceUpdate); - foreach (var lib in await _unitOfWork.LibraryRepository.GetLibrariesAsync()) + logger.LogInformation("[ScannerService] Starting Scan of All Libraries, Forced: {Forced}", forceUpdate); + foreach (var lib in await unitOfWork.LibraryRepository.GetLibrariesAsync()) { // BUG: This will trigger the first N libraries to scan over and over if there is always an interruption later in the chain if (TaskScheduler.HasScanTaskRunningForLibrary(lib.Id)) { // We don't need to send SignalR event as this is a background job that user doesn't need insight into - _logger.LogInformation("[ScannerService] Scan library invoked via nightly scan job but a task is already running for {LibraryName}. Rescheduling for 4 hours", lib.Name); + logger.LogInformation("[ScannerService] Scan library invoked via nightly scan job but a task is already running for {LibraryName}. Rescheduling for 4 hours", lib.Name); await Task.Delay(TimeSpan.FromHours(4)); } await ScanLibrary(lib.Id, forceUpdate, true); } - _logger.LogInformation("[ScannerService] Scan of All Libraries Finished"); + logger.LogInformation("[ScannerService] Scan of All Libraries Finished"); } @@ -540,7 +498,7 @@ public class ScannerService : IScannerService public async Task ScanLibrary(int libraryId, bool forceUpdate = false, bool isSingleScan = true) { var sw = Stopwatch.StartNew(); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); var libraryFolderPaths = library!.Folders.Select(fp => fp.Path).ToList(); @@ -548,92 +506,92 @@ public class ScannerService : IScannerService // Validations are done, now we can start actual scan - _logger.LogInformation("[ScannerService] Beginning file scan on {LibraryName}", library.Name); + logger.LogInformation("[ScannerService] Beginning file scan on {LibraryName}", library.Name); if (!library.EnableMetadata) { - _logger.LogInformation("[ScannerService] Warning! {LibraryName} has metadata turned off", library.Name); + logger.LogInformation("[ScannerService] Warning! {LibraryName} has metadata turned off", library.Name); } // This doesn't work for something like M:/Manga/ and a series has library folder as root - var shouldUseLibraryScan = !(await _unitOfWork.LibraryRepository.DoAnySeriesFoldersMatch(libraryFolderPaths)); + var shouldUseLibraryScan = !(await unitOfWork.LibraryRepository.DoAnySeriesFoldersMatch(libraryFolderPaths)); if (!shouldUseLibraryScan) { - _logger.LogError("[ScannerService] Library {LibraryName} consists of one or more Series folders as a library root, using series scan", library.Name); + logger.LogError("[ScannerService] Library {LibraryName} consists of one or more Series folders as a library root, using series scan", library.Name); } - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 1: Scan & Parse Files", library.Name); + logger.LogDebug("[ScannerService] Library {LibraryName} Step 1: Scan & Parse Files", library.Name); var (scanElapsedTime, parsedSeries) = await ScanFiles(library, libraryFolderPaths, shouldUseLibraryScan, forceUpdate); // We need to remove any keys where there is no actual parser info - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 2: Process and Update Database", library.Name); + logger.LogDebug("[ScannerService] Library {LibraryName} Step 2: Process and Update Database", library.Name); var totalFiles = await ProcessParsedSeries(forceUpdate, parsedSeries, library, scanElapsedTime); UpdateLastScanned(library); - _unitOfWork.LibraryRepository.Update(library); + unitOfWork.LibraryRepository.Update(library); - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 3: Save Library", library.Name); - if (await _unitOfWork.CommitAsync()) + logger.LogDebug("[ScannerService] Library {LibraryName} Step 3: Save Library", library.Name); + if (await unitOfWork.CommitAsync()) { if (totalFiles == 0) { - _logger.LogInformation( + logger.LogInformation( "[ScannerService] Finished library scan of {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}. There were no changes", parsedSeries.Count, sw.ElapsedMilliseconds, library.Name); } else { - _logger.LogInformation( + logger.LogInformation( "[ScannerService] Finished library scan of {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}", totalFiles, parsedSeries.Count, sw.ElapsedMilliseconds, library.Name); } - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 5: Remove Deleted Series", library.Name); + logger.LogDebug("[ScannerService] Library {LibraryName} Step 5: Remove Deleted Series", library.Name); await RemoveSeriesNotFound(parsedSeries, library); } else { - _logger.LogCritical( + logger.LogCritical( "[ScannerService] There was a critical error that resulted in a failed scan. Please check logs and rescan"); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, string.Empty)); - await _metadataService.RemoveAbandonedMetadataKeys(); + await metadataService.RemoveAbandonedMetadataKeys(); - BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.CacheDirectory)); + BackgroundJob.Enqueue(() => directoryService.ClearDirectory(directoryService.CacheDirectory)); } private async Task RemoveSeriesNotFound(Dictionary> parsedSeries, Library library) { try { - _logger.LogDebug("[ScannerService] Removing series that were not found during the scan"); + logger.LogDebug("[ScannerService] Removing series that were not found during the scan"); - var removedSeries = await _unitOfWork.SeriesRepository.RemoveSeriesNotInList(parsedSeries.Keys.ToList(), library.Id); - _logger.LogDebug("[ScannerService] Found {Count} series to remove: {SeriesList}", + var removedSeries = await unitOfWork.SeriesRepository.RemoveSeriesNotInList(parsedSeries.Keys.ToList(), library.Id); + logger.LogDebug("[ScannerService] Found {Count} series to remove: {SeriesList}", removedSeries.Count, string.Join(", ", removedSeries.Select(s => s.Name))); // Commit the changes - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); // Notify for each removed series foreach (var series in removedSeries) { - await _eventHub.SendMessageAsync( + await eventHub.SendMessageAsync( MessageFactory.SeriesRemoved, MessageFactory.SeriesRemovedEvent(series.Id, series.Name, series.LibraryId), false ); } - _logger.LogDebug("[ScannerService] Series removal process completed"); + logger.LogDebug("[ScannerService] Series removal process completed"); } catch (Exception ex) { - _logger.LogCritical(ex, "[ScannerService] Error during series cleanup. Please check logs and rescan"); + logger.LogCritical(ex, "[ScannerService] Error during series cleanup. Please check logs and rescan"); } } @@ -643,13 +601,13 @@ public class ScannerService : IScannerService var toProcess = new Dictionary>(); var scanSw = Stopwatch.StartNew(); - var settings = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); + var settings = await unitOfWork.SettingsRepository.GetMetadataSettingDto(); foreach (var series in parsedSeries) { if (!series.Key.HasChanged) { - _logger.LogDebug("{Series} hasn't changed", series.Key.Name); + logger.LogDebug("{Series} hasn't changed", series.Key.Name); continue; } @@ -689,11 +647,11 @@ public class ScannerService : IScannerService await CreateAllTagsAsync(processedTags); } - _logger.LogInformation("[ScannerService] Found {SeriesCount} Series that need processing in {Time} ms", toProcess.Count, scanSw.ElapsedMilliseconds + scanElapsedTime); + logger.LogInformation("[ScannerService] Found {SeriesCount} Series that need processing in {Time} ms", toProcess.Count, scanSw.ElapsedMilliseconds + scanElapsedTime); var totalFiles = await ProcessParserInfo(settings, toProcess.Values.ToList(), library, forceUpdate); - _logger.LogInformation("[ScannerService] Finished scan in {ScanAndUpdateTime} milliseconds.", scanSw.ElapsedMilliseconds + scanElapsedTime); + logger.LogInformation("[ScannerService] Finished scan in {ScanAndUpdateTime} milliseconds.", scanSw.ElapsedMilliseconds + scanElapsedTime); return totalFiles; } @@ -710,13 +668,13 @@ public class ScannerService : IScannerService { var channel = Channel.CreateUnbounded(); - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var serverSettings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var dbTask = Task.Run(async () => await DbMetadataTask(channel, settings, toProcess, library.Id, library.Name, forceUpdate)); var amountOfProcessors = Environment.ProcessorCount; var usingCount = Math.Max(1, amountOfProcessors / 2); - _logger.LogDebug("[ScannerService] Going to use {Cores} / {TotalCores} threads for I/O tasks this scan", + logger.LogDebug("[ScannerService] Going to use {Cores} / {TotalCores} threads for I/O tasks this scan", usingCount, amountOfProcessors); IList> tasks = []; @@ -731,7 +689,7 @@ public class ScannerService : IScannerService var totalIoTime = tasks.Select(t => t.Result).Sum(); var avgTimePerThread = totalIoTime / usingCount; - _logger.LogDebug("[ScannerService] Spend {Elapsed}ms processing covers & word count, {Average}ms per thread", + logger.LogDebug("[ScannerService] Spend {Elapsed}ms processing covers & word count, {Average}ms per thread", totalIoTime, avgTimePerThread); return (int) dbTask.Result; @@ -751,7 +709,7 @@ public class ScannerService : IScannerService await foreach (var seriesId in channel.Reader.ReadAllAsync()) { - using var scope = _scopeFactory.CreateScope(); + using var scope = scopeFactory.CreateScope(); var metadataService = scope.ServiceProvider.GetRequiredService(); var wordCountAnalyzerService = scope.ServiceProvider.GetRequiredService(); @@ -785,7 +743,7 @@ public class ScannerService : IScannerService { totalFiles += pSeries.Count; - using var scope = _scopeFactory.CreateScope(); + using var scope = scopeFactory.CreateScope(); var unitOfWork = scope.ServiceProvider.GetRequiredService(); var processSeries = scope.ServiceProvider.GetRequiredService(); @@ -813,10 +771,10 @@ public class ScannerService : IScannerService channel.Writer.Complete(); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(libraryName, ProgressEventType.Ended)); - _logger.LogDebug("[ScannerService] Finished writing metadata for {Count} series in {Elapsed}ms", toProcess.Count, sw.ElapsedMilliseconds); + logger.LogDebug("[ScannerService] Finished writing metadata for {Count} series in {Elapsed}ms", toProcess.Count, sw.ElapsedMilliseconds); return totalFiles; } @@ -835,11 +793,11 @@ public class ScannerService : IScannerService private async Task>>> ScanFiles(Library library, IList dirs, bool isLibraryScan, bool forceChecks = false) { - var scanner = new ParseScannedFiles(_logger, _directoryService, _readingItemService, _eventHub); + var scanner = new ParseScannedFiles(logger, directoryService, readingItemService, eventHub); var scanWatch = Stopwatch.StartNew(); var processedSeries = await scanner.ScanLibrariesForSeries(library, dirs, - isLibraryScan, await _unitOfWork.SeriesRepository.GetFolderPathMap(library.Id), forceChecks); + isLibraryScan, await unitOfWork.SeriesRepository.GetFolderPathMap(library.Id), forceChecks); var scanElapsedTime = scanWatch.ElapsedMilliseconds; @@ -855,29 +813,29 @@ public class ScannerService : IScannerService /// private async Task CreateAllGenresAsync(ICollection genres) { - _logger.LogInformation("[ScannerService] Attempting to pre-save all Genres"); + logger.LogInformation("[ScannerService] Attempting to pre-save all Genres"); try { // Pass the non-normalized genres directly to the repository - var nonExistingGenres = await _unitOfWork.GenreRepository.GetAllGenresNotInListAsync(genres); + var nonExistingGenres = await unitOfWork.GenreRepository.GetAllGenresNotInListAsync(genres); // Create and attach new genres using the non-normalized names foreach (var genre in nonExistingGenres) { var newGenre = new GenreBuilder(genre).Build(); - _unitOfWork.GenreRepository.Attach(newGenre); + unitOfWork.GenreRepository.Attach(newGenre); } // Commit changes if (nonExistingGenres.Count > 0) { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); } } catch (Exception ex) { - _logger.LogError(ex, "[ScannerService] There was an unknown issue when pre-saving all Genres"); + logger.LogError(ex, "[ScannerService] There was an unknown issue when pre-saving all Genres"); } } @@ -888,29 +846,29 @@ public class ScannerService : IScannerService /// private async Task CreateAllTagsAsync(ICollection tags) { - _logger.LogInformation("[ScannerService] Attempting to pre-save all Tags"); + logger.LogInformation("[ScannerService] Attempting to pre-save all Tags"); try { // Pass the non-normalized tags directly to the repository - var nonExistingTags = await _unitOfWork.TagRepository.GetAllTagsNotInListAsync(tags); + var nonExistingTags = await unitOfWork.TagRepository.GetAllTagsNotInListAsync(tags); // Create and attach new genres using the non-normalized names foreach (var tag in nonExistingTags) { var newTag = new TagBuilder(tag).Build(); - _unitOfWork.TagRepository.Attach(newTag); + unitOfWork.TagRepository.Attach(newTag); } // Commit changes if (nonExistingTags.Count > 0) { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); } } catch (Exception ex) { - _logger.LogError(ex, "[ScannerService] There was an unknown issue when pre-saving all Tags"); + logger.LogError(ex, "[ScannerService] There was an unknown issue when pre-saving all Tags"); } } } diff --git a/API/Services/SeriesService.cs b/Kavita.Services/SeriesService.cs similarity index 81% rename from API/Services/SeriesService.cs rename to Kavita.Services/SeriesService.cs index 2324a8dca..4c01d05e2 100644 --- a/API/Services/SeriesService.cs +++ b/Kavita.Services/SeriesService.cs @@ -1,56 +1,48 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Comparators; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; -using API.DTOs.Person; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Helpers.Formatting; -using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; +using Kavita.Services.Comparators; +using Kavita.Services.Extensions; +using Kavita.Services.Helpers; +using Kavita.Services.Plus; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface ISeriesService +public class SeriesService( + IUnitOfWork unitOfWork, + IEventHub eventHub, + ITaskScheduler taskScheduler, + ILogger logger, + ILocalizationService localizationService, + IReadingListService readingListService, + IEntityNamingService namingService) + : ISeriesService { - Task GetSeriesDetail(int seriesId, int userId); - Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto); - Task DeleteMultipleSeries(IList seriesIds); - Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto); - Task GetRelatedSeries(int userId, int seriesId); - Task GetEstimatedChapterCreationDate(int seriesId, int userId); - Task> GetCurrentlyReading(int userId, int requestingUserId, UserParams userParams); - Task> GetProfilePrivacyStatements(int userId, int requestingUserId); -} - -public class SeriesService : ISeriesService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - private readonly ITaskScheduler _taskScheduler; - private readonly ILogger _logger; - private readonly ILocalizationService _localizationService; - private readonly IReadingListService _readingListService; - private readonly IEntityNamingService _namingService; - private readonly NextExpectedChapterDto _emptyExpectedChapter = new NextExpectedChapterDto { ExpectedDate = null, @@ -58,19 +50,6 @@ public class SeriesService : ISeriesService VolumeNumber = Parser.LooseLeafVolumeNumber }; - public SeriesService(IUnitOfWork unitOfWork, IEventHub eventHub, ITaskScheduler taskScheduler, - ILogger logger, ILocalizationService localizationService, IReadingListService readingListService, - IEntityNamingService namingService) - { - _unitOfWork = unitOfWork; - _eventHub = eventHub; - _taskScheduler = taskScheduler; - _logger = logger; - _localizationService = localizationService; - _readingListService = readingListService; - _namingService = namingService; - } - /// /// Returns the first chapter for a series to extract metadata from (ie Summary, etc.) /// @@ -103,13 +82,15 @@ public class SeriesService : ISeriesService /// Updates the Series Metadata. /// /// + /// /// - public async Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto) + public async Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto, + CancellationToken ct = default) { try { var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata, ct); if (series == null) return false; series.Metadata ??= new SeriesMetadataBuilder() @@ -164,7 +145,7 @@ public class SeriesService : ISeriesService if (updateSeriesMetadataDto.SeriesMetadata?.Genres != null && updateSeriesMetadataDto.SeriesMetadata.Genres.Count != 0) { - var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresByNamesAsync(updateSeriesMetadataDto.SeriesMetadata.Genres.Select(t => Parser.Normalize(t.Title)))).ToList(); + var allGenres = (await unitOfWork.GenreRepository.GetAllGenresByNamesAsync(updateSeriesMetadataDto.SeriesMetadata.Genres.Select(t => Parser.Normalize(t.Title)), ct)).ToList(); series.Metadata.Genres ??= []; GenreHelper.UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, genre => { @@ -179,8 +160,8 @@ public class SeriesService : ISeriesService if (updateSeriesMetadataDto.SeriesMetadata?.Tags is {Count: > 0}) { - var allTags = (await _unitOfWork.TagRepository - .GetAllTagsByNameAsync(updateSeriesMetadataDto.SeriesMetadata.Tags.Select(t => Parser.Normalize(t.Title)))) + var allTags = (await unitOfWork.TagRepository + .GetAllTagsByNameAsync(updateSeriesMetadataDto.SeriesMetadata.Tags.Select(t => Parser.Normalize(t.Title)), ct)) .ToList(); series.Metadata.Tags ??= []; TagHelper.UpdateTagList(updateSeriesMetadataDto.SeriesMetadata?.Tags, series, allTags, tag => @@ -197,14 +178,14 @@ public class SeriesService : ISeriesService { series.Metadata.AgeRating = updateSeriesMetadataDto.SeriesMetadata?.AgeRating ?? AgeRating.Unknown; series.Metadata.AgeRatingLocked = true; - await _readingListService.UpdateReadingListAgeRatingForSeries(series.Id, series.Metadata.AgeRating); + await readingListService.UpdateReadingListAgeRatingForSeries(series.Id, series.Metadata.AgeRating); series.Metadata.KPlusOverrides.Remove(MetadataSettingField.AgeRating); } else { if (!series.Metadata.AgeRatingLocked) { - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); + var metadataSettings = await unitOfWork.SettingsRepository.GetMetadataSettingDto(ct); var allTags = series.Metadata.Tags.Select(t => t.Title).Concat(series.Metadata.Genres.Select(g => g.Title)); if (metadataSettings.EnableExtendedMetadataProcessing) @@ -228,79 +209,79 @@ public class SeriesService : ISeriesService // Writers if (!series.Metadata.WriterLocked || !updateSeriesMetadataDto.SeriesMetadata.WriterLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Writers, PersonRole.Writer, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Writers, PersonRole.Writer, unitOfWork); } // Cover Artists if (!series.Metadata.CoverArtistLocked || !updateSeriesMetadataDto.SeriesMetadata.CoverArtistLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, PersonRole.CoverArtist, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, PersonRole.CoverArtist, unitOfWork); } // Colorists if (!series.Metadata.ColoristLocked || !updateSeriesMetadataDto.SeriesMetadata.ColoristLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Colorists, PersonRole.Colorist, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Colorists, PersonRole.Colorist, unitOfWork); } // Editors if (!series.Metadata.EditorLocked || !updateSeriesMetadataDto.SeriesMetadata.EditorLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Editors, PersonRole.Editor, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Editors, PersonRole.Editor, unitOfWork); } // Inkers if (!series.Metadata.InkerLocked || !updateSeriesMetadataDto.SeriesMetadata.InkerLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Inkers, PersonRole.Inker, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Inkers, PersonRole.Inker, unitOfWork); } // Letterers if (!series.Metadata.LettererLocked || !updateSeriesMetadataDto.SeriesMetadata.LettererLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Letterers, PersonRole.Letterer, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Letterers, PersonRole.Letterer, unitOfWork); } // Pencillers if (!series.Metadata.PencillerLocked || !updateSeriesMetadataDto.SeriesMetadata.PencillerLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Pencillers, PersonRole.Penciller, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Pencillers, PersonRole.Penciller, unitOfWork); } // Publishers if (!series.Metadata.PublisherLocked || !updateSeriesMetadataDto.SeriesMetadata.PublisherLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Publishers, PersonRole.Publisher, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Publishers, PersonRole.Publisher, unitOfWork); } // Imprints if (!series.Metadata.ImprintLocked || !updateSeriesMetadataDto.SeriesMetadata.ImprintLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Imprints, PersonRole.Imprint, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Imprints, PersonRole.Imprint, unitOfWork); } // Teams if (!series.Metadata.TeamLocked || !updateSeriesMetadataDto.SeriesMetadata.TeamLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Teams, PersonRole.Team, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Teams, PersonRole.Team, unitOfWork); } // Locations if (!series.Metadata.LocationLocked || !updateSeriesMetadataDto.SeriesMetadata.LocationLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Locations, PersonRole.Location, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Locations, PersonRole.Location, unitOfWork); } // Translators if (!series.Metadata.TranslatorLocked || !updateSeriesMetadataDto.SeriesMetadata.TranslatorLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Translators, PersonRole.Translator, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Translators, PersonRole.Translator, unitOfWork); } // Characters if (!series.Metadata.CharacterLocked || !updateSeriesMetadataDto.SeriesMetadata.CharacterLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Characters, PersonRole.Character, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Characters, PersonRole.Character, unitOfWork); } series.Metadata.AgeRatingLocked = updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked; @@ -324,30 +305,30 @@ public class SeriesService : ISeriesService series.Metadata.ReleaseYearLocked = updateSeriesMetadataDto.SeriesMetadata.ReleaseYearLocked; } - if (!_unitOfWork.HasChanges()) + if (!unitOfWork.HasChanges()) { return true; } - _unitOfWork.SeriesRepository.Update(series.Metadata); - await _unitOfWork.CommitAsync(); + unitOfWork.SeriesRepository.Update(series.Metadata); + await unitOfWork.CommitAsync(ct); // Trigger code to clean up tags, collections, people, etc try { - await _taskScheduler.CleanupDbEntries(); + await taskScheduler.CleanupDbEntries(); } catch (Exception ex) { - _logger.LogError(ex, "There was an issue cleaning up DB entries. This may happen if Komf is spamming updates. Nightly cleanup will work"); + logger.LogError(ex, "There was an issue cleaning up DB entries. This may happen if Komf is spamming updates. Nightly cleanup will work"); } return true; } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when updating metadata"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was an exception when updating metadata"); + await unitOfWork.RollbackAsync(ct); } return false; @@ -361,7 +342,7 @@ public class SeriesService : ISeriesService /// public static async Task HandlePeopleUpdateAsync(SeriesMetadata metadata, ICollection peopleDtos, PersonRole role, IUnitOfWork unitOfWork) { - // TODO: Cleanup this code so we aren't using UnitOfWork like this + // default: Cleanup this code so we aren't using UnitOfWork like this // Normalize all names from the DTOs var normalizedNames = peopleDtos @@ -385,7 +366,7 @@ public class SeriesService : ISeriesService // Check if the person exists in the dictionary if (existingPeopleDictionary.TryGetValue(normalizedPersonName, out var p)) { - // TODO: Should I add more controls here to map back? + // default: Should I add more controls here to map back? if (personDto.AniListId > 0 && p.AniListId <= 0 && p.AniListId != personDto.AniListId) { p.AniListId = personDto.AniListId; @@ -456,12 +437,12 @@ public class SeriesService : ISeriesService } - public async Task DeleteMultipleSeries(IList seriesIds) + public async Task DeleteMultipleSeries(IList seriesIds, CancellationToken ct = default) { try { var chapterMappings = - await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync([.. seriesIds]); + await unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync([.. seriesIds], ct); var allChapterIds = new List(); foreach (var mapping in chapterMappings) @@ -470,34 +451,34 @@ public class SeriesService : ISeriesService } // NOTE: This isn't getting all the people and whatnot currently due to the lack of includes - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(seriesIds); - _unitOfWork.SeriesRepository.Remove(series); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdsAsync(seriesIds, ct: ct); + unitOfWork.SeriesRepository.Remove(series); var libraryIds = series.Select(s => s.LibraryId); - var libraries = await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(libraryIds); + var libraries = await unitOfWork.LibraryRepository.GetLibraryForIdsAsync(libraryIds, ct: ct); foreach (var library in libraries) { library.UpdateLastModified(); - _unitOfWork.LibraryRepository.Update(library); + unitOfWork.LibraryRepository.Update(library); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); foreach (var s in series) { - await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, - MessageFactory.SeriesRemovedEvent(s.Id, s.Name, s.LibraryId), false); + await eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, + MessageFactory.SeriesRemovedEvent(s.Id, s.Name, s.LibraryId), false, ct); } - await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); - await _unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(); - _taskScheduler.CleanupChapters([.. allChapterIds]); + await unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(ct); + await unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(ct); + taskScheduler.CleanupChapters([.. allChapterIds]); return true; } catch (Exception ex) { - _logger.LogError(ex, "There was an issue when trying to delete multiple series"); + logger.LogError(ex, "There was an issue when trying to delete multiple series"); return false; } } @@ -507,28 +488,29 @@ public class SeriesService : ISeriesService ///
    /// /// + /// /// - public async Task GetSeriesDetail(int seriesId, int userId) + public async Task GetSeriesDetail(int seriesId, int userId, CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); - if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct); + if (series == null) throw new KavitaException(await localizationService.Translate(userId, "series-doesnt-exist")); - var libraryIds = await _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId); + var libraryIds = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct); if (!libraryIds.Contains(series.LibraryId)) throw new UnauthorizedAccessException("user-no-access-library-from-series"); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, ct: ct); if (user!.AgeRestriction != AgeRating.NotApplicable) { - var seriesMetadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId); + var seriesMetadata = await unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId, ct); if (seriesMetadata!.AgeRating > user.AgeRestriction) throw new UnauthorizedAccessException("series-restricted-age-restriction"); } - var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); - var volumes = await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId); - var namingContext = await LocalizedNamingContext.CreateAsync(_namingService, _localizationService, userId, libraryType); + var libraryType = await unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct); + var volumes = await unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId, ct: ct); + var namingContext = await LocalizedNamingContext.CreateAsync(namingService, localizationService, userId, libraryType); var bookTreatment = libraryType is LibraryType.Book or LibraryType.LightNovel; // For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number. @@ -596,7 +578,7 @@ public class SeriesService : ISeriesService StorylineChapters = storylineChapters, TotalCount = chapters.Count, UnreadCount = chapters.Count(c => c.Pages > 0 && c.PagesRead < c.Pages), - // TODO: See if we can get the ContinueFrom here + // default: See if we can get the ContinueFrom here }; } @@ -615,20 +597,22 @@ public class SeriesService : ISeriesService ///
    /// /// + /// /// - public async Task GetRelatedSeries(int userId, int seriesId) + public async Task GetRelatedSeries(int userId, int seriesId, CancellationToken ct = default) { - return await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId); + return await unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId, ct); } /// /// Update the relations attached to the Series. Generates associated Sequel/Prequel pairs on target series. /// /// + /// /// - public async Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto) + public async Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto, CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related, ct); if (series == null) return false; UpdateRelationForKind(dto.Adaptations, series.Relations.Where(r => r.RelationKind == RelationKind.Adaptation).ToList(), series, RelationKind.Adaptation); @@ -646,8 +630,8 @@ public class SeriesService : ISeriesService await UpdatePrequelSequelRelations(dto.Prequels, series, RelationKind.Prequel); await UpdatePrequelSequelRelations(dto.Sequels, series, RelationKind.Sequel); - if (!_unitOfWork.HasChanges()) return true; - return await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return true; + return await unitOfWork.CommitAsync(ct); } /// @@ -684,7 +668,7 @@ public class SeriesService : ISeriesService await AddReciprocalRelation(series.Id, targetSeriesId, GetOppositeRelationKind(kind)); } - _unitOfWork.SeriesRepository.Update(series); + unitOfWork.SeriesRepository.Update(series); } private static RelationKind GetOppositeRelationKind(RelationKind kind) @@ -694,7 +678,7 @@ public class SeriesService : ISeriesService private async Task AddReciprocalRelation(int sourceSeriesId, int targetSeriesId, RelationKind kind) { - var targetSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(targetSeriesId, SeriesIncludes.Related); + var targetSeries = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(targetSeriesId, SeriesIncludes.Related); if (targetSeries == null) return; if (targetSeries.Relations.Any(r => r.RelationKind == kind && r.TargetSeriesId == sourceSeriesId)) @@ -708,19 +692,19 @@ public class SeriesService : ISeriesService RelationKind = kind }); - _unitOfWork.SeriesRepository.Update(targetSeries); + unitOfWork.SeriesRepository.Update(targetSeries); } private async Task RemoveReciprocalRelation(int sourceSeriesId, int targetSeriesId, RelationKind kind) { - var targetSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(targetSeriesId, SeriesIncludes.Related); + var targetSeries = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(targetSeriesId, SeriesIncludes.Related); if (targetSeries == null) return; var relationToRemove = targetSeries.Relations.FirstOrDefault(r => r.RelationKind == kind && r.TargetSeriesId == sourceSeriesId); if (relationToRemove != null) { targetSeries.Relations.Remove(relationToRemove); - _unitOfWork.SeriesRepository.Update(targetSeries); + unitOfWork.SeriesRepository.Update(targetSeries); } } @@ -755,15 +739,16 @@ public class SeriesService : ISeriesService TargetSeriesId = targetSeriesId, RelationKind = kind }); - _unitOfWork.SeriesRepository.Update(series); + unitOfWork.SeriesRepository.Update(series); } } - public async Task GetEstimatedChapterCreationDate(int seriesId, int userId) + public async Task GetEstimatedChapterCreationDate(int seriesId, int userId, + CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library); - if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - if (!(await _unitOfWork.UserRepository.HasAccessToSeries(userId, seriesId))) + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library, ct); + if (series == null) throw new KavitaException(await localizationService.Translate(userId, "series-doesnt-exist")); + if (!(await unitOfWork.UserRepository.HasAccessToSeries(userId, seriesId, ct))) { throw new UnauthorizedAccessException("user-no-access-library-from-series"); } @@ -783,7 +768,7 @@ public class SeriesService : ISeriesService // Only fetch the fields we need for calculation - avoids loading entire Chapter entities // with all their navigation properties, significantly reducing memory and query time - var chapterData = await _unitOfWork.ChapterRepository.GetChaptersForSeries(seriesId) + var chapterData = await unitOfWork.ChapterRepository.GetChaptersForSeries(seriesId) .Where(c => !c.IsSpecial) .Select(c => new { @@ -792,7 +777,7 @@ public class SeriesService : ISeriesService VolumeMinNumber = c.Volume.MinNumber }) .OrderBy(c => c.CreatedUtc) - .ToListAsync(); + .ToListAsync(cancellationToken: ct); if (chapterData.Count < minimumChaptersRequired) return _emptyExpectedChapter; @@ -903,27 +888,28 @@ public class SeriesService : ISeriesService // Manga uses "Chapter X", Comics use "Issue #X", Books use "Book X" result.Title = series.Library.Type switch { - LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber), - LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", "#", result.ChapterNumber), - LibraryType.ComicVine => await _localizationService.Translate(userId, "issue-num", "#", result.ChapterNumber), - LibraryType.Book => await _localizationService.Translate(userId, "book-num", result.ChapterNumber), - LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", result.ChapterNumber), - _ => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber) + LibraryType.Manga => await localizationService.Translate(userId, "chapter-num", result.ChapterNumber), + LibraryType.Comic => await localizationService.Translate(userId, "issue-num", "#", result.ChapterNumber), + LibraryType.ComicVine => await localizationService.Translate(userId, "issue-num", "#", result.ChapterNumber), + LibraryType.Book => await localizationService.Translate(userId, "book-num", result.ChapterNumber), + LibraryType.LightNovel => await localizationService.Translate(userId, "book-num", result.ChapterNumber), + _ => await localizationService.Translate(userId, "chapter-num", result.ChapterNumber) }; } else { // Volume-only numbering - common for omnibus editions or series without chapter breaks result.VolumeNumber = (int)highestVolumeNumber + 1; - result.Title = await _localizationService.Translate(userId, "volume-num", result.VolumeNumber); + result.Title = await localizationService.Translate(userId, "volume-num", result.VolumeNumber); } return result; } - public async Task> GetCurrentlyReading(int userId, int requestingUserId, UserParams userParams) + public async Task> GetCurrentlyReading(int userId, int requestingUserId, UserParams userParams, + CancellationToken ct = default) { - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var serverSettings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); var filter = new FilterV2Dto { @@ -955,20 +941,21 @@ public class SeriesService : ISeriesService ], }; - filter.Statements.AddRange(await GetProfilePrivacyStatements(userId, requestingUserId)); + filter.Statements.AddRange(await GetProfilePrivacyStatements(userId, requestingUserId, ct)); - return await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filter); + return await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filter, ct: ct); } - public async Task> GetProfilePrivacyStatements(int userId, int requestingUserId) + public async Task> GetProfilePrivacyStatements(int userId, int requestingUserId, + CancellationToken ct = default) { if (userId == requestingUserId) return []; - var socialPreferences = await _unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = (await _unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId))!; + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = (await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct))!; - var librariesUser = await _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId); - var librariesRequestingUser = await _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(requestingUserId); + var librariesUser = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct); + var librariesRequestingUser = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(requestingUserId, ct: ct); var libIds = librariesRequestingUser.Intersect(librariesUser); if (socialPreferences.SocialLibraries.Count > 0) diff --git a/API/Services/SettingsService.cs b/Kavita.Services/SettingsService.cs similarity index 78% rename from API/Services/SettingsService.cs rename to Kavita.Services/SettingsService.cs index 9f961ee68..fd41eca0b 100644 --- a/API/Services/SettingsService.cs +++ b/Kavita.Services/SettingsService.cs @@ -4,79 +4,50 @@ using System.Linq; using System.Net; using System.Security.Claims; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.MetadataMatching; -using API.Extensions; -using API.Logging; -using API.Services.Tasks.Scanner; using Flurl.Http; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Protocols.OpenIdConnect; -namespace API.Services; +namespace Kavita.Services; -public interface ISettingsService + +public class SettingsService( + IUnitOfWork unitOfWork, + IDirectoryService directoryService, + ILibraryWatcher libraryWatcher, + ITaskScheduler taskScheduler, + ILogger logger, + IOidcService oidcService, + ILoggingService loggingService) + : ISettingsService { - Task UpdateMetadataSettings(MetadataSettingsDto dto); - /// - /// Update , , , - /// with data from the given dto. - /// - /// - /// - /// - Task ImportFieldMappings(FieldMappingsDto dto, ImportSettingsDto settings); - Task UpdateSettings(ServerSettingDto updateSettingsDto); - /// - /// Check if the server can reach the authority at the given uri - /// - /// - /// - Task IsValidAuthority(string authority); -} - - -public class SettingsService : ISettingsService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly ILibraryWatcher _libraryWatcher; - private readonly ITaskScheduler _taskScheduler; - private readonly ILogger _logger; - private readonly IOidcService _oidcService; private readonly bool _isDevelopment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development; - public SettingsService(IUnitOfWork unitOfWork, IDirectoryService directoryService, - ILibraryWatcher libraryWatcher, ITaskScheduler taskScheduler, - ILogger logger, IOidcService oidcService) - { - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _libraryWatcher = libraryWatcher; - _taskScheduler = taskScheduler; - _logger = logger; - _oidcService = oidcService; - } - /// /// Update the metadata settings for Kavita+ Metadata feature /// /// + /// /// - public async Task UpdateMetadataSettings(MetadataSettingsDto dto) + public async Task UpdateMetadataSettings(MetadataSettingsDto dto, CancellationToken ct = default) { - var existingMetadataSetting = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var existingMetadataSetting = await unitOfWork.SettingsRepository.GetMetadataSettings(ct); existingMetadataSetting.Enabled = dto.Enabled; existingMetadataSetting.EnableExtendedMetadataProcessing = dto.EnableExtendedMetadataProcessing; existingMetadataSetting.EnableSummary = dto.EnableSummary; @@ -107,7 +78,7 @@ public class SettingsService : ISettingsService // Clear existing mappings existingMetadataSetting.FieldMappings ??= []; - _unitOfWork.SettingsRepository.RemoveRange(existingMetadataSetting.FieldMappings); + unitOfWork.SettingsRepository.RemoveRange(existingMetadataSetting.FieldMappings); existingMetadataSetting.FieldMappings.Clear(); if (dto.FieldMappings != null) @@ -127,13 +98,14 @@ public class SettingsService : ISettingsService } // Save changes - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); // Return updated settings - return await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); + return await unitOfWork.SettingsRepository.GetMetadataSettingDto(ct); } - public async Task ImportFieldMappings(FieldMappingsDto dto, ImportSettingsDto settings) + public async Task ImportFieldMappings(FieldMappingsDto dto, + ImportSettingsDto settings, CancellationToken ct = default) { if (dto.AgeRatingMappings.Keys.Distinct().Count() != dto.AgeRatingMappings.Count) { @@ -161,7 +133,7 @@ public class SettingsService : ISettingsService /// private async Task ReplaceFieldMappings(FieldMappingsDto dto, ImportSettingsDto settings) { - var existingMetadataSetting = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); + var existingMetadataSetting = await unitOfWork.SettingsRepository.GetMetadataSettingDto(); if (settings.Whitelist) { @@ -199,7 +171,7 @@ public class SettingsService : ISettingsService /// private async Task MergeFieldMappings(FieldMappingsDto dto, ImportSettingsDto settings) { - var existingMetadataSetting = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); + var existingMetadataSetting = await unitOfWork.SettingsRepository.GetMetadataSettingDto(); if (settings.Whitelist) { @@ -281,26 +253,27 @@ public class SettingsService : ISettingsService /// Update Server Settings /// /// + /// /// /// - public async Task UpdateSettings(ServerSettingDto updateSettingsDto) + public async Task UpdateSettings(ServerSettingDto updateSettingsDto, CancellationToken ct = default) { // We do not allow CacheDirectory changes, so we will ignore. - var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync(); + var currentSettings = await unitOfWork.SettingsRepository.GetSettingsAsync(ct); var updateBookmarks = false; - var originalBookmarkDirectory = _directoryService.BookmarkDirectory; + var originalBookmarkDirectory = directoryService.BookmarkDirectory; var bookmarkDirectory = updateSettingsDto.BookmarksDirectory; if (!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks") && !updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks/")) { bookmarkDirectory = - _directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks"); + directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks"); } if (string.IsNullOrEmpty(updateSettingsDto.BookmarksDirectory)) { - bookmarkDirectory = _directoryService.BookmarkDirectory; + bookmarkDirectory = directoryService.BookmarkDirectory; } var updateTask = false; @@ -311,14 +284,14 @@ public class SettingsService : ISettingsService updateSettingsDto.OnDeckProgressDays + string.Empty != setting.Value) { setting.Value = updateSettingsDto.OnDeckProgressDays + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.OnDeckUpdateDays && updateSettingsDto.OnDeckUpdateDays + string.Empty != setting.Value) { setting.Value = updateSettingsDto.OnDeckUpdateDays + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value) @@ -327,7 +300,7 @@ public class SettingsService : ISettingsService setting.Value = updateSettingsDto.Port + string.Empty; // Port is managed in appSetting.json Configuration.Port = updateSettingsDto.Port; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.CacheSize && @@ -336,7 +309,7 @@ public class SettingsService : ISettingsService setting.Value = updateSettingsDto.CacheSize + string.Empty; // CacheSize is managed in appSetting.json Configuration.CacheSize = updateSettingsDto.CacheSize; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } updateTask = updateTask || UpdateSchedulingSettings(setting, updateSettingsDto); @@ -361,7 +334,7 @@ public class SettingsService : ISettingsService setting.Value = updateSettingsDto.IpAddresses; // IpAddresses is managed in appSetting.json Configuration.IpAddresses = updateSettingsDto.IpAddresses; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value) @@ -374,56 +347,56 @@ public class SettingsService : ISettingsService : path; setting.Value = path; Configuration.BaseUrl = updateSettingsDto.BaseUrl; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value) { setting.Value = updateSettingsDto.LoggingLevel + string.Empty; - LogLevelOptions.SwitchLogLevel(updateSettingsDto.LoggingLevel); - _unitOfWork.SettingsRepository.Update(setting); + loggingService.SwitchLogLevel(updateSettingsDto.LoggingLevel); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EnableOpds && updateSettingsDto.EnableOpds + string.Empty != setting.Value) { setting.Value = updateSettingsDto.EnableOpds + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EncodeMediaAs && ((int)updateSettingsDto.EncodeMediaAs).ToString() != setting.Value) { setting.Value = ((int)updateSettingsDto.EncodeMediaAs).ToString(); - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.CoverImageSize && ((int)updateSettingsDto.CoverImageSize).ToString() != setting.Value) { setting.Value = ((int)updateSettingsDto.CoverImageSize).ToString(); - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.PdfRenderResolution && ((int)updateSettingsDto.PdfRenderResolution).ToString() != setting.Value) { setting.Value = ((int)updateSettingsDto.PdfRenderResolution).ToString(); - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.HostName && updateSettingsDto.HostName + string.Empty != setting.Value) { setting.Value = (updateSettingsDto.HostName + string.Empty).Trim(); setting.Value = UrlHelper.RemoveEndingSlash(setting.Value); - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value) { // Validate new directory can be used - if (!await _directoryService.CheckWriteAccess(bookmarkDirectory)) + if (!await directoryService.CheckWriteAccess(bookmarkDirectory)) { throw new KavitaException("bookmark-dir-permissions"); } @@ -431,8 +404,8 @@ public class SettingsService : ISettingsService originalBookmarkDirectory = setting.Value; // Normalize the path deliminators. Just to look nice in DB, no functionality - setting.Value = _directoryService.FileSystem.Path.GetFullPath(bookmarkDirectory); - _unitOfWork.SettingsRepository.Update(setting); + setting.Value = directoryService.FileSystem.Path.GetFullPath(bookmarkDirectory); + unitOfWork.SettingsRepository.Update(setting); updateBookmarks = true; } @@ -441,7 +414,7 @@ public class SettingsService : ISettingsService updateSettingsDto.AllowStatCollection + string.Empty != setting.Value) { setting.Value = updateSettingsDto.AllowStatCollection + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.TotalBackups && @@ -453,7 +426,7 @@ public class SettingsService : ISettingsService } setting.Value = updateSettingsDto.TotalBackups + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.TotalLogs && @@ -465,30 +438,30 @@ public class SettingsService : ISettingsService } setting.Value = updateSettingsDto.TotalLogs + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EnableFolderWatching && updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value) { setting.Value = updateSettingsDto.EnableFolderWatching + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } } - if (!_unitOfWork.HasChanges()) return updateSettingsDto; + if (!unitOfWork.HasChanges()) return updateSettingsDto; try { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); if (!updateSettingsDto.AllowStatCollection) { - _taskScheduler.CancelStatsTasks(); + taskScheduler.CancelStatsTasks(); } else { - await _taskScheduler.ScheduleStatsTasks(); + await taskScheduler.ScheduleStatsTasks(); } if (updateBookmarks) @@ -498,7 +471,7 @@ public class SettingsService : ISettingsService if (updateTask) { - BackgroundJob.Enqueue(() => _taskScheduler.ScheduleTasks()); + BackgroundJob.Enqueue(() => taskScheduler.ScheduleTasks()); } if (updatedOidcSettings) @@ -514,27 +487,27 @@ public class SettingsService : ISettingsService if (updateSettingsDto.EnableFolderWatching) { - BackgroundJob.Enqueue(() => _libraryWatcher.StartWatching()); + BackgroundJob.Enqueue(() => libraryWatcher.StartWatching()); } else { - BackgroundJob.Enqueue(() => _libraryWatcher.StopWatching()); + BackgroundJob.Enqueue(() => libraryWatcher.StopWatching()); } } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when updating server settings"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was an exception when updating server settings"); + await unitOfWork.RollbackAsync(ct); throw new KavitaException("generic-error"); } - _logger.LogInformation("Server Settings updated"); + logger.LogInformation("Server Settings updated"); return updateSettingsDto; } - public async Task IsValidAuthority(string authority) + public async Task IsValidAuthority(string authority, CancellationToken ct = default) { if (string.IsNullOrEmpty(authority)) { @@ -551,22 +524,22 @@ public class SettingsService : ISettingsService var hasTrailingSlash = authority.EndsWith('/'); var url = authority + (hasTrailingSlash ? string.Empty : "/") + ".well-known/openid-configuration"; - var json = await url.GetStringAsync(); + var json = await url.GetStringAsync(cancellationToken: ct); var config = OpenIdConnectConfiguration.Create(json); return config.Issuer == authority; } catch (Exception e) { - _logger.LogDebug(e, "OpenIdConfiguration failed: {Reason}", e.Message); + logger.LogDebug(e, "OpenIdConfiguration failed: {Reason}", e.Message); return false; } } private void UpdateBookmarkDirectory(string originalBookmarkDirectory, string bookmarkDirectory) { - _directoryService.ExistOrCreate(bookmarkDirectory); - _directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory); - _directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory); + directoryService.ExistOrCreate(bookmarkDirectory); + directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory); + directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory); } private bool UpdateSchedulingSettings(ServerSetting setting, ServerSettingDto updateSettingsDto) @@ -574,7 +547,7 @@ public class SettingsService : ISettingsService if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value) { setting.Value = updateSettingsDto.TaskBackup; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); return true; } @@ -582,14 +555,14 @@ public class SettingsService : ISettingsService if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value) { setting.Value = updateSettingsDto.TaskScan; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); return true; } if (setting.Key == ServerSettingKey.TaskCleanup && updateSettingsDto.TaskCleanup != setting.Value) { setting.Value = updateSettingsDto.TaskCleanup; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); return true; } return false; @@ -631,12 +604,12 @@ public class SettingsService : ISettingsService throw new KavitaException("oidc-invalid-authority"); } - _logger.LogWarning("OIDC Authority is changing, clearing all external ids"); - await _oidcService.ClearOidcIds(); + logger.LogWarning("OIDC Authority is changing, clearing all external ids"); + await oidcService.ClearOidcIds(); } setting.Value = newValue; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); return true; } @@ -647,63 +620,63 @@ public class SettingsService : ISettingsService updateSettingsDto.SmtpConfig.Host + string.Empty != setting.Value) { setting.Value = updateSettingsDto.SmtpConfig.Host + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EmailPort && updateSettingsDto.SmtpConfig.Port + string.Empty != setting.Value) { setting.Value = updateSettingsDto.SmtpConfig.Port + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EmailAuthPassword && updateSettingsDto.SmtpConfig.Password + string.Empty != setting.Value) { setting.Value = updateSettingsDto.SmtpConfig.Password + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EmailAuthUserName && updateSettingsDto.SmtpConfig.UserName + string.Empty != setting.Value) { setting.Value = updateSettingsDto.SmtpConfig.UserName + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EmailSenderAddress && updateSettingsDto.SmtpConfig.SenderAddress + string.Empty != setting.Value) { setting.Value = updateSettingsDto.SmtpConfig.SenderAddress + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EmailSenderDisplayName && updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty != setting.Value) { setting.Value = updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EmailSizeLimit && updateSettingsDto.SmtpConfig.SizeLimit + string.Empty != setting.Value) { setting.Value = updateSettingsDto.SmtpConfig.SizeLimit + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EmailEnableSsl && updateSettingsDto.SmtpConfig.EnableSsl + string.Empty != setting.Value) { setting.Value = updateSettingsDto.SmtpConfig.EnableSsl + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EmailCustomizedTemplates && updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty != setting.Value) { setting.Value = updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } } } diff --git a/Kavita.Services/SignalR/EventHub.cs b/Kavita.Services/SignalR/EventHub.cs new file mode 100644 index 000000000..cbe0e2bae --- /dev/null +++ b/Kavita.Services/SignalR/EventHub.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Services.SignalR; +using Kavita.Models.DTOs.SignalR; +using Microsoft.AspNetCore.SignalR; + +namespace Kavita.Services.SignalR; + +public class EventHub(IHubContext messageHub, IPresenceTracker presenceTracker, IUnitOfWork unitOfWork) + : IEventHub +{ + // TODO: When sending a message, queue the message up and on re-connect, reply the queued messages. Queue messages expire on a rolling basis (rolling array) + + public async Task SendMessageAsync(string method, SignalRMessage message, bool onlyAdmins = true, CancellationToken ct = default) + { + var users = messageHub.Clients.All; + if (onlyAdmins) + { + var admins = await presenceTracker.GetOnlineAdminIds(); + users = messageHub.Clients.Users(admins.Select(i => i.ToString()).ToArray()); + } + else + { + users = await FilterClientsIfNeeded(users, message, ct); + } + + await users.SendAsync(method, message, cancellationToken: ct); + } + + private async Task FilterClientsIfNeeded(IClientProxy proxy, SignalRMessage message, CancellationToken ct) + { + var libraryId = GetBodyProperty(message.Body, "LibraryId"); + var seriesId = GetBodyProperty(message.Body, "SeriesId"); + + if (!libraryId.HasValue && !seriesId.HasValue) return proxy; + + var admins = await presenceTracker.GetOnlineAdminIds(); + var nonAdmins = await presenceTracker.GetOnlineUserIds(); + + List usersWithAccess = []; + + if (seriesId.HasValue) + { + foreach (var user in nonAdmins) + { + if (await unitOfWork.UserRepository.HasAccessToSeries(user, seriesId.Value, ct)) + usersWithAccess.Add(user); + } + } + else if (libraryId.HasValue) + { + foreach (var user in nonAdmins) + { + if (await unitOfWork.UserRepository.HasAccessToLibrary(user, libraryId.Value, ct)) + usersWithAccess.Add(user); + } + } + + usersWithAccess.AddRange(admins); + + return messageHub.Clients.Users(usersWithAccess.Select(i => i.ToString()).ToArray()); + } + + private static T? GetBodyProperty(object? body, string propertyName) + { + if (body is null) return default; + + var value = body.GetType() + .GetProperty(propertyName) + ?.GetValue(body); + + return value is T typed ? typed : default; + } + + + /// + /// Sends a message directly to a user if they are connected + /// + /// + /// + /// + /// + /// + public async Task SendMessageToAsync(string method, SignalRMessage message, int userId, CancellationToken ct = default) + { + await messageHub.Clients.Users([userId + string.Empty]).SendAsync(method, message, cancellationToken: ct); + } + +} diff --git a/API/SignalR/LogHub.cs b/Kavita.Services/SignalR/LogHub.cs similarity index 84% rename from API/SignalR/LogHub.cs rename to Kavita.Services/SignalR/LogHub.cs index 3a79eed0b..44e592800 100644 --- a/API/SignalR/LogHub.cs +++ b/Kavita.Services/SignalR/LogHub.cs @@ -1,17 +1,16 @@ using System; using System.Threading.Tasks; -using API.Extensions; -using API.Middleware; -using API.SignalR.Presence; +using Kavita.API.Attributes; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.SignalR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; +using Serilog.Sinks.AspNetCore.SignalR.Interfaces; -namespace API.SignalR; -#nullable enable +namespace Kavita.Services.SignalR; -public interface ILogHub : Serilog.Sinks.AspNetCore.SignalR.Interfaces.IHub -{ -} +public interface ILogHub : IHub; [Authorize] [SkipDeviceTracking] diff --git a/API/SignalR/MessageHub.cs b/Kavita.Services/SignalR/MessageHub.cs similarity index 87% rename from API/SignalR/MessageHub.cs rename to Kavita.Services/SignalR/MessageHub.cs index 453fff1e4..eae8ab48a 100644 --- a/API/SignalR/MessageHub.cs +++ b/Kavita.Services/SignalR/MessageHub.cs @@ -1,13 +1,13 @@ using System; using System.Threading.Tasks; -using API.Extensions; -using API.Middleware; -using API.SignalR.Presence; +using Kavita.API.Attributes; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.SignalR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; -namespace API.SignalR; -#nullable enable +namespace Kavita.Services.SignalR; /// /// Generic hub for sending messages to UI diff --git a/API/SignalR/Presence/PresenceTracker.cs b/Kavita.Services/SignalR/PresenceTracker.cs similarity index 86% rename from API/SignalR/Presence/PresenceTracker.cs rename to Kavita.Services/SignalR/PresenceTracker.cs index 62ab400ce..26d7b92d3 100644 --- a/API/SignalR/Presence/PresenceTracker.cs +++ b/Kavita.Services/SignalR/PresenceTracker.cs @@ -1,18 +1,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; +using Kavita.API.Database; +using Kavita.API.Services.SignalR; -namespace API.SignalR.Presence; -#nullable enable - -public interface IPresenceTracker -{ - Task UserConnected(int userId, string connectionId); - Task UserDisconnected(int userId, string connectionId); - Task GetOnlineAdminIds(); - Task> GetConnectionsForUser(int userId); -} +namespace Kavita.Services.SignalR; internal sealed record ConnectionDetail { @@ -97,6 +89,20 @@ public class PresenceTracker(IUnitOfWork unitOfWork) : IPresenceTracker return Task.FromResult(onlineUsers); } + public Task GetOnlineUserIds() + { + int[] onlineUsers; + lock (OnlineUsers) + { + onlineUsers = OnlineUsers.Where(pair => !pair.Value.IsAdmin) + .Select(k => k.Key) + .Order() + .ToArray(); + } + + return Task.FromResult(onlineUsers); + } + public Task> GetConnectionsForUser(int userId) { List? connectionIds; diff --git a/API/Services/SiteThemeService.cs b/Kavita.Services/SiteThemeService.cs similarity index 60% rename from API/Services/SiteThemeService.cs rename to Kavita.Services/SiteThemeService.cs index 6a22f724b..d9ef797e8 100644 --- a/API/Services/SiteThemeService.cs +++ b/Kavita.Services/SiteThemeService.cs @@ -3,26 +3,28 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Theme; -using API.Entities; -using API.Entities.Enums.Theme; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using Flurl.Http; using HtmlAgilityPack; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.Theme; +using Kavita.Services.Scanner; using MarkdownDeep; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Newtonsoft.Json; -namespace API.Services; -#nullable enable +namespace Kavita.Services; internal class GitHubContent { @@ -54,34 +56,19 @@ internal class ThemeMetadata public Version LastCompatible { get; set; } } - -public interface IThemeService +public class ThemeService( + IDirectoryService directoryService, + IUnitOfWork unitOfWork, + IEventHub eventHub, + ILogger logger, + IMemoryCache cache) + : IThemeService { - Task GetContent(int themeId); - Task UpdateDefault(int themeId); - /// - /// Browse theme repo for themes to download - /// - /// - Task> GetDownloadableThemes(); - - Task DownloadRepoTheme(DownloadableSiteThemeDto dto); - Task DeleteTheme(int siteThemeId); - Task CreateThemeFromFile(string tempFile, string username); - Task SyncThemes(); -} - - - -public class ThemeService : IThemeService -{ - private readonly IDirectoryService _directoryService; - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - private readonly ILogger _logger; private readonly Markdown _markdown = new(); - private readonly IMemoryCache _cache; - private readonly MemoryCacheEntryOptions _cacheOptions; + + private readonly MemoryCacheEntryOptions _cacheOptions = new MemoryCacheEntryOptions() + .SetSize(1) + .SetAbsoluteExpiration(TimeSpan.FromMinutes(30)); private const string GithubBaseUrl = "https://api.github.com"; @@ -90,44 +77,31 @@ public class ThemeService : IThemeService /// private const string GithubReadme = "https://raw.githubusercontent.com/Kareadita/Themes/main/README.md"; - public ThemeService(IDirectoryService directoryService, IUnitOfWork unitOfWork, - IEventHub eventHub, ILogger logger, IMemoryCache cache) - { - _directoryService = directoryService; - _unitOfWork = unitOfWork; - _eventHub = eventHub; - _logger = logger; - _cache = cache; - - _cacheOptions = new MemoryCacheEntryOptions() - .SetSize(1) - .SetAbsoluteExpiration(TimeSpan.FromMinutes(30)); - } - /// /// Given a themeId, return the content inside that file /// /// + /// /// - public async Task GetContent(int themeId) + public async Task GetContent(int themeId, CancellationToken ct = default) { - var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId) ?? throw new KavitaException("theme-doesnt-exist"); - var themeFile = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, theme.FileName); - if (string.IsNullOrEmpty(themeFile) || !_directoryService.FileSystem.File.Exists(themeFile)) + var theme = await unitOfWork.SiteThemeRepository.GetThemeDto(themeId) ?? throw new KavitaException("theme-doesnt-exist"); + var themeFile = directoryService.FileSystem.Path.Join(directoryService.SiteThemeDirectory, theme.FileName); + if (string.IsNullOrEmpty(themeFile) || !directoryService.FileSystem.File.Exists(themeFile)) throw new KavitaException("theme-doesnt-exist"); - return await _directoryService.FileSystem.File.ReadAllTextAsync(themeFile); + return await directoryService.FileSystem.File.ReadAllTextAsync(themeFile, ct); } - public async Task> GetDownloadableThemes() + public async Task> GetDownloadableThemes(CancellationToken ct = default) { const string cacheKey = "browse"; // Avoid a duplicate Dark issue some users faced during migration - var existingThemes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos()) + var existingThemes = (await unitOfWork.SiteThemeRepository.GetThemeDtos()) .GroupBy(k => k.Name) .ToDictionary(g => g.Key, g => g.First()); - if (_cache.TryGetValue(cacheKey, out List? themes) && themes != null) + if (cache.TryGetValue(cacheKey, out List? themes) && themes != null) { foreach (var t in themes) { @@ -137,7 +111,7 @@ public class ThemeService : IThemeService } // Fetch contents of the Native Themes directory - var themesContents = await GetDirectoryContent("Native%20Themes"); + var themesContents = await GetDirectoryContent("Native%20Themes", ct); // Filter out directories var themeDirectories = themesContents.Where(c => c.Type == "dir").ToList(); @@ -151,7 +125,7 @@ public class ThemeService : IThemeService var themeName = themeDir.Name.Trim(); // Fetch contents of the theme directory - var themeContents = await GetDirectoryContent(themeDir.Path); + var themeContents = await GetDirectoryContent(themeDir.Path, ct); // Find css and preview files @@ -185,7 +159,7 @@ public class ThemeService : IThemeService themeDtos.Add(dto); } - _cache.Set(cacheKey, themeDtos, _cacheOptions); + cache.Set(cacheKey, themeDtos, _cacheOptions); return themeDtos; } @@ -198,14 +172,14 @@ public class ThemeService : IThemeService .ToList(); } - private static async Task> GetDirectoryContent(string path) + private static async Task> GetDirectoryContent(string path, CancellationToken ct = default) { var json = await $"{GithubBaseUrl}/repos/Kareadita/Themes/contents/{path}" .WithHeader(HeaderNames.Accept, "application/vnd.github+json") .WithHeader(HeaderNames.UserAgent, "Kavita") - .GetStringAsync(); + .GetStringAsync(cancellationToken: ct); - return string.IsNullOrEmpty(json) ? [] : JsonConvert.DeserializeObject>(json); + return string.IsNullOrEmpty(json) ? [] : JsonConvert.DeserializeObject>(json) ?? []; } /// @@ -215,16 +189,16 @@ public class ThemeService : IThemeService private async Task> GetReadme() { // Try and delete a Readme file if it already exists - var existingReadmeFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, "README.md"); - if (_directoryService.FileSystem.File.Exists(existingReadmeFile)) + var existingReadmeFile = directoryService.FileSystem.Path.Join(directoryService.TempDirectory, "README.md"); + if (directoryService.FileSystem.File.Exists(existingReadmeFile)) { - _directoryService.DeleteFiles([existingReadmeFile]); + directoryService.DeleteFiles([existingReadmeFile]); } - var tempDownloadFile = await GithubReadme.DownloadFileAsync(_directoryService.TempDirectory); + var tempDownloadFile = await GithubReadme.DownloadFileAsync(directoryService.TempDirectory); // Read file into Markdown - var htmlContent = _markdown.Transform(await _directoryService.FileSystem.File.ReadAllTextAsync(tempDownloadFile)); + var htmlContent = _markdown.Transform(await directoryService.FileSystem.File.ReadAllTextAsync(tempDownloadFile)); var htmlDoc = new HtmlDocument(); htmlDoc.LoadHtml(htmlContent); @@ -274,12 +248,12 @@ public class ThemeService : IThemeService throw new ArgumentException("SHA cannot be null or empty for already downloaded themes."); } - _directoryService.ExistOrCreate(_directoryService.SiteThemeDirectory); - var existingTempFile = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, - _directoryService.FileSystem.FileInfo.New(dto.CssUrl).Name); - _directoryService.DeleteFiles([existingTempFile]); + directoryService.ExistOrCreate(directoryService.SiteThemeDirectory); + var existingTempFile = directoryService.FileSystem.Path.Join(directoryService.SiteThemeDirectory, + directoryService.FileSystem.FileInfo.New(dto.CssUrl).Name); + directoryService.DeleteFiles([existingTempFile]); - var tempDownloadFile = await dto.CssUrl.DownloadFileAsync(_directoryService.TempDirectory); + var tempDownloadFile = await dto.CssUrl.DownloadFileAsync(directoryService.TempDirectory); // Validate the hash on the downloaded file // if (!_fileService.ValidateSha(tempDownloadFile, dto.Sha)) @@ -287,22 +261,22 @@ public class ThemeService : IThemeService // throw new KavitaException("Cannot download theme, hash does not match"); // } - _directoryService.CopyFileToDirectory(tempDownloadFile, _directoryService.SiteThemeDirectory); - var finalLocation = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, dto.CssFile); + directoryService.CopyFileToDirectory(tempDownloadFile, directoryService.SiteThemeDirectory); + var finalLocation = directoryService.FileSystem.Path.Join(directoryService.SiteThemeDirectory, dto.CssFile); return finalLocation; } - public async Task DownloadRepoTheme(DownloadableSiteThemeDto dto) + public async Task DownloadRepoTheme(DownloadableSiteThemeDto dto, CancellationToken ct = default) { // Validate we don't have a collision with existing or existing doesn't already exist - var existingThemes = _directoryService.ScanFiles(_directoryService.SiteThemeDirectory, string.Empty); + var existingThemes = directoryService.ScanFiles(directoryService.SiteThemeDirectory, string.Empty); if (existingThemes.Any(f => Path.GetFileName(f) == dto.CssFile)) { // This can happen if you delete then immediately download (to refresh). We should just delete the old file and download. Users can always rollback their version with github directly - _directoryService.DeleteFiles(existingThemes.Where(f => Path.GetFileName(f) == dto.CssFile)); + directoryService.DeleteFiles(existingThemes.Where(f => Path.GetFileName(f) == dto.CssFile)); } var finalLocation = await DownloadSiteTheme(dto); @@ -312,7 +286,7 @@ public class ThemeService : IThemeService { Name = dto.Name, NormalizedName = dto.Name.ToNormalized(), - FileName = _directoryService.FileSystem.Path.GetFileName(finalLocation), + FileName = directoryService.FileSystem.Path.GetFileName(finalLocation), Provider = ThemeProvider.Custom, IsDefault = false, GitHubPath = dto.Path, @@ -322,26 +296,26 @@ public class ThemeService : IThemeService ShaHash = dto.Sha, CompatibleVersion = dto.LastCompatibleVersion, }; - _unitOfWork.SiteThemeRepository.Add(theme); + unitOfWork.SiteThemeRepository.Add(theme); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); // Inform about the new theme - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, - ProgressEventType.Ended)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SiteThemeProgressEvent(directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, + ProgressEventType.Ended), ct: ct); return theme; } - public async Task SyncThemes() + public async Task SyncThemes(CancellationToken ct = default) { - var themes = await _unitOfWork.SiteThemeRepository.GetThemes(); + var themes = await unitOfWork.SiteThemeRepository.GetThemes(); var themeMetadata = await GetReadme(); foreach (var theme in themes) { await SyncTheme(theme, themeMetadata); } - _logger.LogInformation("Sync Themes complete"); + logger.LogInformation("Sync Themes complete"); } /// @@ -354,13 +328,13 @@ public class ThemeService : IThemeService // Given a theme, first validate that it is applicable if (theme == null || theme.Provider == ThemeProvider.System || string.IsNullOrEmpty(theme.GitHubPath)) { - _logger.LogInformation("Cannot Sync {ThemeName} as it is not valid", theme?.Name); + logger.LogInformation("Cannot Sync {ThemeName} as it is not valid", theme?.Name); return; } if (new Version(theme.CompatibleVersion) > BuildInfo.Version) { - _logger.LogDebug("{ThemeName} theme supports a more up-to-date version ({Version}) of Kavita. Please update", theme.Name, theme.CompatibleVersion); + logger.LogDebug("{ThemeName} theme supports a more up-to-date version ({Version}) of Kavita. Please update", theme.Name, theme.CompatibleVersion); return; } @@ -382,50 +356,51 @@ public class ThemeService : IThemeService var hasUpdated = cssFile.Sha != theme.ShaHash; if (hasUpdated) { - _logger.LogDebug("Theme {ThemeName} is out of date, updating", theme.Name); - var tempLocation = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, theme.FileName); + logger.LogDebug("Theme {ThemeName} is out of date, updating", theme.Name); + var tempLocation = directoryService.FileSystem.Path.Join(directoryService.TempDirectory, theme.FileName); - _directoryService.DeleteFiles([tempLocation]); + directoryService.DeleteFiles([tempLocation]); - var location = await cssFile.DownloadUrl.DownloadFileAsync(_directoryService.TempDirectory); - if (_directoryService.FileSystem.File.Exists(location)) + var location = await cssFile.DownloadUrl.DownloadFileAsync(directoryService.TempDirectory); + if (directoryService.FileSystem.File.Exists(location)) { - _directoryService.CopyFileToDirectory(location, _directoryService.SiteThemeDirectory); - _logger.LogInformation("Updated Theme on disk for {ThemeName}", theme.Name); + directoryService.CopyFileToDirectory(location, directoryService.SiteThemeDirectory); + logger.LogInformation("Updated Theme on disk for {ThemeName}", theme.Name); } } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); if (hasUpdated) { - await _eventHub.SendMessageAsync(MessageFactory.SiteThemeUpdated, + await eventHub.SendMessageAsync(MessageFactory.SiteThemeUpdated, MessageFactory.SiteThemeUpdatedEvent(theme.Name)); } // Send an update to refresh metadata around the themes - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SiteThemeProgressEvent(directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, ProgressEventType.Ended)); - _logger.LogInformation("Theme Sync complete"); + logger.LogInformation("Theme Sync complete"); } /// /// Deletes a SiteTheme. The CSS file will be moved to temp/ to allow user to recover data /// /// - public async Task DeleteTheme(int siteThemeId) + /// + public async Task DeleteTheme(int siteThemeId, CancellationToken ct = default) { // Validate no one else is using this theme - var inUse = await _unitOfWork.SiteThemeRepository.IsThemeInUse(siteThemeId); + var inUse = await unitOfWork.SiteThemeRepository.IsThemeInUse(siteThemeId); if (inUse) { throw new KavitaException("errors.delete-theme-in-use"); } - var siteTheme = await _unitOfWork.SiteThemeRepository.GetTheme(siteThemeId); + var siteTheme = await unitOfWork.SiteThemeRepository.GetTheme(siteThemeId); if (siteTheme == null) return; await RemoveTheme(siteTheme); @@ -435,26 +410,28 @@ public class ThemeService : IThemeService /// This assumes a file is already in temp directory and will be used for /// /// + /// + /// /// - public async Task CreateThemeFromFile(string tempFile, string username) + public async Task CreateThemeFromFile(string tempFile, string username, CancellationToken ct = default) { - if (!_directoryService.FileSystem.File.Exists(tempFile)) + if (!directoryService.FileSystem.File.Exists(tempFile)) { - _logger.LogInformation("Unable to create theme from manual upload as file not in temp"); + logger.LogInformation("Unable to create theme from manual upload as file not in temp"); throw new KavitaException("errors.theme-manual-upload"); } - var filename = _directoryService.FileSystem.FileInfo.New(tempFile).Name; + var filename = directoryService.FileSystem.FileInfo.New(tempFile).Name; var themeName = Path.GetFileNameWithoutExtension(filename); - if (await _unitOfWork.SiteThemeRepository.GetThemeDtoByName(themeName) != null) + if (await unitOfWork.SiteThemeRepository.GetThemeDtoByName(themeName) != null) { throw new KavitaException("errors.theme-already-in-use"); } - _directoryService.CopyFileToDirectory(tempFile, _directoryService.SiteThemeDirectory); - var finalLocation = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, filename); + directoryService.CopyFileToDirectory(tempFile, directoryService.SiteThemeDirectory); + var finalLocation = directoryService.FileSystem.Path.Join(directoryService.SiteThemeDirectory, filename); // Create a new entry and note that this is downloaded @@ -462,21 +439,21 @@ public class ThemeService : IThemeService { Name = Path.GetFileNameWithoutExtension(filename), NormalizedName = themeName.ToNormalized(), - FileName = _directoryService.FileSystem.Path.GetFileName(finalLocation), + FileName = directoryService.FileSystem.Path.GetFileName(finalLocation), Provider = ThemeProvider.Custom, IsDefault = false, Description = $"Manually uploaded via UI by {username}", PreviewUrls = string.Empty, Author = username, }; - _unitOfWork.SiteThemeRepository.Add(theme); + unitOfWork.SiteThemeRepository.Add(theme); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); // Inform about the new theme - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, - ProgressEventType.Ended)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SiteThemeProgressEvent(directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, + ProgressEventType.Ended), ct: ct); return theme; } @@ -489,63 +466,64 @@ public class ThemeService : IThemeService /// private async Task RemoveTheme(SiteTheme theme) { - _logger.LogInformation("Removing {ThemeName}. File can be found in temp/ until nightly cleanup", theme.Name); - var prefs = await _unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(theme.Id); - var defaultTheme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); + logger.LogInformation("Removing {ThemeName}. File can be found in temp/ until nightly cleanup", theme.Name); + var prefs = await unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(theme.Id); + var defaultTheme = await unitOfWork.SiteThemeRepository.GetDefaultTheme(); foreach (var pref in prefs) { pref.Theme = defaultTheme; - _unitOfWork.UserRepository.Update(pref); + unitOfWork.UserRepository.Update(pref); } try { // Copy the theme file to temp for nightly removal (to give user time to reclaim if made a mistake) var existingLocation = - _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, theme.FileName); + directoryService.FileSystem.Path.Join(directoryService.SiteThemeDirectory, theme.FileName); var newLocation = - _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, theme.FileName); + directoryService.FileSystem.Path.Join(directoryService.TempDirectory, theme.FileName); - if (!_directoryService.FileSystem.File.Exists(newLocation)) + if (!directoryService.FileSystem.File.Exists(newLocation)) { - _logger.LogInformation("Copying Deleted theme file ({FileName}) to config/temp, it will be removed at midnight", theme.FileName); - _directoryService.CopyFileToDirectory(existingLocation, newLocation); + logger.LogInformation("Copying Deleted theme file ({FileName}) to config/temp, it will be removed at midnight", theme.FileName); + directoryService.CopyFileToDirectory(existingLocation, newLocation); } - _directoryService.DeleteFiles([existingLocation]); + directoryService.DeleteFiles([existingLocation]); } catch (Exception) { /* Swallow */ } - _unitOfWork.SiteThemeRepository.Remove(theme); - await _unitOfWork.CommitAsync(); + unitOfWork.SiteThemeRepository.Remove(theme); + await unitOfWork.CommitAsync(); } /// /// Updates the themeId to the default theme, all others are marked as non-default /// /// + /// /// /// If theme does not exist - public async Task UpdateDefault(int themeId) + public async Task UpdateDefault(int themeId, CancellationToken ct = default) { try { - var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId); + var theme = await unitOfWork.SiteThemeRepository.GetThemeDto(themeId); if (theme == null) throw new KavitaException("theme-doesnt-exist"); - foreach (var siteTheme in await _unitOfWork.SiteThemeRepository.GetThemes()) + foreach (var siteTheme in await unitOfWork.SiteThemeRepository.GetThemes()) { siteTheme.IsDefault = (siteTheme.Id == themeId); - _unitOfWork.SiteThemeRepository.Update(siteTheme); + unitOfWork.SiteThemeRepository.Update(siteTheme); } - if (!_unitOfWork.HasChanges()) return; - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return; + await unitOfWork.CommitAsync(ct); } catch (Exception) { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(ct); throw; } } diff --git a/API/Services/StatisticService.cs b/Kavita.Services/StatisticService.cs similarity index 88% rename from API/Services/StatisticService.cs rename to Kavita.Services/StatisticService.cs index 149bfe847..912d75ffd 100644 --- a/API/Services/StatisticService.cs +++ b/Kavita.Services/StatisticService.cs @@ -1,93 +1,51 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.ManualMigrations; -using API.DTOs; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.ReadingLists; -using API.DTOs.Statistics; -using API.DTOs.Stats; -using API.DTOs.Stats.V3.ClientDevice; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; -using API.Helpers.Formatting; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Database.Extensions.Filters; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.Statistics; +using Kavita.Models.DTOs.Stats; +using Kavita.Models.DTOs.Stats.V3.ClientDevice; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.User; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using TimeZoneConverter; -namespace API.Services; -#nullable enable +namespace Kavita.Services; internal sealed record UserReadCount(int ReadingListId, int ChaptersRead); -public interface IStatisticService -{ - Task GetServerStatistics(); - Task GetUserReadStatistics(int userId, IList libraryIds); - Task>> GetYearCount(); - Task>> GetTopYears(); - Task> GetPopularDecades(); - Task>> GetPopularLibraries(); - Task>> GetPopularSeries(); - Task>> GetPopularReadingList(int take = 5); - Task>> GetPopularGenres(); - Task>> GetPopularTags(); - Task>> GetPopularPerson(PersonRole role); - Task>> GetPublicationCount(); - Task>> GetMangaFormatCount(); - Task GetFileBreakdown(); - Task> GetTopUsers(int days); - Task> GetReadingHistory(int userId); - Task>> ReadCountByDay(int userId = 0, int days = 0); - Task>> ReadCounts(StatsFilterDto filter, int userId = 0); - Task>> GetDayBreakdown(int userId = 0); - Task>> GetPagesReadCountByYear(int userId = 0); - Task>> GetWordsReadCountByYear(int userId = 0); - Task UpdateServerStatistics(); - Task> GetFilesByExtension(string fileExtension); - Task GetClientTypeBreakdown(DateTime fromDateUtc); - Task>> GetDeviceTypeCounts(DateTime fromDateUtc); - Task GetReadingActivityGraphData(StatsFilterDto filter, int userId, int year, int requestingUserId); - Task GetReadingPaceForUser(StatsFilterDto filter, int userId, int year, bool booksOnly, int requestingUserId); - Task> GetGenreBreakdownForUser(StatsFilterDto filter, int userId, int requestingUserId); - Task> GetTagBreakdownForUser(StatsFilterDto filter, int userId, int requestingUserId); - Task GetPageSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId); - Task GetWordSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId); - Task>> GetReadsPerMonth(StatsFilterDto filter, int userId, int requestingUserId); - Task> GetMostReadAuthors(StatsFilterDto filter, int userId, int requestingUserId); - Task GetTotalReads(int userId, int requestingUserId); - Task GetTimeReadingByHour(StatsFilterDto filter, int userId, int requestingUserId); - Task GetUserStatBar(StatsFilterDto filter, int userId, int requestingUserId); - Task> GetMostActiveUsers(StatsFilterDto filter); - Task>> GetFilesAddedOverTime(); - Task> GetReadingHistoryItems(StatsFilterDto filter, UserParams userParams, int userId, int requestingUserId); -} - /// /// Responsible for computing statistics for the server /// /// This performs raw queries and does not use a repository -public class StatisticService(ILogger logger, DataContext context, +public class StatisticService(ILogger logger, IDataContext context, IMapper mapper, IUnitOfWork unitOfWork, IEntityNamingService namingService, ILocalizationService localizationService ): IStatisticService { - public async Task GetUserReadStatistics(int userId, IList libraryIds) + public async Task GetUserReadStatistics(int userId, IList libraryIds, + CancellationToken ct = default) { if (libraryIds.Count == 0) { - libraryIds = await context.Library.GetUserLibraries(userId).ToListAsync(); + libraryIds = await context.Library.GetUserLibraries(userId).ToListAsync(cancellationToken: ct); } var activityData = await context.AppUserReadingSessionActivityData @@ -104,7 +62,7 @@ public class StatisticService(ILogger logger, DataContext cont a.LibraryId, a.ChapterId }) - .ToListAsync(); + .ToListAsync(cancellationToken: ct); var totalPagesRead = activityData.Sum(a => a.PagesRead); @@ -119,7 +77,7 @@ public class StatisticService(ILogger logger, DataContext cont .Where(s => s.AppUserId == userId) .Select(s => s.EndTimeUtc) .DefaultIfEmpty() - .MaxAsync(); + .MaxAsync(cancellationToken: ct); // Average reading time per week var earliestReadDate = activityData @@ -144,11 +102,13 @@ public class StatisticService(ILogger logger, DataContext cont AvgHoursPerWeekSpentReading = avgHoursPerWeek }; } + /// /// Returns the Release Years and their count /// + /// /// - public async Task>> GetYearCount() + public async Task>> GetYearCount(CancellationToken ct = default) { return await context.SeriesMetadata .Where(sm => sm.ReleaseYear != 0) @@ -160,10 +120,10 @@ public class StatisticService(ILogger logger, DataContext cont Count = context.SeriesMetadata.Where(sm2 => sm2.ReleaseYear == sm.Key).Distinct().Count() }) .OrderByDescending(d => d.Value) - .ToListAsync(); + .ToListAsync(ct); } - public async Task>> GetTopYears() + public async Task>> GetTopYears(CancellationToken ct = default) { return await context.SeriesMetadata .Where(sm => sm.ReleaseYear != 0) @@ -176,10 +136,10 @@ public class StatisticService(ILogger logger, DataContext cont }) .OrderByDescending(d => d.Count) .Take(5) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetPopularDecades() + public async Task> GetPopularDecades(CancellationToken ct = default) { var decadeGroups = await context.SeriesMetadata .Where(sm => sm.ReleaseYear != 0) @@ -189,7 +149,7 @@ public class StatisticService(ILogger logger, DataContext cont Decade = g.Key, Count = g.Count() }) - .ToListAsync(); + .ToListAsync(ct); var totalCount = decadeGroups.Sum(d => d.Count); @@ -207,7 +167,7 @@ public class StatisticService(ILogger logger, DataContext cont .ToList(); } - public async Task>> GetPopularLibraries() + public async Task>> GetPopularLibraries(CancellationToken ct = default) { var counts = await context.AppUserProgresses .Where(p => p.LibraryId > 0) @@ -216,7 +176,7 @@ public class StatisticService(ILogger logger, DataContext cont var libraries = await context.Library .Where(l => counts.Select(c => c.Id).Contains(l.Id)) .ProjectTo(mapper.ConfigurationProvider) - .ToDictionaryAsync(l => l.Id); + .ToDictionaryAsync(l => l.Id, cancellationToken: ct); return counts .Where(c => libraries.ContainsKey(c.Id)) @@ -228,7 +188,7 @@ public class StatisticService(ILogger logger, DataContext cont .ToList(); } - public async Task>> GetPopularSeries() + public async Task>> GetPopularSeries(CancellationToken ct = default) { var counts = await context.AppUserProgresses .GetTopCounts(p => p.SeriesId, take: 5); @@ -239,7 +199,7 @@ public class StatisticService(ILogger logger, DataContext cont var series = await context.Series .Where(s => counts.Select(c => c.Id).Contains(s.Id)) .ProjectTo(mapper.ConfigurationProvider) - .ToDictionaryAsync(s => s.Id); + .ToDictionaryAsync(s => s.Id, cancellationToken: ct); return counts .Where(c => series.ContainsKey(c.Id)) @@ -251,7 +211,7 @@ public class StatisticService(ILogger logger, DataContext cont .ToList(); } - public async Task>> GetPopularReadingList(int take = 5) + public async Task>> GetPopularReadingList(int take = 5, CancellationToken ct = default) { var readingListChapterCounts = await context.ReadingList .Where(rl => rl.Promoted) @@ -261,7 +221,7 @@ public class StatisticService(ILogger logger, DataContext cont TotalChapters = rl.Items.Count }) .Where(x => x.TotalChapters > 0) - .ToDictionaryAsync(x => x.ReadingListId, x => x.TotalChapters); + .ToDictionaryAsync(x => x.ReadingListId, x => x.TotalChapters, cancellationToken: ct); if (readingListChapterCounts.Count == 0) return []; @@ -280,7 +240,7 @@ public class StatisticService(ILogger logger, DataContext cont .Select(g => new UserReadCount( g.Key.ReadingListId, g.Select(x => x.ChapterId).Distinct().Count())) - .ToListAsync(); + .ToListAsync(ct); if (userReadCounts.Count == 0) return []; @@ -292,7 +252,7 @@ public class StatisticService(ILogger logger, DataContext cont var readingLists = await context.ReadingList .Where(rl => readingListIds.Contains(rl.Id)) .ProjectTo(mapper.ConfigurationProvider) - .ToDictionaryAsync(rl => rl.Id); + .ToDictionaryAsync(rl => rl.Id, cancellationToken: ct); return counts .Where(c => readingLists.ContainsKey(c.ReadingListId)) @@ -334,9 +294,10 @@ public class StatisticService(ILogger logger, DataContext cont /// /// Top 5 genres where there is some reading activity /// + /// /// Since most users only tag the Series level metadata, this will only check against Series. Will count series * totalReads of series /// - public async Task>> GetPopularGenres() + public async Task>> GetPopularGenres(CancellationToken ct = default) { var counts = await context.AppUserProgresses .GetTopCounts(p => p.SeriesId); @@ -352,7 +313,7 @@ public class StatisticService(ILogger logger, DataContext cont sm.SeriesId }) .Where(x => countDict.Keys.Contains(x.SeriesId)) - .ToListAsync(); + .ToListAsync(ct); return genreStats .GroupBy(x => x.Genre) @@ -370,7 +331,7 @@ public class StatisticService(ILogger logger, DataContext cont .ToList(); } - public async Task>> GetPopularTags() + public async Task>> GetPopularTags(CancellationToken ct = default) { var counts = await context.AppUserProgresses .GetTopCounts(p => p.SeriesId); @@ -387,7 +348,7 @@ public class StatisticService(ILogger logger, DataContext cont sm.SeriesId }) .Where(x => countDict.Keys.Contains(x.SeriesId)) - .ToListAsync(); + .ToListAsync(ct); return genreStats .GroupBy(x => x.Tag) @@ -405,7 +366,7 @@ public class StatisticService(ILogger logger, DataContext cont .ToList(); } - public async Task>> GetPopularPerson(PersonRole role) + public async Task>> GetPopularPerson(PersonRole role, CancellationToken ct = default) { var counts = await context.AppUserProgresses .GetTopCounts(p => p.SeriesId); @@ -422,7 +383,7 @@ public class StatisticService(ILogger logger, DataContext cont smp.Person, smp.SeriesMetadata.SeriesId }) - .ToListAsync(); + .ToListAsync(ct); return authorStats .GroupBy(x => x.Person) @@ -446,7 +407,7 @@ public class StatisticService(ILogger logger, DataContext cont - public async Task>> GetPublicationCount() + public async Task>> GetPublicationCount(CancellationToken ct = default) { return await context.SeriesMetadata .AsSplitQuery() @@ -456,10 +417,10 @@ public class StatisticService(ILogger logger, DataContext cont Value = sm.Key, Count = context.SeriesMetadata.Where(sm2 => sm2.PublicationStatus == sm.Key).Distinct().Count() }) - .ToListAsync(); + .ToListAsync(ct); } - public async Task>> GetMangaFormatCount() + public async Task>> GetMangaFormatCount(CancellationToken ct = default) { return await context.MangaFile .AsSplitQuery() @@ -469,10 +430,10 @@ public class StatisticService(ILogger logger, DataContext cont Value = mf.Key, Count = context.MangaFile.Where(mf2 => mf2.Format == mf.Key).Distinct().Count() }) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetServerStatistics() + public async Task GetServerStatistics(CancellationToken ct = default) { var counts = await context.Chapter .Select(_ => new @@ -486,15 +447,15 @@ public class StatisticService(ILogger logger, DataContext cont Volumes = context.Volume.Count(v => Math.Abs(v.MinNumber - Parser.LooseLeafVolumeNumber) > 0.001f), TotalBytes = context.MangaFile.Sum(m => m.Bytes) }) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (counts == null) return new ServerStatisticsDto(); var totalReadingHours = await context.AppUserReadingSessionActivityData .Where(a => a.EndTimeUtc != null) .Select(a => new { a.StartTimeUtc, EndTimeUtc = a.EndTimeUtc!.Value }) - .ToListAsync() - .ContinueWith(t => t.Result.Sum(a => (a.EndTimeUtc - a.StartTimeUtc).TotalHours)); + .ToListAsync(ct) + .ContinueWith(t => t.Result.Sum(a => (a.EndTimeUtc - a.StartTimeUtc).TotalHours), ct); return new ServerStatisticsDto { @@ -510,7 +471,7 @@ public class StatisticService(ILogger logger, DataContext cont }; } - public async Task GetFileBreakdown() + public async Task GetFileBreakdown(CancellationToken ct = default) { return new FileExtensionBreakdownDto() { @@ -526,15 +487,15 @@ public class StatisticService(ILogger logger, DataContext cont TotalFiles = context.MangaFile.Where(mf2 => mf2.Extension == mf.Key).Distinct().Count() }) .OrderBy(d => d.TotalFiles) - .ToListAsync(), + .ToListAsync(ct), TotalFileSize = await context.MangaFile .AsNoTracking() .AsSplitQuery() - .SumAsync(f => f.Bytes) + .SumAsync(f => f.Bytes, cancellationToken: ct) }; } - public async Task> GetReadingHistory(int userId) + public async Task> GetReadingHistory(int userId, CancellationToken ct = default) { return await context.AppUserProgresses .Where(u => u.AppUserId == userId) @@ -553,10 +514,11 @@ public class StatisticService(ILogger logger, DataContext cont ChapterNumber = context.Chapter.Single(c => c.Id == u.ChapterId).MinNumber }) .OrderByDescending(d => d.ReadDate) - .ToListAsync(); + .ToListAsync(ct); } - public async Task>> ReadCountByDay(int userId = 0, int days = 0) + public async Task>> ReadCountByDay(int userId = 0, int days = 0, + CancellationToken ct = default) { var query = context.AppUserProgresses .AsSplitQuery() @@ -584,7 +546,7 @@ public class StatisticService(ILogger logger, DataContext cont x.chapter.AvgHoursToRead * (x.appUserProgresses.PagesRead / (1.0f * x.chapter.Pages))) }) .OrderBy(d => d.Value) - .ToListAsync(); + .ToListAsync(ct); if (results.Count > 0) { @@ -637,7 +599,8 @@ public class StatisticService(ILogger logger, DataContext cont return results.OrderBy(r => r.Value); } - public async Task>> ReadCounts(StatsFilterDto filter, int userId = 0) + public async Task>> ReadCounts(StatsFilterDto filter, int userId = 0, + CancellationToken ct = default) { var userTimeZone = GetTimeZoneOrUtc(filter.TimeZoneId); var startDate = filter.StartDate?.ToUniversalTime() ?? DateTime.MinValue; @@ -655,7 +618,7 @@ public class StatisticService(ILogger logger, DataContext cont EndTimeUtc = a.EndTimeUtc!.Value, a.Format }) - .ToListAsync(); + .ToListAsync(ct); var results = rawData .GroupBy(a => new @@ -723,7 +686,7 @@ public class StatisticService(ILogger logger, DataContext cont } } - public async Task>> GetDayBreakdown(int userId = 0) + public async Task>> GetDayBreakdown(int userId = 0, CancellationToken ct = default) { return await context.AppUserReadingSessionActivityData .AsNoTracking() @@ -735,13 +698,13 @@ public class StatisticService(ILogger logger, DataContext cont Value = g.Key, Count = g.Count() }) - .ToListAsync(); + .ToListAsync(ct); } /// /// Return a list of pages read per year for the given userId /// - public async Task>> GetPagesReadCountByYear(int userId = 0) + public async Task>> GetPagesReadCountByYear(int userId = 0, CancellationToken ct = default) { return await context.AppUserReadingSessionActivityData .AsNoTracking() @@ -753,13 +716,13 @@ public class StatisticService(ILogger logger, DataContext cont Value = g.Key, Count = g.Sum(a => a.PagesRead) }) - .ToListAsync(); + .ToListAsync(ct); } /// /// Return a list of words read per year for the given userId /// - public async Task>> GetWordsReadCountByYear(int userId = 0) + public async Task>> GetWordsReadCountByYear(int userId = 0, CancellationToken ct = default) { return await context.AppUserReadingSessionActivityData .AsNoTracking() @@ -772,28 +735,29 @@ public class StatisticService(ILogger logger, DataContext cont Value = g.Key, Count = g.Sum(a => a.WordsRead) }) - .ToListAsync(); + .ToListAsync(ct); } /// /// Updates the ServerStatistics table for the current year /// + /// /// This commits /// - public async Task UpdateServerStatistics() + public async Task UpdateServerStatistics(CancellationToken ct = default) { var year = DateTime.Today.Year; - var existingRecord = await context.ServerStatistics.SingleOrDefaultAsync(s => s.Year == year) ?? new ServerStatistics(); + var existingRecord = await context.ServerStatistics.SingleOrDefaultAsync(s => s.Year == year, cancellationToken: ct) ?? new ServerStatistics(); existingRecord.Year = year; - existingRecord.ChapterCount = await context.Chapter.CountAsync(); - existingRecord.VolumeCount = await context.Volume.CountAsync(); - existingRecord.FileCount = await context.MangaFile.CountAsync(); - existingRecord.SeriesCount = await context.Series.CountAsync(); - existingRecord.UserCount = await context.Users.CountAsync(); - existingRecord.GenreCount = await context.Genre.CountAsync(); - existingRecord.TagCount = await context.Tag.CountAsync(); + existingRecord.ChapterCount = await context.Chapter.CountAsync(ct); + existingRecord.VolumeCount = await context.Volume.CountAsync(ct); + existingRecord.FileCount = await context.MangaFile.CountAsync(ct); + existingRecord.SeriesCount = await context.Series.CountAsync(ct); + existingRecord.UserCount = await context.Users.CountAsync(ct); + existingRecord.GenreCount = await context.Genre.CountAsync(ct); + existingRecord.TagCount = await context.Tag.CountAsync(ct); existingRecord.PersonCount = context.Person .AsSplitQuery() .AsEnumerable() @@ -807,7 +771,7 @@ public class StatisticService(ILogger logger, DataContext cont { context.Entry(existingRecord).State = EntityState.Modified; } - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); } public async Task TimeSpentReadingForUsersAsync(IList userIds, IList libraryIds) @@ -827,22 +791,24 @@ public class StatisticService(ILogger logger, DataContext cont p.chapter.AvgHoursToRead * (p.progress.PagesRead / (1.0f * p.chapter.Pages)))); } - public async Task> GetFilesByExtension(string fileExtension) + public async Task> GetFilesByExtension(string fileExtension, + CancellationToken ct = default) { var query = context.MangaFile .Where(f => f.Extension == fileExtension) .ProjectTo(mapper.ConfigurationProvider) .OrderBy(f => f.FilePath); - return await query.ToListAsync(); + return await query.ToListAsync(ct); } - public async Task GetClientTypeBreakdown(DateTime fromDateUtc) + public async Task GetClientTypeBreakdown(DateTime fromDateUtc, + CancellationToken ct = default) { var devices = await context.ClientDevice .Where(d => d.IsActive && d.LastSeenUtc >= fromDateUtc) .Select(d => d.CurrentClientInfo.ClientType) - .ToListAsync(); + .ToListAsync(ct); var grouped = devices .GroupBy(clientType => clientType) @@ -862,12 +828,12 @@ public class StatisticService(ILogger logger, DataContext cont } - public async Task>> GetDeviceTypeCounts(DateTime fromDateUtc) + public async Task>> GetDeviceTypeCounts(DateTime fromDateUtc, CancellationToken ct = default) { var devices = await context.ClientDevice .Where(d => d.IsActive && d.LastSeenUtc >= fromDateUtc) .Select(d => d.CurrentClientInfo.DeviceType) - .ToListAsync(); + .ToListAsync(ct); // Define the expected device types var knownDeviceTypes = new[] { "mobile", "desktop", "tablet" }; @@ -890,10 +856,13 @@ public class StatisticService(ILogger logger, DataContext cont return result; } - public async Task GetReadingActivityGraphData(StatsFilterDto filter, int userId, int year, int requestingUserId) + public async Task GetReadingActivityGraphData(StatsFilterDto filter, int userId, int year, + int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new ReadingActivityGraphDto(); + var userTimeZone = GetTimeZoneOrUtc(filter.TimeZoneId); // Define year boundaries as local dates in user's timezone @@ -924,7 +893,7 @@ public class StatisticService(ILogger logger, DataContext cont activity.TotalPages, activity.EndPage, }) - .ToListAsync(); + .ToListAsync(ct); var result = new ReadingActivityGraphDto(); @@ -999,12 +968,14 @@ public class StatisticService(ILogger logger, DataContext cont } } - public async Task GetReadingPaceForUser(StatsFilterDto filter, int userId, int year, bool booksOnly, int requestingUserId) + public async Task GetReadingPaceForUser(StatsFilterDto filter, int userId, int year, bool booksOnly, + int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new ReadingPaceDto(); - var firstProgress = await unitOfWork.AppUserProgressRepository.GetFirstProgressForUser(userId); + var firstProgress = await unitOfWork.AppUserProgressRepository.GetFirstProgressForUser(userId, ct); if (firstProgress == null) { return new ReadingPaceDto(); @@ -1030,7 +1001,7 @@ public class StatisticService(ILogger logger, DataContext cont }) .WhereIf(booksOnly, d => d.SeriesFormat == MangaFormat.Pdf || d.SeriesFormat == MangaFormat.Epub) .WhereIf(!booksOnly, d => d.SeriesFormat != MangaFormat.Pdf && d.SeriesFormat != MangaFormat.Epub) - .ToListAsync(); + .ToListAsync(ct); var sessionDurations = activities .Where(a => a.SessionEnd.HasValue) @@ -1069,10 +1040,12 @@ public class StatisticService(ILogger logger, DataContext cont }; } - public async Task> GetGenreBreakdownForUser(StatsFilterDto filter, int userId, int requestingUserId) + public async Task> GetGenreBreakdownForUser(StatsFilterDto filter, int userId, + int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new BreakDownDto(); var readsPerGenre = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, onlyCompleted: false) @@ -1112,27 +1085,27 @@ public class StatisticService(ILogger logger, DataContext cont }) .OrderByDescending(x => x.Count) .Take(10) - .ToListAsync(); + .ToListAsync(ct); var totalMissingData = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser) .Select(p => p.SeriesId) .Distinct() .Join(context.SeriesMetadata, p => p, sm => sm.SeriesId, (g, m) => m.Genres) - .CountAsync(g => !g.Any()); + .CountAsync(g => !g.Any(), cancellationToken: ct); var totalReads = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser) .Select(p => p.SeriesId) .Distinct() - .CountAsync(); + .CountAsync(ct); var totalReadGenres = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser) .Join(context.Chapter, p => p.ChapterId, c => c.Id, (p, c) => c.Genres) .SelectMany(g => g.Select(gg => gg.NormalizedTitle)) .Distinct() - .CountAsync(); + .CountAsync(ct); return new BreakDownDto() { @@ -1144,10 +1117,12 @@ public class StatisticService(ILogger logger, DataContext cont } - public async Task> GetTagBreakdownForUser(StatsFilterDto filter, int userId, int requestingUserId) + public async Task> GetTagBreakdownForUser(StatsFilterDto filter, int userId, + int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new BreakDownDto(); var readsPerTagTask = context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, onlyCompleted: false) @@ -1187,27 +1162,27 @@ public class StatisticService(ILogger logger, DataContext cont }) .OrderByDescending(x => x.Count) .Take(10) - .ToListAsync(); + .ToListAsync(ct); var totalMissingDataTask = context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser) .Select(p => p.SeriesId) .Distinct() .Join(context.SeriesMetadata, p => p, sm => sm.SeriesId, (g, m) => m.Tags) - .CountAsync(g => !g.Any()); + .CountAsync(g => !g.Any(), cancellationToken: ct); var totalReadsTask = context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser) .Select(p => p.SeriesId) .Distinct() - .CountAsync(); + .CountAsync(ct); var totalReadTagsTask = context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser) .Join(context.Chapter, p => p.ChapterId, c => c.Id, (p, c) => c.Tags) .SelectMany(g => g.Select(gg => gg.NormalizedTitle)) .Distinct() - .CountAsync(); + .CountAsync(ct); await Task.WhenAll(readsPerTagTask, totalMissingDataTask, totalReadsTask, totalReadTagsTask); @@ -1220,10 +1195,12 @@ public class StatisticService(ILogger logger, DataContext cont }; } - public async Task GetPageSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId) + public async Task GetPageSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId, + CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new SpreadStatsDto(); var fullyReadChapters = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, isAggregate: true) @@ -1234,7 +1211,7 @@ public class StatisticService(ILogger logger, DataContext cont (progress, chapter) => new { progress, chapter } ) .Select(x => x.chapter.Pages) - .ToListAsync(); + .ToListAsync(ct); var totalCount = fullyReadChapters.Count; var highest = fullyReadChapters.MaxOrDefault(x => x, 0); @@ -1280,10 +1257,12 @@ public class StatisticService(ILogger logger, DataContext cont }; } - public async Task GetWordSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId) + public async Task GetWordSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId, + CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new SpreadStatsDto(); var wordsInFullyReadChapters = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, isAggregate: true) @@ -1295,7 +1274,7 @@ public class StatisticService(ILogger logger, DataContext cont ) .Where(x => x.chapter.WordCount > 0) .Select(x => x.chapter.WordCount) - .ToListAsync(); + .ToListAsync(ct); var totalCount = wordsInFullyReadChapters.Count; var highest = wordsInFullyReadChapters.MaxOrDefault(x => x, 0); @@ -1343,18 +1322,21 @@ public class StatisticService(ILogger logger, DataContext cont } - public async Task GetTimeReadingByHour(StatsFilterDto filter, int userId, int requestingUserId) + public async Task GetTimeReadingByHour(StatsFilterDto filter, int userId, int requestingUserId, + CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return null; + var userTimeZone = GetTimeZoneOrUtc(filter.TimeZoneId); var sessionRecordedSince = await unitOfWork.DataContext.ManualMigrationHistory - .FirstOrDefaultAsync(mm => mm.Name == MigrateProgressToReadingSessions.Name); + .FirstOrDefaultAsync(mm => mm.Name == "MigrateProgressToReadingSessions", cancellationToken: ct); if (sessionRecordedSince == null) { - logger.LogWarning("{Migration} never happened! Cannot compute time by hour", MigrateProgressToReadingSessions.Name); + logger.LogWarning("{Migration} never happened! Cannot compute time by hour", "MigrateProgressToReadingSessions"); return null; } @@ -1369,7 +1351,7 @@ public class StatisticService(ILogger logger, DataContext cont s.StartTimeUtc, s.EndTimeUtc }) - .ToListAsync(); + .ToListAsync(ct); var hourStats = sessions .Where(s => s.EndTimeUtc.HasValue) @@ -1427,10 +1409,12 @@ public class StatisticService(ILogger logger, DataContext cont }; } - public async Task GetUserStatBar(StatsFilterDto filter, int userId, int requestingUserId) + public async Task GetUserStatBar(StatsFilterDto filter, int userId, int requestingUserId, + CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new ProfileStatBarDto(); var chapterData = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, isAggregate: true, onlyCompleted: false) @@ -1442,13 +1426,13 @@ public class StatisticService(ILogger logger, DataContext cont d.WordsRead, Finished = d.EndPage >= d.Chapter.Pages }) - .ToListAsync(); + .ToListAsync(ct); // Early exit if no data if (chapterData.Count == 0) { // Still need reviews/ratings - run in parallel - var (reviews, ratings) = await GetReviewsAndRatings(filter, userId, socialPreferences); + var (reviews, ratings) = await GetReviewsAndRatings(filter, userId, socialPreferences, ct); return new ProfileStatBarDto { Reviews = reviews, @@ -1496,8 +1480,8 @@ public class StatisticService(ILogger logger, DataContext cont } } - var authorsTask = GetAuthorsCount(chapterIds); - var reviewsRatingsTask = GetReviewsAndRatings(filter, userId, socialPreferences); + var authorsTask = GetAuthorsCount(chapterIds, ct); + var reviewsRatingsTask = GetReviewsAndRatings(filter, userId, socialPreferences, ct); await Task.WhenAll(authorsTask, reviewsRatingsTask); @@ -1515,7 +1499,7 @@ public class StatisticService(ILogger logger, DataContext cont }; } - public async Task> GetMostActiveUsers(StatsFilterDto filter) + public async Task> GetMostActiveUsers(StatsFilterDto filter, CancellationToken ct = default) { var startDate = filter.StartDate?.ToUniversalTime() ?? DateTime.MinValue; var endDate = filter.EndDate?.ToUniversalTime() ?? DateTime.UtcNow; @@ -1533,7 +1517,7 @@ public class StatisticService(ILogger logger, DataContext cont a.StartTimeUtc, EndTimeUtc = a.EndTimeUtc!.Value }) - .ToListAsync(); + .ToListAsync(ct); if (activityData.Count == 0) return []; @@ -1584,7 +1568,7 @@ public class StatisticService(ILogger logger, DataContext cont var users = await context.AppUser .Where(u => userIds.Contains(u.Id)) .Select(u => new { u.Id, u.UserName, u.CoverImage }) - .ToDictionaryAsync(u => u.Id); + .ToDictionaryAsync(u => u.Id, cancellationToken: ct); // Fetch TotalReads for each user's series var allSeriesIds = userStats @@ -1601,7 +1585,7 @@ public class StatisticService(ILogger logger, DataContext cont g.Key.SeriesId, MinTotalReads = g.Min(p => p.TotalReads) }) - .ToListAsync(); + .ToListAsync(ct); var progressLookup = progressData.ToLookup(p => p.AppUserId); @@ -1609,7 +1593,7 @@ public class StatisticService(ILogger logger, DataContext cont var seriesLookup = await context.Series .Where(s => allSeriesIds.Contains(s.Id)) .ProjectTo(mapper.ConfigurationProvider) - .ToDictionaryAsync(s => s.Id); + .ToDictionaryAsync(s => s.Id, cancellationToken: ct); var result = new List(); foreach (var stat in userStats) @@ -1642,11 +1626,11 @@ public class StatisticService(ILogger logger, DataContext cont return result; } - public async Task>> GetFilesAddedOverTime() + public async Task>> GetFilesAddedOverTime(CancellationToken ct = default) { var results = await context.MangaFile .AsNoTracking() - .GroupBy(f => new { Date = f.CreatedUtc.Date, f.Format }) + .GroupBy(f => new { f.CreatedUtc.Date, f.Format }) .Select(g => new StatCountWithFormat { Value = g.Key.Date, @@ -1654,15 +1638,18 @@ public class StatisticService(ILogger logger, DataContext cont Format = g.Key.Format }) .OrderBy(d => d.Value) - .ToListAsync(); + .ToListAsync(ct); return results; } - public async Task> GetReadingHistoryItems(StatsFilterDto filter, UserParams userParams, int userId, int requestingUserId) + public async Task> GetReadingHistoryItems(StatsFilterDto filter, + UserParams userParams, int userId, int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return PagedList.Create([], 0, 0, 0); + var userTimeZone = GetTimeZoneOrUtc(filter.TimeZoneId); var query = context.AppUserReadingSessionActivityData @@ -1711,13 +1698,13 @@ public class StatisticService(ILogger logger, DataContext cont .OrderByDescending(a => a.StartTimeUtc); // Get total count before pagination - var totalCount = await query.CountAsync(); + var totalCount = await query.CountAsync(ct); // Paginate and materialize var items = await query .Skip((userParams.PageNumber - 1) * userParams.PageSize) .Take(userParams.PageSize) - .ToListAsync(); + .ToListAsync(ct); var libraryTypes = items.Select(i => i.LibraryType).Distinct().ToList(); var namingContexts = new Dictionary(); @@ -1814,7 +1801,7 @@ public class StatisticService(ILogger logger, DataContext cont return PagedList.Create(dtos, totalCount, userParams.PageNumber, userParams.PageSize); } - private async Task GetAuthorsCount(HashSet chapterIds) + private async Task GetAuthorsCount(HashSet chapterIds, CancellationToken ct = default) { if (chapterIds.Count == 0) return 0; @@ -1824,7 +1811,7 @@ public class StatisticService(ILogger logger, DataContext cont .Where(cp => cp.Role == PersonRole.Writer && chapterIds.Contains(cp.ChapterId)) .Select(cp => cp.PersonId) .Distinct() - .CountAsync(); + .CountAsync(ct); } var authorIds = new HashSet(); @@ -1834,7 +1821,7 @@ public class StatisticService(ILogger logger, DataContext cont var batchAuthors = await context.ChapterPeople .Where(cp => cp.Role == PersonRole.Writer && batchSet.Contains(cp.ChapterId)) .Select(cp => cp.PersonId) - .ToListAsync(); + .ToListAsync(ct); foreach (var id in batchAuthors) authorIds.Add(id); @@ -1843,7 +1830,7 @@ public class StatisticService(ILogger logger, DataContext cont } private async Task<(int Reviews, int Ratings)> GetReviewsAndRatings( - StatsFilterDto filter, int userId, AppUserSocialPreferences socialPreferences) + StatsFilterDto filter, int userId, AppUserSocialPreferences socialPreferences, CancellationToken ct = default) { var baseQuery = BuildRatingQuery(filter, userId, socialPreferences); @@ -1854,7 +1841,7 @@ public class StatisticService(ILogger logger, DataContext cont Reviews = g.Count(r => r.Review != null && r.Review != ""), Ratings = g.Count(r => r.HasBeenRated) }) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); return counts != null ? (counts.Reviews, counts.Ratings) : (0, 0); } @@ -1879,16 +1866,19 @@ public class StatisticService(ILogger logger, DataContext cont r.Series.Metadata.AgeRating == AgeRating.Unknown)); } - public async Task>> GetReadsPerMonth(StatsFilterDto filter, int userId, int requestingUserId) + public async Task>> GetReadsPerMonth(StatsFilterDto filter, int userId, + int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return []; + var userTimeZone = GetTimeZoneOrUtc(filter.TimeZoneId); var rawData = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, isAggregate: true) .Select(s => s.ReadingSession.CreatedUtc) - .ToListAsync(); + .ToListAsync(ct); return rawData .Select(utc => TimeZoneInfo.ConvertTimeFromUtc(utc, userTimeZone)) @@ -1907,10 +1897,12 @@ public class StatisticService(ILogger logger, DataContext cont .ToList(); } - public async Task> GetMostReadAuthors(StatsFilterDto filter, int userId, int requestingUserId) + public async Task> GetMostReadAuthors(StatsFilterDto filter, int userId, + int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return []; var res = await context.ChapterPeople .Where(cp => cp.Role == PersonRole.Writer) @@ -1930,7 +1922,7 @@ public class StatisticService(ILogger logger, DataContext cont }) .OrderByDescending(x => x.TotalChaptersRead) .Take(5) - .ToListAsync(); + .ToListAsync(ct); var final = new List(); @@ -1942,9 +1934,9 @@ public class StatisticService(ILogger logger, DataContext cont { Chapter = c, SeriesId = c.Volume.Series.Id, - LibraryId = c.Volume.Series.LibraryId, + c.Volume.Series.LibraryId, }) - .ToListAsync(); + .ToListAsync(ct); final.Add(new MostReadAuthorsDto @@ -1957,7 +1949,7 @@ public class StatisticService(ILogger logger, DataContext cont LibraryId = x.LibraryId, SeriesId = x.SeriesId, ChapterId = x.Chapter.Id, - Title = x.Chapter.TitleName, // TODO: Use that method that makes a smart title? Do we have that? Where it falls back to Chapter #3 or whatever + Title = x.Chapter.TitleName, // default: Use that method that makes a smart title? Do we have that? Where it falls back to Chapter #3 or whatever }).ToList(), }); } @@ -1966,12 +1958,13 @@ public class StatisticService(ILogger logger, DataContext cont } - public async Task GetTotalReads(int userId, int requestingUserId) + public async Task GetTotalReads(int userId, int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return 0; - var librariesForUser = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId); + var librariesForUser = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct); var filter = new StatsFilterDto { Libraries = librariesForUser, @@ -1979,14 +1972,14 @@ public class StatisticService(ILogger logger, DataContext cont return await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, isAggregate: true) - .CountAsync(); + .CountAsync(ct); } - public async Task> GetTopUsers(int days) + public async Task> GetTopUsers(int days, CancellationToken ct = default) { - var libraries = (await unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); - var users = (await unitOfWork.UserRepository.GetAllUsersAsync()).ToList(); + var libraries = (await unitOfWork.LibraryRepository.GetLibrariesAsync(ct: ct)).ToList(); + var users = (await unitOfWork.UserRepository.GetAllUsersAsync(ct: ct)).ToList(); var minDate = DateTime.Now.Subtract(TimeSpan.FromDays(days)); var topUsersAndReadChapters = context.AppUserProgresses diff --git a/API/Services/Tasks/StatsService.cs b/Kavita.Services/StatsService.cs similarity index 93% rename from API/Services/Tasks/StatsService.cs rename to Kavita.Services/StatsService.cs index 7b725114a..0c503f7d2 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/Kavita.Services/StatsService.cs @@ -5,37 +5,32 @@ using System.Globalization; using System.Linq; using System.Net.Http; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Misc; -using API.Data.Repositories; -using API.DTOs.Stats; -using API.DTOs.Stats.V3; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; using Flurl.Http; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Database; +using Kavita.Models.DTOs.Stats; +using Kavita.Models.DTOs.Stats.V3; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace API.Services.Tasks; +namespace Kavita.Services; -#nullable enable - -public interface IStatsService -{ - Task Send(); - Task GetServerInfoSlim(); - Task SendCancellation(); -} /// /// This is for reporting to the stat server /// @@ -72,9 +67,10 @@ public class StatsService : IStatsService /// Due to all instances firing this at the same time, we can DDOS our server. This task when fired will schedule the task to be run /// randomly over a six-hour spread /// - public async Task Send() + /// + public async Task Send(CancellationToken ct = default) { - var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).AllowStatCollection; + var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).AllowStatCollection; if (!allowStatCollection) { return; @@ -87,17 +83,17 @@ public class StatsService : IStatsService /// This must be public for Hangfire. Do not call this directly. ///
    // ReSharper disable once MemberCanBePrivate.Global - public async Task SendData() + public async Task SendData(CancellationToken ct = default) { var sw = Stopwatch.StartNew(); var data = await GetStatV3Payload(); _logger.LogDebug("Collecting stats took {Time} ms", sw.ElapsedMilliseconds); sw.Stop(); - await SendDataToStatsServer(data); + await SendDataToStatsServer(data, ct); } - private async Task SendDataToStatsServer(ServerInfoV3Dto data) + private async Task SendDataToStatsServer(ServerInfoV3Dto data, CancellationToken ct = default) { var responseContent = string.Empty; @@ -105,7 +101,7 @@ public class StatsService : IStatsService { var response = await (_apiUrl + "/api/v3/stats") .WithBasicHeaders(ApiKey) - .PostJsonAsync(data); + .PostJsonAsync(data, cancellationToken: ct); if (response.StatusCode != StatusCodes.Status200OK) { @@ -118,7 +114,7 @@ public class StatsService : IStatsService UPDATE ServerSetting SET Value = CAST(CAST(Value AS INTEGER) + 1 AS TEXT) WHERE Key = {ServerSettingKey.StatsApiHits} - """); + """, cancellationToken: ct); } catch (HttpRequestException e) @@ -138,9 +134,9 @@ public class StatsService : IStatsService } - public async Task GetServerInfoSlim() + public async Task GetServerInfoSlim(CancellationToken ct = default) { - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); return new ServerInfoSlimDto() { InstallId = serverSettings.InstallId, @@ -151,10 +147,10 @@ public class StatsService : IStatsService }; } - public async Task SendCancellation() + public async Task SendCancellation(CancellationToken ct = default) { _logger.LogInformation("Informing KavitaStats that this instance is no longer sending stats"); - var installId = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).InstallId; + var installId = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).InstallId; var responseContent = string.Empty; @@ -163,7 +159,7 @@ public class StatsService : IStatsService var response = await (_apiUrl + "/api/v2/stats/opt-out?installId=" + installId) .WithBasicHeaders(ApiKey) .WithTimeout(TimeSpan.FromSeconds(30)) - .PostAsync(); + .PostAsync(cancellationToken: ct); if (response.StatusCode != StatusCodes.Status200OK) { diff --git a/Kavita.Services/StreamService.cs b/Kavita.Services/StreamService.cs new file mode 100644 index 000000000..d85858cae --- /dev/null +++ b/Kavita.Services/StreamService.cs @@ -0,0 +1,415 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Models.Helpers; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class StreamService( + IUnitOfWork unitOfWork, + IEventHub eventHub, + ILocalizationService localizationService, + ILogger logger) + : IStreamService +{ + public async Task> GetDashboardStreams(int userId, bool visibleOnly = true, + CancellationToken ct = default) + { + return await unitOfWork.UserRepository.GetDashboardStreams(userId, visibleOnly, ct); + } + + public async Task> GetSidenavStreams(int userId, bool visibleOnly = true, CancellationToken ct = default) + { + return await unitOfWork.UserRepository.GetSideNavStreams(userId, visibleOnly, ct); + } + + public async Task> GetExternalSources(int userId, CancellationToken ct = default) + { + return await unitOfWork.AppUserExternalSourceRepository.GetExternalSources(userId, ct); + } + + public async Task CreateDashboardStreamFromSmartFilter(int userId, int smartFilterId, + CancellationToken ct = default) + { + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.DashboardStreams, ct); + if (user == null) throw new KavitaException(await localizationService.Translate(userId, "no-user")); + + var smartFilter = await unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId, ct); + if (smartFilter == null) throw new KavitaException(await localizationService.Translate(userId, "smart-filter-doesnt-exist")); + + var stream = user.DashboardStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); + if (stream != null) throw new KavitaException(await localizationService.Translate(userId, "smart-filter-already-in-use")); + + var maxOrder = user!.DashboardStreams.Max(d => d.Order); + var createdStream = new AppUserDashboardStream() + { + Name = smartFilter.Name, + IsProvided = false, + StreamType = DashboardStreamType.SmartFilter, + Visible = true, + Order = maxOrder + 1, + SmartFilter = smartFilter + }; + + user.DashboardStreams.Add(createdStream); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); + + var ret = new DashboardStreamDto() + { + Id = createdStream.Id, + Name = createdStream.Name, + IsProvided = createdStream.IsProvided, + Visible = createdStream.Visible, + Order = createdStream.Order, + SmartFilterEncoded = smartFilter.Filter, + StreamType = createdStream.StreamType + }; + + await eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id), + userId, ct); + + return ret; + } + + public async Task UpdateDashboardStream(int userId, DashboardStreamDto dto, CancellationToken ct = default) + { + var stream = await unitOfWork.UserRepository.GetDashboardStream(dto.Id, ct); + if (stream == null) throw new KavitaException(await localizationService.Translate(userId, "dashboard-stream-doesnt-exist")); + stream.Visible = dto.Visible; + + unitOfWork.UserRepository.Update(stream); + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(userId), + userId, ct); + } + + public async Task UpdateDashboardStreamPosition(int userId, UpdateStreamPositionDto dto, + CancellationToken ct = default) + { + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, + AppUserIncludes.DashboardStreams, ct); + var stream = user?.DashboardStreams.FirstOrDefault(d => d.Id == dto.Id); + if (stream == null) + { + throw new KavitaException(await localizationService.Translate(userId, "dashboard-stream-doesnt-exist")); + } + + if (stream.Order == dto.ToPosition) return; + + var list = user!.DashboardStreams.OrderBy(s => s.Order).ToList(); + OrderableHelper.ReorderItems(list, stream.Id, dto.ToPosition); + user.DashboardStreams = list; + + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); + if (!stream.Visible) return; + await eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id), + user.Id, ct); + } + + public async Task UpdateSideNavStreamBulk(int userId, BulkUpdateSideNavStreamVisibilityDto dto, + CancellationToken ct = default) + { + var streams = await unitOfWork.UserRepository.GetDashboardStreamsByIds(dto.Ids, ct); + foreach (var stream in streams) + { + stream.Visible = dto.Visibility; + unitOfWork.UserRepository.Update(stream); + } + + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), + userId, ct); + } + + public async Task CreateSideNavStreamFromSmartFilter(int userId, int smartFilterId, + CancellationToken ct = default) + { + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.SideNavStreams, ct); + if (user == null) throw new KavitaException(await localizationService.Translate(userId, "no-user")); + + var smartFilter = await unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId, ct); + if (smartFilter == null) throw new KavitaException(await localizationService.Translate(userId, "smart-filter-doesnt-exist")); + + var stream = user.SideNavStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); + if (stream != null) throw new KavitaException(await localizationService.Translate(userId, "smart-filter-already-in-use")); + + var maxOrder = user!.SideNavStreams.Max(d => d.Order); + var createdStream = new AppUserSideNavStream() + { + Name = smartFilter.Name, + IsProvided = false, + StreamType = SideNavStreamType.SmartFilter, + Visible = true, + Order = maxOrder + 1, + SmartFilter = smartFilter + }; + + user.SideNavStreams.Add(createdStream); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); + + var ret = new SideNavStreamDto() + { + Id = createdStream.Id, + Name = createdStream.Name, + IsProvided = createdStream.IsProvided, + Visible = createdStream.Visible, + Order = createdStream.Order, + SmartFilterEncoded = smartFilter.Filter, + StreamType = createdStream.StreamType + }; + + + await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), + userId, ct); + return ret; + } + + public async Task CreateSideNavStreamFromExternalSource(int userId, int externalSourceId, + CancellationToken ct = default) + { + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.SideNavStreams, ct); + if (user == null) throw new KavitaException(await localizationService.Translate(userId, "no-user")); + + var externalSource = await unitOfWork.AppUserExternalSourceRepository.GetById(externalSourceId, ct); + if (externalSource == null) throw new KavitaException(await localizationService.Translate(userId, "external-source-doesnt-exist")); + + var stream = user?.SideNavStreams.FirstOrDefault(d => d.ExternalSourceId == externalSourceId); + if (stream != null) throw new KavitaException(await localizationService.Translate(userId, "external-source-already-in-use")); + + var maxOrder = user!.SideNavStreams.Max(d => d.Order); + var createdStream = new AppUserSideNavStream() + { + Name = externalSource.Name, + IsProvided = false, + StreamType = SideNavStreamType.ExternalSource, + Visible = true, + Order = maxOrder + 1, + ExternalSourceId = externalSource.Id + }; + + user.SideNavStreams.Add(createdStream); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); + + var ret = new SideNavStreamDto() + { + Name = createdStream.Name, + IsProvided = createdStream.IsProvided, + Visible = createdStream.Visible, + Order = createdStream.Order, + StreamType = createdStream.StreamType, + ExternalSource = new ExternalSourceDto() + { + Host = externalSource.Host, + Id = externalSource.Id, + Name = externalSource.Name, + ApiKey = externalSource.ApiKey + } + }; + + + await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), + userId, ct); + return ret; + } + + public async Task UpdateSideNavStream(int userId, SideNavStreamDto dto, CancellationToken ct = default) + { + var stream = await unitOfWork.UserRepository.GetSideNavStream(dto.Id, ct); + if (stream == null) + throw new KavitaException(await localizationService.Translate(userId, "sidenav-stream-doesnt-exist")); + + stream.Visible = dto.Visible; + + unitOfWork.UserRepository.Update(stream); + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), + userId, ct); + } + + public async Task UpdateSideNavStreamPosition(int userId, UpdateStreamPositionDto dto, CancellationToken ct = default) + { + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, + AppUserIncludes.SideNavStreams, ct); + var stream = user?.SideNavStreams.FirstOrDefault(d => d.Id == dto.Id); + if (stream == null) throw new KavitaException(await localizationService.Translate(userId, "sidenav-stream-doesnt-exist")); + + if (stream.Order == dto.ToPosition) return; + + var list = user!.SideNavStreams.OrderBy(s => s.Order).ToList(); + + var wantedPosition = dto.ToPosition; + if (!dto.PositionIncludesInvisible) + { + var visibleItems = list.Where(i => i.Visible).ToList(); + if (dto.ToPosition < 0 || dto.ToPosition >= visibleItems.Count) return; + + var itemAtWantedPosition = visibleItems[dto.ToPosition]; + wantedPosition = list.IndexOf(itemAtWantedPosition); + } + + OrderableHelper.ReorderItems(list, stream.Id, wantedPosition); + user.SideNavStreams = list; + + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); + if (!stream.Visible) return; + await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), + userId, ct); + } + + public async Task CreateExternalSource(int userId, ExternalSourceDto dto, + CancellationToken ct = default) + { + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, + AppUserIncludes.ExternalSources, ct); + if (user == null) throw new KavitaException("not-authenticated"); + + if (user.ExternalSources.Any(s => s.Host == dto.Host)) + { + throw new KavitaException("external-source-already-exists"); + } + + if (string.IsNullOrEmpty(dto.ApiKey) || string.IsNullOrEmpty(dto.Name)) throw new KavitaException("external-source-required"); + if (!UrlHelper.StartsWithHttpOrHttps(dto.Host)) throw new KavitaException("external-source-host-format"); + + + var newSource = new AppUserExternalSource() + { + Name = dto.Name, + Host = UrlHelper.EnsureEndsWithSlash( + UrlHelper.EnsureStartsWithHttpOrHttps(dto.Host)), + ApiKey = dto.ApiKey + }; + user.ExternalSources.Add(newSource); + + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); + + dto.Id = newSource.Id; + + return dto; + } + + public async Task UpdateExternalSource(int userId, ExternalSourceDto dto, + CancellationToken ct = default) + { + var source = await unitOfWork.AppUserExternalSourceRepository.GetById(dto.Id, ct); + if (source == null) throw new KavitaException("external-source-doesnt-exist"); + if (source.AppUserId != userId) throw new KavitaException("external-source-doesnt-exist"); + + if (string.IsNullOrEmpty(dto.ApiKey) || string.IsNullOrEmpty(dto.Host) || string.IsNullOrEmpty(dto.Name)) throw new KavitaException("external-source-required"); + + source.Host = UrlHelper.EnsureEndsWithSlash( + UrlHelper.EnsureStartsWithHttpOrHttps(dto.Host)); + source.ApiKey = dto.ApiKey; + source.Name = dto.Name; + + unitOfWork.AppUserExternalSourceRepository.Update(source); + await unitOfWork.CommitAsync(ct); + + dto.Host = source.Host; + return dto; + } + + public async Task DeleteExternalSource(int userId, int externalSourceId, CancellationToken ct = default) + { + var source = await unitOfWork.AppUserExternalSourceRepository.GetById(externalSourceId, ct); + if (source == null) throw new KavitaException("external-source-doesnt-exist"); + if (source.AppUserId != userId) throw new KavitaException("external-source-doesnt-exist"); + + unitOfWork.AppUserExternalSourceRepository.Delete(source); + + // Find all SideNav's with this source and delete them as well + var streams2 = await unitOfWork.UserRepository.GetSideNavStreamWithExternalSource(externalSourceId, ct); + unitOfWork.UserRepository.Delete(streams2); + + await unitOfWork.CommitAsync(ct); + } + + public async Task DeleteSideNavSmartFilterStream(int userId, int sideNavStreamId, CancellationToken ct = default) + { + try + { + var stream = await unitOfWork.UserRepository.GetSideNavStream(sideNavStreamId, ct); + if (stream == null) throw new KavitaException("sidenav-stream-doesnt-exist"); + + if (stream.AppUserId != userId) throw new KavitaException("sidenav-stream-doesnt-exist"); + + + if (stream.StreamType != SideNavStreamType.SmartFilter) + { + throw new KavitaException("sidenav-stream-only-delete-smart-filter"); + } + + unitOfWork.UserRepository.Delete(stream); + + await unitOfWork.CommitAsync(ct); + } + catch (Exception ex) + { + logger.LogError(ex, "There was an exception deleting SideNav Smart Filter Stream: {FilterId}", sideNavStreamId); + throw; + } + } + + public async Task DeleteDashboardSmartFilterStream(int userId, int dashboardStreamId, CancellationToken ct = default) + { + try + { + var stream = await unitOfWork.UserRepository.GetDashboardStream(dashboardStreamId, ct); + if (stream == null) throw new KavitaException("dashboard-stream-doesnt-exist"); + + if (stream.AppUserId != userId) throw new KavitaException("dashboard-stream-doesnt-exist"); + + if (stream.StreamType != DashboardStreamType.SmartFilter) + { + throw new KavitaException("dashboard-stream-only-delete-smart-filter"); + } + + unitOfWork.UserRepository.Delete(stream); + + await unitOfWork.CommitAsync(ct); + } catch (Exception ex) + { + logger.LogError(ex, "There was an exception deleting Dashboard Smart Filter Stream: {FilterId}", dashboardStreamId); + throw; + } + } + + public async Task RenameSmartFilterStreams(AppUserSmartFilter smartFilter, CancellationToken ct = default) + { + var sideNavStreams = await unitOfWork.UserRepository.GetSideNavStreamWithFilter(smartFilter.Id, ct); + var dashboardStreams = await unitOfWork.UserRepository.GetDashboardStreamWithFilter(smartFilter.Id, ct); + + foreach (var sideNavStream in sideNavStreams) + { + sideNavStream.Name = smartFilter.Name; + } + + foreach (var dashboardStream in dashboardStreams) + { + dashboardStream.Name = smartFilter.Name; + } + + await unitOfWork.CommitAsync(ct); + } +} diff --git a/API/Services/TachiyomiService.cs b/Kavita.Services/TachiyomiService.cs similarity index 52% rename from API/Services/TachiyomiService.cs rename to Kavita.Services/TachiyomiService.cs index e5e733bef..f6d810946 100644 --- a/API/Services/TachiyomiService.cs +++ b/Kavita.Services/TachiyomiService.cs @@ -1,81 +1,60 @@ using System; -using API.DTOs; using System.Threading.Tasks; -using API.Data; using System.Collections.Immutable; using System.Collections.Generic; using System.Globalization; using System.Linq; -using API.Comparators; -using API.Entities; -using API.Entities.Progress; -using API.Extensions; -using API.Services.Reading; -using API.Services.Tasks.Scanner.Parser; +using System.Threading; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Services.Comparators; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable - -public interface ITachiyomiService -{ - Task GetLatestChapter(int seriesId, int userId); - Task MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber); -} +namespace Kavita.Services; /// /// All APIs are for Tachiyomi extension and app. They have hacks for our implementation and should not be used for any /// other purposes. /// -public class TachiyomiService : ITachiyomiService +public class TachiyomiService( + IUnitOfWork unitOfWork, + IMapper mapper, + ILogger logger, + IReaderService readerService) + : ITachiyomiService { - private readonly IUnitOfWork _unitOfWork; - private readonly IMapper _mapper; - private readonly ILogger _logger; - private readonly IReaderService _readerService; - private static readonly CultureInfo EnglishCulture = CultureInfo.CreateSpecificCulture("en-US"); - public TachiyomiService(IUnitOfWork unitOfWork, IMapper mapper, ILogger logger, IReaderService readerService) + public async Task GetLatestChapter(int seriesId, int userId, CancellationToken ct = default) { - _unitOfWork = unitOfWork; - _readerService = readerService; - _mapper = mapper; - _logger = logger; - } - - /// - /// Gets the latest chapter/volume read. - /// - /// - /// - /// Due to how Tachiyomi works we need a hack to properly return both chapters and volumes. - /// If its a chapter, return the chapterDto as is. - /// If it's a volume, the volume number gets returned in the 'Number' attribute of a chapterDto encoded. - /// The volume number gets divided by 10,000 because that's how Tachiyomi interprets volumes - public async Task GetLatestChapter(int seriesId, int userId) - { - var currentChapter = await _readerService.GetContinuePoint(seriesId, userId); + var currentChapter = await readerService.GetContinuePoint(seriesId, userId); var prevChapterId = - await _readerService.GetPrevChapterIdAsync(seriesId, currentChapter.VolumeId, currentChapter.Id, userId); + await readerService.GetPrevChapterIdAsync(seriesId, currentChapter.VolumeId, currentChapter.Id, userId); // If prevChapterId is -1, this means either nothing is read or everything is read. if (prevChapterId == -1) { - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct); var userHasProgress = series.PagesRead != 0 && series.PagesRead <= series.Pages; // If the user doesn't have progress, then return null, which the extension will catch as 204 (no content) and report nothing as read if (!userHasProgress) return null; // Else return the max chapter to Tachiyomi so it can consider everything read - var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(seriesId)).ToImmutableList(); + var volumes = (await unitOfWork.VolumeRepository.GetVolumes(seriesId, ct)).ToImmutableList(); var looseLeafChapterVolume = volumes.GetLooseLeafVolumeOrDefault(); if (looseLeafChapterVolume == null) { - var volumeChapter = _mapper.Map(volumes + var volumeChapter = mapper.Map(volumes [^1].Chapters .OrderBy(c => c.MinNumber, ChapterSortComparerDefaultFirst.Default) .Last()); @@ -93,13 +72,13 @@ public class TachiyomiService : ITachiyomiService .OrderBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default) .Last(); - return _mapper.Map(lastChapter); + return mapper.Map(lastChapter); } // There is progress, we now need to figure out the highest volume or chapter and return that. - var prevChapter = (await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId, userId))!; + var prevChapter = (await unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId, userId, ct))!; - var volumeWithProgress = (await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId))!; + var volumeWithProgress = (await unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId, ct))!; // We only encode for single-file volumes if (!volumeWithProgress.IsLooseLeaf() && volumeWithProgress.Chapters.Count == 1) { @@ -108,7 +87,7 @@ public class TachiyomiService : ITachiyomiService } // Progress is just on a chapter, return as is - return _mapper.Map(prevChapter); + return mapper.Map(prevChapter); } private static TachiyomiChapterDto CreateTachiyomiChapterDto(float number) @@ -121,16 +100,10 @@ public class TachiyomiService : ITachiyomiService }; } - /// - /// Marks every chapter and volume that is sorted below the passed number as Read. This will not mark any specials as read. - /// Passed number will also be marked as read - /// - /// - /// - /// Can also be a Tachiyomi encoded volume number - public async Task MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber) + public async Task MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber, + CancellationToken ct = default) { - userWithProgress.Progresses ??= new List(); + userWithProgress.Progresses ??= []; switch (chapterNumber) { @@ -143,22 +116,22 @@ public class TachiyomiService : ITachiyomiService { // This is a hack to track volume number. We need to map it back by x10,000 var volumeNumber = int.Parse($"{(int)(chapterNumber * 10_000)}", EnglishCulture); - await _readerService.MarkVolumesUntilAsRead(userWithProgress, seriesId, volumeNumber); + await readerService.MarkVolumesUntilAsRead(userWithProgress, seriesId, volumeNumber); break; } default: - await _readerService.MarkChaptersUntilAsRead(userWithProgress, seriesId, chapterNumber); + await readerService.MarkChaptersUntilAsRead(userWithProgress, seriesId, chapterNumber); break; } try { - _unitOfWork.UserRepository.Update(userWithProgress); + unitOfWork.UserRepository.Update(userWithProgress); - if (!_unitOfWork.HasChanges()) return true; - if (await _unitOfWork.CommitAsync()) return true; + if (!unitOfWork.HasChanges()) return true; + if (await unitOfWork.CommitAsync(ct)) return true; } catch (Exception ex) { - _logger.LogError(ex, "There was an error saving progress from tachiyomi"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was an error saving progress from tachiyomi"); + await unitOfWork.RollbackAsync(ct); } return false; } diff --git a/API/Services/TaskScheduler.cs b/Kavita.Services/TaskScheduler.cs similarity index 81% rename from API/Services/TaskScheduler.cs rename to Kavita.Services/TaskScheduler.cs index 5964f8c3a..9a9c36fe9 100644 --- a/API/Services/TaskScheduler.cs +++ b/Kavita.Services/TaskScheduler.cs @@ -2,51 +2,33 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.Entities.Enums; -using API.Entities.Enums.User; -using API.Extensions; -using API.Helpers; -using API.Helpers.Converters; -using API.Services.Caching; -using API.Services.Plus; -using API.Services.Reading; -using API.Services.Tasks; -using API.Services.Tasks.Metadata; -using API.SignalR; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.Scanner; +using Kavita.API.Services.SignalR; +using Kavita.Common.Constants; using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Extensions; +using Kavita.Services.Plus; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Polly; using Polly.Retry; -namespace API.Services; +namespace Kavita.Services; -public interface ITaskScheduler -{ - Task ScheduleTasks(); - Task ScheduleStatsTasks(); - void ScheduleUpdaterTasks(); - Task ScheduleKavitaPlusTasks(); - void ScanFolder(string folderPath, string originalPath, TimeSpan delay); - void ScanFolder(string folderPath, bool abortOnNoSeriesMatch = false); - Task ScanLibrary(int libraryId, bool force = false); - Task ScanLibraries(bool force = false); - void CleanupChapters(int[] chapterIds); - void RefreshMetadata(int libraryId, bool forceUpdate = true, bool forceColorscape = true); - Task RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false, bool forceColorscape = false); - Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = false); - void AnalyzeFilesForSeries(int libraryId, int seriesId, bool forceUpdate = false); - void CancelStatsTasks(); - Task RunStatCollection(); - void CovertAllCoversToEncoding(); - Task CleanupDbEntries(); - Task CheckForUpdate(); - Task SyncThemes(); -} public class TaskScheduler : ITaskScheduler { private readonly ICacheService _cacheService; @@ -59,7 +41,6 @@ public class TaskScheduler : ITaskScheduler private readonly IStatsService _statsService; private readonly IVersionUpdaterService _versionUpdaterService; - private readonly IThemeService _themeService; private readonly IWordCountAnalyzerService _wordCountAnalyzerService; private readonly IStatisticService _statisticService; private readonly IMediaConversionService _mediaConversionService; @@ -70,30 +51,30 @@ public class TaskScheduler : ITaskScheduler private readonly IWantToReadSyncService _wantToReadSyncService; private readonly IEventHub _eventHub; private readonly IEmailService _emailService; - private readonly IAuthKeyCacheInvalidator _authKeyCacheInvalidator; + private readonly IAuthKeyService _authKeyService; public static BackgroundJobServer Client => new (); - public const string ScanQueue = "scan"; - public const string DefaultQueue = "default"; - public const string RemoveFromWantToReadTaskId = "remove-from-want-to-read"; - public const string UpdateYearlyStatsTaskId = "update-yearly-stats"; - public const string SyncThemesTaskId = "sync-themes"; - public const string CheckForUpdateId = "check-updates"; - public const string CleanupDbTaskId = "cleanup-db"; - public const string CleanupTaskId = "cleanup"; - public const string BackupTaskId = "backup"; - public const string ScanLibrariesTaskId = "scan-libraries"; - public const string ReportStatsTaskId = "report-stats"; - public const string CheckScrobblingTokensId = "check-scrobbling-tokens"; - public const string ProcessScrobblingEventsId = "process-scrobbling-events"; - public const string ProcessProcessedScrobblingEventsId = "process-processed-scrobbling-events"; - public const string LicenseCheckId = "license-check"; - public const string KavitaPlusDataRefreshId = "kavita+-data-refresh"; - public const string KavitaPlusStackSyncId = "kavita+-stack-sync"; - public const string KavitaPlusWantToReadSyncId = "kavita+-want-to-read-sync"; - public const string ReadingHistoryAggregationId = "reading-history-aggregation"; - public const string AuthKeyExpirationId = "auth-key-expiration"; - public const string EnsureSideNavId = "ensure-sidenav"; + public const string ScanQueue = TaskSchedulerConstants.ScanQueue; + public const string DefaultQueue = TaskSchedulerConstants.DefaultQueue; + public const string RemoveFromWantToReadTaskId = TaskSchedulerConstants.RemoveFromWantToReadTaskId; + public const string UpdateYearlyStatsTaskId = TaskSchedulerConstants.UpdateYearlyStatsTaskId; + public const string SyncThemesTaskId = TaskSchedulerConstants.SyncThemesTaskId; + public const string CheckForUpdateId = TaskSchedulerConstants.CheckForUpdateId; + public const string CleanupDbTaskId = TaskSchedulerConstants.CleanupDbTaskId; + public const string CleanupTaskId = TaskSchedulerConstants.CleanupTaskId; + public const string BackupTaskId = TaskSchedulerConstants.BackupTaskId; + public const string ScanLibrariesTaskId = TaskSchedulerConstants.ScanLibrariesTaskId; + public const string ReportStatsTaskId = TaskSchedulerConstants.ReportStatsTaskId; + public const string CheckScrobblingTokensId = TaskSchedulerConstants.CheckScrobblingTokensId; + public const string ProcessScrobblingEventsId = TaskSchedulerConstants.ProcessScrobblingEventsId; + public const string ProcessProcessedScrobblingEventsId = TaskSchedulerConstants.ProcessProcessedScrobblingEventsId; + public const string LicenseCheckId = TaskSchedulerConstants.LicenseCheckId; + public const string KavitaPlusDataRefreshId = TaskSchedulerConstants.KavitaPlusDataRefreshId; + public const string KavitaPlusStackSyncId = TaskSchedulerConstants.KavitaPlusStackSyncId; + public const string KavitaPlusWantToReadSyncId = TaskSchedulerConstants.KavitaPlusWantToReadSyncId; + public const string ReadingHistoryAggregationId = TaskSchedulerConstants.ReadingHistoryAggregationId; + public const string AuthKeyExpirationId = TaskSchedulerConstants.AuthKeyExpirationId; + public const string EnsureSideNavId = TaskSchedulerConstants.EnsureSideNavId; private const int BaseRetryDelay = 60; // 1-minute @@ -116,11 +97,11 @@ public class TaskScheduler : ITaskScheduler public TaskScheduler(ICacheService cacheService, ILogger logger, IScannerService scannerService, IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService, ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService, - IThemeService themeService, IWordCountAnalyzerService wordCountAnalyzerService, IStatisticService statisticService, + IWordCountAnalyzerService wordCountAnalyzerService, IStatisticService statisticService, IMediaConversionService mediaConversionService, IScrobblingService scrobblingService, ILicenseService licenseService, IExternalMetadataService externalMetadataService, ISmartCollectionSyncService smartCollectionSyncService, IWantToReadSyncService wantToReadSyncService, IEventHub eventHub, IEmailService emailService, - IAuthKeyCacheInvalidator authKeyCacheInvalidator) + IAuthKeyService authKeyService) { _cacheService = cacheService; _logger = logger; @@ -131,7 +112,6 @@ public class TaskScheduler : ITaskScheduler _cleanupService = cleanupService; _statsService = statsService; _versionUpdaterService = versionUpdaterService; - _themeService = themeService; _wordCountAnalyzerService = wordCountAnalyzerService; _statisticService = statisticService; _mediaConversionService = mediaConversionService; @@ -142,7 +122,7 @@ public class TaskScheduler : ITaskScheduler _wantToReadSyncService = wantToReadSyncService; _eventHub = eventHub; _emailService = emailService; - _authKeyCacheInvalidator = authKeyCacheInvalidator; + _authKeyService = authKeyService; _defaultRetryPolicy = Policy .Handle() @@ -163,12 +143,12 @@ public class TaskScheduler : ITaskScheduler ); } - public async Task ScheduleTasks() + public async Task ScheduleTasks(CancellationToken cancellationToken = default) { _logger.LogInformation("Scheduling reoccurring tasks"); - var setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).Value; + var setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan, cancellationToken)).Value; if (IsInvalidCronSetting(setting)) { _logger.LogError("Scan Task has invalid cron, defaulting to Daily"); @@ -184,11 +164,11 @@ public class TaskScheduler : ITaskScheduler } - setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskBackup)).Value; + setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskBackup, cancellationToken)).Value; if (IsInvalidCronSetting(setting)) { _logger.LogError("Backup Task has invalid cron, defaulting to Weekly"); - RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), + RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(CancellationToken.None), Cron.Weekly, RecurringJobOptions); } else @@ -200,43 +180,48 @@ public class TaskScheduler : ITaskScheduler // Override daily and make 2am so that everything on system has cleaned up and no blocking schedule = Cron.Daily(2); } - RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), + RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(CancellationToken.None), () => schedule, RecurringJobOptions); } - setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskCleanup)).Value; + setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskCleanup, cancellationToken)).Value; if (IsInvalidCronSetting(setting)) { _logger.LogError("Cleanup Task has invalid cron, defaulting to Daily"); - RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), + RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(CancellationToken.None), Cron.Daily, RecurringJobOptions); } else { _logger.LogDebug("Scheduling Cleanup Task for {Setting}", setting); - RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), + RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(CancellationToken.None), CronConverter.ConvertToCronNotation(setting), RecurringJobOptions); } - RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), + RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, + () => _cleanupService.CleanupWantToRead(CancellationToken.None), Cron.Daily, RecurringJobOptions); - RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), + RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, + () => _statisticService.UpdateServerStatistics(CancellationToken.None), Cron.Monthly, RecurringJobOptions); - RecurringJob.AddOrUpdate(SyncThemesTaskId, () => SyncThemes(), + RecurringJob.AddOrUpdate(SyncThemesTaskId, + themeService => themeService.SyncThemes(CancellationToken.None), Cron.Daily, RecurringJobOptions); - RecurringJob.AddOrUpdate(AuthKeyExpirationId, () => CheckExpiredOrExpiringAuthKeys(), + RecurringJob.AddOrUpdate(AuthKeyExpirationId, () => CheckExpiredOrExpiringAuthKeys(CancellationToken.None), Cron.Daily, RecurringJobOptions); - RecurringJob.AddOrUpdate(EnsureSideNavId, () => EnsureSideNav(), Cron.Daily(1), RecurringJobOptions); + RecurringJob.AddOrUpdate(EnsureSideNavId, () => EnsureSideNav(CancellationToken.None), + Cron.Daily(1), RecurringJobOptions); - RecurringJob.AddOrUpdate(ReadingHistoryAggregationId, service => service.AggregateYesterdaysActivity(), + RecurringJob.AddOrUpdate(ReadingHistoryAggregationId, + service => service.AggregateYesterdaysActivity(CancellationToken.None), "5 0 * * *", RecurringJobOptions); // 12:05 AM daily - await ScheduleKavitaPlusTasks(); + await ScheduleKavitaPlusTasks(cancellationToken); } private static bool IsInvalidCronSetting(string setting) @@ -244,46 +229,50 @@ public class TaskScheduler : ITaskScheduler return setting == null || (!NonCronOptions.Contains(setting) && !CronHelper.IsValidCron(setting)); } - public async Task ScheduleKavitaPlusTasks() + public async Task ScheduleKavitaPlusTasks(CancellationToken cancellationToken = default) { // KavitaPlus based (needs license check) - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - if (string.IsNullOrEmpty(license) || !await _licenseService.HasActiveSubscription(license)) // TODO: Need to convert this to a non-blocking request + var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, cancellationToken)).Value; + if (string.IsNullOrEmpty(license) || !await _licenseService.HasActiveSubscription(license, cancellationToken)) // TODO: Need to convert this to a non-blocking request { return; } - RecurringJob.AddOrUpdate(CheckScrobblingTokensId, () => _scrobblingService.CheckExternalAccessTokens(), + RecurringJob.AddOrUpdate(CheckScrobblingTokensId, + () => _scrobblingService.CheckExternalAccessTokens(CancellationToken.None), Cron.Daily, RecurringJobOptions); - BackgroundJob.Enqueue(() => _scrobblingService.CheckExternalAccessTokens()); // We also kick off an immediate check on startup + // We also kick off an immediate check on startup + BackgroundJob.Enqueue(() => _scrobblingService.CheckExternalAccessTokens(CancellationToken.None)); // Get the License Info (and cache it) on first load. This will internally cache the Github releases for the Version Service - BackgroundJob.Enqueue(() => _licenseService.GetLicenseInfo(true)); // Kick this off first to cache it then let it refresh every 9 hours (8 hour cache) - RecurringJob.AddOrUpdate(LicenseCheckId, () => _licenseService.GetLicenseInfo(false), + BackgroundJob.Enqueue(() => _licenseService.GetLicenseInfo(true, cancellationToken)); // Kick this off first to cache it then let it refresh every 9 hours (8 hour cache) + RecurringJob.AddOrUpdate(LicenseCheckId, () => _licenseService.GetLicenseInfo(false, cancellationToken), LicenseService.Cron, RecurringJobOptions); // KavitaPlus Scrobbling (every hour) - randomise minutes to spread requests out for K+ var randomMinute = Rnd.Next(0, 60); - RecurringJob.AddOrUpdate(ProcessScrobblingEventsId, () => _scrobblingService.ProcessUpdatesSinceLastSync(), + RecurringJob.AddOrUpdate(ProcessScrobblingEventsId, + () => _scrobblingService.ProcessUpdatesSinceLastSync(CancellationToken.None), Cron.Hourly(randomMinute), RecurringJobOptions); - RecurringJob.AddOrUpdate(ProcessProcessedScrobblingEventsId, () => _scrobblingService.ClearProcessedEvents(), + RecurringJob.AddOrUpdate(ProcessProcessedScrobblingEventsId, + () => _scrobblingService.ClearProcessedEvents(CancellationToken.None), Cron.Daily, RecurringJobOptions); // Backfilling/Freshening Reviews/Rating/Recommendations var randomKPlusBackfill = Rnd.Next(1, 5); RecurringJob.AddOrUpdate(KavitaPlusDataRefreshId, - () => _externalMetadataService.FetchExternalDataTask(), Cron.Daily(randomKPlusBackfill), - RecurringJobOptions); + () => _externalMetadataService.FetchExternalDataTask(CancellationToken.None), + Cron.Daily(randomKPlusBackfill), RecurringJobOptions); // This shouldn't be so close to fetching data due to Rate limit concerns var randomKPlusStackSync = Rnd.Next(6, 10); RecurringJob.AddOrUpdate(KavitaPlusStackSyncId, - () => _smartCollectionSyncService.Sync(), Cron.Daily(randomKPlusStackSync), - RecurringJobOptions); + () => _smartCollectionSyncService.Sync(CancellationToken.None), + Cron.Daily(randomKPlusStackSync), RecurringJobOptions); RecurringJob.AddOrUpdate(KavitaPlusWantToReadSyncId, - () => _wantToReadSyncService.Sync(), Cron.Weekly(DayOfWeekHelper.Random()), - RecurringJobOptions); + () => _wantToReadSyncService.Sync(CancellationToken.None), + Cron.Weekly(DayOfWeekHelper.Random()), RecurringJobOptions); } @@ -304,9 +293,9 @@ public class TaskScheduler : ITaskScheduler #region StatsTasks - public async Task ScheduleStatsTasks() + public async Task ScheduleStatsTasks(CancellationToken cancellationToken = default) { - var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).AllowStatCollection; + var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(cancellationToken)).AllowStatCollection; if (!allowStatCollection) { _logger.LogDebug("User has opted out of stat collection, not registering tasks"); @@ -317,7 +306,7 @@ public class TaskScheduler : ITaskScheduler var hour = Rnd.Next(0, 22); _logger.LogDebug("Scheduling stat collection daily at {Hour}:00", hour); - RecurringJob.AddOrUpdate(ReportStatsTaskId, () => _statsService.Send(), Cron.Daily(hour), RecurringJobOptions); + RecurringJob.AddOrUpdate(ReportStatsTaskId, () => _statsService.Send(CancellationToken.None), Cron.Daily(hour), RecurringJobOptions); } @@ -346,7 +335,7 @@ public class TaskScheduler : ITaskScheduler BackgroundJob.Schedule(() => _statsService.Send(), DateTimeOffset.Now.AddDays(1)); } - public void CovertAllCoversToEncoding() + public void ConvertAllCoversToEncoding() { var defaultParams = Array.Empty(); if (MediaConversionService.ConversionMethods.Any(method => @@ -365,8 +354,8 @@ public class TaskScheduler : ITaskScheduler public void ScheduleUpdaterTasks() { _logger.LogInformation("Scheduling Auto-Update tasks"); - RecurringJob.AddOrUpdate(CheckForUpdateId, () => CheckForUpdate(), $"0 */{Rnd.Next(4, 6)} * * *", RecurringJobOptions); - BackgroundJob.Enqueue(() => CheckForUpdate()); + RecurringJob.AddOrUpdate(CheckForUpdateId, () => CheckForUpdate(CancellationToken.None), $"0 */{Rnd.Next(4, 6)} * * *", RecurringJobOptions); + BackgroundJob.Enqueue(() => CheckForUpdate(CancellationToken.None)); } /// @@ -377,8 +366,8 @@ public class TaskScheduler : ITaskScheduler /// public void ScanFolder(string folderPath, string originalPath, TimeSpan delay) { - var normalizedFolder = Tasks.Scanner.Parser.Parser.NormalizePath(folderPath); - var normalizedOriginal = Tasks.Scanner.Parser.Parser.NormalizePath(originalPath); + var normalizedFolder = Parser.NormalizePath(folderPath); + var normalizedOriginal = Parser.NormalizePath(originalPath); if (HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", [normalizedFolder, normalizedOriginal]) || HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", [normalizedFolder, string.Empty])) @@ -397,7 +386,7 @@ public class TaskScheduler : ITaskScheduler public void ScanFolder(string folderPath, bool abortOnNoSeriesMatch = false) { - var normalizedFolder = Tasks.Scanner.Parser.Parser.NormalizePath(folderPath); + var normalizedFolder = Parser.NormalizePath(folderPath); if (HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", [normalizedFolder, string.Empty])) { _logger.LogTrace("Skipped scheduling ScanFolder for {Folder} as a job already queued", @@ -541,30 +530,25 @@ public class TaskScheduler : ITaskScheduler /// Not an external call. Only public so that we can call this for a Task /// // ReSharper disable once MemberCanBePrivate.Global - public async Task CheckForUpdate() + public async Task CheckForUpdate(CancellationToken cancellationToken = default) { await _defaultRetryPolicy.ExecuteAsync(async () => { - var update = await _versionUpdaterService.CheckForUpdate(); + var update = await _versionUpdaterService.CheckForUpdate(cancellationToken); if (update == null) return; - await _versionUpdaterService.PushUpdate(update); + await _versionUpdaterService.PushUpdate(update, cancellationToken); }); } - public async Task SyncThemes() - { - await _themeService.SyncThemes(); - } - /// /// Checks for any user that does not have a Library Side nav, when they should /// /// 2 users reported this issue, I cannot reproduce, this is a precaution - public async Task EnsureSideNav() + public async Task EnsureSideNav(CancellationToken ct = default) { - var users = await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams); - var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync(LibraryIncludes.AppUser)).ToList(); + var users = await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams, ct: ct); + var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync(LibraryIncludes.AppUser, ct: ct)).ToList(); var libraryLookup = libraries.ToDictionary(l => l.Id); // Build a lookup: userId -> set of library IDs they have access to @@ -596,7 +580,7 @@ public class TaskScheduler : ITaskScheduler var existingLibraryIds = user.SideNavStreams? .Where(s => s.LibraryId.HasValue) .Select(s => s.LibraryId!.Value) - .ToHashSet(); + .ToHashSet() ?? []; var missingLibIds = accessibleLibraryIds.Except(existingLibraryIds).ToList(); @@ -617,20 +601,20 @@ public class TaskScheduler : ITaskScheduler if (hasChanges) { - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); } } /// /// Checks for soon to be expired and expired Auth keys and attempts to email the users /// - public async Task CheckExpiredOrExpiringAuthKeys() + public async Task CheckExpiredOrExpiringAuthKeys(CancellationToken ct = default) { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); if (!settings.IsEmailSetup()) return; _logger.LogInformation("Checking for Expired or Expiring Auth Keys"); - var users = await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.AuthKeys); + var users = await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.AuthKeys, ct: ct); foreach (var user in users) { // Implies only the default keys @@ -665,7 +649,7 @@ public class TaskScheduler : ITaskScheduler foreach (var expiredKey in expiredKeys) { - await _authKeyCacheInvalidator.InvalidateAsync(expiredKey.Key); + await _authKeyService.InvalidateAsync(expiredKey.Key, ct); } } } diff --git a/API/Services/TokenService.cs b/Kavita.Services/TokenService.cs similarity index 56% rename from API/Services/TokenService.cs rename to Kavita.Services/TokenService.cs index a2a82e384..6d081cd3f 100644 --- a/API/Services/TokenService.cs +++ b/Kavita.Services/TokenService.cs @@ -6,10 +6,11 @@ using System.Security.Claims; using System.Text; using System.Threading; using System.Threading.Tasks; -using API.DTOs.Account; -using API.DTOs.Internal; -using API.Entities; -using API.Helpers; +using Kavita.API.Services; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Internal; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -18,35 +19,21 @@ using static System.Security.Claims.ClaimTypes; using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface ITokenService + +public class TokenService( + IOptions config, + UserManager userManager, + ILogger logger) + : ITokenService { - Task CreateToken(AppUser user); - Task ValidateRefreshToken(TokenRequestDto request); - Task CreateRefreshToken(AppUser user); - Task GetJwtFromUser(AppUser user); -} - - -public class TokenService : ITokenService -{ - private readonly UserManager _userManager; - private readonly ILogger _logger; - private readonly SymmetricSecurityKey _key; - private const string RefreshTokenName = "RefreshToken"; private static readonly SemaphoreSlim RefreshTokenLock = new(1, 1); - public TokenService(IOptions config, UserManager userManager, ILogger logger) - { - _userManager = userManager; - _logger = logger; + private const string RefreshTokenName = "RefreshToken"; + private readonly SymmetricSecurityKey _key = new(Encoding.UTF8.GetBytes(config.Value.TokenKey)); - _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.Value.TokenKey)); - } - - public async Task CreateToken(AppUser user) + public async Task CreateToken(AppUser user, CancellationToken ct = default) { var claims = new List { @@ -54,7 +41,7 @@ public class TokenService : ITokenService new(NameIdentifier, user.Id.ToString()), }; - var roles = await _userManager.GetRolesAsync(user); + var roles = await userManager.GetRolesAsync(user); claims.AddRange(roles.Select(role => new Claim(Role, role))); var credentials = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature); @@ -71,17 +58,17 @@ public class TokenService : ITokenService return tokenHandler.WriteToken(token); } - public async Task CreateRefreshToken(AppUser user) + public async Task CreateRefreshToken(AppUser user, CancellationToken ct = default) { - await _userManager.RemoveAuthenticationTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName); - var refreshToken = await _userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName); - await _userManager.SetAuthenticationTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, refreshToken); + await userManager.RemoveAuthenticationTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName); + var refreshToken = await userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName); + await userManager.SetAuthenticationTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, refreshToken); return refreshToken; } - public async Task ValidateRefreshToken(TokenRequestDto request) + public async Task ValidateRefreshToken(TokenRequestDto request, CancellationToken ct = default) { - await RefreshTokenLock.WaitAsync(); + await RefreshTokenLock.WaitAsync(ct); try { @@ -90,46 +77,46 @@ public class TokenService : ITokenService var username = tokenContent.Claims.FirstOrDefault(q => q.Type == JwtRegisteredClaimNames.Name)?.Value; if (string.IsNullOrEmpty(username)) { - _logger.LogDebug("[RefreshToken] failed to validate due to not finding user in RefreshToken"); + logger.LogDebug("[RefreshToken] failed to validate due to not finding user in RefreshToken"); return null; } - var user = await _userManager.FindByNameAsync(username); + var user = await userManager.FindByNameAsync(username); if (user == null) { - _logger.LogDebug("[RefreshToken] failed to validate due to not finding user in DB"); + logger.LogDebug("[RefreshToken] failed to validate due to not finding user in DB"); return null; } - var validated = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, + var validated = await userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, request.RefreshToken); if (!validated && tokenContent.ValidTo <= DateTime.UtcNow.Add(TimeSpan.FromHours(1))) { - _logger.LogDebug("[RefreshToken] failed to validate due to invalid refresh token"); + logger.LogDebug("[RefreshToken] failed to validate due to invalid refresh token"); return null; } // Remove the old refresh token first - await _userManager.RemoveAuthenticationTokenAsync(user, + await userManager.RemoveAuthenticationTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName); return new TokenRequestDto() { - Token = await CreateToken(user), - RefreshToken = await CreateRefreshToken(user) + Token = await CreateToken(user, ct), + RefreshToken = await CreateRefreshToken(user, ct) }; } catch (SecurityTokenExpiredException ex) { // Handle expired token - _logger.LogError(ex, "Failed to validate refresh token"); + logger.LogError(ex, "Failed to validate refresh token"); return null; } catch (Exception ex) { // Handle other exceptions - _logger.LogError(ex, "Failed to validate refresh token"); + logger.LogError(ex, "Failed to validate refresh token"); return null; } finally @@ -138,9 +125,9 @@ public class TokenService : ITokenService } } - public async Task GetJwtFromUser(AppUser user) + public async Task GetJwtFromUser(AppUser user, CancellationToken ct = default) { - var userClaims = await _userManager.GetClaimsAsync(user); + var userClaims = await userManager.GetClaimsAsync(user); var jwtClaim = userClaims.FirstOrDefault(claim => claim.Type == "jwt"); return jwtClaim?.Value; } diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/Kavita.Services/VersionUpdaterService.cs similarity index 97% rename from API/Services/Tasks/VersionUpdaterService.cs rename to Kavita.Services/VersionUpdaterService.cs index c8e118443..16595fb44 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/Kavita.Services/VersionUpdaterService.cs @@ -5,19 +5,21 @@ using System.IO; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.Update; -using API.Extensions; -using API.SignalR; using Flurl.Http; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.DTOs.Update; using MarkdownDeep; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace API.Services.Tasks; -#nullable enable +namespace Kavita.Services; internal class GithubReleaseMetadata { @@ -47,15 +49,6 @@ internal class GithubReleaseMetadata public required string Published_At { get; init; } } -public interface IVersionUpdaterService -{ - Task CheckForUpdate(); - Task PushUpdate(UpdateNotificationDto update); - Task> GetAllReleases(int count = 0); - Task GetNumberOfReleasesBehind(bool stableOnly = false); - void BustGithubCache(); -} - public partial class VersionUpdaterService : IVersionUpdaterService { @@ -103,8 +96,9 @@ public partial class VersionUpdaterService : IVersionUpdaterService /// /// Fetches the latest (stable) release from GitHub. Does not do any extra nightly release parsing. /// + /// /// Latest update - public async Task CheckForUpdate() + public async Task CheckForUpdate(CancellationToken ct = default) { // Attempt to fetch from cache var cachedRelease = await TryGetCachedLatestRelease(); @@ -150,7 +144,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService var nightlyDto = new UpdateNotificationDto { - // TODO: I should pass Title to the FE so that Nightly Release can be localized + // default: I should pass Title to the FE so that Nightly Release can be localized UpdateTitle = $"Nightly Release {nightly.Version} - {prInfo.Title}", UpdateVersion = nightly.Version, CurrentVersion = dto.CurrentVersion, @@ -314,7 +308,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService } } - public async Task> GetAllReleases(int count = 0) + public async Task> GetAllReleases(int count = 0, CancellationToken ct = default) { // Attempt to fetch from cache var cachedReleases = await TryGetCachedReleases(); @@ -483,10 +477,11 @@ public partial class VersionUpdaterService : IVersionUpdaterService /// then include nightly releases, otherwise only count Stable releases. /// /// Only count Stable releases + /// /// - public async Task GetNumberOfReleasesBehind(bool stableOnly = false) + public async Task GetNumberOfReleasesBehind(bool stableOnly = false, CancellationToken ct = default) { - var updates = await GetAllReleases(); + var updates = await GetAllReleases(ct: ct); // If the user is on nightly, then we need to handle releases behind differently if (!stableOnly && (updates[0].IsPrerelease || updates[0].IsOnNightlyInRelease)) @@ -502,7 +497,8 @@ public partial class VersionUpdaterService : IVersionUpdaterService /// /// Clears the Github cache /// - public void BustGithubCache() + /// + public void BustGithubCache(CancellationToken ct = default) { try { @@ -558,7 +554,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService } - public async Task PushUpdate(UpdateNotificationDto? update) + public async Task PushUpdate(UpdateNotificationDto update, CancellationToken ct = default) { if (update == null) return; @@ -568,7 +564,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService { _logger.LogWarning("Server is out of date. Current: {CurrentVersion}. Available: {AvailableUpdate}", BuildInfo.Version, updateVersion); await _eventHub.SendMessageAsync(MessageFactory.UpdateAvailable, MessageFactory.UpdateVersionEvent(update), - true); + true, ct); } } diff --git a/Kavita.sln b/Kavita.sln index 670808870..a279abd73 100644 --- a/Kavita.sln +++ b/Kavita.sln @@ -3,13 +3,29 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API.Tests", "API.Tests\API.Tests.csproj", "{6F7910F2-1B95-4570-A490-519C8935B9D1}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Common", "Kavita.Common\Kavita.Common.csproj", "{165A86F5-9E74-4C05-9305-A6F0BA32C9EE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API.Benchmark", "API.Benchmark\API.Benchmark.csproj", "{3D781D18-2452-421F-A81A-59254FEE1FEC}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.API", "Kavita.API\Kavita.API.csproj", "{01191616-53A6-4C12-B39E-CC6B30364399}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Server", "Kavita.Server\Kavita.Server.csproj", "{D5AED3C5-1CB2-43EC-B17B-6635FD88407B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Services", "Kavita.Services\Kavita.Services.csproj", "{A45040F3-81F3-45C7-9EBD-BF4289E39F68}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Models", "Kavita.Models\Kavita.Models.csproj", "{1F33A4EA-5F5D-453A-991B-BAECA4AECB65}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Database", "Kavita.Database\Kavita.Database.csproj", "{1133F869-1D61-466C-8B33-0E3286861F25}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Database.Tests", "Kavita.Database.Tests\Kavita.Database.Tests.csproj", "{D150983F-448E-465A-A4A4-9DC08095E22D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Services.Tests", "Kavita.Services.Tests\Kavita.Services.Tests.csproj", "{6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Server.Tests", "Kavita.Server.Tests\Kavita.Server.Tests.csproj", "{1A3CF436-9715-4942-A584-801F3CE10A86}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Common.Tests", "Kavita.Common.Tests\Kavita.Common.Tests.csproj", "{5C8EE151-361E-4C2C-BB1D-9828A7922876}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Models.Tests", "Kavita.Models.Tests\Kavita.Models.Tests.csproj", "{7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Benchmark", "Kavita.Benchmark\Kavita.Benchmark.csproj", "{CF765586-FAF5-4701-900D-C2CE8897CCC7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -24,30 +40,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Debug|x64.ActiveCfg = Debug|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Debug|x64.Build.0 = Debug|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Debug|x86.ActiveCfg = Debug|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Debug|x86.Build.0 = Debug|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Release|Any CPU.Build.0 = Release|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Release|x64.ActiveCfg = Release|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Release|x64.Build.0 = Release|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Release|x86.ActiveCfg = Release|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Release|x86.Build.0 = Release|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Debug|x64.ActiveCfg = Debug|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Debug|x64.Build.0 = Debug|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Debug|x86.ActiveCfg = Debug|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Debug|x86.Build.0 = Debug|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|Any CPU.Build.0 = Release|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|x64.ActiveCfg = Release|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|x64.Build.0 = Release|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|x86.ActiveCfg = Release|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|x86.Build.0 = Release|Any CPU {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Debug|Any CPU.Build.0 = Debug|Any CPU {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -60,17 +52,137 @@ Global {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x64.Build.0 = Release|Any CPU {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x86.ActiveCfg = Release|Any CPU {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x86.Build.0 = Release|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|x64.ActiveCfg = Debug|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|x64.Build.0 = Debug|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|x86.ActiveCfg = Debug|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|x86.Build.0 = Debug|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|Any CPU.Build.0 = Release|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x64.ActiveCfg = Release|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x64.Build.0 = Release|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x86.ActiveCfg = Release|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x86.Build.0 = Release|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Debug|x64.ActiveCfg = Debug|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Debug|x64.Build.0 = Debug|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Debug|x86.ActiveCfg = Debug|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Debug|x86.Build.0 = Debug|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Release|Any CPU.Build.0 = Release|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Release|x64.ActiveCfg = Release|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Release|x64.Build.0 = Release|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Release|x86.ActiveCfg = Release|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Release|x86.Build.0 = Release|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Debug|x64.ActiveCfg = Debug|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Debug|x64.Build.0 = Debug|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Debug|x86.ActiveCfg = Debug|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Debug|x86.Build.0 = Debug|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Release|Any CPU.Build.0 = Release|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Release|x64.ActiveCfg = Release|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Release|x64.Build.0 = Release|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Release|x86.ActiveCfg = Release|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Release|x86.Build.0 = Release|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Debug|x64.ActiveCfg = Debug|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Debug|x64.Build.0 = Debug|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Debug|x86.ActiveCfg = Debug|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Debug|x86.Build.0 = Debug|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Release|Any CPU.Build.0 = Release|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Release|x64.ActiveCfg = Release|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Release|x64.Build.0 = Release|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Release|x86.ActiveCfg = Release|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Release|x86.Build.0 = Release|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Debug|x64.ActiveCfg = Debug|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Debug|x64.Build.0 = Debug|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Debug|x86.Build.0 = Debug|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Release|Any CPU.Build.0 = Release|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Release|x64.ActiveCfg = Release|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Release|x64.Build.0 = Release|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Release|x86.ActiveCfg = Release|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Release|x86.Build.0 = Release|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Debug|x64.ActiveCfg = Debug|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Debug|x64.Build.0 = Debug|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Debug|x86.ActiveCfg = Debug|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Debug|x86.Build.0 = Debug|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Release|Any CPU.Build.0 = Release|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Release|x64.ActiveCfg = Release|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Release|x64.Build.0 = Release|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Release|x86.ActiveCfg = Release|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Release|x86.Build.0 = Release|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Debug|x64.ActiveCfg = Debug|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Debug|x64.Build.0 = Debug|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Debug|x86.ActiveCfg = Debug|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Debug|x86.Build.0 = Debug|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Release|Any CPU.Build.0 = Release|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Release|x64.ActiveCfg = Release|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Release|x64.Build.0 = Release|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Release|x86.ActiveCfg = Release|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Release|x86.Build.0 = Release|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Debug|x64.ActiveCfg = Debug|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Debug|x64.Build.0 = Debug|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Debug|x86.ActiveCfg = Debug|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Debug|x86.Build.0 = Debug|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Release|Any CPU.Build.0 = Release|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Release|x64.ActiveCfg = Release|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Release|x64.Build.0 = Release|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Release|x86.ActiveCfg = Release|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Release|x86.Build.0 = Release|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Debug|x64.Build.0 = Debug|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Debug|x86.Build.0 = Debug|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Release|Any CPU.Build.0 = Release|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Release|x64.ActiveCfg = Release|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Release|x64.Build.0 = Release|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Release|x86.ActiveCfg = Release|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Release|x86.Build.0 = Release|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Debug|x64.ActiveCfg = Debug|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Debug|x64.Build.0 = Debug|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Debug|x86.ActiveCfg = Debug|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Debug|x86.Build.0 = Debug|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Release|Any CPU.Build.0 = Release|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Release|x64.ActiveCfg = Release|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Release|x64.Build.0 = Release|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Release|x86.ActiveCfg = Release|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Release|x86.Build.0 = Release|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Debug|x64.ActiveCfg = Debug|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Debug|x64.Build.0 = Debug|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Debug|x86.Build.0 = Debug|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Release|Any CPU.Build.0 = Release|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Release|x64.ActiveCfg = Release|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Release|x64.Build.0 = Release|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Release|x86.ActiveCfg = Release|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Release|x86.Build.0 = Release|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Debug|x64.ActiveCfg = Debug|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Debug|x64.Build.0 = Debug|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Debug|x86.ActiveCfg = Debug|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Debug|x86.Build.0 = Debug|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Release|Any CPU.Build.0 = Release|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Release|x64.ActiveCfg = Release|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Release|x64.Build.0 = Release|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Release|x86.ActiveCfg = Release|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 3edec9f7a..3ffe1c14a 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -9,25 +9,25 @@ "version": "0.7.12.1", "dependencies": { "@angular-slider/ngx-slider": "^21.0.0", - "@angular/animations": "^21.0.6", - "@angular/cdk": "^21.0.3", - "@angular/common": "^21.0.6", - "@angular/compiler": "^21.0.6", - "@angular/core": "^21.0.6", - "@angular/forms": "^21.0.6", - "@angular/localize": "^21.0.6", - "@angular/platform-browser": "^21.0.6", - "@angular/platform-browser-dynamic": "^21.0.6", - "@angular/router": "^21.0.6", - "@fortawesome/fontawesome-free": "^7.1.0", - "@iharbeck/ngx-virtual-scroller": "^19.0.1", + "@angular/animations": "^21.2.1", + "@angular/cdk": "^21.2.1", + "@angular/common": "^21.2.1", + "@angular/compiler": "^21.2.1", + "@angular/core": "^21.2.1", + "@angular/forms": "^21.2.1", + "@angular/localize": "^21.2.1", + "@angular/platform-browser": "^21.2.1", + "@angular/platform-browser-dynamic": "^21.2.1", + "@angular/router": "^21.2.1", + "@fortawesome/fontawesome-free": "^7.2.0", + "@iharbeck/ngx-virtual-scroller": "^20.0.0", "@iplab/ngx-color-picker": "^21.0.0", "@iplab/ngx-file-upload": "^21.0.0", - "@jsverse/transloco": "^8.2.0", - "@jsverse/transloco-locale": "^8.2.0", - "@jsverse/transloco-persist-lang": "^8.2.0", - "@jsverse/transloco-persist-translations": "^8.2.0", - "@jsverse/transloco-preload-langs": "^8.2.0", + "@jsverse/transloco": "^8.2.1", + "@jsverse/transloco-locale": "^8.2.1", + "@jsverse/transloco-persist-lang": "^8.2.1", + "@jsverse/transloco-persist-translations": "^8.2.1", + "@jsverse/transloco-preload-langs": "^8.2.1", "@microsoft/signalr": "^10.0.0", "@ng-bootstrap/ng-bootstrap": "^20.0.0", "@popperjs/core": "^2.11.7", @@ -49,89 +49,89 @@ "ngx-stars": "^1.6.5", "ngx-toastr": "^19.1.0", "nosleep.js": "^0.12.0", - "quill": "^2.0.3", + "quill": "^2.0.2", "rxjs": "^7.8.2", "screenfull": "^6.0.2", - "swiper": "^12.0.3", + "swiper": "^12.1.2", "tslib": "^2.8.1", - "zone.js": "^0.16.0" + "zone.js": "^0.16.1" }, "devDependencies": { - "@angular-eslint/builder": "^21.1.0", - "@angular-eslint/eslint-plugin": "^21.1.0", - "@angular-eslint/eslint-plugin-template": "^21.1.0", - "@angular-eslint/schematics": "^21.1.0", - "@angular-eslint/template-parser": "^21.1.0", - "@angular/build": "^21.0.3", - "@angular/cli": "^21.0.3", - "@angular/compiler-cli": "^21.0.6", + "@angular-eslint/builder": "^21.3.0", + "@angular-eslint/eslint-plugin": "^21.3.0", + "@angular-eslint/eslint-plugin-template": "^21.3.0", + "@angular-eslint/schematics": "^21.3.0", + "@angular-eslint/template-parser": "^21.3.0", + "@angular/build": "^21.2.1", + "@angular/cli": "^21.2.1", + "@angular/compiler-cli": "^21.2.1", "@types/d3": "^7.4.3", "@types/file-saver": "^2.0.7", "@types/luxon": "^3.7.1", "@types/marked": "^5.0.2", - "@types/node": "^25.0.2", - "@typescript-eslint/eslint-plugin": "^8.50.0", - "@typescript-eslint/parser": "^8.50.0", - "eslint": "^9.39.2", + "@types/node": "^25.3.3", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^10.0.2", "jsonminify": "^0.4.2", "karma-coverage": "~2.2.0", "ts-node": "~10.9.1", "typescript": "^5.9.3", - "webpack-bundle-analyzer": "^5.1.0" + "webpack-bundle-analyzer": "^5.2.0" } }, "node_modules/@algolia/abtesting": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.6.1.tgz", - "integrity": "sha512-wV/gNRkzb7sI9vs1OneG129hwe3Q5zPj7zigz3Ps7M5Lpo2hSorrOnXNodHEOV+yXE/ks4Pd+G3CDFIjFTWhMQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", + "integrity": "sha512-Dkj0BgPiLAaim9sbQ97UKDFHJE/880wgStAM18U++NaJ/2Cws34J5731ovJifr6E3Pv4T2CqvMXf8qLCC417Ew==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-abtesting": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.40.1.tgz", - "integrity": "sha512-cxKNATPY5t+Mv8XAVTI57altkaPH+DZi4uMrnexPxPHODMljhGYY+GDZyHwv9a+8CbZHcY372OkxXrDMZA4Lnw==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.1.tgz", + "integrity": "sha512-LV5qCJdj+/m9I+Aj91o+glYszrzd7CX6NgKaYdTOj4+tUYfbS62pwYgUfZprYNayhkQpVFcrW8x8ZlIHpS23Vw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.40.1.tgz", - "integrity": "sha512-XP008aMffJCRGAY8/70t+hyEyvqqV7YKm502VPu0+Ji30oefrTn2al7LXkITz7CK6I4eYXWRhN6NaIUi65F1OA==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.1.tgz", + "integrity": "sha512-/AVoMqHhPm14CcHq7mwB+bUJbfCv+jrxlNvRjXAuO+TQa+V37N8k1b0ijaRBPdmSjULMd8KtJbQyUyabXOu6Kg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.40.1.tgz", - "integrity": "sha512-gWfQuQUBtzUboJv/apVGZMoxSaB0M4Imwl1c9Ap+HpCW7V0KhjBddqF2QQt5tJZCOFsfNIgBbZDGsEPaeKUosw==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.1.tgz", + "integrity": "sha512-VXO+qu2Ep6ota28ktvBm3sG53wUHS2n7bgLWmce5jTskdlCD0/JrV4tnBm1l7qpla1CeoQb8D7ShFhad+UoSOw==", "dev": true, "license": "MIT", "engines": { @@ -139,151 +139,151 @@ } }, "node_modules/@algolia/client-insights": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.40.1.tgz", - "integrity": "sha512-RTLjST/t+lsLMouQ4zeLJq2Ss+UNkLGyNVu+yWHanx6kQ3LT5jv8UvPwyht9s7R6jCPnlSI77WnL80J32ZuyJg==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.1.tgz", + "integrity": "sha512-zl+Qyb0nLg+Y5YvKp1Ij+u9OaPaKg2/EPzTwKNiVyOHnQJlFxmXyUZL1EInczAZsEY8hVpPCLtNfhMhfxluXKQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.40.1.tgz", - "integrity": "sha512-2FEK6bUomBzEYkTKzD0iRs7Ljtjb45rKK/VSkyHqeJnG+77qx557IeSO0qVFE3SfzapNcoytTofnZum0BQ6r3Q==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.1.tgz", + "integrity": "sha512-r89Qf9Oo9mKWQXumRu/1LtvVJAmEDpn8mHZMc485pRfQUMAwSSrsnaw1tQ3sszqzEgAr1c7rw6fjBI+zrAXTOw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.40.1.tgz", - "integrity": "sha512-Nju4NtxAvXjrV2hHZNLKVJLXjOlW6jAXHef/CwNzk1b2qIrCWDO589ELi5ZHH1uiWYoYyBXDQTtHmhaOVVoyXg==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.1.tgz", + "integrity": "sha512-TPKNPKfghKG/bMSc7mQYD9HxHRUkBZA4q1PEmHgICaSeHQscGqL4wBrKkhfPlDV1uYBKW02pbFMUhsOt7p4ZpA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.40.1.tgz", - "integrity": "sha512-Mw6pAUF121MfngQtcUb5quZVqMC68pSYYjCRZkSITC085S3zdk+h/g7i6FxnVdbSU6OztxikSDMh1r7Z+4iPlA==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.1.tgz", + "integrity": "sha512-4Fu7dnzQyQmMFknYwTiN/HxPbH4DyxvQ1m+IxpPp5oslOgz8m6PG5qhiGbqJzH4HiT1I58ecDiCAC716UyVA8Q==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/ingestion": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.40.1.tgz", - "integrity": "sha512-z+BPlhs45VURKJIxsR99NNBWpUEEqIgwt10v/fATlNxc4UlXvALdOsWzaFfe89/lbP5Bu4+mbO59nqBC87ZM/g==", + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.1.tgz", + "integrity": "sha512-/RFq3TqtXDUUawwic/A9xylA2P3LDMO8dNhphHAUOU51b1ZLHrmZ6YYJm3df1APz7xLY1aht6okCQf+/vmrV9w==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.40.1.tgz", - "integrity": "sha512-VJMUMbO0wD8Rd2VVV/nlFtLJsOAQvjnVNGkMkspFiFhpBA7s/xJOb+fJvvqwKFUjbKTUA7DjiSi1ljSMYBasXg==", + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.1.tgz", + "integrity": "sha512-Of0jTeAZRyRhC7XzDSjJef0aBkgRcvRAaw0ooYRlOw57APii7lZdq+layuNdeL72BRq1snaJhoMMwkmLIpJScw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.40.1.tgz", - "integrity": "sha512-ehvJLadKVwTp9Scg9NfzVSlBKH34KoWOQNTaN8i1Ac64AnO6iH2apJVSP6GOxssaghZ/s8mFQsDH3QIZoluFHA==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.1.tgz", + "integrity": "sha512-bE7JcpFXzxF5zHwj/vkl2eiCBvyR1zQ7aoUdO+GDXxGp0DGw7nI0p8Xj6u8VmRQ+RDuPcICFQcCwRIJT5tDJFw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.40.1.tgz", - "integrity": "sha512-PbidVsPurUSQIr6X9/7s34mgOMdJnn0i6p+N6Ab+lsNhY5eiu+S33kZEpZwkITYBCIbhzDLOvb7xZD3gDi+USA==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.1.tgz", + "integrity": "sha512-MK3wZ2koLDnvH/AmqIF1EKbJlhRS5j74OZGkLpxI4rYvNi9Jn/C7vb5DytBnQ4KUWts7QsmbdwHkxY5txQHXVw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1" + "@algolia/client-common": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.40.1.tgz", - "integrity": "sha512-ThZ5j6uOZCF11fMw9IBkhigjOYdXGXQpj6h4k+T9UkZrF2RlKcPynFzDeRgaLdpYk8Yn3/MnFbwUmib7yxj5Lw==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.1.tgz", + "integrity": "sha512-2oDT43Y5HWRSIQMPQI4tA/W+TN/N2tjggZCUsqQV440kxzzoPGsvv9QP1GhQ4CoDa+yn6ygUsGp6Dr+a9sPPSg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1" + "@algolia/client-common": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.40.1.tgz", - "integrity": "sha512-H1gYPojO6krWHnUXu/T44DrEun/Wl95PJzMXRcM/szstNQczSbwq6wIFJPI9nyE95tarZfUNU3rgorT+wZ6iCQ==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.1.tgz", + "integrity": "sha512-xcaCqbhupVWhuBP1nwbk1XNvwrGljozutEiLx06mvqDf3o8cHyEgQSHS4fKJM+UAggaWVnnFW+Nne5aQ8SUJXg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1" + "@algolia/client-common": "5.48.1" }, "engines": { "node": ">= 14.0.0" @@ -304,21 +304,103 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2100.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2100.5.tgz", - "integrity": "sha512-KKmZMXzHCX0cWHY7xo9yy1J0fV7S/suhPO00YTcHBgLivkLsnbI177CrmWiMdLxSJD3NqTVkBEMPFQ2I2ooDFw==", + "version": "0.2102.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.1.tgz", + "integrity": "sha512-x2Qqz6oLYvEh9UBUG0AP1A4zROO/VP+k+zM9+4c2uZw1uqoBQFmutqgzncjVU7cR9R0RApgx9JRZHDFtQru68w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.0.5", + "@angular-devkit/core": "21.2.1", "rxjs": "7.8.2" }, + "bin": { + "architect": "bin/cli.js" + }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": { + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.1.tgz", + "integrity": "sha512-TpXGjERqVPN8EPt7LdmWAwh0oNQ/6uWFutzGZiXhJy81n1zb1O1XrqhRAmvP1cAo5O+na6IV2JkkCmxL6F8GUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/architect/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/architect/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular-devkit/architect/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-devkit/core": { "version": "21.0.5", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.0.5.tgz", @@ -348,16 +430,16 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "21.0.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.0.5.tgz", - "integrity": "sha512-U6Z/OEce3R9CJl8/xuVrNVp0uhv3Ac4wRjpG18kE0dh5R87ablhqr/wkP3rZbWpdGwuGSJ+cR7LE5IbwSswejA==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.1.tgz", + "integrity": "sha512-CWoamHaasAHMjHcYqxbj0tMnoXxdGotcAz2SpiuWtH28Lnf5xfbTaJn/lwdMP8Wdh4tgA+uYh2l45A5auCwmkw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.0.5", + "@angular-devkit/core": "21.2.1", "jsonc-parser": "3.3.1", - "magic-string": "0.30.19", - "ora": "9.0.0", + "magic-string": "0.30.21", + "ora": "9.3.0", "rxjs": "7.8.2" }, "engines": { @@ -366,10 +448,89 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.1.tgz", + "integrity": "sha512-TpXGjERqVPN8EPt7LdmWAwh0oNQ/6uWFutzGZiXhJy81n1zb1O1XrqhRAmvP1cAo5O+na6IV2JkkCmxL6F8GUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-eslint/builder": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.1.0.tgz", - "integrity": "sha512-pcUlDkGqeZ+oQC0oEjnkDDlB96gbgHQhnBUKdhYAiAOSuiBod4+npP0xQOq5chYtRNPBprhDqgrJrp5DBeDMOA==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.3.0.tgz", + "integrity": "sha512-26QUUouei52biUFAlJSrWNAU9tuF2miKwd8uHdxWwCF31xz+OxC5+NfudWvt1AFaYow7gWueX1QX3rNNtSPDrg==", "dev": true, "license": "MIT", "dependencies": { @@ -378,67 +539,67 @@ }, "peerDependencies": { "@angular/cli": ">= 21.0.0 < 22.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.1.0.tgz", - "integrity": "sha512-t52J6FszgEHaJ+IjuzU9qaWfVxsjlVNkAP+B5z2t4NDgbbDDsmI+QJh0OtP1qdlqzjh2pbocEml30KhYmNZm/Q==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.3.0.tgz", + "integrity": "sha512-l521I24J9gJxyMbRkrM24Tc7W8J8BP+TDAmVs2nT8+lXbS3kg8QpWBRtd+hNUgq6o+vt+lKBkytnEfu8OiqeRg==", "dev": true, "license": "MIT" }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.1.0.tgz", - "integrity": "sha512-oNp+4UzN2M3KwGwEw03NUdXz93vqJd9sMzTbGXWF9+KVfA2LjckGDTrI6g6asGcJMdyTo07rDcnw0m0MkLB5VA==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.3.0.tgz", + "integrity": "sha512-Whf/AUUBekOlfSJRS78m76YGrBQAZ3waXE7oOdlW5xEQvn8jBDN9EGuNnjg/syZzvzjK4ZpYC4g1XYXrc+fQIg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.1.0", - "@angular-eslint/utils": "21.1.0", + "@angular-eslint/bundled-angular-compiler": "21.3.0", + "@angular-eslint/utils": "21.3.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.1.0.tgz", - "integrity": "sha512-FlbRfOCn8IUHvP1ebcCSQFVNh+4X/HqZqL7SW5oj9WIYPiOX9ijS03ndNbfX/pBPSIi8GHLKMjLt8zIy1l5Lww==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.3.0.tgz", + "integrity": "sha512-lVixd/KypPWgA/5/pUOhJV9MTcaHjYZEqyOi+IiLk+h+maGxn6/s6Ot+20n+XGS85zAgOY+qUw6EEQ11hoojIQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.1.0", - "@angular-eslint/utils": "21.1.0", + "@angular-eslint/bundled-angular-compiler": "21.3.0", + "@angular-eslint/utils": "21.3.0", "aria-query": "5.3.2", "axobject-query": "4.1.0" }, "peerDependencies": { - "@angular-eslint/template-parser": "21.1.0", + "@angular-eslint/template-parser": "21.3.0", "@typescript-eslint/types": "^7.11.0 || ^8.0.0", "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/schematics": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.1.0.tgz", - "integrity": "sha512-Hal1mYwx4MTjCcNHqfIlua31xrk2tZJoyTiXiGQ21cAeK4sFuY+9V7/8cxbwJMGftX0G4J7uhx8woOdIFuqiZw==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.3.0.tgz", + "integrity": "sha512-8deU/zVY9f8k8kAQQ9PL130ox2VlrZw3fMxgsPNAY5tjQ0xk0J2YVSszYHhcqdMGG1J01IsxIjvQaJ4pFfEmMw==", "dev": true, "license": "MIT", "dependencies": { "@angular-devkit/core": ">= 21.0.0 < 22.0.0", "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", - "@angular-eslint/eslint-plugin": "21.1.0", - "@angular-eslint/eslint-plugin-template": "21.1.0", + "@angular-eslint/eslint-plugin": "21.3.0", + "@angular-eslint/eslint-plugin-template": "21.3.0", "ignore": "7.0.5", - "semver": "7.7.3", + "semver": "7.7.4", "strip-json-comments": "3.1.1" }, "peerDependencies": { @@ -446,32 +607,32 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.1.0.tgz", - "integrity": "sha512-PYVgNbjNtuD5/QOuS6cHR8A7bRqsVqxtUUXGqdv76FYMAajQcAvyfR0QxOkqf3NmYxgNgO3hlUHWq0ILjVbcow==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.3.0.tgz", + "integrity": "sha512-ysyou1zAY6M6rSZNdIcYKGd4nk6TCapamyFNB3ivmTlVZ0O35TS9o/rJ0aUttuHgDp+Ysgs3ql+LA746PXgCyQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.1.0", - "eslint-scope": "^9.0.0" + "@angular-eslint/bundled-angular-compiler": "21.3.0", + "eslint-scope": "^9.1.1" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/utils": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.1.0.tgz", - "integrity": "sha512-rWINgxGREu+NFUPCpAVsBGG8B4hfXxyswM0N5GbjykvsfB5W6PUix2Gsoh++iEsZPT+c9lvgXL5GbpwfanjOow==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.3.0.tgz", + "integrity": "sha512-oNigH6w3l+owTMboj/uFG0tHOy43uH8BpQRtBOQL1/s2+5in/BJ2Fjobv3SyizxTgeJ1FhRefbkT8GmVjK7jAA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.1.0" + "@angular-eslint/bundled-angular-compiler": "21.3.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, @@ -492,9 +653,9 @@ } }, "node_modules/@angular/animations": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.0.7.tgz", - "integrity": "sha512-TfGE+emi67LAIUYmyiHfnL8BVqk26ZZVNEz7hDfbFztbZ5qhtHeKoG+97bAKtJDTTkxgs1JvB8escZExe1JkdA==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.1.tgz", + "integrity": "sha512-zT/S29pUTbziCLvZ2itBdNWd5i8tsXexofH7KA4n2yvYmK1EhNpE7TlHRjghmsHgtDt4VnGiMW4zXEyrl05Dwg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -503,43 +664,43 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.0.7" + "@angular/core": "21.2.1" } }, "node_modules/@angular/build": { - "version": "21.0.5", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.0.5.tgz", - "integrity": "sha512-4Ejb5pA118GGyZOAGjSmZMCx5HbovRSjiqLuCmpjf9hUgs50GPNJbigWW1ewz5+KmFrc8ouEoirpgTQyaKKZ3Q==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.1.tgz", + "integrity": "sha512-cUpLNHJp9taII/FOcJHHfQYlMcZSRaf6eIxgSNS6Xfx1CeGoJNDN+J8+GFk+H1CPJt1EvbfyZ+dE5DbsgTD/QQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2100.5", - "@babel/core": "7.28.4", + "@angular-devkit/architect": "0.2102.1", + "@babel/core": "7.29.0", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", - "@inquirer/confirm": "5.1.19", - "@vitejs/plugin-basic-ssl": "2.1.0", - "beasties": "0.3.5", + "@inquirer/confirm": "5.1.21", + "@vitejs/plugin-basic-ssl": "2.1.4", + "beasties": "0.4.1", "browserslist": "^4.26.0", - "esbuild": "0.26.0", + "esbuild": "0.27.3", "https-proxy-agent": "7.0.6", "istanbul-lib-instrument": "6.0.3", "jsonc-parser": "3.3.1", "listr2": "9.0.5", - "magic-string": "0.30.19", + "magic-string": "0.30.21", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "8.0.0", "picomatch": "4.0.3", - "piscina": "5.1.3", - "rolldown": "1.0.0-beta.47", - "sass": "1.93.2", - "semver": "7.7.3", + "piscina": "5.1.4", + "rolldown": "1.0.0-rc.4", + "sass": "1.97.3", + "semver": "7.7.4", "source-map-support": "0.5.21", "tinyglobby": "0.2.15", - "undici": "7.16.0", - "vite": "7.2.2", - "watchpack": "2.4.4" + "undici": "7.22.0", + "vite": "7.3.1", + "watchpack": "2.5.1" }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0", @@ -547,7 +708,7 @@ "yarn": ">= 1.13.0" }, "optionalDependencies": { - "lmdb": "3.4.3" + "lmdb": "3.5.1" }, "peerDependencies": { "@angular/compiler": "^21.0.0", @@ -557,7 +718,7 @@ "@angular/platform-browser": "^21.0.0", "@angular/platform-server": "^21.0.0", "@angular/service-worker": "^21.0.0", - "@angular/ssr": "^21.0.5", + "@angular/ssr": "^21.2.1", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^21.0.0", @@ -607,9 +768,9 @@ } }, "node_modules/@angular/cdk": { - "version": "21.0.5", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.0.5.tgz", - "integrity": "sha512-yO/IRYEZ5wJkpwg3GT3b6RST4pqNFTAhuyPdEdLcE81cs283K3aKOsCYh2xUR3bR4WxBh2kBPSJ31AFZyJXbSA==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.1.tgz", + "integrity": "sha512-JUFV8qLnO7CU5v4W0HzXSQrFkkJ4RH/qqdwrf9lup7YEnsLxB7cTGhsVisc9pWKAJsoNZ4pXCVOkqKc1mFL7dw==", "license": "MIT", "dependencies": { "parse5": "^8.0.0", @@ -618,35 +779,35 @@ "peerDependencies": { "@angular/common": "^21.0.0 || ^22.0.0", "@angular/core": "^21.0.0 || ^22.0.0", + "@angular/platform-browser": "^21.0.0 || ^22.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/cli": { - "version": "21.0.5", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.0.5.tgz", - "integrity": "sha512-UYFQqn9Ow1wFVSwdB/xfjmZo4Yb7CUNxilbeYDFIybesfxXSdjMJBbXLtV0+icIhjmqfSUm2gTls6WIrG8qv9A==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.1.tgz", + "integrity": "sha512-5SRfMTgwFj1zXOpfeZWHsxZBni0J4Xz7/CbewG47D6DmbstOrSdgt6eNzJ62R650t0G9dpri2YvToZgImtbjOQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2100.5", - "@angular-devkit/core": "21.0.5", - "@angular-devkit/schematics": "21.0.5", - "@inquirer/prompts": "7.9.0", + "@angular-devkit/architect": "0.2102.1", + "@angular-devkit/core": "21.2.1", + "@angular-devkit/schematics": "21.2.1", + "@inquirer/prompts": "7.10.1", "@listr2/prompt-adapter-inquirer": "3.0.5", - "@modelcontextprotocol/sdk": "1.25.2", - "@schematics/angular": "21.0.5", + "@modelcontextprotocol/sdk": "1.26.0", + "@schematics/angular": "21.2.1", "@yarnpkg/lockfile": "1.1.0", - "algoliasearch": "5.40.1", - "ini": "5.0.0", + "algoliasearch": "5.48.1", + "ini": "6.0.0", "jsonc-parser": "3.3.1", "listr2": "9.0.5", - "npm-package-arg": "13.0.1", - "pacote": "21.0.3", + "npm-package-arg": "13.0.2", + "pacote": "21.3.1", "parse5-html-rewriting-stream": "8.0.0", - "resolve": "1.22.11", - "semver": "7.7.3", + "semver": "7.7.4", "yargs": "18.0.0", - "zod": "4.1.13" + "zod": "4.3.6" }, "bin": { "ng": "bin/ng.js" @@ -657,10 +818,89 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.1.tgz", + "integrity": "sha512-TpXGjERqVPN8EPt7LdmWAwh0oNQ/6uWFutzGZiXhJy81n1zb1O1XrqhRAmvP1cAo5O+na6IV2JkkCmxL6F8GUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular/cli/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/cli/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular/common": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.7.tgz", - "integrity": "sha512-KNstFFCv6//x33F+YBPEIztDSNBVyLH99C8yFPmb7vawxGbR9liKSHC1WnEk+GR5KgV3I5lFOJyWL7Elfm0K5A==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.1.tgz", + "integrity": "sha512-xhv2i1Q9s1kpGbGsfj+o36+XUC/TQLcZyRuRxn3GwaN7Rv34FabC88ycpvoE+sW/txj4JRx9yPA0dRSZjwZ+Gg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -669,14 +909,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.0.7", + "@angular/core": "21.2.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.7.tgz", - "integrity": "sha512-Qsjx0OrOquyx10fMynkHilRRoZy9qJcstHdML7NGKg887xqHW4YvgNKREIXmKYjnV6sUBBUxJUD1L5ouarb/YA==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.1.tgz", + "integrity": "sha512-FxWaSaii1vfHIFA+JksqQ8NGB2frfqCrs7Ju50a44kbwR4fmanfn/VsiS/CbwBp9vcyT/Br9X/jAG4RuK/U2nw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -686,14 +926,14 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.0.7.tgz", - "integrity": "sha512-M4ePAA7AwjTsbUq6Qpremgo7qIP9GIgWqV5FoJPUEthtFGPNEiKGYjpOtXJ/OLB1J2Tn0ygrqe0PAYE0YxeEUA==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.1.tgz", + "integrity": "sha512-qYCWLGtEju4cDtYLi4ZzbwKoF0lcGs+Lc31kuESvAzYvWNgk2EUOtwWo8kbgpAzAwSYodtxW6Q90iWEwfU6elw==", "license": "MIT", "dependencies": { - "@babel/core": "7.28.4", + "@babel/core": "7.29.0", "@jridgewell/sourcemap-codec": "^1.4.14", - "chokidar": "^4.0.0", + "chokidar": "^5.0.0", "convert-source-map": "^1.5.1", "reflect-metadata": "^0.2.0", "semver": "^7.0.0", @@ -708,8 +948,8 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.0.7", - "typescript": ">=5.9 <6.0" + "@angular/compiler": "21.2.1", + "typescript": ">=5.9 <6.1" }, "peerDependenciesMeta": { "typescript": { @@ -717,10 +957,38 @@ } } }, + "node_modules/@angular/compiler-cli/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/compiler-cli/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular/core": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.7.tgz", - "integrity": "sha512-MvgRRse2PaEleQFp+35rj7ew5gBmBh3wp5yNDYPTiPaVp1I3fJ08VYSpldodaXmdkdWRB+OU4WJhnFkagyRx7A==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.1.tgz", + "integrity": "sha512-pFTbg03s2ZI5cHNT+eWsGjwIIKiYkeAnodFbCAHjwFi9KCEYlTykFLjr9lcpGrBddfmAH7GE08Q73vgmsdcNHw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -729,7 +997,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.0.7", + "@angular/compiler": "21.2.1", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0 || ~0.16.0" }, @@ -743,9 +1011,9 @@ } }, "node_modules/@angular/forms": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.7.tgz", - "integrity": "sha512-HUfUaO6+cxam9wug3Upc83ueBIDSgJwxzYIuPCP4AjL5DhT6Fbqv/Zq+nLbLF7rklbKdqzYsMjse97pxmxJGLQ==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.1.tgz", + "integrity": "sha512-6aqOPk9xoa0dfeUDeEbhaiPhmt6MQrdn59qbGAomn9RMXA925TrHbJhSIkp9tXc2Fr4aJRi8zkD/cdXEc1IYeA==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -755,19 +1023,19 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.0.7", - "@angular/core": "21.0.7", - "@angular/platform-browser": "21.0.7", + "@angular/common": "21.2.1", + "@angular/core": "21.2.1", + "@angular/platform-browser": "21.2.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/localize": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-21.0.7.tgz", - "integrity": "sha512-H1BdOOe0prtQa/EjWyzyZ9Ls4dPHcPNK/oN4fAYkpaZzyyqhvmPU64TYHa/3DNxFQrbSYjVMcpRXIJFThLeOZQ==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-21.2.1.tgz", + "integrity": "sha512-2QsN33fLO3N/RRFfUxDKHMX/Y/2TH90Tx51Wi6hi1do9IJdlfEe1qBw+5F0g1F1CuFEYgZWMJdZIK7LPHpuDzw==", "license": "MIT", "dependencies": { - "@babel/core": "7.28.4", + "@babel/core": "7.29.0", "@types/babel__core": "7.20.5", "tinyglobby": "^0.2.12", "yargs": "^18.0.0" @@ -781,14 +1049,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.0.7", - "@angular/compiler-cli": "21.0.7" + "@angular/compiler": "21.2.1", + "@angular/compiler-cli": "21.2.1" } }, "node_modules/@angular/platform-browser": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.7.tgz", - "integrity": "sha512-mhsN2hn5qG0Oelqpko3uLmYdqadruzG2rY3CJ7duRdOrzs5g5F8QhzphoI/ljgLyxrrgZT6Nykyyf6RNhowf2A==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.1.tgz", + "integrity": "sha512-k4SJLxIaLT26vLjLuFL+ho0BiG5PrdxEsjsXFC7w5iUhomeouzkHVTZ4t7gaLNKrdRD7QNtU4Faw0nL0yx0ZPQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -797,9 +1065,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/animations": "21.0.7", - "@angular/common": "21.0.7", - "@angular/core": "21.0.7" + "@angular/animations": "21.2.1", + "@angular/common": "21.2.1", + "@angular/core": "21.2.1" }, "peerDependenciesMeta": { "@angular/animations": { @@ -808,9 +1076,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.0.7.tgz", - "integrity": "sha512-PnARi0eleIEZ/sqU286zDRLwiNI9hz16M9NRzC1kBZ+/LAj8iNWz1ZERyb4gGDOBDM/9NjdL7PU7UJvqgvvlzA==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.2.1.tgz", + "integrity": "sha512-J4KnrXjgSuk7KjEm79/RK1yyzR867sIyT5mcG6jx2KmkjspFJd4OeOux7Oj7lSBM7+nDEsKC9F6s0x3dC0hCPQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -819,16 +1087,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.0.7", - "@angular/compiler": "21.0.7", - "@angular/core": "21.0.7", - "@angular/platform-browser": "21.0.7" + "@angular/common": "21.2.1", + "@angular/compiler": "21.2.1", + "@angular/core": "21.2.1", + "@angular/platform-browser": "21.2.1" } }, "node_modules/@angular/router": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.0.7.tgz", - "integrity": "sha512-MBmryTBCkyc4EjfI0NWfNNTS6Dcx/yQ77hOdDrqLMdbtOtbbD9BnUXd1qRcs73s0D5Stjk1IH49D66JMKn9Xew==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.1.tgz", + "integrity": "sha512-FUKG+8ImQYxmlDUdAs7+VeS/VrBNrbo0zGiKkzVNU/bbcCyroKXJLXFtkFI3qmROiJNyIta2IMBCHJvIjLIMig==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -837,19 +1105,19 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.0.7", - "@angular/core": "21.0.7", - "@angular/platform-browser": "21.0.7", + "@angular/common": "21.2.1", + "@angular/core": "21.2.1", + "@angular/platform-browser": "21.2.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -858,29 +1126,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -912,13 +1180,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -941,12 +1209,12 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -975,27 +1243,27 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1045,25 +1313,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1073,31 +1341,31 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -1105,9 +1373,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1186,9 +1454,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.26.0.tgz", - "integrity": "sha512-hj0sKNCQOOo2fgyII3clmJXP28VhgDfU5iy3GNHlWO76KG6N7x4D9ezH5lJtQTG+1J6MFDAJXC1qsI+W+LvZoA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -1203,9 +1471,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.26.0.tgz", - "integrity": "sha512-C0hkDsYNHZkBtPxxDx177JN90/1MiCpvBNjz1f5yWJo1+5+c5zr8apjastpEG+wtPjo9FFtGG7owSsAxyKiHxA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -1220,9 +1488,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.26.0.tgz", - "integrity": "sha512-DDnoJ5eoa13L8zPh87PUlRd/IyFaIKOlRbxiwcSbeumcJ7UZKdtuMCHa1Q27LWQggug6W4m28i4/O2qiQQ5NZQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -1237,9 +1505,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.26.0.tgz", - "integrity": "sha512-bKDkGXGZnj0T70cRpgmv549x38Vr2O3UWLbjT2qmIkdIWcmlg8yebcFWoT9Dku7b5OV3UqPEuNKRzlNhjwUJ9A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -1254,9 +1522,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.26.0.tgz", - "integrity": "sha512-6Z3naJgOuAIB0RLlJkYc81An3rTlQ/IeRdrU3dOea8h/PvZSgitZV+thNuIccw0MuK1GmIAnAmd5TrMZad8FTQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -1271,9 +1539,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.26.0.tgz", - "integrity": "sha512-OPnYj0zpYW0tHusMefyaMvNYQX5pNQuSsHFTHUBNp3vVXupwqpxofcjVsUx11CQhGVkGeXjC3WLjh91hgBG2xw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -1288,9 +1556,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.26.0.tgz", - "integrity": "sha512-jix2fa6GQeZhO1sCKNaNMjfj5hbOvoL2F5t+w6gEPxALumkpOV/wq7oUBMHBn2hY2dOm+mEV/K+xfZy3mrsxNQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -1305,9 +1573,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.26.0.tgz", - "integrity": "sha512-tccJaH5xHJD/239LjbVvJwf6T4kSzbk6wPFerF0uwWlkw/u7HL+wnAzAH5GB2irGhYemDgiNTp8wJzhAHQ64oA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -1322,9 +1590,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.26.0.tgz", - "integrity": "sha512-JY8NyU31SyRmRpuc5W8PQarAx4TvuYbyxbPIpHAZdr/0g4iBr8KwQBS4kiiamGl2f42BBecHusYCsyxi7Kn8UQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -1339,9 +1607,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.26.0.tgz", - "integrity": "sha512-IMJYN7FSkLttYyTbsbme0Ra14cBO5z47kpamo16IwggzzATFY2lcZAwkbcNkWiAduKrTgFJP7fW5cBI7FzcuNQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -1356,9 +1624,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.26.0.tgz", - "integrity": "sha512-XITaGqGVLgk8WOHw8We9Z1L0lbLFip8LyQzKYFKO4zFo1PFaaSKsbNjvkb7O8kEXytmSGRkYpE8LLVpPJpsSlw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -1373,9 +1641,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.26.0.tgz", - "integrity": "sha512-MkggfbDIczStUJwq9wU7gQ7kO33d8j9lWuOCDifN9t47+PeI+9m2QVh51EI/zZQ1spZtFMC1nzBJ+qNGCjJnsg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -1390,9 +1658,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.26.0.tgz", - "integrity": "sha512-fUYup12HZWAeccNLhQ5HwNBPr4zXCPgUWzEq2Rfw7UwqwfQrFZ0SR/JljaURR8xIh9t+o1lNUFTECUTmaP7yKA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -1407,9 +1675,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.26.0.tgz", - "integrity": "sha512-MzRKhM0Ip+//VYwC8tialCiwUQ4G65WfALtJEFyU0GKJzfTYoPBw5XNWf0SLbCUYQbxTKamlVwPmcw4DgZzFxg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -1424,9 +1692,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.26.0.tgz", - "integrity": "sha512-QhCc32CwI1I4Jrg1enCv292sm3YJprW8WHHlyxJhae/dVs+KRWkbvz2Nynl5HmZDW/m9ZxrXayHzjzVNvQMGQA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -1441,9 +1709,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.26.0.tgz", - "integrity": "sha512-1D6vi6lfI18aNT1aTf2HV+RIlm6fxtlAp8eOJ4mmnbYmZ4boz8zYDar86sIYNh0wmiLJEbW/EocaKAX6Yso2fw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -1458,9 +1726,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.26.0.tgz", - "integrity": "sha512-rnDcepj7LjrKFvZkx+WrBv6wECeYACcFjdNPvVPojCPJD8nHpb3pv3AuR9CXgdnjH1O23btICj0rsp0L9wAnHA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -1475,9 +1743,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.26.0.tgz", - "integrity": "sha512-FSWmgGp0mDNjEXXFcsf12BmVrb+sZBBBlyh3LwB/B9ac3Kkc8x5D2WimYW9N7SUkolui8JzVnVlWh7ZmjCpnxw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -1492,9 +1760,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.26.0.tgz", - "integrity": "sha512-0QfciUDFryD39QoSPUDshj4uNEjQhp73+3pbSAaxjV2qGOEDsM67P7KbJq7LzHoVl46oqhIhJ1S+skKGR7lMXA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -1509,9 +1777,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.26.0.tgz", - "integrity": "sha512-vmAK+nHhIZWImwJ3RNw9hX3fU4UGN/OqbSE0imqljNbUQC3GvVJ1jpwYoTfD6mmXmQaxdJY6Hn4jQbLGJKg5Yw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -1526,9 +1794,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.26.0.tgz", - "integrity": "sha512-GPXF7RMkJ7o9bTyUsnyNtrFMqgM3X+uM/LWw4CeHIjqc32fm0Ir6jKDnWHpj8xHFstgWDUYseSABK9KCkHGnpg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -1543,9 +1811,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.26.0.tgz", - "integrity": "sha512-nUHZ5jEYqbBthbiBksbmHTlbb5eElyVfs/s1iHQ8rLBq1eWsd5maOnDpCocw1OM8kFK747d1Xms8dXJHtduxSw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -1560,9 +1828,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.26.0.tgz", - "integrity": "sha512-TMg3KCTCYYaVO+R6P5mSORhcNDDlemUVnUbb8QkboUtOhb5JWKAzd5uMIMECJQOxHZ/R+N8HHtDF5ylzLfMiLw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -1577,9 +1845,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.26.0.tgz", - "integrity": "sha512-apqYgoAUd6ZCb9Phcs8zN32q6l0ZQzQBdVXOofa6WvHDlSOhwCWgSfVQabGViThS40Y1NA4SCvQickgZMFZRlA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -1594,9 +1862,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.26.0.tgz", - "integrity": "sha512-FGJAcImbJNZzLWu7U6WB0iKHl4RuY4TsXEwxJPl9UZLS47agIZuILZEX3Pagfw7I4J3ddflomt9f0apfaJSbaw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -1611,9 +1879,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.26.0.tgz", - "integrity": "sha512-WAckBKaVnmFqbEhbymrPK7M086DQMpL1XoRbpmN0iW8k5JSXjDRQBhcZNa0VweItknLq9eAeCL34jK7/CDcw7A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -1657,202 +1925,104 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", + "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^3.0.2", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^10.2.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", + "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0" + "@eslint/core": "^1.1.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", + "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", + "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", + "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.1.0", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.1.0.tgz", - "integrity": "sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz", + "integrity": "sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==", "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", "engines": { "node": ">=6" } }, + "node_modules/@gar/promise-retry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.2.tgz", + "integrity": "sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "^0.13.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", + "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, "node_modules/@hono/node-server": { - "version": "1.19.7", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", - "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", "dev": true, "license": "MIT", "engines": { @@ -1915,9 +2085,9 @@ } }, "node_modules/@iharbeck/ngx-virtual-scroller": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@iharbeck/ngx-virtual-scroller/-/ngx-virtual-scroller-19.0.1.tgz", - "integrity": "sha512-dtn4CpbEY92H9nd1A48WNhsyUgtFBjC83xcsc9VzlSQT/KN2fEx0oBs0Obnn6ZdPanDP/IQdlBgmANmlds/wHA==", + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@iharbeck/ngx-virtual-scroller/-/ngx-virtual-scroller-20.0.0.tgz", + "integrity": "sha512-D78O3XPLzQrIZAnJ797rTyoyiUJdw/V71yj9E21gEQ3Gt6Ykt4hmYp+X6+b4llJ6rQyp3maIEEru5sp8UE6HCw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1962,14 +2132,14 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.19", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.19.tgz", - "integrity": "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==", + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.0", - "@inquirer/type": "^3.0.9" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -2157,22 +2327,22 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.9.0.tgz", - "integrity": "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.3.0", - "@inquirer/confirm": "^5.1.19", - "@inquirer/editor": "^4.2.21", - "@inquirer/expand": "^4.0.21", - "@inquirer/input": "^4.2.5", - "@inquirer/number": "^3.0.21", - "@inquirer/password": "^4.0.21", - "@inquirer/rawlist": "^4.1.9", - "@inquirer/search": "^3.2.0", - "@inquirer/select": "^4.4.0" + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" }, "engines": { "node": ">=18" @@ -2306,29 +2476,6 @@ "rxjs": "^7.0.0" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -2398,12 +2545,12 @@ } }, "node_modules/@jsverse/transloco": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@jsverse/transloco/-/transloco-8.2.0.tgz", - "integrity": "sha512-5SU9mjmKHlTraW/GKSUsWEjt7ATBLzKcKd6w+mTbRrnU38ZyYdCJoR2W/ii8lWiRwhfgbXTFCsTUueW5Ak61WA==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@jsverse/transloco/-/transloco-8.2.1.tgz", + "integrity": "sha512-uuapT1vNi/P9wqklO2VY/sIj8HPVQJ1h+IJFhPbiQvk1FP/vgn2LLwGz/iIcet2bAMJVKKxO8FXytdrwRXXyvg==", "license": "MIT", "dependencies": { - "@jsverse/transloco-utils": "^8.2.0", + "@jsverse/transloco-utils": "^8.2.1", "@jsverse/utils": "1.0.0-beta.5", "tslib": "^2.2.0" }, @@ -2413,9 +2560,9 @@ } }, "node_modules/@jsverse/transloco-locale": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-locale/-/transloco-locale-8.2.0.tgz", - "integrity": "sha512-EMj9f1ugqKT0m6V3heTrJ4dm9UV5vNiLj3WnMKWoiNfqsZtUr6FTeTsTNoDCBSel4ucC9pCVfmcFk6SUUzfIAQ==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@jsverse/transloco-locale/-/transloco-locale-8.2.1.tgz", + "integrity": "sha512-EyfFKLLp4c4PKYJcON7UyguF/VYH7LlcGSwkissNaqqlaCPQASeM188kpvLfn2inekX26UAj59lCXQpsHPEdYA==", "license": "MIT", "dependencies": { "tslib": "^2.2.0" @@ -2427,9 +2574,9 @@ } }, "node_modules/@jsverse/transloco-persist-lang": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-persist-lang/-/transloco-persist-lang-8.2.0.tgz", - "integrity": "sha512-accsQa5eFgR4yv+v7Uv5gydexb8jHIKymP/tYzGqOavpThkqUzlbVS1A8VhhsiB98w3FPy7lx+pn+pSCye7noA==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@jsverse/transloco-persist-lang/-/transloco-persist-lang-8.2.1.tgz", + "integrity": "sha512-7M/uvPFvOq2pCMIQHD20o8DnsrCIFncc2J98U+DNPn/DatgZRn+YJVjJ697Y8MzRnvjJF4D7YoZvQ+GzZ5ka8Q==", "license": "MIT", "dependencies": { "tslib": "^2.2.0" @@ -2441,9 +2588,9 @@ } }, "node_modules/@jsverse/transloco-persist-translations": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-persist-translations/-/transloco-persist-translations-8.2.0.tgz", - "integrity": "sha512-UF443fwRnhjYAWuhedyGNpUnHPuVH+vN0zNMJKi/WpGI3gZa+VINaIDANJVtJ0jQcY4ONx5dP3P71j1XL2jfnw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@jsverse/transloco-persist-translations/-/transloco-persist-translations-8.2.1.tgz", + "integrity": "sha512-oSeKTKnmh1eloX5Au6ic/dFiGV/X6RfB69v5FujOIdR/3yvIos5cNE64F49Jmgdfq89UePyTLu0k+H0q5Awu+w==", "license": "MIT", "dependencies": { "tslib": "^2.2.0" @@ -2455,9 +2602,9 @@ } }, "node_modules/@jsverse/transloco-preload-langs": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-preload-langs/-/transloco-preload-langs-8.2.0.tgz", - "integrity": "sha512-O8VH8cDoeHIxj9+1reagOPk7FCSFK04iRbyKsPIlJSkhXBzAc9mbwykwZ+Aa3Wt7GWeRo75Tp/sMDaMIFzCXhA==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@jsverse/transloco-preload-langs/-/transloco-preload-langs-8.2.1.tgz", + "integrity": "sha512-YSfq7FwYeDXBUVSXhmhZfswKZxlqLevicpnnO2L2pxxDEzYWa8+poD5J4IehtexaXEyisJXOvOMJH/yaAJSGtg==", "license": "MIT", "dependencies": { "tslib": "^2.2.0" @@ -2468,9 +2615,9 @@ } }, "node_modules/@jsverse/transloco-utils": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-utils/-/transloco-utils-8.2.0.tgz", - "integrity": "sha512-rDactF2Qmu4JKBpecyYLzD3spPZ0U+6wgoQS2OIcVraq5riV8eE3sPYb5dgL2wxMgGtJRuT8PgMMAD7LUOcCNw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@jsverse/transloco-utils/-/transloco-utils-8.2.1.tgz", + "integrity": "sha512-sAKJQuGgAYRYwndM8X1xVbwOrjENBxKxOwhXE7gFnS8fWUEwBGMswp3wbAOS5jZlLDhyaReesU16ToXLegBCjg==", "license": "MIT", "dependencies": { "cosmiconfig": "^8.1.3", @@ -2504,9 +2651,9 @@ } }, "node_modules/@lmdb/lmdb-darwin-arm64": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.4.3.tgz", - "integrity": "sha512-zR6Y45VNtW5s+A+4AyhrJk0VJKhXdkLhrySCpCu7PSdnakebsOzNxf58p5Xoq66vOSuueGAxlqDAF49HwdrSTQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.1.tgz", + "integrity": "sha512-tpfN4kKrrMpQ+If1l8bhmoNkECJi0iOu6AEdrTJvWVC+32sLxTARX5Rsu579mPImRP9YFWfWgeRQ5oav7zApQQ==", "cpu": [ "arm64" ], @@ -2518,9 +2665,9 @@ ] }, "node_modules/@lmdb/lmdb-darwin-x64": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.4.3.tgz", - "integrity": "sha512-nfGm5pQksBGfaj9uMbjC0YyQreny/Pl7mIDtHtw6g7WQuCgeLullr9FNRsYyKplaEJBPrCVpEjpAznxTBIrXBw==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.1.tgz", + "integrity": "sha512-+a2tTfc3rmWhLAolFUWRgJtpSuu+Fw/yjn4rF406NMxhfjbMuiOUTDRvRlMFV+DzyjkwnokisskHbCWkS3Ly5w==", "cpu": [ "x64" ], @@ -2532,9 +2679,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.4.3.tgz", - "integrity": "sha512-Kjqomp7i0rgSbYSUmv9JnXpS55zYT/YcW3Bdf9oqOTjcH0/8tFAP8MLhu/i9V2pMKIURDZk63Ww49DTK0T3c/Q==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.1.tgz", + "integrity": "sha512-0EgcE6reYr8InjD7V37EgXcYrloqpxVPINy3ig1MwDSbl6LF/vXTYRH9OE1Ti1D8YZnB35ZH9aTcdfSb5lql2A==", "cpu": [ "arm" ], @@ -2546,9 +2693,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm64": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.4.3.tgz", - "integrity": "sha512-uX9eaPqWb740wg5D3TCvU/js23lSRSKT7lJrrQ8IuEG/VLgpPlxO3lHDywU44yFYdGS7pElBn6ioKFKhvALZlw==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.1.tgz", + "integrity": "sha512-aoERa5B6ywXdyFeYGQ1gbQpkMkDbEo45qVoXE5QpIRavqjnyPwjOulMkmkypkmsbJ5z4Wi0TBztON8agCTG0Vg==", "cpu": [ "arm64" ], @@ -2560,9 +2707,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-x64": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.4.3.tgz", - "integrity": "sha512-7/8l20D55CfwdMupkc3fNxNJdn4bHsti2X0cp6PwiXlLeSFvAfWs5kCCx+2Cyje4l4GtN//LtKWjTru/9hDJQg==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.1.tgz", + "integrity": "sha512-SqNDY1+vpji7bh0sFH5wlWyFTOzjbDOl0/kB5RLLYDAFyd/uw3n7wyrmas3rYPpAW7z18lMOi1yKlTPv967E3g==", "cpu": [ "x64" ], @@ -2574,9 +2721,9 @@ ] }, "node_modules/@lmdb/lmdb-win32-arm64": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.4.3.tgz", - "integrity": "sha512-yWVR0e5Gl35EGJBsAuqPOdjtUYuN8CcTLKrqpQFoM+KsMadViVCulhKNhkcjSGJB88Am5bRPjMro4MBB9FS23Q==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.1.tgz", + "integrity": "sha512-50v0O1Lt37cwrmR9vWZK5hRW0Aw+KEmxJJ75fge/zIYdvNKB/0bSMSVR5Uc2OV9JhosIUyklOmrEvavwNJ8D6w==", "cpu": [ "arm64" ], @@ -2588,9 +2735,9 @@ ] }, "node_modules/@lmdb/lmdb-win32-x64": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.4.3.tgz", - "integrity": "sha512-1JdBkcO0Vrua4LUgr4jAe4FUyluwCeq/pDkBrlaVjX3/BBWP1TzVjCL+TibWNQtPAL1BITXPAhlK5Ru4FBd/hg==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz", + "integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==", "cpu": [ "x64" ], @@ -2615,13 +2762,13 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.25.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz", - "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "dev": true, "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.7", + "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -2629,14 +2776,15 @@ "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" @@ -3126,9 +3274,9 @@ } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3149,18 +3297,18 @@ } }, "node_modules/@npmcli/git": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.1.tgz", - "integrity": "sha512-+XTFxK2jJF/EJJ5SoAzXk3qwIDfvFc5/g+bD274LZ7uY7LE8sTfG6Z8rOanPl2ZEvZWqNvmEdtXC25cE54VcoA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", "dev": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^6.0.0" }, @@ -3168,67 +3316,34 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/git/node_modules/@npmcli/promise-spawn": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", - "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/git/node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@npmcli/git/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "node_modules/@npmcli/git/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@npmcli/git/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" @@ -3238,20 +3353,20 @@ } }, "node_modules/@npmcli/installed-package-contents": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", - "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", "dev": true, "license": "ISC", "dependencies": { - "npm-bundled": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" }, "bin": { "installed-package-contents": "bin/index.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/node-gyp": { @@ -3265,9 +3380,9 @@ } }, "node_modules/@npmcli/package-json": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.4.tgz", - "integrity": "sha512-0wInJG3j/K40OJt/33ax47WfWMzZTm6OQxB9cDhTt5huCP2a9g2GnlsxmfN+PulItNPIpPrZ+kfwwUil7eHcZQ==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3277,59 +3392,49 @@ "json-parse-even-better-errors": "^5.0.0", "proc-log": "^6.0.0", "semver": "^7.5.3", - "validate-npm-package-license": "^3.0.4" + "spdx-expression-parse": "^4.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/package-json/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@npmcli/promise-spawn": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.3.tgz", - "integrity": "sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", "dev": true, "license": "ISC", "dependencies": { - "which": "^5.0.0" + "which": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/promise-spawn/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/redact": { @@ -3343,9 +3448,9 @@ } }, "node_modules/@npmcli/run-script": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.3.tgz", - "integrity": "sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", "dev": true, "license": "ISC", "dependencies": { @@ -3353,66 +3458,16 @@ "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", - "proc-log": "^6.0.0", - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/@npmcli/promise-spawn": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", - "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/@npmcli/run-script/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" + "proc-log": "^6.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@oxc-project/types": { - "version": "0.96.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.96.0.tgz", - "integrity": "sha512-r/xkmoXA0xEpU6UGtn18CNVjXH6erU3KCpCDbpLmbVxBFor1U9MqN5Z2uMmCHJuXjJzlnDR+hWY+yPoLo8oHDw==", + "version": "0.113.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", + "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", "dev": true, "license": "MIT", "funding": { @@ -3420,18 +3475,18 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" @@ -3441,25 +3496,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -3478,9 +3533,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -3499,9 +3554,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -3520,9 +3575,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], @@ -3541,9 +3596,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], @@ -3562,9 +3617,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], @@ -3583,9 +3638,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], @@ -3604,9 +3659,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], @@ -3625,9 +3680,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], @@ -3646,9 +3701,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], @@ -3667,9 +3722,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -3688,9 +3743,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -3709,9 +3764,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -3729,20 +3784,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/@parcel/watcher/node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -3769,9 +3810,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.47.tgz", - "integrity": "sha512-vPP9/MZzESh9QtmvQYojXP/midjgkkc1E4AdnPPAzQXo668ncHJcVLKjJKzoBdsQmaIvNjrMdsCwES8vTQHRQw==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==", "cpu": [ "arm64" ], @@ -3786,9 +3827,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.47.tgz", - "integrity": "sha512-Lc3nrkxeaDVCVl8qR3qoxh6ltDZfkQ98j5vwIr5ALPkgjZtDK4BGCrrBoLpGVMg+csWcaqUbwbKwH5yvVa0oOw==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==", "cpu": [ "arm64" ], @@ -3803,9 +3844,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.47.tgz", - "integrity": "sha512-eBYxQDwP0O33plqNVqOtUHqRiSYVneAknviM5XMawke3mwMuVlAsohtOqEjbCEl/Loi/FWdVeks5WkqAkzkYWQ==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==", "cpu": [ "x64" ], @@ -3820,9 +3861,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.47.tgz", - "integrity": "sha512-Ns+kgp2+1Iq/44bY/Z30DETUSiHY7ZuqaOgD5bHVW++8vme9rdiWsN4yG4rRPXkdgzjvQ9TDHmZZKfY4/G11AA==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==", "cpu": [ "x64" ], @@ -3837,9 +3878,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.47.tgz", - "integrity": "sha512-4PecgWCJhTA2EFOlptYJiNyVP2MrVP4cWdndpOu3WmXqWqZUmSubhb4YUAIxAxnXATlGjC1WjxNPhV7ZllNgdA==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.4.tgz", + "integrity": "sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==", "cpu": [ "arm" ], @@ -3854,9 +3895,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.47.tgz", - "integrity": "sha512-CyIunZ6D9U9Xg94roQI1INt/bLkOpPsZjZZkiaAZ0r6uccQdICmC99M9RUPlMLw/qg4yEWLlQhG73W/mG437NA==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==", "cpu": [ "arm64" ], @@ -3871,9 +3912,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.47.tgz", - "integrity": "sha512-doozc/Goe7qRCSnzfJbFINTHsMktqmZQmweull6hsZZ9sjNWQ6BWQnbvOlfZJe4xE5NxM1NhPnY5Giqnl3ZrYQ==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==", "cpu": [ "arm64" ], @@ -3888,9 +3929,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.47.tgz", - "integrity": "sha512-fodvSMf6Aqwa0wEUSTPewmmZOD44rc5Tpr5p9NkwQ6W1SSpUKzD3SwpJIgANDOhwiYhDuiIaYPGB7Ujkx1q0UQ==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==", "cpu": [ "x64" ], @@ -3905,9 +3946,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.47.tgz", - "integrity": "sha512-Rxm5hYc0mGjwLh5sjlGmMygxAaV2gnsx7CNm2lsb47oyt5UQyPDZf3GP/ct8BEcwuikdqzsrrlIp8+kCSvMFNQ==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==", "cpu": [ "x64" ], @@ -3922,9 +3963,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.47.tgz", - "integrity": "sha512-YakuVe+Gc87jjxazBL34hbr8RJpRuFBhun7NEqoChVDlH5FLhLXjAPHqZd990TVGVNkemourf817Z8u2fONS8w==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==", "cpu": [ "arm64" ], @@ -3939,9 +3980,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.47.tgz", - "integrity": "sha512-ak2GvTFQz3UAOw8cuQq8pWE+TNygQB6O47rMhvevvTzETh7VkHRFtRUwJynX5hwzFvQMP6G0az5JrBGuwaMwYQ==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.4.tgz", + "integrity": "sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==", "cpu": [ "wasm32" ], @@ -3949,16 +3990,16 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.0.7" + "@napi-rs/wasm-runtime": "^1.1.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.47.tgz", - "integrity": "sha512-o5BpmBnXU+Cj+9+ndMcdKjhZlPb79dVPBZnWwMnI4RlNSSq5yOvFZqvfPYbyacvnW03Na4n5XXQAPhu3RydZ0w==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==", "cpu": [ "arm64" ], @@ -3972,27 +4013,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-win32-ia32-msvc": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.47.tgz", - "integrity": "sha512-FVOmfyYehNE92IfC9Kgs913UerDog2M1m+FADJypKz0gmRg3UyTt4o1cZMCAl7MiR89JpM9jegNO1nXuP1w1vw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.47.tgz", - "integrity": "sha512-by/70F13IUE101Bat0oeH8miwWX5mhMFPk1yjCdxoTNHTyTdLgb0THNaebRM6AP7Kz+O3O2qx87sruYuF5UxHg==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==", "cpu": [ "x64" ], @@ -4007,16 +4031,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", - "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz", + "integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", - "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -4028,9 +4052,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", - "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -4042,9 +4066,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", - "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -4056,9 +4080,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", - "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -4070,9 +4094,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", - "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -4084,9 +4108,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", - "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -4098,9 +4122,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", - "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -4112,9 +4136,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", - "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -4126,9 +4150,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", - "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -4140,9 +4164,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", - "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -4154,9 +4178,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", - "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -4168,9 +4192,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", - "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -4182,9 +4206,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", - "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -4196,9 +4220,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", - "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -4210,9 +4234,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", - "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -4224,9 +4248,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", - "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -4238,9 +4262,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", - "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -4252,9 +4276,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", - "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -4266,9 +4290,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", - "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -4280,9 +4304,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", - "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -4294,9 +4318,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", - "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -4308,9 +4332,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", - "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -4322,9 +4346,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", - "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -4336,9 +4360,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", - "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -4350,9 +4374,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", - "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -4364,14 +4388,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "21.0.5", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.0.5.tgz", - "integrity": "sha512-uNBIilq5bGnln3D7Nbm3/K+Ot++eGj4rygU0DCw//IZiTQU/iSyF3UAsN++iRetu/OMs+97T/RoGPjD22ryiZg==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.1.tgz", + "integrity": "sha512-DjrHRMoILhbZ6tc7aNZWuHA1wCm1iU/JN1TxAwNEyIBgyU3Fx8Z5baK4w0TCpOIPt0RLWVgP2L7kka9aXWCUFA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.0.5", - "@angular-devkit/schematics": "21.0.5", + "@angular-devkit/core": "21.2.1", + "@angular-devkit/schematics": "21.2.1", "jsonc-parser": "3.3.1" }, "engines": { @@ -4380,6 +4404,85 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.1.tgz", + "integrity": "sha512-TpXGjERqVPN8EPt7LdmWAwh0oNQ/6uWFutzGZiXhJy81n1zb1O1XrqhRAmvP1cAo5O+na6IV2JkkCmxL6F8GUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@schematics/angular/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@schematics/angular/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@siemens/ngx-datatable": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@siemens/ngx-datatable/-/ngx-datatable-25.0.0.tgz", @@ -4446,16 +4549,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@sigstore/sign/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@sigstore/tuf": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.1.tgz", @@ -4543,22 +4636,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@tweenjs/tween.js": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", @@ -4910,6 +4987,13 @@ "@types/zrender": "*" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -4953,13 +5037,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", + "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/zrender": { @@ -4969,17 +5053,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.52.0.tgz", - "integrity": "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.52.0", - "@typescript-eslint/type-utils": "8.52.0", - "@typescript-eslint/utils": "8.52.0", - "@typescript-eslint/visitor-keys": "8.52.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -4992,22 +5076,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.52.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.52.0.tgz", - "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.52.0", - "@typescript-eslint/types": "8.52.0", - "@typescript-eslint/typescript-estree": "8.52.0", - "@typescript-eslint/visitor-keys": "8.52.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "engines": { @@ -5018,19 +5102,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.52.0.tgz", - "integrity": "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.52.0", - "@typescript-eslint/types": "^8.52.0", + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "engines": { @@ -5045,14 +5129,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.52.0.tgz", - "integrity": "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.52.0", - "@typescript-eslint/visitor-keys": "8.52.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5063,9 +5147,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.52.0.tgz", - "integrity": "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", "engines": { @@ -5080,15 +5164,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.52.0.tgz", - "integrity": "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.52.0", - "@typescript-eslint/typescript-estree": "8.52.0", - "@typescript-eslint/utils": "8.52.0", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -5100,14 +5184,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.52.0.tgz", - "integrity": "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { @@ -5119,18 +5203,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.52.0.tgz", - "integrity": "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.52.0", - "@typescript-eslint/tsconfig-utils": "8.52.0", - "@typescript-eslint/types": "8.52.0", - "@typescript-eslint/visitor-keys": "8.52.0", + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -5147,16 +5231,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.52.0.tgz", - "integrity": "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.52.0", - "@typescript-eslint/types": "8.52.0", - "@typescript-eslint/typescript-estree": "8.52.0" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5166,19 +5250,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.52.0.tgz", - "integrity": "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.52.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5189,22 +5273,22 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", - "integrity": "sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", + "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", "dev": true, "license": "MIT", "engines": { @@ -5258,9 +5342,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -5345,26 +5429,26 @@ } }, "node_modules/algoliasearch": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.40.1.tgz", - "integrity": "sha512-iUNxcXUNg9085TJx0HJLjqtDE0r1RZ0GOGrt8KNQqQT5ugu8lZsHuMUYW/e0lHhq6xBvmktU9Bw4CXP9VQeKrg==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz", + "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/abtesting": "1.6.1", - "@algolia/client-abtesting": "5.40.1", - "@algolia/client-analytics": "5.40.1", - "@algolia/client-common": "5.40.1", - "@algolia/client-insights": "5.40.1", - "@algolia/client-personalization": "5.40.1", - "@algolia/client-query-suggestions": "5.40.1", - "@algolia/client-search": "5.40.1", - "@algolia/ingestion": "1.40.1", - "@algolia/monitoring": "1.40.1", - "@algolia/recommend": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/abtesting": "1.14.1", + "@algolia/client-abtesting": "5.48.1", + "@algolia/client-analytics": "5.48.1", + "@algolia/client-common": "5.48.1", + "@algolia/client-insights": "5.48.1", + "@algolia/client-personalization": "5.48.1", + "@algolia/client-query-suggestions": "5.48.1", + "@algolia/client-search": "5.48.1", + "@algolia/ingestion": "1.48.1", + "@algolia/monitoring": "1.48.1", + "@algolia/recommend": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" @@ -5464,9 +5548,9 @@ } }, "node_modules/beasties": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.5.tgz", - "integrity": "sha512-NaWu+f4YrJxEttJSm16AzMIFtVldCvaJ68b1L098KpqXmxt9xOLtKoLkKxb8ekhOrLqEJAbvT6n6SEvB/sac7A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz", + "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5477,10 +5561,11 @@ "htmlparser2": "^10.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.49", - "postcss-media-query-parser": "^0.2.3" + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, "node_modules/body-parser": { @@ -5535,27 +5620,26 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "fill-range": "^7.1.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/browserslist": { @@ -5632,28 +5716,15 @@ } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "node_modules/cacache/node_modules/ssri": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", - "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -5714,23 +5785,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/chardet": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", @@ -5748,6 +5802,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -5786,9 +5841,9 @@ } }, "node_modules/cli-spinners": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.3.0.tgz", - "integrity": "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", "dev": true, "license": "MIT", "engines": { @@ -5980,9 +6035,9 @@ } }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "dev": true, "license": "MIT", "dependencies": { @@ -5991,6 +6046,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cosmiconfig": { @@ -6139,9 +6198,9 @@ } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6353,9 +6412,9 @@ } }, "node_modules/esbuild": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.26.0.tgz", - "integrity": "sha512-3Hq7jri+tRrVWha+ZeIVhl4qJRha/XjRNSopvTsOaCvfPHrflTYTcUFcEjMKdxofsXXsdc4zjg5NOTnL4Gl57Q==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6366,32 +6425,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.26.0", - "@esbuild/android-arm": "0.26.0", - "@esbuild/android-arm64": "0.26.0", - "@esbuild/android-x64": "0.26.0", - "@esbuild/darwin-arm64": "0.26.0", - "@esbuild/darwin-x64": "0.26.0", - "@esbuild/freebsd-arm64": "0.26.0", - "@esbuild/freebsd-x64": "0.26.0", - "@esbuild/linux-arm": "0.26.0", - "@esbuild/linux-arm64": "0.26.0", - "@esbuild/linux-ia32": "0.26.0", - "@esbuild/linux-loong64": "0.26.0", - "@esbuild/linux-mips64el": "0.26.0", - "@esbuild/linux-ppc64": "0.26.0", - "@esbuild/linux-riscv64": "0.26.0", - "@esbuild/linux-s390x": "0.26.0", - "@esbuild/linux-x64": "0.26.0", - "@esbuild/netbsd-arm64": "0.26.0", - "@esbuild/netbsd-x64": "0.26.0", - "@esbuild/openbsd-arm64": "0.26.0", - "@esbuild/openbsd-x64": "0.26.0", - "@esbuild/openharmony-arm64": "0.26.0", - "@esbuild/sunos-x64": "0.26.0", - "@esbuild/win32-arm64": "0.26.0", - "@esbuild/win32-ia32": "0.26.0", - "@esbuild/win32-x64": "0.26.0" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/escalade": { @@ -6424,33 +6483,30 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz", + "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.2", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.0", + "@eslint/plugin-kit": "^0.6.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.1", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -6460,8 +6516,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.1", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -6469,7 +6524,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -6484,12 +6539,14 @@ } }, "node_modules/eslint-scope": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.0.0.tgz", - "integrity": "sha512-+Yh0LeQKq+mW/tQArNj67tljR3L1HajDTQPuZOEwC00oBdoIDQrr89yBgjAlzAwRrY/5zDkM3v99iGHwz9y0dw==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", + "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, @@ -6514,9 +6571,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -6530,42 +6587,14 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6588,45 +6617,32 @@ "dev": true, "license": "MIT" }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", + "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6774,11 +6790,14 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", + "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", "dev": true, "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, "engines": { "node": ">= 16" }, @@ -6879,20 +6898,6 @@ "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", "license": "MIT" }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -7081,18 +7086,18 @@ } }, "node_modules/glob": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", - "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -7118,35 +7123,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -7204,12 +7180,11 @@ } }, "node_modules/hono": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.3.tgz", - "integrity": "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", + "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -7228,9 +7203,9 @@ } }, "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -7245,9 +7220,9 @@ "license": "MIT" }, "node_modules/htmlparser2": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", - "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -7260,14 +7235,14 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" + "domutils": "^3.2.2", + "entities": "^7.0.1" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -7334,9 +7309,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", - "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { @@ -7373,26 +7348,10 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/ignore-walk/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "dev": true, "license": "MIT" }, @@ -7430,13 +7389,13 @@ "license": "ISC" }, "node_modules/ini": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", - "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/ip-address": { @@ -7465,22 +7424,6 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7533,17 +7476,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -7653,9 +7585,9 @@ } }, "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.0.tgz", + "integrity": "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==", "dev": true, "license": "MIT", "funding": { @@ -7817,9 +7749,9 @@ } }, "node_modules/karma-coverage/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -7937,14 +7869,15 @@ } }, "node_modules/lmdb": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.4.3.tgz", - "integrity": "sha512-GWV1kVi6uhrXWqe+3NXWO73OYe8fto6q8JMo0HOpk1vf8nEyFWgo4CSNJpIFzsOxOrysVUlcO48qRbQfmKd1gA==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", + "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { + "@harperfast/extended-iterable": "^1.0.3", "msgpackr": "^1.11.2", "node-addon-api": "^6.1.0", "node-gyp-build-optional-packages": "5.2.2", @@ -7955,13 +7888,13 @@ "download-lmdb-prebuilds": "bin/download-prebuilds.js" }, "optionalDependencies": { - "@lmdb/lmdb-darwin-arm64": "3.4.3", - "@lmdb/lmdb-darwin-x64": "3.4.3", - "@lmdb/lmdb-linux-arm": "3.4.3", - "@lmdb/lmdb-linux-arm64": "3.4.3", - "@lmdb/lmdb-linux-x64": "3.4.3", - "@lmdb/lmdb-win32-arm64": "3.4.3", - "@lmdb/lmdb-win32-x64": "3.4.3" + "@lmdb/lmdb-darwin-arm64": "3.5.1", + "@lmdb/lmdb-darwin-x64": "3.5.1", + "@lmdb/lmdb-linux-arm": "3.5.1", + "@lmdb/lmdb-linux-arm64": "3.5.1", + "@lmdb/lmdb-linux-x64": "3.5.1", + "@lmdb/lmdb-win32-arm64": "3.5.1", + "@lmdb/lmdb-win32-x64": "3.5.1" } }, "node_modules/locate-path": { @@ -7981,9 +7914,9 @@ } }, "node_modules/lodash-es": { - "version": "4.17.22", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz", - "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, "node_modules/lodash.clonedeep": { @@ -7999,13 +7932,6 @@ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/log-symbols": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", @@ -8111,9 +8037,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8144,12 +8070,13 @@ "license": "ISC" }, "node_modules/make-fetch-happen": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", - "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.4.tgz", + "integrity": "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==", "dev": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", @@ -8159,36 +8086,12 @@ "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "ssri": "^13.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/make-fetch-happen/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/make-fetch-happen/node_modules/ssri": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", - "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8222,35 +8125,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -8292,27 +8166,27 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -8331,21 +8205,21 @@ } }, "node_modules/minipass-fetch": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.0.tgz", - "integrity": "sha512-fiCdUALipqgPWrOVTz9fw0XhcazULXOSU6ie40DDbX1F49p1dBrSRBuswndTx1x3vEb/g0FT7vC4c4C2u/mh3A==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", + "minipass-sized": "^2.0.0", "minizlib": "^3.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" }, "optionalDependencies": { - "encoding": "^0.1.13" + "iconv-lite": "^0.7.2" } }, "node_modules/minipass-flush": { @@ -8415,38 +8289,18 @@ "license": "ISC" }, "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", "dev": true, "license": "ISC", "dependencies": { - "minipass": "^3.0.0" + "minipass": "^7.1.2" }, "engines": { "node": ">=8" } }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/minizlib": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", @@ -8715,9 +8569,9 @@ } }, "node_modules/node-gyp": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.1.0.tgz", - "integrity": "sha512-W+RYA8jBnhSr2vrTtlPYPc1K+CSjGpVDRZxcqJcERZ8ND3A1ThWPHRwctTx3qC3oW99jt726jhdz3Y6ky87J4g==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", + "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8728,7 +8582,7 @@ "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^7.5.2", + "tar": "^7.5.4", "tinyglobby": "^0.2.12", "which": "^6.0.0" }, @@ -8756,33 +8610,23 @@ } }, "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" - } - }, - "node_modules/node-gyp/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=20" } }, "node_modules/node-gyp/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" @@ -8820,16 +8664,16 @@ "license": "MIT" }, "node_modules/npm-bundled": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", - "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", "dev": true, "license": "ISC", "dependencies": { - "npm-normalize-package-bin": "^4.0.0" + "npm-normalize-package-bin": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-install-checks": { @@ -8846,35 +8690,35 @@ } }, "node_modules/npm-normalize-package-bin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", - "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-package-arg": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.1.tgz", - "integrity": "sha512-6zqls5xFvJbgFjB1B2U6yITtyGBjDBORB7suI4zA4T/sZ1OmkMFlaQSNB/4K0LtXNA1t4OprAFxPisadK5O2ag==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", "dev": true, "license": "ISC", "dependencies": { "hosted-git-info": "^9.0.0", - "proc-log": "^5.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" + "validate-npm-package-name": "^7.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-packlist": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.3.tgz", - "integrity": "sha512-zPukTwJMOu5X5uvm0fztwS5Zxyvmk38H/LfidkOMt3gbZVCyro2cD/ETzwzVPcWZA3JOyPznfUN/nkyFiyUbxg==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", "dev": true, "license": "ISC", "dependencies": { @@ -8885,16 +8729,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm-packlist/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/npm-pick-manifest": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", @@ -8911,16 +8745,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm-pick-manifest/node_modules/npm-normalize-package-bin": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", - "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/npm-registry-fetch": { "version": "19.1.1", "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", @@ -8941,16 +8765,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm-registry-fetch/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -9055,9 +8869,9 @@ } }, "node_modules/ora": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", - "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", + "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", "dev": true, "license": "MIT", "dependencies": { @@ -9067,9 +8881,8 @@ "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", - "stdin-discarder": "^0.2.2", - "string-width": "^8.1.0", - "strip-ansi": "^7.1.2" + "stdin-discarder": "^0.3.1", + "string-width": "^8.1.0" }, "engines": { "node": ">=20" @@ -9145,16 +8958,16 @@ } }, "node_modules/pacote": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.3.tgz", - "integrity": "sha512-itdFlanxO0nmQv4ORsvA9K1wv40IPfB9OmWqfaJWvoJ30VKyHsqNgDVeG+TVhI7Gk7XW8slUy7cA9r6dF5qohw==", + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.3.1.tgz", + "integrity": "sha512-O0EDXi85LF4AzdjG74GUwEArhdvawi/YOHcsW6IijKNj7wm8IvEWNF5GnfuxNpQ/ZpO3L37+v8hqdVh8GgWYhg==", "dev": true, "license": "ISC", "dependencies": { "@npmcli/git": "^7.0.0", - "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", - "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/promise-spawn": "^9.0.0", "@npmcli/run-script": "^10.0.0", "cacache": "^20.0.0", "fs-minipass": "^3.0.0", @@ -9163,10 +8976,10 @@ "npm-packlist": "^10.0.1", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", - "proc-log": "^5.0.0", + "proc-log": "^6.0.0", "promise-retry": "^2.0.1", "sigstore": "^4.0.0", - "ssri": "^12.0.0", + "ssri": "^13.0.0", "tar": "^7.4.3" }, "bin": { @@ -9313,17 +9126,10 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -9331,16 +9137,16 @@ "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -9386,9 +9192,9 @@ } }, "node_modules/piscina": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.3.tgz", - "integrity": "sha512-0u3N7H4+hbr40KjuVn2uNhOcthu/9usKhnw5vT3J7ply79v3D3M8naI00el9Klcy16x557VsEkkUQaHCWFXC/g==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", + "integrity": "sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==", "dev": true, "license": "MIT", "engines": { @@ -9409,9 +9215,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -9444,6 +9250,33 @@ "dev": true, "license": "MIT" }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9455,13 +9288,13 @@ } }, "node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/promise-retry": { @@ -9478,6 +9311,16 @@ "node": ">=10" } }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -9514,9 +9357,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9536,9 +9379,9 @@ "license": "MIT" }, "node_modules/quill": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz", - "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz", + "integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==", "license": "BSD-3-Clause", "dependencies": { "eventemitter3": "^5.0.1", @@ -9594,6 +9437,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -9625,27 +9469,6 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -9673,9 +9496,9 @@ } }, "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, "license": "MIT", "engines": { @@ -9690,14 +9513,14 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.47.tgz", - "integrity": "sha512-Mid74GckX1OeFAOYz9KuXeWYhq3xkXbMziYIC+ULVdUzPTG9y70OBSBQDQn9hQP8u/AfhuYw1R0BSg15nBI4Dg==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.4.tgz", + "integrity": "sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.96.0", - "@rolldown/pluginutils": "1.0.0-beta.47" + "@oxc-project/types": "=0.113.0", + "@rolldown/pluginutils": "1.0.0-rc.4" }, "bin": { "rolldown": "bin/cli.mjs" @@ -9706,26 +9529,25 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-beta.47", - "@rolldown/binding-darwin-arm64": "1.0.0-beta.47", - "@rolldown/binding-darwin-x64": "1.0.0-beta.47", - "@rolldown/binding-freebsd-x64": "1.0.0-beta.47", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.47", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.47", - "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.47", - "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.47", - "@rolldown/binding-linux-x64-musl": "1.0.0-beta.47", - "@rolldown/binding-openharmony-arm64": "1.0.0-beta.47", - "@rolldown/binding-wasm32-wasi": "1.0.0-beta.47", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.47", - "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.47", - "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.47" + "@rolldown/binding-android-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-x64": "1.0.0-rc.4", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.4", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.4", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.4", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.4", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4" } }, "node_modules/rollup": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", - "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -9739,31 +9561,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.55.1", - "@rollup/rollup-android-arm64": "4.55.1", - "@rollup/rollup-darwin-arm64": "4.55.1", - "@rollup/rollup-darwin-x64": "4.55.1", - "@rollup/rollup-freebsd-arm64": "4.55.1", - "@rollup/rollup-freebsd-x64": "4.55.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", - "@rollup/rollup-linux-arm-musleabihf": "4.55.1", - "@rollup/rollup-linux-arm64-gnu": "4.55.1", - "@rollup/rollup-linux-arm64-musl": "4.55.1", - "@rollup/rollup-linux-loong64-gnu": "4.55.1", - "@rollup/rollup-linux-loong64-musl": "4.55.1", - "@rollup/rollup-linux-ppc64-gnu": "4.55.1", - "@rollup/rollup-linux-ppc64-musl": "4.55.1", - "@rollup/rollup-linux-riscv64-gnu": "4.55.1", - "@rollup/rollup-linux-riscv64-musl": "4.55.1", - "@rollup/rollup-linux-s390x-gnu": "4.55.1", - "@rollup/rollup-linux-x64-gnu": "4.55.1", - "@rollup/rollup-linux-x64-musl": "4.55.1", - "@rollup/rollup-openbsd-x64": "4.55.1", - "@rollup/rollup-openharmony-arm64": "4.55.1", - "@rollup/rollup-win32-arm64-msvc": "4.55.1", - "@rollup/rollup-win32-ia32-msvc": "4.55.1", - "@rollup/rollup-win32-x64-gnu": "4.55.1", - "@rollup/rollup-win32-x64-msvc": "4.55.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -9801,9 +9623,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.93.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", - "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "dev": true, "license": "MIT", "dependencies": { @@ -9834,9 +9656,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10036,9 +9858,9 @@ } }, "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, "license": "MIT", "dependencies": { @@ -10047,7 +9869,7 @@ "totalist": "^3.0.0" }, "engines": { - "node": ">= 10" + "node": ">=18" } }, "node_modules/slice-ansi": { @@ -10162,17 +9984,6 @@ "node": ">=0.10.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, "node_modules/spdx-exceptions": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", @@ -10181,9 +9992,9 @@ "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10192,23 +10003,23 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.22", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, "node_modules/ssri": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/statuses": { @@ -10222,9 +10033,9 @@ } }, "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", + "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==", "dev": true, "license": "MIT", "engines": { @@ -10292,23 +10103,10 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/swiper": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/swiper/-/swiper-12.0.3.tgz", - "integrity": "sha512-BHd6U1VPEIksrXlyXjMmRWO0onmdNPaTAFduzqR3pgjvi7KfmUCAm/0cj49u2D7B0zNjMw02TSeXfinC1hDCXg==", + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-12.1.2.tgz", + "integrity": "sha512-4gILrI3vXZqoZh71I1PALqukCFgk+gpOwe1tOvz5uE9kHtl2gTDzmYflYCwWvR4LOvCrJi6UEEU+gnuW5BtkgQ==", "funding": [ { "type": "patreon", @@ -10325,9 +10123,9 @@ } }, "node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", + "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -10367,20 +10165,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -10543,9 +10327,9 @@ } }, "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", "dev": true, "license": "MIT", "engines": { @@ -10553,9 +10337,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, @@ -10661,25 +10445,14 @@ "dev": true, "license": "MIT" }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/validate-npm-package-name": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", - "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/vary": { @@ -10693,13 +10466,13 @@ } }, "node_modules/vite": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", - "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -10767,494 +10540,10 @@ } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "license": "MIT", "dependencies": { @@ -11280,9 +10569,9 @@ "license": "BSD-2-Clause" }, "node_modules/webpack-bundle-analyzer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-5.1.1.tgz", - "integrity": "sha512-UzoaIA0Aigo5lUvoUkIkSoHtUK5rBJh9e2vW3Eqct0jc/L8hcruBCz/jsXEvB1hDU1G3V94jo2EJqPcFKeSSeQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-5.2.0.tgz", + "integrity": "sha512-Etrauj1wYO/xjiz/Vfd6bW1lG9fEhrJpNmu10tv0X9kv+gyY3qiE09uYepqg1Xd0PxOvllRXwWYWjtQYoO/glQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11295,8 +10584,8 @@ "html-escaper": "^2.0.2", "opener": "^1.5.2", "picocolors": "^1.0.0", - "sirv": "^2.0.3", - "ws": "^7.3.1" + "sirv": "^3.0.2", + "ws": "^8.19.0" }, "bin": { "webpack-bundle-analyzer": "lib/bin/analyzer.js" @@ -11305,6 +10594,28 @@ "node": ">= 20.9.0" } }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -11547,9 +10858,9 @@ } }, "node_modules/zod": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", - "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", "funding": { @@ -11567,9 +10878,9 @@ } }, "node_modules/zone.js": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.0.tgz", - "integrity": "sha512-LqLPpIQANebrlxY6jKcYKdgN5DTXyyHAKnnWWjE5pPfEQ4n7j5zn7mOEEpwNZVKGqx3kKKmvplEmoBrvpgROTA==", + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.1.tgz", + "integrity": "sha512-dpvY17vxYIW3+bNrP0ClUlaiY0CiIRK3tnoLaGoQsQcY9/I/NpzIWQ7tQNhbV7LacQMpCII6wVzuL3tuWOyfuA==", "license": "MIT" }, "node_modules/zrender": { diff --git a/UI/Web/package.json b/UI/Web/package.json index 127544aeb..26c14768a 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -20,25 +20,25 @@ "private": true, "dependencies": { "@angular-slider/ngx-slider": "^21.0.0", - "@angular/animations": "^21.0.6", - "@angular/cdk": "^21.0.3", - "@angular/common": "^21.0.6", - "@angular/compiler": "^21.0.6", - "@angular/core": "^21.0.6", - "@angular/forms": "^21.0.6", - "@angular/localize": "^21.0.6", - "@angular/platform-browser": "^21.0.6", - "@angular/platform-browser-dynamic": "^21.0.6", - "@angular/router": "^21.0.6", - "@fortawesome/fontawesome-free": "^7.1.0", - "@iharbeck/ngx-virtual-scroller": "^19.0.1", + "@angular/animations": "^21.2.1", + "@angular/cdk": "^21.2.1", + "@angular/common": "^21.2.1", + "@angular/compiler": "^21.2.1", + "@angular/core": "^21.2.1", + "@angular/forms": "^21.2.1", + "@angular/localize": "^21.2.1", + "@angular/platform-browser": "^21.2.1", + "@angular/platform-browser-dynamic": "^21.2.1", + "@angular/router": "^21.2.1", + "@fortawesome/fontawesome-free": "^7.2.0", + "@iharbeck/ngx-virtual-scroller": "^20.0.0", "@iplab/ngx-color-picker": "^21.0.0", "@iplab/ngx-file-upload": "^21.0.0", - "@jsverse/transloco": "^8.2.0", - "@jsverse/transloco-locale": "^8.2.0", - "@jsverse/transloco-persist-lang": "^8.2.0", - "@jsverse/transloco-persist-translations": "^8.2.0", - "@jsverse/transloco-preload-langs": "^8.2.0", + "@jsverse/transloco": "^8.2.1", + "@jsverse/transloco-locale": "^8.2.1", + "@jsverse/transloco-persist-lang": "^8.2.1", + "@jsverse/transloco-persist-translations": "^8.2.1", + "@jsverse/transloco-preload-langs": "^8.2.1", "@microsoft/signalr": "^10.0.0", "@ng-bootstrap/ng-bootstrap": "^20.0.0", "@popperjs/core": "^2.11.7", @@ -60,34 +60,34 @@ "ngx-stars": "^1.6.5", "ngx-toastr": "^19.1.0", "nosleep.js": "^0.12.0", - "quill": "^2.0.3", + "quill": "^2.0.2", "rxjs": "^7.8.2", "screenfull": "^6.0.2", - "swiper": "^12.0.3", + "swiper": "^12.1.2", "tslib": "^2.8.1", - "zone.js": "^0.16.0" + "zone.js": "^0.16.1" }, "devDependencies": { - "@angular-eslint/builder": "^21.1.0", - "@angular-eslint/eslint-plugin": "^21.1.0", - "@angular-eslint/eslint-plugin-template": "^21.1.0", - "@angular-eslint/schematics": "^21.1.0", - "@angular-eslint/template-parser": "^21.1.0", - "@angular/build": "^21.0.3", - "@angular/cli": "^21.0.3", - "@angular/compiler-cli": "^21.0.6", + "@angular-eslint/builder": "^21.3.0", + "@angular-eslint/eslint-plugin": "^21.3.0", + "@angular-eslint/eslint-plugin-template": "^21.3.0", + "@angular-eslint/schematics": "^21.3.0", + "@angular-eslint/template-parser": "^21.3.0", + "@angular/build": "^21.2.1", + "@angular/cli": "^21.2.1", + "@angular/compiler-cli": "^21.2.1", "@types/d3": "^7.4.3", "@types/file-saver": "^2.0.7", "@types/luxon": "^3.7.1", "@types/marked": "^5.0.2", - "@types/node": "^25.0.2", - "@typescript-eslint/eslint-plugin": "^8.50.0", - "@typescript-eslint/parser": "^8.50.0", - "eslint": "^9.39.2", + "@types/node": "^25.3.3", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^10.0.2", "jsonminify": "^0.4.2", "karma-coverage": "~2.2.0", "ts-node": "~10.9.1", "typescript": "^5.9.3", - "webpack-bundle-analyzer": "^5.1.0" + "webpack-bundle-analyzer": "^5.2.0" } } diff --git a/UI/Web/src/app/_directives/echarts.directive.ts b/UI/Web/src/app/_directives/echarts.directive.ts index 3769c4a27..9dc1a14fd 100644 --- a/UI/Web/src/app/_directives/echarts.directive.ts +++ b/UI/Web/src/app/_directives/echarts.directive.ts @@ -10,19 +10,22 @@ import { OnInit, untracked } from '@angular/core'; -import {EChartsInitOpts, init, EChartsType, ComposeOption, registerTheme} from "echarts/core"; -import { BarSeriesOption, LineSeriesOption, PieSeriesOption } from 'echarts/charts'; +import {ComposeOption, EChartsInitOpts, EChartsType, init, registerTheme} from "echarts/core"; +import {BarSeriesOption, LineSeriesOption, PieSeriesOption} from 'echarts/charts'; import { - TitleComponentOption, - TooltipComponentOption, DatasetComponentOption, LegendComponentOption, + TitleComponentOption, ToolboxComponentOption, + TooltipComponentOption, } from 'echarts/components'; import {ThemeService} from "../_services/theme.service"; -import {asyncScheduler, Subject, Subscription, tap} from "rxjs"; +import {asyncScheduler, Subject, tap} from "rxjs"; import {throttleTime} from "rxjs/operators"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {registerECharts} from "../../echarts"; + +registerECharts(); export type ECOption = ComposeOption< | BarSeriesOption diff --git a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html index 78ff32a54..2243f1894 100644 --- a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html +++ b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html @@ -2,12 +2,12 @@
    + [imageUrl]="imageUrl()" />
    - @if (entity.title | safeHtml; as info) { + @if (entity().title | safeHtml; as info) { @if (info !== '') {
    @@ -20,7 +20,7 @@
    - {{title}} + {{title()}}
    diff --git a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts index 0d9b9e123..2d4911a53 100644 --- a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts +++ b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; import {ImageComponent} from "../../shared/image/image.component"; import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter"; import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; @@ -12,25 +12,21 @@ import {translate, TranslocoDirective} from "@jsverse/transloco"; styleUrl: './next-expected-card.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) -export class NextExpectedCardComponent implements OnInit { - private readonly cdRef = inject(ChangeDetectorRef); - +export class NextExpectedCardComponent { + private readonly utcPipe = new UtcToLocalTimePipe(); /** * Card item url. Will internally handle error and missing covers */ - @Input() imageUrl = ''; + imageUrl = input.required(); /** * This is the entity we are representing. It will be returned if an action is executed. */ - @Input({required: true}) entity!: NextExpectedChapter; - title: string = ''; - - ngOnInit(): void { - if (this.entity.expectedDate) { - const utcPipe = new UtcToLocalTimePipe(); - this.title = translate('next-expected-card.title', {date: utcPipe.transform(this.entity.expectedDate, 'shortDate')}); + entity = input.required(); + title = computed(() => { + const expectedDate = this.entity()?.expectedDate; + if (expectedDate) { + return translate('next-expected-card.title', {date: this.utcPipe.transform(expectedDate, 'shortDate')}) } - this.cdRef.markForCheck(); - } - + return ''; + }); } diff --git a/UI/Web/src/app/cards/person-card/person-card.component.ts b/UI/Web/src/app/cards/person-card/person-card.component.ts index 848e0c3a8..b8c026aaf 100644 --- a/UI/Web/src/app/cards/person-card/person-card.component.ts +++ b/UI/Web/src/app/cards/person-card/person-card.component.ts @@ -3,7 +3,6 @@ import { ChangeDetectorRef, Component, contentChild, - DestroyRef, HostListener, inject, Input, @@ -12,7 +11,6 @@ import { } from '@angular/core'; import {ImageService} from "../../_services/image.service"; import {BulkSelectionService} from "../bulk-selection.service"; -import {MessageHubService} from "../../_services/message-hub.service"; import {ScrollService} from "../../_services/scroll.service"; import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component"; @@ -41,10 +39,8 @@ import {ActionItem} from "../../_models/actionables/action-item"; }) export class PersonCardComponent { - private readonly destroyRef = inject(DestroyRef); public readonly imageService = inject(ImageService); public readonly bulkSelectionService = inject(BulkSelectionService); - private readonly messageHub = inject(MessageHubService); private readonly scrollService = inject(ScrollService); private readonly cdRef = inject(ChangeDetectorRef); diff --git a/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.ts b/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.ts index 712a34987..a2116d6cb 100644 --- a/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.ts +++ b/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.ts @@ -3,14 +3,14 @@ import { ChangeDetectorRef, Component, computed, + contentChild, CUSTOM_ELEMENTS_SCHEMA, inject, input, Input, - signal, - TemplateRef, output, - contentChild + signal, + TemplateRef } from '@angular/core'; import {Swiper} from 'swiper/types'; import {register} from 'swiper/element/bundle'; diff --git a/UI/Web/src/app/ng-swipe/ng-swipe.directive.ts b/UI/Web/src/app/ng-swipe/ng-swipe.directive.ts index bd44ab5a1..5ab5d5ee2 100644 --- a/UI/Web/src/app/ng-swipe/ng-swipe.directive.ts +++ b/UI/Web/src/app/ng-swipe/ng-swipe.directive.ts @@ -1,4 +1,4 @@ -import {Directive, ElementRef, inject, Input, NgZone, OnDestroy, OnInit, output} from '@angular/core'; +import {Directive, ElementRef, inject, input, NgZone, OnDestroy, OnInit, output} from '@angular/core'; import {Subscription} from 'rxjs'; import {createSwipeSubscription, SwipeDirection, SwipeEvent} from './ag-swipe.core'; @@ -7,12 +7,10 @@ import {createSwipeSubscription, SwipeDirection, SwipeEvent} from './ag-swipe.co standalone: true }) export class SwipeDirective implements OnInit, OnDestroy { - private elementRef = inject(ElementRef); - private zone = inject(NgZone); + private readonly elementRef = inject(ElementRef); + private readonly zone = inject(NgZone); - private swipeSubscription: Subscription | undefined; - - @Input() restrictSwipeToLeftSide: boolean = false; + restrictSwipeToLeftSide = input(false); readonly swipeMove = output(); readonly swipeEnd = output(); readonly swipeLeft = output(); @@ -20,6 +18,8 @@ export class SwipeDirective implements OnInit, OnDestroy { readonly swipeUp = output(); readonly swipeDown = output(); + private swipeSubscription: Subscription | undefined; + ngOnInit() { this.zone.runOutsideAngular(() => { this.swipeSubscription = createSwipeSubscription({ @@ -36,7 +36,7 @@ export class SwipeDirective implements OnInit, OnDestroy { } private isSwipeWithinRestrictedArea(swipeEvent: SwipeEvent): boolean { - if (!this.restrictSwipeToLeftSide) return true; // If restriction is disabled, allow all swipes + if (!this.restrictSwipeToLeftSide()) return true; // If restriction is disabled, allow all swipes const elementRect = this.elementRef.nativeElement.getBoundingClientRect(); const touchAreaWidth = elementRect.width * 0.3; // Define the left area (30% of the element's width) diff --git a/UI/Web/src/main.ts b/UI/Web/src/main.ts index 7c9e9533d..bfb178099 100644 --- a/UI/Web/src/main.ts +++ b/UI/Web/src/main.ts @@ -22,8 +22,6 @@ import {getSaver, SAVER} from "./app/_providers/saver.provider"; import {APP_BASE_HREF, PlatformLocation} from "@angular/common"; import {provideTranslocoPersistTranslations} from '@jsverse/transloco-persist-translations'; import {HttpLoader} from "./httpLoader"; -import {register as registerSwiperElements} from 'swiper/element/bundle'; -import {ColorPickerModule} from "@iplab/ngx-color-picker"; import {clientInfoInterceptor} from "./app/_interceptors/client-info.interceptor"; import { PreloadAllModules, @@ -36,7 +34,6 @@ import { } from "@angular/router"; import {KavitaTitleStrategy} from "./app/_services/kavita-title.strategy"; import {routingErrorHandler} from "./app/_interceptors/routing-error.handler"; -import {registerECharts} from "./echarts"; import {NgbModalConfig, NgbRatingConfig} from "@ng-bootstrap/ng-bootstrap"; import {DefaultModalOptions} from "./app/_models/modal/modal-options"; import {ToastNoAnimationModule} from "ngx-toastr"; @@ -49,8 +46,6 @@ if (disableAnimations) { document.documentElement.classList.add('no-animations'); } -registerSwiperElements(); -registerECharts(); function transformLanguageCodes(arr: Array) { const transformedArray: Array = []; @@ -176,8 +171,7 @@ bootstrapApplication(AppComponent, { countDuplicates: true, autoDismiss: true }), - NgCircleProgressModule.forRoot(), - ColorPickerModule, + NgCircleProgressModule.forRoot() ), provideRouter(routes, withComponentInputBinding(), diff --git a/build.sh b/build.sh index e34754794..59346154a 100755 --- a/build.sh +++ b/build.sh @@ -50,15 +50,15 @@ BuildUI() { ProgressStart 'Building UI' echo 'Removing old wwwroot' - rm -rf API/wwwroot/* + rm -rf Kavita.Server/wwwroot/* cd UI/Web/ || exit echo 'Installing web dependencies' npm install --legacy-peer-deps echo 'Building UI' npm run prod echo 'Copying back to Kavita wwwroot' - mkdir -p ../../API/wwwroot - cp -R dist/browser/* ../../API/wwwroot + mkdir -p ../../Kavita.Server/wwwroot + cp -R dist/browser/* ../../Kavita.Server/wwwroot cd ../../ || exit ProgressEnd 'Building UI' } @@ -72,7 +72,7 @@ Package() # TODO: Use no-restore? Because Build should have already done it for us echo "Building" - cd API + cd Kavita.Server echo dotnet publish -c Release --self-contained --runtime $runtime -o "$lOutputFolder" dotnet publish -c Release --self-contained --runtime $runtime -o "$lOutputFolder" @@ -92,20 +92,17 @@ Package() echo "Copying LICENSE" cp ../LICENSE "$lOutputFolder"/LICENSE.txt - echo "Renaming API -> Kavita" + echo "Renaming Kavita.Server -> Kavita" if [ $runtime == "win-x64" ] || [ $runtime == "win-x86" ] then - mv "$lOutputFolder"/API.exe "$lOutputFolder"/Kavita.exe + mv "$lOutputFolder"/Kavita.Server.exe "$lOutputFolder"/Kavita.exe else - mv "$lOutputFolder"/API "$lOutputFolder"/Kavita + mv "$lOutputFolder"/Kavita.Server "$lOutputFolder"/Kavita fi + mkdir -p $lOutputFolder/config echo "Copying appsettings.json" cp config/appsettings.json $lOutputFolder/config/appsettings-init.json - echo "Removing appsettings.Development.json" - rm $lOutputFolder/config/appsettings.Development.json - echo "Removing appsettings.json" - rm $lOutputFolder/config/appsettings.json echo "Creating tar" cd ../$outputFolder/"$runtime"/ diff --git a/docker-build.sh b/docker-build.sh index 072676a78..0659144fe 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -32,15 +32,15 @@ BuildUI() { ProgressStart 'Building UI' echo 'Removing old wwwroot' - rm -rf API/wwwroot/* + rm -rf Kavita.Server/wwwroot/* cd UI/Web/ || exit echo 'Installing web dependencies' npm install --legacy-peer-deps echo 'Building UI' npm run prod echo 'Copying back to Kavita wwwroot' - mkdir -p ../../API/wwwroot - cp -R dist/browser/* ../../API/wwwroot + mkdir -p ../../Kavita.Server/wwwroot + cp -R dist/browser/* ../../Kavita.Server/wwwroot cd ../../ || exit ProgressEnd 'Building UI' } @@ -54,7 +54,7 @@ Package() # TODO: Use no-restore? Because Build should have already done it for us echo "Building" - cd API + cd Kavita.Server echo dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" @@ -64,8 +64,8 @@ Package() echo "Copying LICENSE" cp ../LICENSE "$lOutputFolder"/LICENSE.txt - echo "Renaming API -> Kavita" - mv "$lOutputFolder"/API "$lOutputFolder"/Kavita + echo "Renaming Kavita.Server -> Kavita" + mv "$lOutputFolder"/Kavita.Server "$lOutputFolder"/Kavita echo "Creating tar" cd ../$outputFolder/"$runtime"/