Basic wiring with Kavita+ for CBR implemented. Metadata writing needs to be hooked up.

This commit is contained in:
Joseph Milazzo 2025-03-16 13:42:46 -05:00
parent 75419fb62b
commit eba5e6875f
9 changed files with 52 additions and 33 deletions

View file

@ -648,13 +648,13 @@ public class SeriesController : BaseApiController
/// <summary> /// <summary>
/// This will perform the fix match /// This will perform the fix match
/// </summary> /// </summary>
/// <param name="aniListId"></param> /// <param name="match"></param>
/// <param name="seriesId"></param> /// <param name="seriesId"></param>
/// <returns></returns> /// <returns></returns>
[HttpPost("update-match")] [HttpPost("update-match")]
public ActionResult UpdateSeriesMatch([FromQuery] int seriesId, [FromQuery] int aniListId, [FromQuery] long? malId) public ActionResult UpdateSeriesMatch([FromQuery] int seriesId, [FromQuery] int? aniListId, [FromQuery] long? malId, [FromQuery] int? cbrId)
{ {
BackgroundJob.Enqueue(() => _externalMetadataService.FixSeriesMatch(seriesId, aniListId, malId)); BackgroundJob.Enqueue(() => _externalMetadataService.FixSeriesMatch(seriesId, aniListId, malId, cbrId));
return Ok(); return Ok();
} }

View file

@ -13,4 +13,5 @@ internal class SeriesDetailPlusApiDto
public ExternalSeriesDetailDto? Series { get; set; } public ExternalSeriesDetailDto? Series { get; set; }
public int? AniListId { get; set; } public int? AniListId { get; set; }
public long? MalId { get; set; } public long? MalId { get; set; }
public int? CbrId { get; set; }
} }

View file

@ -15,6 +15,7 @@ public class ExternalSeriesDetailDto
public string Name { get; set; } public string Name { get; set; }
public int? AniListId { get; set; } public int? AniListId { get; set; }
public long? MALId { get; set; } public long? MALId { get; set; }
public int? CbrId { get; set; }
public IList<string> Synonyms { get; set; } = []; public IList<string> Synonyms { get; set; } = [];
public PlusMediaFormat PlusMediaFormat { get; set; } public PlusMediaFormat PlusMediaFormat { get; set; }
public string? SiteUrl { get; set; } public string? SiteUrl { get; set; }

View file

@ -9,6 +9,10 @@ public record PlusSeriesRequestDto
public long? MalId { get; set; } public long? MalId { get; set; }
public string? GoogleBooksId { get; set; } public string? GoogleBooksId { get; set; }
public string? MangaDexId { get; set; } public string? MangaDexId { get; set; }
/// <summary>
/// ComicBookRoundup Id
/// </summary>
public int? CbrId { get; set; }
public string SeriesName { get; set; } public string SeriesName { get; set; }
public string? AltSeriesName { get; set; } public string? AltSeriesName { get; set; }
public PlusMediaFormat MediaFormat { get; set; } public PlusMediaFormat MediaFormat { get; set; }

View file

