From 10d4f8bdfd1ca9cecf5b263e7bca965aad5261de Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 08:21:37 -0600 Subject: [PATCH 01/16] Refactored the code so that when running on debug it will hit the local stats server automatically, like Kavita+ does. Added a new stat for if using metadata downloading feature. --- API/DTOs/Stats/V3/ServerInfoV3Dto.cs | 5 ++++ API/Extensions/FlurlExtensions.cs | 12 +++++++++ API/Services/SeriesService.cs | 2 ++ API/Services/Tasks/StatsService.cs | 38 ++++++++++++---------------- Kavita.Common/Configuration.cs | 1 + README.md | 1 + openapi.json | 2 +- 7 files changed, 38 insertions(+), 23 deletions(-) diff --git a/API/DTOs/Stats/V3/ServerInfoV3Dto.cs b/API/DTOs/Stats/V3/ServerInfoV3Dto.cs index edc2ad2b47..0bf95403f6 100644 --- a/API/DTOs/Stats/V3/ServerInfoV3Dto.cs +++ b/API/DTOs/Stats/V3/ServerInfoV3Dto.cs @@ -55,6 +55,11 @@ public class ServerInfoV3Dto /// /// This pings a health check and does not capture any IP Information public long TimeToPingKavitaStatsApi { get; set; } + /// + /// If using the downloading metadata feature + /// + /// Kavita+ Only + public bool MatchedMetadataEnabled { get; set; } diff --git a/API/Extensions/FlurlExtensions.cs b/API/Extensions/FlurlExtensions.cs index 38e75fbc8d..67d201afb2 100644 --- a/API/Extensions/FlurlExtensions.cs +++ b/API/Extensions/FlurlExtensions.cs @@ -18,4 +18,16 @@ public static IFlurlRequest WithKavitaPlusHeaders(this string request, string li .WithHeader("Content-Type", "application/json") .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)); } + + public static IFlurlRequest WithBasicHeaders(this string request, string apiKey) + { + return request + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-api-key", apiKey) + .WithHeader("x-installId", HashUtil.ServerToken()) + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)); + } } diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 37051f98dd..38e75c2a37 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -382,10 +382,12 @@ public static async Task HandlePeopleUpdateAsync(SeriesMetadata metadata, IColle // 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? if (personDto.AniListId > 0 && p.AniListId <= 0 && p.AniListId != personDto.AniListId) { p.AniListId = personDto.AniListId; } + p.Description = string.IsNullOrEmpty(p.Description) ? personDto.Description : p.Description; continue; // If we ever want to update metadata for existing people, we'd do it here } diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index 4faf55436f..2e3a0f43c1 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -13,14 +13,17 @@ 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.Common; using Kavita.Common.EnvironmentInfo; using Kavita.Common.Helpers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; @@ -45,12 +48,12 @@ public class StatsService : IStatsService private readonly UserManager _userManager; private readonly IEmailService _emailService; private readonly ICacheService _cacheService; - private const string ApiUrl = "https://stats.kavitareader.com"; + private readonly string _apiUrl = ""; private const string ApiKey = "MsnvA2DfQqxSK5jh"; // It's not important this is public, just a way to keep bots from hitting the API willy-nilly public StatsService(ILogger logger, IUnitOfWork unitOfWork, DataContext context, ILicenseService licenseService, UserManager userManager, IEmailService emailService, - ICacheService cacheService) + ICacheService cacheService, IHostEnvironment environment) { _logger = logger; _unitOfWork = unitOfWork; @@ -60,7 +63,9 @@ public StatsService(ILogger logger, IUnitOfWork unitOfWork, DataCo _emailService = emailService; _cacheService = cacheService; - FlurlConfiguration.ConfigureClientForUrl(ApiUrl); + FlurlConfiguration.ConfigureClientForUrl(Configuration.StatsApiUrl); + + _apiUrl = environment.IsDevelopment() ? "http://localhost:5001" : Configuration.StatsApiUrl; } /// @@ -98,13 +103,8 @@ private async Task SendDataToStatsServer(ServerInfoV3Dto data) try { - var response = await (ApiUrl + "/api/v3/stats") - .WithHeader("Accept", "application/json") - .WithHeader("User-Agent", "Kavita") - .WithHeader("x-api-key", ApiKey) - .WithHeader("x-kavita-version", BuildInfo.Version) - .WithHeader("Content-Type", "application/json") - .WithTimeout(TimeSpan.FromSeconds(30)) + var response = await (_apiUrl + "/api/v3/stats") + .WithBasicHeaders(ApiKey) .PostJsonAsync(data); if (response.StatusCode != StatusCodes.Status200OK) @@ -151,12 +151,8 @@ public async Task SendCancellation() try { - var response = await (ApiUrl + "/api/v2/stats/opt-out?installId=" + installId) - .WithHeader("Accept", "application/json") - .WithHeader("User-Agent", "Kavita") - .WithHeader("x-api-key", ApiKey) - .WithHeader("x-kavita-version", BuildInfo.Version) - .WithHeader("Content-Type", "application/json") + var response = await (_apiUrl + "/api/v2/stats/opt-out?installId=" + installId) + .WithBasicHeaders(ApiKey) .WithTimeout(TimeSpan.FromSeconds(30)) .PostAsync(); @@ -180,12 +176,8 @@ private static async Task PingStatsApi() try { var sw = Stopwatch.StartNew(); - var response = await (ApiUrl + "/api/health/") - .WithHeader("Accept", "application/json") - .WithHeader("User-Agent", "Kavita") - .WithHeader("x-api-key", ApiKey) - .WithHeader("x-kavita-version", BuildInfo.Version) - .WithHeader("Content-Type", "application/json") + var response = await (Configuration.StatsApiUrl + "/api/health/") + .WithBasicHeaders(ApiKey) .WithTimeout(TimeSpan.FromSeconds(30)) .GetAsync(); @@ -244,6 +236,7 @@ private async Task MaxChaptersInASeries() private async Task GetStatV3Payload() { var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var mediaSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); var dto = new ServerInfoV3Dto() { InstallId = serverSettings.InstallId, @@ -256,6 +249,7 @@ private async Task GetStatV3Payload() DotnetVersion = Environment.Version.ToString(), OpdsEnabled = serverSettings.EnableOpds, EncodeMediaAs = serverSettings.EncodeMediaAs, + MatchedMetadataEnabled = mediaSettings.Enabled }; dto.OsLocale = CultureInfo.CurrentCulture.EnglishName; diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index ca0fc40ece..00ec84d069 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -17,6 +17,7 @@ public static class Configuration private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename()); public static string KavitaPlusApiUrl = "https://plus.kavitareader.com"; + public static string StatsApiUrl = "https://stats.kavitareader.com"; public static int Port { diff --git a/README.md b/README.md index a2aa1ac8e8..1ea5a94cad 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ your reading collection with your friends and family! - Rich web readers supporting webtoon, continuous reading mode (continue without leaving the reader), virtual pages (epub), etc - Ability to customize your dashboard and side nav with smart filters, custom order and visibility toggles - Full Localization Support +- Ability to download metadata (available via [Kavita+](https://wiki.kavitareader.com/kavita+)) ## Support diff --git a/openapi.json b/openapi.json index ab22e3a256..33c00de853 100644 --- a/openapi.json +++ b/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.1", "info": { "title": "Kavita", - "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.4.11", + "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.4.12", "license": { "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" From 5b16c590673475c3b3cfc0d88ec647823f3b7e2b Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 08:30:19 -0600 Subject: [PATCH 02/16] Rearranged the ordering for Manage Metadata preference item. Fixed an issue where Whitelist setting could get set with an empty string, thus breaking tag processing. --- API/Controllers/SettingsController.cs | 4 ++-- .../app/sidenav/preference-nav/preference-nav.component.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index e2d77d674e..3f12a059de 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -572,8 +572,8 @@ public async Task> UpdateMetadataSettings(Meta existingMetadataSetting.AgeRatingMappings = dto.AgeRatingMappings ?? []; - existingMetadataSetting.Blacklist = dto.Blacklist.DistinctBy(d => d.ToNormalized()).ToList() ?? []; - existingMetadataSetting.Whitelist = dto.Whitelist.DistinctBy(d => d.ToNormalized()).ToList() ?? []; + existingMetadataSetting.Blacklist = dto.Blacklist.Where(s => !string.IsNullOrWhiteSpace(s)).DistinctBy(d => d.ToNormalized()).ToList() ?? []; + existingMetadataSetting.Whitelist = dto.Whitelist.Where(s => !string.IsNullOrWhiteSpace(s)).DistinctBy(d => d.ToNormalized()).ToList() ?? []; existingMetadataSetting.Overrides = dto.Overrides.ToList() ?? []; // Handle Field Mappings diff --git a/UI/Web/src/app/sidenav/preference-nav/preference-nav.component.ts b/UI/Web/src/app/sidenav/preference-nav/preference-nav.component.ts index fd0d3cb458..b53a5d792f 100644 --- a/UI/Web/src/app/sidenav/preference-nav/preference-nav.component.ts +++ b/UI/Web/src/app/sidenav/preference-nav/preference-nav.component.ts @@ -230,11 +230,13 @@ export class PreferenceNavComponent implements AfterViewInit { if (res) { const kavitaPlusSection = this.sections[4]; if (kavitaPlusSection.children.length === 1) { + kavitaPlusSection.children.push(new SideNavItem(SettingsTabId.ManageUserTokens, [Role.Admin])); + kavitaPlusSection.children.push(new SideNavItem(SettingsTabId.Metadata, [Role.Admin])); + + // Keep all setting type of screens above this line kavitaPlusSection.children.push(new SideNavItem(SettingsTabId.MatchedMetadata, [Role.Admin], this.matchedMetadataBadgeCount$ )); - kavitaPlusSection.children.push(new SideNavItem(SettingsTabId.ManageUserTokens, [Role.Admin])); - kavitaPlusSection.children.push(new SideNavItem(SettingsTabId.Metadata, [Role.Admin])); // Scrobbling History needs to be per-user and allow admin to view all kavitaPlusSection.children.push(new SideNavItem(SettingsTabId.ScrobblingHolds, [])); From 34822d1e6e801dad21657582a28ecc20da894ebd Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 09:17:38 -0600 Subject: [PATCH 03/16] Properly fixed the api so whitelist isn't set with an empty string, allowing tags to be processed. --- .../ExternalSeriesMetadataRepository.cs | 1 + API/Services/Plus/ExternalMetadataService.cs | 18 +++++++++--------- .../manage-metadata-settings.component.ts | 4 ++-- UI/Web/src/assets/langs/en.json | 3 +-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs index d7b6c11bfe..b15541ba8e 100644 --- a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs +++ b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs @@ -214,6 +214,7 @@ public async Task> GetAllSeriesIdsWithoutMetadata(int limit) return await _context.Series .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) .Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow) + .Where(s => s.Library.AllowMetadataMatching) .OrderByDescending(s => s.Library.Type) .ThenBy(s => s.NormalizedName) .Select(s => s.Id) diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 055a5fc28c..955fa6956c 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -44,8 +44,8 @@ public interface IExternalMetadataService /// /// /// - /// - Task FetchSeriesMetadata(int seriesId, LibraryType libraryType); + /// If the fetch was made + Task FetchSeriesMetadata(int seriesId, LibraryType libraryType); Task> GetStacksForUser(int userId); Task> MatchSeries(MatchSeriesDto dto); @@ -118,9 +118,9 @@ public async Task FetchExternalDataTask() foreach (var seriesId in ids) { var libraryType = libTypes[seriesId]; - await FetchSeriesMetadata(seriesId, libraryType); + var success = await FetchSeriesMetadata(seriesId, libraryType); + if (success) count++; await Task.Delay(1500); - count++; } _logger.LogInformation("[Kavita+ Data Refresh] Finished Refreshing {Count} series data from Kavita+", count); } @@ -131,10 +131,10 @@ public async Task FetchExternalDataTask() /// /// /// - public async Task FetchSeriesMetadata(int seriesId, LibraryType libraryType) + public async Task FetchSeriesMetadata(int seriesId, LibraryType libraryType) { - if (!IsPlusEligible(libraryType)) return; - if (!await _licenseService.HasActiveLicense()) return; + if (!IsPlusEligible(libraryType)) return false; + if (!await _licenseService.HasActiveLicense()) 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 @@ -142,14 +142,14 @@ public async Task FetchSeriesMetadata(int seriesId, LibraryType libraryType) { // Request not allowed due to rate limit _logger.LogDebug("Rate Limit hit for Kavita+ prefetch"); - return; + return false; } _logger.LogDebug("Prefetching Kavita+ data for Series {SeriesId}", seriesId); // Prefetch SeriesDetail data await GetSeriesDetailPlus(seriesId, libraryType); - + return true; } public async Task> GetStacksForUser(int userId) diff --git a/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts b/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts index bd6426dda0..62af4626af 100644 --- a/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts +++ b/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts @@ -182,8 +182,8 @@ export class ManageMetadataSettingsComponent implements OnInit { ...model, ageRatingMappings, fieldMappings: withFieldMappings ? fieldMappings : [], - blacklist: (model.blacklist || '').split(',').map((item: string) => item.trim()), - whitelist: (model.whitelist || '').split(',').map((item: string) => item.trim()), + blacklist: (model.blacklist || '').split(',').map((item: string) => item.trim()).filter((tag: string) => tag.length > 0), + whitelist: (model.whitelist || '').split(',').map((item: string) => item.trim()).filter((tag: string) => tag.length > 0), personRoles: Object.entries(this.settingsForm.get('personRoles')!.value) .filter(([_, value]) => value) .map(([key, _]) => this.personRoles[parseInt(key.split('_')[1], 10)]), diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 67e74d80e2..54c42a0351 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -821,8 +821,7 @@ "first-last-name-tooltip": "Ensure People's names are written First then Last", "person-roles-label": "Roles", "overrides-label": "Overrides", - "overrides-description": "Allow Kavita to write over locked fields" - + "overrides-description": "Allow Kavita to write over locked fields." }, "book-line-overlay": { From b56539a5ece5d74b17efc6356cc36ff93686329d Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 10:29:07 -0600 Subject: [PATCH 04/16] Hide Valid util from matched metadata screen because it's only applicable for a filter that I'm not enabling atm. Fixed sorting in some tables and added it matched metadata --- .../user-scrobble-history.component.html | 10 +++--- .../email-history.component.html | 8 ++--- .../manage-matched-metadata.component.html | 35 ++++++++++--------- .../manage-matched-metadata.component.ts | 2 ++ .../manage-media-issues.component.html | 6 ++-- .../manage-metadata-settings.component.ts | 5 ++- .../manage-scrobble-errors.component.html | 6 ++-- .../manage-tasks-settings.component.html | 6 ++-- .../manage-user-tokens.component.html | 6 ++-- .../manage-devices.component.html | 6 ++-- .../scrobbling-holds.component.html | 6 ++-- 11 files changed, 49 insertions(+), 47 deletions(-) diff --git a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html index 60d9301005..60b62a9c46 100644 --- a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html +++ b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html @@ -37,7 +37,7 @@ [limit]="pageInfo.size" > - + {{t('last-modified-header')}} @@ -46,7 +46,7 @@ - + {{t('type-header')}} @@ -55,7 +55,7 @@ - + {{t('series-header')}} @@ -64,7 +64,7 @@ - + {{t('data-header')}} @@ -98,7 +98,7 @@ - + {{t('is-processed-header')}} diff --git a/UI/Web/src/app/admin/email-history/email-history.component.html b/UI/Web/src/app/admin/email-history/email-history.component.html index 9ab6645ae3..30dac84630 100644 --- a/UI/Web/src/app/admin/email-history/email-history.component.html +++ b/UI/Web/src/app/admin/email-history/email-history.component.html @@ -9,7 +9,7 @@ [footerHeight]="50" > - + {{t('template-header')}} @@ -19,7 +19,7 @@ - + {{t('date-header')}} @@ -28,7 +28,7 @@ - + {{t('user-header')}} @@ -37,7 +37,7 @@ - + {{t('sent-header')}} diff --git a/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.html b/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.html index 07129cfba1..60f494f389 100644 --- a/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.html +++ b/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.html @@ -24,7 +24,7 @@ [footerHeight]="50" > - + {{t('series-name-header')}} @@ -34,7 +34,7 @@ - + {{t('library-name-header')}} @@ -43,8 +43,7 @@ - - + {{t('status-header')}} @@ -64,20 +63,22 @@ - - - {{t('valid-until-header')}} - - - @if (item.series.isBlacklisted || item.series.dontMatch || !item.isMatched) { - {{null | defaultValue}} - } @else { - {{item.validUntilUtc | utcToLocalTime}} - } - - + @if (filterGroup.get('matchState')?.value === MatchStateOption.Matched) { + + + {{t('valid-until-header')}} + + + @if (item.series.isBlacklisted || item.series.dontMatch || !item.isMatched) { + {{null | defaultValue}} + } @else { + {{item.validUntilUtc | utcToLocalTime}} + } + + + } - + {{t('actions-header')}} diff --git a/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts b/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts index 25a38b9609..8433a56e3e 100644 --- a/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts +++ b/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts @@ -122,4 +122,6 @@ export class ManageMatchedMetadataComponent implements OnInit { this.loadData().subscribe(); }); } + + protected readonly MatchStateOption = MatchStateOption; } diff --git a/UI/Web/src/app/admin/manage-media-issues/manage-media-issues.component.html b/UI/Web/src/app/admin/manage-media-issues/manage-media-issues.component.html index 59f135682d..e9fb8b49b3 100644 --- a/UI/Web/src/app/admin/manage-media-issues/manage-media-issues.component.html +++ b/UI/Web/src/app/admin/manage-media-issues/manage-media-issues.component.html @@ -21,7 +21,7 @@ [limit]="15" > - + {{t('file-header')}} @@ -31,7 +31,7 @@ - + {{t('comment-header')}} @@ -40,7 +40,7 @@ - + {{t('created-header')}} diff --git a/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts b/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts index 62af4626af..b1719fdf5e 100644 --- a/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts +++ b/UI/Web/src/app/admin/manage-metadata-settings/manage-metadata-settings.component.ts @@ -16,8 +16,7 @@ import {AgeRatingDto} from "../../_models/metadata/age-rating-dto"; import {MetadataFieldMapping, MetadataFieldType} from "../_models/metadata-settings"; import {PersonRole} from "../../_models/metadata/person"; import {PersonRolePipe} from "../../_pipes/person-role.pipe"; -import {NgClass} from "@angular/common"; -import {allMetadataSettingField} from "../_models/metadata-setting-field"; +import {allMetadataSettingField, MetadataSettingField} from "../_models/metadata-setting-field"; import {MetadataSettingFiledPipe} from "../../_pipes/metadata-setting-filed.pipe"; @@ -94,7 +93,7 @@ export class ManageMetadataSettingsComponent implements OnInit { this.settingsForm.addControl('overrides', this.fb.group( Object.fromEntries( - this.allMetadataSettingFields.map((role, index) => [ + this.allMetadataSettingFields.map((role: MetadataSettingField, index: number) => [ `override_${index}`, this.fb.control((settings.overrides || []).includes(role)), ]) diff --git a/UI/Web/src/app/admin/manage-scrobble-errors/manage-scrobble-errors.component.html b/UI/Web/src/app/admin/manage-scrobble-errors/manage-scrobble-errors.component.html index b2e8db72d5..7b1d9659fd 100644 --- a/UI/Web/src/app/admin/manage-scrobble-errors/manage-scrobble-errors.component.html +++ b/UI/Web/src/app/admin/manage-scrobble-errors/manage-scrobble-errors.component.html @@ -24,7 +24,7 @@

