diff --git a/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs b/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs index d2e8247cb..1dd26a7bc 100644 --- a/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs +++ b/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs @@ -43,6 +43,29 @@ public class MetadataSettingsDto /// public bool EnableCoverImage { get; set; } + #region Chapter Metadata + /// + /// Allow Summary to be set within Chapter/Issue + /// + public bool EnableChapterSummary { get; set; } + /// + /// Allow Release Date to be set within Chapter/Issue + /// + public bool EnableChapterReleaseDate { get; set; } + /// + /// Allow Title to be set within Chapter/Issue + /// + public bool EnableChapterTitle { get; set; } + /// + /// Allow Publisher to be set within Chapter/Issue + /// + public bool EnableChapterPublisher { get; set; } + /// + /// Allow setting the cover image for the Chapter/Issue + /// + public bool EnableChapterCoverImage { get; set; } + #endregion + // Need to handle the Genre/tags stuff public bool EnableGenres { get; set; } = true; public bool EnableTags { get; set; } = true; diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 2c385a852..74bfbb296 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -312,6 +312,11 @@ public static class Seed EnableLocalizedName = false, FirstLastPeopleNaming = true, EnableCoverImage = true, + EnableChapterTitle = false, + EnableChapterSummary = true, + EnableChapterPublisher = true, + EnableChapterCoverImage = false, + EnableChapterReleaseDate = true, PersonRoles = [PersonRole.Writer, PersonRole.CoverArtist, PersonRole.Character] }; await context.MetadataSettings.AddAsync(existing); diff --git a/API/Entities/MetadataMatching/MetadataSettingField.cs b/API/Entities/MetadataMatching/MetadataSettingField.cs index 89ca5ee3e..9333c269e 100644 --- a/API/Entities/MetadataMatching/MetadataSettingField.cs +++ b/API/Entities/MetadataMatching/MetadataSettingField.cs @@ -5,6 +5,7 @@ /// public enum MetadataSettingField { + #region Series Metadata Summary = 1, PublicationStatus = 2, StartDate = 3, @@ -13,5 +14,18 @@ public enum MetadataSettingField LocalizedName = 6, Covers = 7, AgeRating = 8, - People = 9 + People = 9, + #endregion + + #region Chapter Metadata + + ChapterTitle = 10, + ChapterSummary = 11, + ChapterReleaseDate = 12, + ChapterPublisher = 13, + ChapterCovers = 14, + + #endregion + + } diff --git a/API/Entities/MetadataMatching/MetadataSettings.cs b/API/Entities/MetadataMatching/MetadataSettings.cs index bdf7f979f..aeb44b619 100644 --- a/API/Entities/MetadataMatching/MetadataSettings.cs +++ b/API/Entities/MetadataMatching/MetadataSettings.cs @@ -14,6 +14,8 @@ public class MetadataSettings /// public bool Enabled { get; set; } + #region Series Metadata + /// /// Allow the Summary to be written /// @@ -42,6 +44,30 @@ public class MetadataSettings /// Allow setting the cover image /// public bool EnableCoverImage { get; set; } + #endregion + + #region Chapter Metadata + /// + /// Allow Summary to be set within Chapter/Issue + /// + public bool EnableChapterSummary { get; set; } + /// + /// Allow Release Date to be set within Chapter/Issue + /// + public bool EnableChapterReleaseDate { get; set; } + /// + /// Allow Title to be set within Chapter/Issue + /// + public bool EnableChapterTitle { get; set; } + /// + /// Allow Publisher to be set within Chapter/Issue + /// + public bool EnableChapterPublisher { get; set; } + /// + /// Allow setting the cover image for the Chapter/Issue + /// + public bool EnableChapterCoverImage { get; set; } + #endregion // Need to handle the Genre/tags stuff public bool EnableGenres { get; set; } = true; diff --git a/API/Entities/Person/ChapterPeople.cs b/API/Entities/Person/ChapterPeople.cs index 15da3994d..c6a08a7dd 100644 --- a/API/Entities/Person/ChapterPeople.cs +++ b/API/Entities/Person/ChapterPeople.cs @@ -10,5 +10,14 @@ public class ChapterPeople public int PersonId { get; set; } public virtual Person Person { get; set; } + /// + /// The source of this connection. If not Kavita, this implies Metadata Download linked this and it can be removed between matches + /// + public bool KavitaPlusConnection { get; set; } + /// + /// A weight that allows lower numbers to sort first + /// + public int OrderWeight { get; set; } + public required PersonRole Role { get; set; } } diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 00d83173c..211215e87 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -1075,51 +1075,171 @@ public class ExternalMetadataService : IExternalMetadataService _logger.LogDebug("Updating {ChapterNumber} with metadata", chapter.Range); // Write the metadata - if (!string.IsNullOrEmpty(potentialMatch.Title) && !potentialMatch.Title.Contains(series.Name)) - { - chapter.Title = potentialMatch.Title; - _unitOfWork.ChapterRepository.Update(chapter); - madeModification = true; - } + madeModification = UpdateChapterTitle(chapter, settings, potentialMatch.Title, series.Name) || madeModification; + madeModification = UpdateChapterSummary(chapter, settings, potentialMatch.Summary) || madeModification; + madeModification = UpdateChapterReleaseDate(chapter, settings, potentialMatch.ReleaseDate) || madeModification; + madeModification = await UpdateChapterPublisher(chapter, settings, potentialMatch.Publisher) || madeModification; - if (!chapter.SummaryLocked && string.IsNullOrEmpty(chapter.Summary) & !string.IsNullOrEmpty(potentialMatch.Summary)) - { - chapter.Summary = potentialMatch.Summary; - _unitOfWork.ChapterRepository.Update(chapter); - madeModification = true; - } + madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.CoverArtist, potentialMatch.Artists) || madeModification; + madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.Writer, potentialMatch.Writers) || madeModification; - // ReleaseDate - // Cover Image + madeModification = await UpdateChapterCoverImage(chapter, settings, potentialMatch.CoverImageUrl) || madeModification; - - // Update People (Writer/Artist/Publisher) - if (!chapter.PublisherLocked) - { - // Update publisher - } - - //var artists = potentialMatch.Artists.Select(p => new PersonDto()) - - // await SeriesService.HandlePeopleUpdateAsync(series.Metadata, artists, PersonRole.CoverArtist, _unitOfWork); - // - // foreach (var person in series.Metadata.People.Where(p => p.Role == PersonRole.CoverArtist)) - // { - // var meta = upstreamArtists.FirstOrDefault(c => c.Name == person.Person.Name); - // person.OrderWeight = 0; - // if (meta != null) - // { - // person.KavitaPlusConnection = true; - // } - // } + _unitOfWork.ChapterRepository.Update(chapter); + await _unitOfWork.CommitAsync(); } + return madeModification; + } - _unitOfWork.SeriesRepository.Update(series); + + private static bool UpdateChapterSummary(Chapter chapter, MetadataSettingsDto settings, string? summary) + { + if (!settings.EnableChapterSummary) return false; + + if (string.IsNullOrEmpty(summary)) return false; + + if (chapter.SummaryLocked && !settings.HasOverride(MetadataSettingField.ChapterSummary)) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(summary) && !settings.HasOverride(MetadataSettingField.ChapterSummary)) + { + return false; + } + + chapter.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(summary)); + return true; + } + + private static bool UpdateChapterTitle(Chapter chapter, MetadataSettingsDto settings, string? title, string seriesName) + { + if (!settings.EnableChapterTitle) return false; + + if (string.IsNullOrEmpty(title)) return false; + + if (chapter.TitleNameLocked && !settings.HasOverride(MetadataSettingField.ChapterTitle)) + { + return false; + } + + if (!title.Contains(seriesName) && !settings.HasOverride(MetadataSettingField.ChapterTitle)) + { + return false; + } + + chapter.TitleName = title; + return true; + } + + private static bool UpdateChapterReleaseDate(Chapter chapter, MetadataSettingsDto settings, DateTime? releaseDate) + { + if (!settings.EnableChapterReleaseDate) return false; + + if (releaseDate == null || releaseDate == DateTime.MinValue) return false; + + if (chapter.ReleaseDateLocked && !settings.HasOverride(MetadataSettingField.ChapterReleaseDate)) + { + return false; + } + + if (!settings.HasOverride(MetadataSettingField.ChapterReleaseDate)) + { + return false; + } + + chapter.ReleaseDate = releaseDate.Value; + return true; + } + + private async Task UpdateChapterPublisher(Chapter chapter, MetadataSettingsDto settings, string? publisher) + { + if (!settings.EnableChapterPublisher) return false; + + if (string.IsNullOrEmpty(publisher)) return false; + + if (chapter.PublisherLocked && !settings.HasOverride(MetadataSettingField.ChapterPublisher)) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(publisher) && !settings.HasOverride(MetadataSettingField.ChapterPublisher)) + { + return false; + } + + return await UpdateChapterPeople(chapter, settings, PersonRole.Publisher, [publisher]); + } + + private async Task UpdateChapterCoverImage(Chapter chapter, MetadataSettingsDto settings, string? coverUrl) + { + if (!settings.EnableChapterCoverImage) return false; + + if (string.IsNullOrEmpty(coverUrl)) return false; + + if (chapter.CoverImageLocked && !settings.HasOverride(MetadataSettingField.ChapterCovers)) + { + return false; + } + + if (string.IsNullOrEmpty(coverUrl)) + { + return false; + } + + await DownloadChapterCovers(chapter, coverUrl); + return true; + } + + private async Task UpdateChapterPeople(Chapter chapter, MetadataSettingsDto settings, PersonRole role, IList? staff) + { + if (!settings.EnablePeople) return false; + + if (staff?.Count == 0) return false; + + if (chapter.CoverArtistLocked && !settings.HasOverride(MetadataSettingField.People)) + { + return false; + } + + if (!settings.IsPersonAllowed(role)) + { + return false; + } + + chapter.People ??= []; + var artists = staff + .Select(w => new PersonDto() + { + Name = w, + }) + .Concat(chapter.People + .Where(p => p.Role == role) + .Where(p => !p.KavitaPlusConnection) + .Select(p => _mapper.Map(p.Person)) + ) + .DistinctBy(p => Parser.Normalize(p.Name)) + .ToList(); + + await PersonHelper.UpdateChapterPeopleAsync(chapter, staff, role, _unitOfWork); + + foreach (var person in chapter.People.Where(p => p.Role == role)) + { + var meta = artists.FirstOrDefault(c => c.Name == person.Person.Name); + person.OrderWeight = 0; + + if (meta != null) + { + person.KavitaPlusConnection = true; + } + } + + _unitOfWork.ChapterRepository.Update(chapter); await _unitOfWork.CommitAsync(); - return madeModification; + return true; } private async Task UpdateCoverImage(Series series, MetadataSettingsDto settings, ExternalSeriesDetailDto externalMetadata) @@ -1244,6 +1364,18 @@ public class ExternalMetadataService : IExternalMetadataService } } + private async Task DownloadChapterCovers(Chapter chapter, string coverUrl) + { + try + { + await _coverDbService.SetChapterCoverByUrl(chapter, coverUrl, false, true); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an exception downloading cover image for Chapter {ChapterName} ({SeriesId})", chapter.Range, chapter.Id); + } + } + private async Task DownloadAndSetPersonCovers(List people) { foreach (var staff in people) diff --git a/API/Services/Tasks/Metadata/CoverDbService.cs b/API/Services/Tasks/Metadata/CoverDbService.cs index b90bdcc38..c76bb99d1 100644 --- a/API/Services/Tasks/Metadata/CoverDbService.cs +++ b/API/Services/Tasks/Metadata/CoverDbService.cs @@ -32,6 +32,7 @@ public interface ICoverDbService Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url); Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false); Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, bool chooseBetterImage = false); + Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false); } @@ -580,6 +581,51 @@ public class CoverDbService : ICoverDbService } } + public async Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false) + { + if (!string.IsNullOrEmpty(url)) + { + var filePath = await CreateThumbnail(url, $"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}", fromBase64); + + if (!string.IsNullOrEmpty(filePath)) + { + // Additional check to see if downloaded image is similar and we have a higher resolution + if (chooseBetterImage && !string.IsNullOrEmpty(chapter.CoverImage)) + { + try + { + var betterImage = Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage) + .GetBetterImage(Path.Join(_directoryService.CoverImageDirectory, filePath))!; + filePath = Path.GetFileName(betterImage); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an issue trying to choose a better cover image for Chapter: {FileName} ({ChapterId})", chapter.Range, chapter.Id); + } + } + + chapter.CoverImage = filePath; + chapter.CoverImageLocked = true; + _imageService.UpdateColorScape(chapter); + _unitOfWork.ChapterRepository.Update(chapter); + } + } + else + { + chapter.CoverImage = null; + chapter.CoverImageLocked = false; + _imageService.UpdateColorScape(chapter); + _unitOfWork.ChapterRepository.Update(chapter); + } + + if (_unitOfWork.HasChanges()) + { + await _unitOfWork.CommitAsync(); + await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, + MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false); + } + } + private async Task CreateThumbnail(string url, string filename, bool fromBase64 = true) { var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); diff --git a/UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts b/UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts index a9b763c50..3f980ea12 100644 --- a/UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts +++ b/UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts @@ -1,4 +1,4 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; import {MetadataSettingField} from "../admin/_models/metadata-setting-field"; import {translate} from "@jsverse/transloco"; @@ -10,9 +10,19 @@ export class MetadataSettingFiledPipe implements PipeTransform { transform(value: MetadataSettingField): string { switch (value) { + case MetadataSettingField.ChapterTitle: + return translate('metadata-setting-field-pipe.title'); + case MetadataSettingField.ChapterSummary: + return translate('metadata-setting-field-pipe.summary'); + case MetadataSettingField.ChapterReleaseDate: + return translate('metadata-setting-field-pipe.release-date'); + case MetadataSettingField.ChapterPublisher: + return translate('metadata-setting-field-pipe.publisher'); + case MetadataSettingField.ChapterCovers: + return translate('metadata-setting-field-pipe.covers'); case MetadataSettingField.AgeRating: return translate('metadata-setting-field-pipe.age-rating'); - case MetadataSettingField.People: + case MetadataSettingField.People: return translate('metadata-setting-field-pipe.people'); case MetadataSettingField.Covers: return translate('metadata-setting-field-pipe.covers'); diff --git a/UI/Web/src/app/admin/_models/metadata-setting-field.ts b/UI/Web/src/app/admin/_models/metadata-setting-field.ts index 0448e1af2..fb99e8a07 100644 --- a/UI/Web/src/app/admin/_models/metadata-setting-field.ts +++ b/UI/Web/src/app/admin/_models/metadata-setting-field.ts @@ -7,7 +7,14 @@ export enum MetadataSettingField { LocalizedName = 6, Covers = 7, AgeRating = 8, - People = 9 + People = 9, + + // Chapter fields + ChapterTitle = 10, + ChapterSummary = 11, + ChapterReleaseDate = 12, + ChapterPublisher = 13, + ChapterCovers = 14, } export const allMetadataSettingField = Object.keys(MetadataSettingField) diff --git a/UI/Web/src/app/admin/_models/metadata-settings.ts b/UI/Web/src/app/admin/_models/metadata-settings.ts index d88e14312..9743dd578 100644 --- a/UI/Web/src/app/admin/_models/metadata-settings.ts +++ b/UI/Web/src/app/admin/_models/metadata-settings.ts @@ -25,6 +25,14 @@ export interface MetadataSettings { enableStartDate: boolean; enableCoverImage: boolean; enableLocalizedName: boolean; + + enableChapterSummary: boolean; + enableChapterReleaseDate: boolean; + enableChapterTitle: boolean; + enableChapterPublisher: boolean; + enableChapterCoverImage: boolean; + + enableGenres: boolean; enableTags: boolean; firstLastPeopleNaming: boolean; diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 4d8a07210..007ecb1fb 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -2685,7 +2685,10 @@ "start-date": "{{manage-metadata-settings.enable-start-date-label}}", "genres": "{{metadata-fields.genres-title}}", "tags": "{{metadata-fields.tags-title}}", - "localized-name": "{{edit-series-modal.localized-name-label}}" + "localized-name": "{{edit-series-modal.localized-name-label}}", + "release-date": "Release Date", + "publisher": "{{person-role-pipe.publisher}}", + "title": "Title" },