@ -49,7 +49,7 @@ public interface IExternalMetadataService
Task<IList<MalStackDto>> GetStacksForUser(int userId); Task<IList<MalStackDto>> GetStacksForUser(int userId);
Task<IList<ExternalSeriesMatchDto>> MatchSeries(MatchSeriesDto dto); Task<IList<ExternalSeriesMatchDto>> MatchSeries(MatchSeriesDto dto);
Task FixSeriesMatch(int seriesId, int anilistId, long? malId); Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId);
Task UpdateSeriesDontMatch(int seriesId, bool dontMatch); Task UpdateSeriesDontMatch(int seriesId, bool dontMatch);
Task<bool> WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId); Task<bool> WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId);
} }
@ -196,7 +196,7 @@ public class ExternalMetadataService : IExternalMetadataService
{ {
var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId,
SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata); SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata | SeriesIncludes.Library);
if (series == null) return []; if (series == null) return [];
var potentialAnilistId = ScrobblingService.ExtractId<int?>(dto.Query, ScrobblingService.AniListWeblinkWebsite); var potentialAnilistId = ScrobblingService.ExtractId<int?>(dto.Query, ScrobblingService.AniListWeblinkWebsite);
@ -210,7 +210,7 @@ public class ExternalMetadataService : IExternalMetadataService
var matchRequest = new MatchSeriesRequestDto() var matchRequest = new MatchSeriesRequestDto()
{ {
Format = series.Format == MangaFormat.Epub ? PlusMediaFormat.LightNovel : PlusMediaFormat.Manga, Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format),
Query = dto.Query, Query = dto.Query,
SeriesName = series.Name, SeriesName = series.Name,
AlternativeNames = altNames.Where(s => !string.IsNullOrEmpty(s)).ToList(), AlternativeNames = altNames.Where(s => !string.IsNullOrEmpty(s)).ToList(),
@ -312,8 +312,10 @@ public class ExternalMetadataService : IExternalMetadataService
/// This will override any sort of matching that was done prior and force it to be what the user Selected /// This will override any sort of matching that was done prior and force it to be what the user Selected
/// </summary> /// </summary>
/// <param name="seriesId"></param> /// <param name="seriesId"></param>
/// <param name="anilistId"></param> /// <param name="aniListId"></param>
public async Task FixSeriesMatch(int seriesId, int anilistId, long? malId) /// <param name="malId"></param>
/// <param name="cbrId"></param>
public async Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId)
{ {
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library);
if (series == null) return; if (series == null) return;
@ -329,15 +331,17 @@ public class ExternalMetadataService : IExternalMetadataService
var metadata = await FetchExternalMetadataForSeries(seriesId, series.Library.Type, var metadata = await FetchExternalMetadataForSeries(seriesId, series.Library.Type,
new PlusSeriesRequestDto() new PlusSeriesRequestDto()
{ {
AniListId = anilistId, AniListId = aniListId,
MalId = malId, MalId = malId,
CbrId = cbrId,
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 AniList/Mal Id are passed
}); });
if (metadata.Series == null) if (metadata.Series == null)
{ {
_logger.LogError("Unable to Match {SeriesName} with Kavita+ Series AniList Id: {AniListId}", _logger.LogError("Unable to Match {SeriesName} with Kavita+ Series with Id: {AniListId}/{MalId}/{CbrId}",
series.Name, anilistId); series.Name, aniListId, malId, cbrId);
return; return;
} }
@ -421,8 +425,7 @@ public class ExternalMetadataService : IExternalMetadataService
result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail")
.WithKavitaPlusHeaders(license, token) .WithKavitaPlusHeaders(license, token)
.PostJsonAsync(data) .PostJsonAsync(data)
.ReceiveJson< .ReceiveJson<SeriesDetailPlusApiDto>(); // This returns an AniListSeries and Match returns ExternalSeriesDto
SeriesDetailPlusApiDto>(); // This returns an AniListSeries and Match returns ExternalSeriesDto
} }
catch (FlurlHttpException ex) catch (FlurlHttpException ex)
{ {
@ -438,8 +441,7 @@ public class ExternalMetadataService : IExternalMetadataService
result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail")
.WithKavitaPlusHeaders(license, token) .WithKavitaPlusHeaders(license, token)
.PostJsonAsync(data) .PostJsonAsync(data)
.ReceiveJson< .ReceiveJson<SeriesDetailPlusApiDto>();
SeriesDetailPlusApiDto>();
} }
} }

View file

