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"
},