{{t('title')}}

[limit]="15" > - + {{t('series-header')}} @@ -34,7 +34,7 @@

{{t('title')}}

- + {{t('created-header')}} @@ -43,7 +43,7 @@

{{t('title')}}

- + {{t('comment-header')}} diff --git a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html index f404efa872..554ff34a15 100644 --- a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html +++ b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html @@ -160,7 +160,7 @@

{{t('recurring-tasks-title')}}

[limit]="15" > - + {{t('job-title-header')}} @@ -170,7 +170,7 @@

{{t('recurring-tasks-title')}}

- + {{t('last-executed-header')}} @@ -179,7 +179,7 @@

{{t('recurring-tasks-title')}}

- + {{t('cron-header')}} diff --git a/UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.html b/UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.html index 9f5c83d02b..982a77e849 100644 --- a/UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.html +++ b/UI/Web/src/app/admin/manage-user-tokens/manage-user-tokens.component.html @@ -11,7 +11,7 @@ [footerHeight]="50" > - + {{t('username-header')}} @@ -21,7 +21,7 @@ - + {{t('anilist-header')}} @@ -34,7 +34,7 @@ - + {{t('mal-header')}} diff --git a/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.html b/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.html index 89dc4d77a8..d3c09bd202 100644 --- a/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.html +++ b/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.html @@ -17,7 +17,7 @@ [limit]="15" > - + {{t('name-label')}} @@ -27,7 +27,7 @@ - + {{t('email-label')}} @@ -36,7 +36,7 @@ - + {{t('platform-label')}} diff --git a/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html b/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html index 4912e74425..5ac7b3cc33 100644 --- a/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html +++ b/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html @@ -11,7 +11,7 @@ [footerHeight]="50" > - + {{t('series-name-header')}} @@ -22,7 +22,7 @@ - + {{t('created-header')}} @@ -31,7 +31,7 @@ - + From 834c49cdecf2814398fa269c823262c381ba4892 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 10:52:50 -0600 Subject: [PATCH 05/16] Fixed a long standing bug where background series processing (fetching metadata for K+) wasn't pulling anything more than the first 25 series over and over. --- API/Data/Repositories/ExternalSeriesMetadataRepository.cs | 7 ++++--- API/Services/Plus/ExternalMetadataService.cs | 2 +- .../user-holds/scrobbling-holds.component.html | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs index b15541ba8e..6f5730d931 100644 --- a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs +++ b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs @@ -36,7 +36,7 @@ public interface IExternalSeriesMetadataRepository Task GetSeriesDetailPlusDto(int seriesId); Task LinkRecommendationsToSeries(Series series); Task IsBlacklistedSeries(int seriesId); - Task> GetAllSeriesIdsWithoutMetadata(int limit); + Task> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false); Task> GetAllSeries(ManageMatchFilterDto filter); } @@ -209,11 +209,12 @@ public Task IsBlacklistedSeries(int seriesId) } - public async Task> GetAllSeriesIdsWithoutMetadata(int limit) + public async Task> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false) { return await _context.Series .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) - .Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow) + .WhereIf(includeStaleData, s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow) + .Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc == DateTime.MinValue) .Where(s => s.Library.AllowMetadataMatching) .OrderByDescending(s => s.Library.Type) .ThenBy(s => s.NormalizedName) diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 955fa6956c..0ad390d17e 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -109,7 +109,7 @@ public static bool IsPlusEligible(LibraryType type) public async Task FetchExternalDataTask() { // Find all Series that are eligible and limit - var ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetAllSeriesIdsWithoutMetadata(25); + var ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25); if (ids.Count == 0) return; _logger.LogInformation("[Kavita+ Data Refresh] Started Refreshing {Count} series data from Kavita+", ids.Count); diff --git a/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html b/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html index 5ac7b3cc33..80e0d5662b 100644 --- a/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html +++ b/UI/Web/src/app/user-settings/user-holds/scrobbling-holds.component.html @@ -22,7 +22,7 @@ - + {{t('created-header')}} @@ -31,7 +31,7 @@ - + From 305381b7e77064cdddcb99e7126c42c2f7fa95e1 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 11:02:24 -0600 Subject: [PATCH 06/16] Don't overwrite summary/release year when unlocked and no override. --- API/Services/Plus/ExternalMetadataService.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 0ad390d17e..de24c37440 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -528,15 +528,16 @@ private async Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto e madeModification = true; } - if (settings.EnableSummary && (!series.Metadata.SummaryLocked || - settings.HasOverride(MetadataSettingField.Summary))) + if (settings.EnableSummary && (settings.HasOverride(MetadataSettingField.Summary) || + (!series.Metadata.SummaryLocked && !string.IsNullOrWhiteSpace(externalMetadata.Summary)))) { series.Metadata.Summary = CleanSummary(externalMetadata.Summary); madeModification = true; } - if (settings.EnableStartDate && externalMetadata.StartDate.HasValue && (!series.Metadata.ReleaseYearLocked || - settings.HasOverride(MetadataSettingField.StartDate))) + if (settings.EnableStartDate && externalMetadata.StartDate.HasValue && (settings.HasOverride(MetadataSettingField.StartDate) || + (!series.Metadata.ReleaseYearLocked && + series.Metadata.ReleaseYear == 0))) { series.Metadata.ReleaseYear = externalMetadata.StartDate.Value.Year; madeModification = true; From 32a9f6d841026504cfb7feaa8d46e328dbd6b4c1 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 11:46:55 -0600 Subject: [PATCH 07/16] Ensure Localized name doesn't get set if there was existing data and no override. --- API/Services/Plus/ExternalMetadataService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index de24c37440..5e9d84cfde 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -512,7 +512,8 @@ private async Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto e var madeModification = false; - if (settings.EnableLocalizedName && (!series.LocalizedNameLocked || settings.HasOverride(MetadataSettingField.LocalizedName))) + if (settings.EnableLocalizedName && (settings.HasOverride(MetadataSettingField.LocalizedName) + || !series.LocalizedNameLocked && !string.IsNullOrWhiteSpace(series.LocalizedName))) { // We need to make the best appropriate guess if (externalMetadata.Name == series.Name) @@ -529,7 +530,7 @@ private async Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto e } if (settings.EnableSummary && (settings.HasOverride(MetadataSettingField.Summary) || - (!series.Metadata.SummaryLocked && !string.IsNullOrWhiteSpace(externalMetadata.Summary)))) + (!series.Metadata.SummaryLocked && !string.IsNullOrWhiteSpace(series.Metadata.Summary)))) { series.Metadata.Summary = CleanSummary(externalMetadata.Summary); madeModification = true; From b5110a54c0f1ddff6aedcfc087a6f77bc6189c33 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 11:56:40 -0600 Subject: [PATCH 08/16] Fixed inability to turn off Do not match on a series --- .../match-series-modal/match-series-modal.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.ts b/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.ts index 6c1d704784..77166a22a8 100644 --- a/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.ts +++ b/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.ts @@ -70,8 +70,10 @@ export class MatchSeriesModalComponent implements OnInit { const model: any = this.formGroup.value; model.seriesId = this.series.id; + const dontMatchChanged = this.series.dontMatch !== model.dontMatch; + // We need to update the dontMatch status - if (model.dontMatch) { + if (dontMatchChanged) { this.seriesService.updateDontMatch(this.series.id, model.dontMatch).subscribe(_ => { this.modalService.close(true); }); From e0b454ca9ae597302905efa25a405e3482bdd635 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 12:10:12 -0600 Subject: [PATCH 09/16] Reverted a change to the read more which made it not wide enough for me. --- UI/Web/src/app/shared/read-more/read-more.component.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UI/Web/src/app/shared/read-more/read-more.component.scss b/UI/Web/src/app/shared/read-more/read-more.component.scss index 1680ce481b..8e7103aa72 100644 --- a/UI/Web/src/app/shared/read-more/read-more.component.scss +++ b/UI/Web/src/app/shared/read-more/read-more.component.scss @@ -10,8 +10,8 @@ div { word-break: break-word; - max-width: 75ch; - + max-width: 120ch; + @media (max-width: $grid-breakpoints-sm) { max-width: 50ch; } From 909d5b0c4b5834590777901334b3d3e5e19f465a Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 13:18:24 -0600 Subject: [PATCH 10/16] Cleaned up the localized name setting code to only allow roman character strings. --- API/Services/Plus/ExternalMetadataService.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 5e9d84cfde..8705fc028b 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -73,6 +73,7 @@ public class ExternalMetadataService : IExternalMetadataService }; // Allow 50 requests per 24 hours private static readonly RateLimiter RateLimiter = new RateLimiter(50, TimeSpan.FromHours(24), false); + static bool IsRomanCharacters(string input) => Regex.IsMatch(input, @"^[\p{IsBasicLatin}\p{IsLatin-1Supplement}]+$"); public ExternalMetadataService(IUnitOfWork unitOfWork, ILogger logger, IMapper mapper, ILicenseService licenseService, IScrobblingService scrobblingService, IEventHub eventHub, ICoverDbService coverDbService) @@ -519,13 +520,23 @@ private async Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto e if (externalMetadata.Name == series.Name) { // Choose closest (usually last) synonym - series.LocalizedName = externalMetadata.Synonyms.Last(); + var validSynonyms = externalMetadata.Synonyms + .Where(IsRomanCharacters) + .Where(s => s.ToNormalized() != series.Name.ToNormalized()) + .ToList(); + if (validSynonyms.Count != 0) + { + series.LocalizedName = validSynonyms[^1]; + series.LocalizedNameLocked = true; + } } - else + else if (IsRomanCharacters(externalMetadata.Name)) { series.LocalizedName = externalMetadata.Name; + series.LocalizedNameLocked = true; } + madeModification = true; } From 11e3ae69d951ac7fa095ffcbea0406b269eddba0 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 13:30:51 -0600 Subject: [PATCH 11/16] Fixed allow metadata matching library setting not showing disabled and off. --- .../library-settings-modal.component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts index 80b64b2589..c1b6871b7f 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts @@ -214,6 +214,8 @@ export class LibrarySettingsModalComponent implements OnInit { } this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible); + this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsKavitaPlusEligible); + if (!this.IsKavitaPlusEligible) { this.libraryForm.get('allowScrobbling')?.disable(); this.libraryForm.get('allowMetadataMatching')?.disable(); @@ -238,10 +240,12 @@ export class LibrarySettingsModalComponent implements OnInit { this.libraryForm.get('manageCollections')?.setValue(this.library.manageCollections); this.libraryForm.get('manageReadingLists')?.setValue(this.library.manageReadingLists); this.libraryForm.get('collapseSeriesRelationships')?.setValue(this.library.collapseSeriesRelationships); - this.libraryForm.get('allowScrobbling')?.setValue(this.library.allowScrobbling); - this.libraryForm.get('allowMetadataMatching')?.setValue(this.library.allowMetadataMatching); + this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible ? this.library.allowScrobbling : false); + this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsKavitaPlusEligible ? this.library.allowMetadataMatching : false); this.selectedFolders = this.library.folders; + this.madeChanges = false; + for(let fileTypeGroup of allFileTypeGroup) { this.libraryForm.addControl(fileTypeGroup + '', new FormControl(this.library.libraryFileTypes.includes(fileTypeGroup), [])); } From af696ce001979dc9f5fc7032f14d4b73d19f77a1 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 13:42:58 -0600 Subject: [PATCH 12/16] Preemptively remove a manually matched series once the modal closes on matched metadata. --- .../manage-matched-metadata.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts b/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts index 8433a56e3e..bdcbe1d7b5 100644 --- a/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts +++ b/UI/Web/src/app/admin/manage-matched-metadata/manage-matched-metadata.component.ts @@ -45,6 +45,7 @@ import {ScanSeriesEvent} from "../../_models/events/scan-series-event"; }) export class ManageMatchedMetadataComponent implements OnInit { protected readonly ColumnMode = ColumnMode; + protected readonly MatchStateOption = MatchStateOption; protected readonly allMatchStates = allMatchStates.filter(m => m !== MatchStateOption.Matched); // Matched will have too many private readonly licenseService = inject(LicenseService); @@ -119,9 +120,8 @@ export class ManageMatchedMetadataComponent implements OnInit { fixMatch(series: Series) { this.actionService.matchSeries(series, result => { if (!result) return; - this.loadData().subscribe(); + this.data = [...this.data.filter(s => s.series.id !== series.id)]; + this.cdRef.markForCheck(); }); } - - protected readonly MatchStateOption = MatchStateOption; } From 990e0fe214dbffd5fd677c9da849023a6bf9b748 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 13:45:44 -0600 Subject: [PATCH 13/16] Fixed a bug where Needs Manual Match (error) was showing stuff in Dont match. --- API/Extensions/QueryExtensions/QueryableExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API/Extensions/QueryExtensions/QueryableExtensions.cs b/API/Extensions/QueryExtensions/QueryableExtensions.cs index d6fef254e9..8c8640be47 100644 --- a/API/Extensions/QueryExtensions/QueryableExtensions.cs +++ b/API/Extensions/QueryExtensions/QueryableExtensions.cs @@ -294,7 +294,7 @@ public static IQueryable FilterMatchState(this IQueryable query, MatchStateOption.NotMatched => query. Include(s => s.ExternalSeriesMetadata) .Where(s => (s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc == DateTime.MinValue) && !s.IsBlacklisted && !s.DontMatch), - MatchStateOption.Error => query.Where(s => s.IsBlacklisted), + MatchStateOption.Error => query.Where(s => s.IsBlacklisted && !s.DontMatch), MatchStateOption.DontMatch => query.Where(s => s.DontMatch), _ => query }; From 4d731daa9ad906d4332a5e0b41b827be05c904ac Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 13:57:12 -0600 Subject: [PATCH 14/16] Fixed add device being disabled for everyone --- .../user-settings/manage-devices/manage-devices.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.html b/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.html index d3c09bd202..7ccb6ad47c 100644 --- a/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.html +++ b/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.html @@ -1,7 +1,7 @@
-
From f193f350011e471aef4ceb6d6fad0ce9a267573e Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 14:18:55 -0600 Subject: [PATCH 15/16] Fixed a bug where an admin editing a user was unable to save the form without a valid email, even though non-valid emails are allowed. --- .../admin/edit-user/edit-user.component.html | 4 ++++ .../admin/edit-user/edit-user.component.ts | 22 +++++++++++++++---- .../manage-devices.component.ts | 13 +++-------- UI/Web/src/assets/langs/en.json | 2 +- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/UI/Web/src/app/admin/edit-user/edit-user.component.html b/UI/Web/src/app/admin/edit-user/edit-user.component.html index ef0e608cbc..cbcaad08b2 100644 --- a/UI/Web/src/app/admin/edit-user/edit-user.component.html +++ b/UI/Web/src/app/admin/edit-user/edit-user.component.html @@ -50,8 +50,12 @@

{{t('account-detail-title')}}

} } + } + @if (isEmailInvalid$ | async) { +
{{t('invalid-email-warning')}}
+ } } diff --git a/UI/Web/src/app/admin/edit-user/edit-user.component.ts b/UI/Web/src/app/admin/edit-user/edit-user.component.ts index 4de2e5205b..4d787c4f02 100644 --- a/UI/Web/src/app/admin/edit-user/edit-user.component.ts +++ b/UI/Web/src/app/admin/edit-user/edit-user.component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; import {AgeRestriction} from 'src/app/_models/metadata/age-restriction'; @@ -9,23 +9,28 @@ import {SentenceCasePipe} from '../../_pipes/sentence-case.pipe'; import {RestrictionSelectorComponent} from '../../user-settings/restriction-selector/restriction-selector.component'; import {LibrarySelectorComponent} from '../library-selector/library-selector.component'; import {RoleSelectorComponent} from '../role-selector/role-selector.component'; -import {NgIf} from '@angular/common'; +import {AsyncPipe, NgIf} from '@angular/common'; import {TranslocoDirective} from "@jsverse/transloco"; +import {debounceTime, distinctUntilChanged, Observable, startWith, switchMap, tap} from "rxjs"; +import {map} from "rxjs/operators"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; const AllowedUsernameCharacters = /^[\sa-zA-Z0-9\-._@+/\s]*$/; +const EmailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @Component({ selector: 'app-edit-user', templateUrl: './edit-user.component.html', styleUrls: ['./edit-user.component.scss'], standalone: true, - imports: [ReactiveFormsModule, NgIf, RoleSelectorComponent, LibrarySelectorComponent, RestrictionSelectorComponent, SentenceCasePipe, TranslocoDirective], + imports: [ReactiveFormsModule, RoleSelectorComponent, LibrarySelectorComponent, RestrictionSelectorComponent, SentenceCasePipe, TranslocoDirective, AsyncPipe], changeDetection: ChangeDetectionStrategy.OnPush }) export class EditUserComponent implements OnInit { private readonly accountService = inject(AccountService); private readonly cdRef = inject(ChangeDetectorRef); + private readonly destroyRef = inject(DestroyRef); protected readonly modal = inject(NgbActiveModal); @Input({required: true}) member!: Member; @@ -36,6 +41,7 @@ export class EditUserComponent implements OnInit { isSaving: boolean = false; userForm: FormGroup = new FormGroup({}); + isEmailInvalid$!: Observable; allowedCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+/'; @@ -47,9 +53,17 @@ export class EditUserComponent implements OnInit { ngOnInit(): void { - this.userForm.addControl('email', new FormControl(this.member.email, [Validators.required, Validators.email])); + this.userForm.addControl('email', new FormControl(this.member.email, [Validators.required])); this.userForm.addControl('username', new FormControl(this.member.username, [Validators.required, Validators.pattern(AllowedUsernameCharacters)])); + this.isEmailInvalid$ = this.userForm.get('email')!.valueChanges.pipe( + startWith(this.member.email), + distinctUntilChanged(), + debounceTime(10), + map(value => !EmailRegex.test(value)), + takeUntilDestroyed(this.destroyRef) + ); + this.selectedRestriction = this.member.ageRestriction; this.cdRef.markForCheck(); } diff --git a/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.ts b/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.ts index 86173f8d57..994ee1088d 100644 --- a/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.ts +++ b/UI/Web/src/app/user-settings/manage-devices/manage-devices.component.ts @@ -8,16 +8,10 @@ import { import { Device } from 'src/app/_models/device/device'; import { DeviceService } from 'src/app/_services/device.service'; import { DevicePlatformPipe } from '../../_pipes/device-platform.pipe'; -import { SentenceCasePipe } from '../../_pipes/sentence-case.pipe'; -import {NgbCollapse, NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; import {translate, TranslocoDirective} from "@jsverse/transloco"; import {SettingsService} from "../../admin/settings.service"; import {ConfirmService} from "../../shared/confirm.service"; -import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; -import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; -import {ScrobbleEventTypePipe} from "../../_pipes/scrobble-event-type.pipe"; -import {SortableHeader} from "../../_single-module/table/_directives/sortable-header.directive"; -import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; import {EditDeviceModalComponent} from "../_modals/edit-device-modal/edit-device-modal.component"; import {DefaultModalOptions} from "../../_models/default-modal-options"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; @@ -25,7 +19,7 @@ import {map} from "rxjs"; import {shareReplay} from "rxjs/operators"; import {AccountService} from "../../_services/account.service"; import {ColumnMode, NgxDatatableModule} from "@siemens/ngx-datatable"; -import {AsyncPipe, TitleCasePipe} from "@angular/common"; +import {AsyncPipe} from "@angular/common"; @Component({ selector: 'app-manage-devices', @@ -33,8 +27,7 @@ import {AsyncPipe, TitleCasePipe} from "@angular/common"; styleUrls: ['./manage-devices.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [NgbCollapse, SentenceCasePipe, DevicePlatformPipe, TranslocoDirective, SettingItemComponent, - DefaultValuePipe, ScrobbleEventTypePipe, SortableHeader, UtcToLocalTimePipe, AsyncPipe, NgxDatatableModule, TitleCasePipe] + imports: [DevicePlatformPipe, TranslocoDirective, AsyncPipe, NgxDatatableModule] }) export class ManageDevicesComponent implements OnInit { diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 54c42a0351..afa17e68c1 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -24,7 +24,7 @@ "username": "{{common.username}}", "required": "{{validation.required-field}}", "email": "{{common.email}}", - "not-valid-email": "{{validation.valid-email}}", + "invalid-email-warning": "A non-valid email will block some functionalities of Kavita", "cancel": "{{common.cancel}}", "saving": "Saving…", "update": "Update", From a527f25de2ab9a01cf4e18ef1ed5e15c6cc07c10 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 9 Feb 2025 14:28:32 -0600 Subject: [PATCH 16/16] Ensure links to person detail page are encoded correctly --- .../_components/series-detail/series-detail.component.html | 2 +- .../_components/series-detail/series-detail.component.ts | 2 ++ .../src/app/shared/person-badge/person-badge.component.html | 4 ++-- UI/Web/src/app/shared/person-badge/person-badge.component.ts | 2 ++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html index 255c4ccdaa..5572e89d18 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html @@ -122,7 +122,7 @@

[allowToggle]="false" (toggle)="switchTabsToDetail()"> - {{item.name}} + {{item.name}} diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 8ad9a82d0a..d74dc5e94c 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -1173,4 +1173,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { } }, 10); } + + protected readonly encodeURIComponent = encodeURIComponent; } diff --git a/UI/Web/src/app/shared/person-badge/person-badge.component.html b/UI/Web/src/app/shared/person-badge/person-badge.component.html index 6f95ef85f1..4b1eebb025 100644 --- a/UI/Web/src/app/shared/person-badge/person-badge.component.html +++ b/UI/Web/src/app/shared/person-badge/person-badge.component.html @@ -1,11 +1,11 @@ @if (person !== undefined) { - +
@if (HasCoverImage) {