@ -27,8 +27,9 @@ export interface MetadataTagDto {
export interface ExternalSeriesDetail { export interface ExternalSeriesDetail {
name: string; name: string;
aniListId?: number; aniListId?: number | null;
malId?: number; malId?: number | null;
cbrId?: number | null;
synonyms: Array<string>; synonyms: Array<string>;
plusMediaFormat: PlusMediaFormat; plusMediaFormat: PlusMediaFormat;
siteUrl?: string; siteUrl?: string;

View file

@ -242,7 +242,7 @@ export class SeriesService {
} }
updateMatch(seriesId: number, series: ExternalSeriesDetail) { updateMatch(seriesId: number, series: ExternalSeriesDetail) {
return this.httpClient.post<string>(this.baseUrl + `series/update-match?seriesId=${seriesId}&aniListId=${series.aniListId}${series.malId ? '&malId=' + series.malId : ''}`, {}, TextResonse); return this.httpClient.post<string>(this.baseUrl + `series/update-match?seriesId=${seriesId}&aniListId=${series.aniListId || 0}&malId=${series.malId || 0}&cbrId=${series.cbrId || 0}`, {}, TextResonse);
} }
updateDontMatch(seriesId: number, dontMatch: boolean) { updateDontMatch(seriesId: number, dontMatch: boolean) {

View file

@ -92,7 +92,7 @@ export class MatchSeriesModalComponent implements OnInit {
data.tags = data.tags || []; data.tags = data.tags || [];
data.genres = data.genres || []; data.genres = data.genres || [];
this.seriesService.updateMatch(this.series.id, data).subscribe(_ => { this.seriesService.updateMatch(this.series.id, item.series).subscribe(_ => {
this.save(); this.save();
}); });
} }

View file

@ -1,12 +1,4 @@
import { import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit} from '@angular/core';
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Input,
OnInit
} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import { import {
NgbActiveModal, NgbActiveModal,
@ -133,6 +125,11 @@ export class LibrarySettingsModalComponent implements OnInit {
return libType === LibraryType.Manga || libType === LibraryType.LightNovel; return libType === LibraryType.Manga || libType === LibraryType.LightNovel;
} }
get IsMetadataDownloadEligible() {
const libType = parseInt(this.libraryForm.get('type')?.value + '', 10) as LibraryType;
return libType === LibraryType.Manga || libType === LibraryType.LightNovel || libType === LibraryType.ComicVine;
}
ngOnInit(): void { ngOnInit(): void {
this.settingService.getLibraryTypes().subscribe((types) => { this.settingService.getLibraryTypes().subscribe((types) => {
this.libraryTypes = types; this.libraryTypes = types;
@ -151,11 +148,19 @@ export class LibrarySettingsModalComponent implements OnInit {
if (this.library && !(this.library.type === LibraryType.Manga || this.library.type === LibraryType.LightNovel) ) { if (this.library && !(this.library.type === LibraryType.Manga || this.library.type === LibraryType.LightNovel) ) {
this.libraryForm.get('allowScrobbling')?.setValue(false); this.libraryForm.get('allowScrobbling')?.setValue(false);
this.libraryForm.get('allowMetadataMatching')?.setValue(false);
this.libraryForm.get('allowScrobbling')?.disable(); this.libraryForm.get('allowScrobbling')?.disable();
this.libraryForm.get('allowMetadataMatching')?.disable();
if (this.IsMetadataDownloadEligible) {
this.libraryForm.get('allowMetadataMatching')?.setValue(this.library.allowMetadataMatching);
this.libraryForm.get('allowMetadataMatching')?.enable();
} else {
this.libraryForm.get('allowMetadataMatching')?.setValue(false);
this.libraryForm.get('allowMetadataMatching')?.disable();
}
} }
this.libraryForm.get('name')?.valueChanges.pipe( this.libraryForm.get('name')?.valueChanges.pipe(
debounceTime(100), debounceTime(100),
distinctUntilChanged(), distinctUntilChanged(),
@ -218,11 +223,16 @@ export class LibrarySettingsModalComponent implements OnInit {
if (!this.IsKavitaPlusEligible) { if (!this.IsKavitaPlusEligible) {
this.libraryForm.get('allowScrobbling')?.disable(); this.libraryForm.get('allowScrobbling')?.disable();
this.libraryForm.get('allowMetadataMatching')?.disable();
} else { } else {
this.libraryForm.get('allowScrobbling')?.enable(); this.libraryForm.get('allowScrobbling')?.enable();
this.libraryForm.get('allowMetadataMatching')?.enable();
} }
if (this.IsMetadataDownloadEligible) {
this.libraryForm.get('allowMetadataMatching')?.enable();
} else {
this.libraryForm.get('allowMetadataMatching')?.disable();
}
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}), }),
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
@ -241,7 +251,7 @@ export class LibrarySettingsModalComponent implements OnInit {
this.libraryForm.get('manageReadingLists')?.setValue(this.library.manageReadingLists); this.libraryForm.get('manageReadingLists')?.setValue(this.library.manageReadingLists);
this.libraryForm.get('collapseSeriesRelationships')?.setValue(this.library.collapseSeriesRelationships); this.libraryForm.get('collapseSeriesRelationships')?.setValue(this.library.collapseSeriesRelationships);
this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible ? this.library.allowScrobbling : false); this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible ? this.library.allowScrobbling : false);
this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsKavitaPlusEligible ? this.library.allowMetadataMatching : false); this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsMetadataDownloadEligible ? this.library.allowMetadataMatching : false);
this.selectedFolders = this.library.folders; this.selectedFolders = this.library.folders;
this.madeChanges = false; this.madeChanges = false;