Added the rest of the fields, no migration yet, needs unit tests.

This commit is contained in:
Joseph Milazzo 2025-04-15 14:46:10 -05:00
parent 7c692a1b46
commit c4419dcf28
11 changed files with 324 additions and 41 deletions

View file

@ -43,6 +43,29 @@ public class MetadataSettingsDto
/// </summary>
public bool EnableCoverImage { get; set; }
#region Chapter Metadata
/// <summary>
/// Allow Summary to be set within Chapter/Issue
/// </summary>
public bool EnableChapterSummary { get; set; }
/// <summary>
/// Allow Release Date to be set within Chapter/Issue
/// </summary>
public bool EnableChapterReleaseDate { get; set; }
/// <summary>
/// Allow Title to be set within Chapter/Issue
/// </summary>
public bool EnableChapterTitle { get; set; }
/// <summary>
/// Allow Publisher to be set within Chapter/Issue
/// </summary>
public bool EnableChapterPublisher { get; set; }
/// <summary>
/// Allow setting the cover image for the Chapter/Issue
/// </summary>
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;

View file

@ -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);

View file

@ -5,6 +5,7 @@
/// </summary>
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
}

View file

@ -14,6 +14,8 @@ public class MetadataSettings
/// </summary>
public bool Enabled { get; set; }
#region Series Metadata
/// <summary>
/// Allow the Summary to be written
/// </summary>
@ -42,6 +44,30 @@ public class MetadataSettings
/// Allow setting the cover image
/// </summary>
public bool EnableCoverImage { get; set; }
#endregion
#region Chapter Metadata
/// <summary>
/// Allow Summary to be set within Chapter/Issue
/// </summary>
public bool EnableChapterSummary { get; set; }
/// <summary>
/// Allow Release Date to be set within Chapter/Issue
/// </summary>
public bool EnableChapterReleaseDate { get; set; }
/// <summary>
/// Allow Title to be set within Chapter/Issue
/// </summary>
public bool EnableChapterTitle { get; set; }
/// <summary>
/// Allow Publisher to be set within Chapter/Issue
/// </summary>
public bool EnableChapterPublisher { get; set; }
/// <summary>
/// Allow setting the cover image for the Chapter/Issue
/// </summary>
public bool EnableChapterCoverImage { get; set; }
#endregion
// Need to handle the Genre/tags stuff
public bool EnableGenres { get; set; } = true;

View file

@ -10,5 +10,14 @@ public class ChapterPeople
public int PersonId { get; set; }
public virtual Person Person { get; set; }
/// <summary>
/// The source of this connection. If not Kavita, this implies Metadata Download linked this and it can be removed between matches
/// </summary>
public bool KavitaPlusConnection { get; set; }
/// <summary>
/// A weight that allows lower numbers to sort first
/// </summary>
public int OrderWeight { get; set; }
public required PersonRole Role { get; set; }
}

View file

@ -1075,53 +1075,173 @@ 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;
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;
madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.CoverArtist, potentialMatch.Artists) || madeModification;
madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.Writer, potentialMatch.Writers) || madeModification;
madeModification = await UpdateChapterCoverImage(chapter, settings, potentialMatch.CoverImageUrl) || madeModification;
_unitOfWork.ChapterRepository.Update(chapter);
madeModification = true;
}
if (!chapter.SummaryLocked && string.IsNullOrEmpty(chapter.Summary) & !string.IsNullOrEmpty(potentialMatch.Summary))
{
chapter.Summary = potentialMatch.Summary;
_unitOfWork.ChapterRepository.Update(chapter);
madeModification = true;
}
// ReleaseDate
// Cover Image
// 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.SeriesRepository.Update(series);
await _unitOfWork.CommitAsync();
}
return madeModification;
}
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<bool> 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<bool> 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<bool> UpdateChapterPeople(Chapter chapter, MetadataSettingsDto settings, PersonRole role, IList<string>? 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<PersonDto>(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 true;
}
private async Task<bool> UpdateCoverImage(Series series, MetadataSettingsDto settings, ExternalSeriesDetailDto externalMetadata)
{
if (!settings.EnableCoverImage) return false;
@ -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<SeriesStaffDto> people)
{
foreach (var staff in people)

View file

@ -32,6 +32,7 @@ public interface ICoverDbService
Task<string?> 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<string> CreateThumbnail(string url, string filename, bool fromBase64 = true)
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();

View file

@ -10,6 +10,16 @@ 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:

View file

@ -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)

View file

@ -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;

View file

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