diff --git a/Kavita.API/Services/Plus/IExternalMetadataService.cs b/Kavita.API/Services/Plus/IExternalMetadataService.cs index 3ae60084e..65d5f7caf 100644 --- a/Kavita.API/Services/Plus/IExternalMetadataService.cs +++ b/Kavita.API/Services/Plus/IExternalMetadataService.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Kavita.Common; using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; using Kavita.Models.DTOs.KavitaPlus.ExternalMetadata.Covers; using Kavita.Models.DTOs.KavitaPlus.Metadata; using Kavita.Models.DTOs.Metadata.Matching; @@ -69,11 +70,9 @@ public interface IExternalMetadataService /// 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); + Task FixSeriesMatch(int seriesId, ExternalMetadataIdsDto ids, CancellationToken ct = default); /// /// Sets a series to Don't Match and removes all previously cached diff --git a/Kavita.API/Services/Plus/IScrobblingService.cs b/Kavita.API/Services/Plus/IScrobblingService.cs index b69ded6e0..1b9671910 100644 --- a/Kavita.API/Services/Plus/IScrobblingService.cs +++ b/Kavita.API/Services/Plus/IScrobblingService.cs @@ -131,7 +131,7 @@ public static class ScrobblingHelper { if (series.MalId != 0) return series.MalId; - return WeblinkParser.GetMalId(series.Metadata.WebLinks) ?? series.ExternalSeriesMetadata?.MalId; + return ExternalIdParser.GetMalId(series.Metadata.WebLinks) ?? series.ExternalSeriesMetadata?.MalId; } @@ -139,7 +139,7 @@ public static class ScrobblingHelper { if (seriesWithExternalMetadata.AniListId != 0) return seriesWithExternalMetadata.AniListId; - var aniListId = WeblinkParser.GetAniListId(seriesWithExternalMetadata.Metadata.WebLinks); + var aniListId = ExternalIdParser.GetAniListId(seriesWithExternalMetadata.Metadata.WebLinks); return aniListId ?? seriesWithExternalMetadata.ExternalSeriesMetadata?.AniListId; } diff --git a/Kavita.Common.Tests/Helpers/WeblinkParserTests.cs b/Kavita.Common.Tests/Helpers/ExternalIdParserTests.cs similarity index 76% rename from Kavita.Common.Tests/Helpers/WeblinkParserTests.cs rename to Kavita.Common.Tests/Helpers/ExternalIdParserTests.cs index 93b4d1479..0b5e3646b 100644 --- a/Kavita.Common.Tests/Helpers/WeblinkParserTests.cs +++ b/Kavita.Common.Tests/Helpers/ExternalIdParserTests.cs @@ -2,7 +2,7 @@ namespace Kavita.Common.Tests.Helpers; -public class WeblinkParserTests +public class ExternalIdParserTests { [Theory] [InlineData("https://anilist.co/manga/35851/Byeontaega-Doeja/", 35851)] @@ -10,27 +10,27 @@ public class WeblinkParserTests [InlineData("https://anilist.co/manga/30105/Kekkaishi/", 30105)] public void CanParseWeblink_AniList(string link, int? expectedId) { - Assert.Equal(WeblinkParser.GetAniListId(link), expectedId); + Assert.Equal(ExternalIdParser.GetAniListId(link), expectedId); } [Theory] [InlineData("https://mangadex.org/title/316d3d09-bb83-49da-9d90-11dc7ce40967/honzuki-no-gekokujou-shisho-ni-naru-tame-ni-wa-shudan-wo-erandeiraremasen-dai-3-bu-ryouchi-ni-hon-o", "316d3d09-bb83-49da-9d90-11dc7ce40967")] public void CanParseWeblink_MangaDex(string link, string expectedId) { - Assert.Equal(WeblinkParser.GetMangaDexId(link), expectedId); + Assert.Equal(ExternalIdParser.GetMangaDexId(link), expectedId); } [Theory] [InlineData("https://comicvine.gamespot.com/chew-1-taster-s-choice-part-1-of-5/4000-159233/", "159233")] public void CanParseWeblink_ComicVine(string link, string expectedId) { - Assert.Equal(WeblinkParser.GetComicVineId(link).Item1, expectedId); + Assert.Equal(ExternalIdParser.GetComicVineId(link).Item1, expectedId); } [Theory] [InlineData("https://mangabaka.org/3391", 3391)] public void CanParseWeblink_MangaBaka(string link, long expectedId) { - Assert.Equal(WeblinkParser.GetMangaBakaId(link), expectedId); + Assert.Equal(ExternalIdParser.GetMangaBakaId(link), expectedId); } } diff --git a/Kavita.Common/Helpers/WeblinkParser.cs b/Kavita.Common/Helpers/ExternalIdParser.cs similarity index 75% rename from Kavita.Common/Helpers/WeblinkParser.cs rename to Kavita.Common/Helpers/ExternalIdParser.cs index d8d1e9c1c..d75897da5 100644 --- a/Kavita.Common/Helpers/WeblinkParser.cs +++ b/Kavita.Common/Helpers/ExternalIdParser.cs @@ -1,12 +1,16 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Linq; namespace Kavita.Common.Helpers; #nullable enable -public static class WeblinkParser +/// +/// Handles all things parsing of External Ids (weblinks, not set checks, anilist:X) +/// +public static class ExternalIdParser { private const string AniListWeblinkWebsite = "https://anilist.co/manga/"; private const string MalWeblinkWebsite = "https://myanimelist.net/manga/"; @@ -90,6 +94,51 @@ public static class WeblinkParser return ExtractId(weblinks, MangaBakaWebsite) ?? 0; } + #region Header-based Parsing + public static bool TryParseAniListHeader(string? text, out int id) => + TryParseHeader(text, "ANILIST", out id); + + public static bool TryParseHardcoverHeader(string? text, out string id) => + TryParseHeader(text, "HARDCOVER", out id); + + public static bool TryParseMangaBakaHeader(string? text, out long id) => + TryParseHeader(text, "MANGABAKA", out id); + + public static bool TryParseMalHeader(string? text, out int id) => + TryParseHeader(text, "MAL", out id); + + public static int? ParseAniListHeader(string? text) => ParseHeader(text, "ANILIST"); + + public static string? ParseHardcoverHeader(string? text) => ParseHeader(text, "HARDCOVER"); + + public static long? ParseMangaBakaHeader(string? text) => ParseHeader(text, "MANGABAKA"); + + public static int? ParseMalHeader(string? text) => ParseHeader(text, "MAL"); + + private static T? ParseHeader(string? text, string header) + where T : IParsable + { + if (string.IsNullOrWhiteSpace(text)) return default; + if (!text.StartsWith(header + ":", StringComparison.InvariantCultureIgnoreCase)) return default; + var valuePart = text.Split(':', 2)[1]; + + return T.TryParse(valuePart, CultureInfo.InvariantCulture, out var result) ? result : default; + } + + private static bool TryParseHeader(string? text, string header, out T id) + where T : IParsable + { + var result = ParseHeader(text, header); + if (result is not null) + { + id = result; + return true; + } + id = default!; + return false; + } + + #endregion /// diff --git a/Kavita.Database/Repositories/SeriesRepository.cs b/Kavita.Database/Repositories/SeriesRepository.cs index 366c34b42..d8e65d99c 100644 --- a/Kavita.Database/Repositories/SeriesRepository.cs +++ b/Kavita.Database/Repositories/SeriesRepository.cs @@ -614,16 +614,16 @@ public class SeriesRepository(DataContext context, IMapper mapper) : ISeriesRepo // TODO: Refactor this to check from ExternalMetadataIds first then ExternalSeriesMetadata. Weblink parsing is pointless as it updates in externalMetadata AniListId = series.ExternalSeriesMetadata.AniListId != 0 ? series.ExternalSeriesMetadata.AniListId - : WeblinkParser.GetAniListId(series.Metadata.WebLinks), + : ExternalIdParser.GetAniListId(series.Metadata.WebLinks), MalId = series.ExternalSeriesMetadata.MalId != 0 ? series.ExternalSeriesMetadata.MalId - : WeblinkParser.GetMalId(series.Metadata.WebLinks), + : ExternalIdParser.GetMalId(series.Metadata.WebLinks), CbrId = series.ExternalSeriesMetadata.CbrId, GoogleBooksId = !string.IsNullOrEmpty(series.ExternalSeriesMetadata.GoogleBooksId) ? series.ExternalSeriesMetadata.GoogleBooksId - : WeblinkParser.GetGoogleBooksId(series.Metadata.WebLinks), + : ExternalIdParser.GetGoogleBooksId(series.Metadata.WebLinks), MangabakaId = (int?) series.MangaBakaId, - MangaDexId = WeblinkParser.GetMangaDexId(series.Metadata.WebLinks), + MangaDexId = ExternalIdParser.GetMangaDexId(series.Metadata.WebLinks), VolumeCount = series.Volumes.Count, ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial), Year = series.Metadata.ReleaseYear diff --git a/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs index 19f8ea62f..b16e3165d 100644 --- a/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs @@ -15,7 +15,7 @@ public sealed record MatchSeriesRequestDto public string? Query { get; set; } public int? AniListId { get; set; } public long? MalId { get; set; } - public string? HardcoverId { get; set; } + public string? HardcoverSlug { get; set; } public int? MangabakaId { get; set; } public int? CbrId { get; set; } public PlusMediaFormat Format { get; set; } diff --git a/Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs b/Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs index 17d74ed96..cced26c90 100644 --- a/Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs @@ -15,7 +15,11 @@ public sealed record ExternalSeriesDetailDto public string Name { get; set; } public int? AniListId { get; set; } public long? MALId { get; set; } + /// + /// ComicBookRoundup Id for direct matching + /// public int? CbrId { get; set; } + public int? HardcoverId { get; set; } public int? MangabakaId { get; set; } public IList Synonyms { get; set; } = []; public PlusMediaFormat PlusMediaFormat { get; set; } diff --git a/Kavita.Models/DTOs/Scrobbling/PlusSeriesDto.cs b/Kavita.Models/DTOs/Scrobbling/PlusSeriesDto.cs index 9ee0bf945..f3536c198 100644 --- a/Kavita.Models/DTOs/Scrobbling/PlusSeriesDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/PlusSeriesDto.cs @@ -11,6 +11,7 @@ public class PlusSeriesRequestDto public string? GoogleBooksId { get; set; } public string? MangaDexId { get; set; } public int? MangabakaId { get; set; } + public int? HardcoverId { get; set; } /// /// ComicBookRoundup Id /// diff --git a/Kavita.Server/Controllers/SeriesController.cs b/Kavita.Server/Controllers/SeriesController.cs index 51aabfb63..1342f2bfb 100644 --- a/Kavita.Server/Controllers/SeriesController.cs +++ b/Kavita.Server/Controllers/SeriesController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using EasyCaching.Core; using Hangfire; @@ -17,6 +18,7 @@ using Kavita.Models.DTOs.Filtering.v2; using Kavita.Models.DTOs.Filtering.v2.Requests; using Kavita.Models.DTOs.Metadata.Matching; using Kavita.Models.DTOs.Recommendation; +using Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; using Kavita.Models.DTOs.SeriesDetail; using Kavita.Models.Entities.Enums; using Kavita.Models.Entities.MetadataMatching; @@ -583,18 +585,15 @@ public class SeriesController( /// /// This will perform the fix match /// - /// /// - /// - /// - /// + /// /// [KPlus] [HttpPost("update-match")] [Authorize(Policy = PolicyGroups.AdminPolicy)] - public ActionResult UpdateSeriesMatch([FromQuery] int seriesId, [FromQuery] int? aniListId, [FromQuery] long? malId, [FromQuery] int? cbrId) + public ActionResult UpdateSeriesMatch([FromQuery] int seriesId, [FromBody] ExternalMetadataIdsDto ids) { - BackgroundJob.Enqueue(() => externalMetadataService.FixSeriesMatch(seriesId, aniListId, malId, cbrId)); + BackgroundJob.Enqueue(() => externalMetadataService.FixSeriesMatch(seriesId, ids, CancellationToken.None)); return Ok(); } diff --git a/Kavita.Services/Plus/ExternalMetadataService.cs b/Kavita.Services/Plus/ExternalMetadataService.cs index 79deaf745..91841da97 100644 --- a/Kavita.Services/Plus/ExternalMetadataService.cs +++ b/Kavita.Services/Plus/ExternalMetadataService.cs @@ -182,14 +182,31 @@ public class ExternalMetadataService : IExternalMetadataService public async Task> MatchSeries(MatchSeriesDto dto, CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata | SeriesIncludes.Library, ct); if (series == null) return []; - var potentialAnilistId = WeblinkParser.GetAniListId(dto.Query); - var potentialMalId = WeblinkParser.GetMalId(dto.Query); - var potentialMangabakaId = WeblinkParser.GetMangaBakaId(dto.Query); + var query = dto.Query; + + var potentialAnilistId = ExternalIdParser.TryParseAniListHeader(query, out var aniListId) + ? aniListId : ExternalIdParser.GetAniListId(query); + + var potentialMalId = ExternalIdParser.TryParseMalHeader(query, out var malId) + ? malId : ExternalIdParser.GetMalId(query); + + var potentialMangabakaId = ExternalIdParser.TryParseMangaBakaHeader(query, out var mangabakaId) + ? mangabakaId : ExternalIdParser.GetMangaBakaId(query); + + var potentialHardcoverSlug = ExternalIdParser.TryParseHardcoverHeader(query, out var hardcoverId) + ? hardcoverId : null; + + // If any ID was extracted (header syntax or URL), the raw query string is meaningless to the backend + var wasHeaderQuery = potentialAnilistId.HasValue + || potentialMalId.HasValue + || potentialMangabakaId > 0 + || !string.IsNullOrEmpty(potentialHardcoverSlug); + + query = wasHeaderQuery ? null : dto.Query; var format = series.Library.Type.ConvertToPlusMediaFormat(series.Format); var otherNames = ExtractAlternativeNames(series); @@ -208,13 +225,14 @@ public class ExternalMetadataService : IExternalMetadataService var matchRequest = new MatchSeriesRequestDto() { Format = format, - Query = dto.Query, + Query = query, SeriesName = series.Name, AlternativeNames = otherNames, Year = year, - AniListId = potentialAnilistId ?? ScrobblingHelper.GetAniListId(series), + AniListId = potentialAnilistId ?? ScrobblingHelper.GetAniListId(series), // TODO: Opportunity to streamline this with ExternalIdParser and the default > 0/empty string checks MalId = potentialMalId ?? ScrobblingHelper.GetMalId(series), - MangabakaId = potentialMangabakaId > 0 ? (int) potentialMangabakaId : (int?) series.MangaBakaId + MangabakaId = potentialMangabakaId > 0 ? (int) potentialMangabakaId : (int?) series.MangaBakaId, + HardcoverSlug = potentialHardcoverSlug }; try @@ -289,7 +307,7 @@ public class ExternalMetadataService : IExternalMetadataService } } - public async Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId, CancellationToken ct = default) + public async Task FixSeriesMatch(int seriesId, ExternalMetadataIdsDto ids, CancellationToken ct = default) { var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library, ct); if (series == null) return; @@ -306,17 +324,19 @@ public class ExternalMetadataService : IExternalMetadataService var metadata = await FetchExternalMetadataForSeries(seriesId, series.Library.Type, new PlusSeriesRequestDto() { - AniListId = aniListId, - MalId = malId, - CbrId = cbrId, + AniListId = ids.AniListId, + MalId = ids.MalId, + CbrId = ids.CbrId, + MangabakaId = ids.MangabakaId, + HardcoverId = ids.HardcoverId, MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format), - SeriesName = series.Name // Required field, not used since AniList/Mal Id are passed + SeriesName = series.Name // Required field, not used since provider Ids are passed }, true, ct); if (metadata.Series == null) { - _logger.LogError("Unable to Match {SeriesName} with Kavita+ Series with Id: {AniListId}/{MalId}/{CbrId}", - series.Name, aniListId, malId, cbrId); + _logger.LogError("Unable to Match {SeriesName} with Kavita+ Series with Ids: {AniListId}/{MalId}/{CbrId}/{MangabakaId}/{HardcoverId}", + series.Name, ids.AniListId, ids.MalId, ids.CbrId, ids.MangabakaId, ids.HardcoverId); return; } @@ -374,6 +394,9 @@ public class ExternalMetadataService : IExternalMetadataService await _unitOfWork.CommitAsync(ct); + // Send a series Update to ensure pages get the new information + await _eventHub.SendMessageAsync(MessageFactory.SeriesUpdated, MessageFactory.SeriesUpdatedEvent(series.Id), ct: ct); + await _auditService.LogMatchAsync(KavitaPlusEventType.SeriesDontMatchSet, seriesId, new AuditLogMatchDontMatchParamsDto { SeriesName = series.Name, DontMatch = dontMatch }, ct: ct); } @@ -905,7 +928,7 @@ public class ExternalMetadataService : IExternalMetadataService .Select(w => new PersonDto() { Name = w.Name.Trim(), - AniListId = WeblinkParser.GetAniListCharacterId(w.Url), + AniListId = ExternalIdParser.GetAniListCharacterId(w.Url), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) .Concat(series.Metadata.People @@ -947,7 +970,7 @@ public class ExternalMetadataService : IExternalMetadataService foreach (var character in externalCharacters) { - var aniListId = WeblinkParser.GetAniListCharacterId(character.Url); + var aniListId = ExternalIdParser.GetAniListCharacterId(character.Url); if (aniListId <= 0) continue; var person = await _unitOfWork.PersonRepository.GetPersonByAniListId(aniListId); if (person != null && !string.IsNullOrEmpty(character.ImageUrl) && string.IsNullOrEmpty(person.CoverImage)) @@ -986,7 +1009,7 @@ public class ExternalMetadataService : IExternalMetadataService .Select(w => new PersonDto() { Name = w.Name.Trim(), - AniListId = WeblinkParser.GetAniListStaffId(w.Url), + AniListId = ExternalIdParser.GetAniListStaffId(w.Url), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) .Concat(series.Metadata.People @@ -1043,7 +1066,7 @@ public class ExternalMetadataService : IExternalMetadataService .Select(w => new PersonDto() { Name = w.Name.Trim(), - AniListId = WeblinkParser.GetAniListStaffId(w.Url), + AniListId = ExternalIdParser.GetAniListStaffId(w.Url), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) .Concat(series.Metadata.People @@ -1734,7 +1757,7 @@ public class ExternalMetadataService : IExternalMetadataService { foreach (var staff in people) { - var aniListId = WeblinkParser.GetAniListStaffId(staff.Url); + var aniListId = ExternalIdParser.GetAniListStaffId(staff.Url); if (aniListId <= 0) continue; var person = await _unitOfWork.PersonRepository.GetPersonByAniListId(aniListId); if (person == null || string.IsNullOrEmpty(staff.ImageUrl) || @@ -2061,11 +2084,11 @@ public class ExternalMetadataService : IExternalMetadataService { if (payload.AniListId <= 0) { - payload.AniListId = WeblinkParser.GetAniListId(series.Metadata.WebLinks); + payload.AniListId = ExternalIdParser.GetAniListId(series.Metadata.WebLinks); } if (payload.MalId <= 0) { - payload.MalId = WeblinkParser.GetMalId(series.Metadata.WebLinks); + payload.MalId = ExternalIdParser.GetMalId(series.Metadata.WebLinks); } payload.SeriesName = series.Name; payload.LocalizedSeriesName = series.LocalizedName; diff --git a/Kavita.Services/Scanner/DefaultParser.cs b/Kavita.Services/Scanner/DefaultParser.cs index b4a0fd19e..32c78501c 100644 --- a/Kavita.Services/Scanner/DefaultParser.cs +++ b/Kavita.Services/Scanner/DefaultParser.cs @@ -158,12 +158,12 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau var notes = info.ComicInfo?.Notes; var weblinks = info.ComicInfo?.Web; - info.AniListId = WeblinkParser.GetAniListId(weblinks); - info.MalId = WeblinkParser.GetMalId(weblinks); - info.MangaBakaId = WeblinkParser.GetMangaBakaId(weblinks); + info.AniListId = ExternalIdParser.GetAniListId(weblinks); + info.MalId = ExternalIdParser.GetMalId(weblinks); + info.MangaBakaId = ExternalIdParser.GetMangaBakaId(weblinks); var comicvineId = Parser.ParseComicVineIdFromComicInfoNote(notes); - var parsedCvWeblink = WeblinkParser.GetComicVineId(weblinks); + var parsedCvWeblink = ExternalIdParser.GetComicVineId(weblinks); info.ComicVineId = comicvineId; // If we have a seriesId, set it. Otherwise, we set the issue id diff --git a/Kavita.Services/Scanner/ProcessSeries.cs b/Kavita.Services/Scanner/ProcessSeries.cs index 296036551..a0d5b890a 100644 --- a/Kavita.Services/Scanner/ProcessSeries.cs +++ b/Kavita.Services/Scanner/ProcessSeries.cs @@ -360,10 +360,10 @@ public class ProcessSeries( { // TODO: Come back and clean this up, we call this code in DefaultParser AND ProcessSeries series.Metadata.WebLinks = firstChapter.WebLinks; - series.AniListId = WeblinkParser.GetAniListId(series.Metadata.WebLinks) ?? 0; - series.MalId = WeblinkParser.GetMalId(series.Metadata.WebLinks) ?? 0; - series.ComicVineId = WeblinkParser.GetComicVineId(series.Metadata.WebLinks).Item1; - series.MangaBakaId = WeblinkParser.GetMangaBakaId(series.Metadata.WebLinks); + series.AniListId = ExternalIdParser.GetAniListId(series.Metadata.WebLinks) ?? 0; + series.MalId = ExternalIdParser.GetMalId(series.Metadata.WebLinks) ?? 0; + series.ComicVineId = ExternalIdParser.GetComicVineId(series.Metadata.WebLinks).Item1; + series.MangaBakaId = ExternalIdParser.GetMangaBakaId(series.Metadata.WebLinks); } if (!string.IsNullOrEmpty(firstChapter?.SeriesGroup) && library.ManageCollections) diff --git a/UI/Web/src/app/_models/series-detail/external-series-detail.ts b/UI/Web/src/app/_models/series-detail/external-series-detail.ts index db25782ca..860d18815 100644 --- a/UI/Web/src/app/_models/series-detail/external-series-detail.ts +++ b/UI/Web/src/app/_models/series-detail/external-series-detail.ts @@ -29,6 +29,8 @@ export interface ExternalSeriesDetail { name: string; aniListId?: number | null; malId?: number | null; + mangaBakaId?: number | null; + hardcoverId?: number | null; cbrId?: number | null; synonyms: Array; plusMediaFormat: PlusMediaFormat; @@ -43,6 +45,9 @@ export interface ExternalSeriesDetail { */ volumes?: number; chapters?: number; + startDate?: string | null; + endDate?: string | null; + averageScore?: number | null; staff: Array; tags: Array; provider: ScrobbleProvider; diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index c10787c04..ba47c84c5 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -233,7 +233,7 @@ export class ActionService { ref.setInput('series', series); return from(ref.closed).pipe( filter((saved: boolean) => saved), - map(() => this.fromAction(action, series, 'none')) + map(() => this.fromAction(action, series, 'reload')) ); } diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 1be97b990..98a7bc8a4 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -250,7 +250,14 @@ export class SeriesService { } updateMatch(seriesId: number, series: ExternalSeriesDetail) { - return this.httpClient.post(this.baseUrl + `series/update-match?seriesId=${seriesId}&aniListId=${series.aniListId || 0}&malId=${series.malId || 0}&cbrId=${series.cbrId || 0}`, {}, TextResonse); + const ids = { + aniListId: series.aniListId ?? null, + malId: series.malId ?? null, + cbrId: series.cbrId ?? null, + mangabakaId: series.mangaBakaId ?? null, + hardcoverId: series.hardcoverId ?? null, + }; + return this.httpClient.post(this.baseUrl + `series/update-match?seriesId=${seriesId}`, ids, TextResonse); } updateDontMatch(seriesId: number, dontMatch: boolean) { diff --git a/UI/Web/src/app/_single-module/kavitaplus-timeline/kavitaplus-timeline.component.html b/UI/Web/src/app/_single-module/kavitaplus-timeline/kavitaplus-timeline.component.html index 41eda076f..636b04a74 100644 --- a/UI/Web/src/app/_single-module/kavitaplus-timeline/kavitaplus-timeline.component.html +++ b/UI/Web/src/app/_single-module/kavitaplus-timeline/kavitaplus-timeline.component.html @@ -6,7 +6,10 @@ } @else if (entries().length === 0) { -
{{t('no-events')}}
+ + + } @else { @for (group of groupedEntries(); track group.key; let isFirst = $first) {
diff --git a/UI/Web/src/app/_single-module/kavitaplus-timeline/kavitaplus-timeline.component.ts b/UI/Web/src/app/_single-module/kavitaplus-timeline/kavitaplus-timeline.component.ts index 49df9962b..f20ff6aae 100644 --- a/UI/Web/src/app/_single-module/kavitaplus-timeline/kavitaplus-timeline.component.ts +++ b/UI/Web/src/app/_single-module/kavitaplus-timeline/kavitaplus-timeline.component.ts @@ -19,6 +19,7 @@ import {AuditLogErrorPipe} from "../../_pipes/audit-log-error.pipe"; import { KavitaPlusAuditEventTypeIconComponent } from "../../shared/_components/kavitaplus-event-type-icon/kavita-plus-audit-event-type-icon.component"; +import {EmptyStateComponent} from "../../shared/_components/empty-state/empty-state.component"; interface DayGroup { key: string; @@ -69,6 +70,7 @@ function groupByDay(entries: KavitaPlusAuditEntry[]): DayGroup[] { AuditLogErrorPipe, KavitaPlusAuditEventTypeIconComponent, NgTemplateOutlet, + EmptyStateComponent, ], }) export class KavitaplusTimelineComponent { diff --git a/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.html b/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.html index 7cc9992bb..75d0a8d63 100644 --- a/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.html +++ b/UI/Web/src/app/_single-module/match-series-modal/match-series-modal.component.html @@ -1,68 +1,141 @@ - -
- -