mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Kavita+ Comic Metadata Matching (#3740)
This commit is contained in:
parent
4521965315
commit
ed154e4768
@ -649,13 +649,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();
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
36
API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs
Normal file
36
API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using API.DTOs.SeriesDetail;
|
||||||
|
|
||||||
|
namespace API.DTOs.KavitaPlus.Metadata;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Information about an individual issue/chapter/book from Kavita+
|
||||||
|
/// </summary>
|
||||||
|
public class ExternalChapterDto
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
public string IssueNumber { get; set; }
|
||||||
|
|
||||||
|
public decimal? CriticRating { get; set; }
|
||||||
|
|
||||||
|
public decimal? UserRating { get; set; }
|
||||||
|
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
|
||||||
|
public IList<string>? Writers { get; set; }
|
||||||
|
|
||||||
|
public IList<string>? Artists { get; set; }
|
||||||
|
|
||||||
|
public DateTime? ReleaseDate { get; set; }
|
||||||
|
|
||||||
|
public string? Publisher { get; set; }
|
||||||
|
|
||||||
|
public string? CoverImageUrl { get; set; }
|
||||||
|
|
||||||
|
public string? IssueUrl { get; set; }
|
||||||
|
|
||||||
|
public IList<UserReviewDto> CriticReviews { get; set; }
|
||||||
|
public IList<UserReviewDto> UserReviews { get; set; }
|
||||||
|
}
|
@ -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; }
|
||||||
@ -33,5 +34,13 @@ public class ExternalSeriesDetailDto
|
|||||||
public IList<SeriesRelationship>? Relations { get; set; } = [];
|
public IList<SeriesRelationship>? Relations { get; set; } = [];
|
||||||
public IList<SeriesCharacter>? Characters { get; set; } = [];
|
public IList<SeriesCharacter>? Characters { get; set; } = [];
|
||||||
|
|
||||||
|
#region Comic Only
|
||||||
|
public string? Publisher { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Only from CBR for <see cref="ScrobbleProvider.Cbr"/>. Full metadata about issues
|
||||||
|
/// </summary>
|
||||||
|
public IList<ExternalChapterDto>? ChapterDtos { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,29 @@ public class MetadataSettingsDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableCoverImage { get; set; }
|
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
|
// Need to handle the Genre/tags stuff
|
||||||
public bool EnableGenres { get; set; } = true;
|
public bool EnableGenres { get; set; } = true;
|
||||||
public bool EnableTags { get; set; } = true;
|
public bool EnableTags { get; set; } = true;
|
||||||
|
@ -10,6 +10,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; }
|
||||||
|
3433
API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs
generated
Normal file
3433
API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
106
API/Data/Migrations/20250415194829_KavitaPlusCBR.cs
Normal file
106
API/Data/Migrations/20250415194829_KavitaPlusCBR.cs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class KavitaPlusCBR : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "EnableChapterCoverImage",
|
||||||
|
table: "MetadataSettings",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "EnableChapterPublisher",
|
||||||
|
table: "MetadataSettings",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "EnableChapterReleaseDate",
|
||||||
|
table: "MetadataSettings",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "EnableChapterSummary",
|
||||||
|
table: "MetadataSettings",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "EnableChapterTitle",
|
||||||
|
table: "MetadataSettings",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "CbrId",
|
||||||
|
table: "ExternalSeriesMetadata",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "KavitaPlusConnection",
|
||||||
|
table: "ChapterPeople",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "OrderWeight",
|
||||||
|
table: "ChapterPeople",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EnableChapterCoverImage",
|
||||||
|
table: "MetadataSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EnableChapterPublisher",
|
||||||
|
table: "MetadataSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EnableChapterReleaseDate",
|
||||||
|
table: "MetadataSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EnableChapterSummary",
|
||||||
|
table: "MetadataSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EnableChapterTitle",
|
||||||
|
table: "MetadataSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CbrId",
|
||||||
|
table: "ExternalSeriesMetadata");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "KavitaPlusConnection",
|
||||||
|
table: "ChapterPeople");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "OrderWeight",
|
||||||
|
table: "ChapterPeople");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.4");
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
{
|
{
|
||||||
@ -1429,6 +1429,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<int>("AverageExternalRating")
|
b.Property<int>("AverageExternalRating")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("CbrId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("GoogleBooksId")
|
b.Property<string>("GoogleBooksId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -1645,6 +1648,21 @@ namespace API.Data.Migrations
|
|||||||
b.Property<string>("Blacklist")
|
b.Property<string>("Blacklist")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableChapterCoverImage")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableChapterPublisher")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableChapterReleaseDate")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableChapterSummary")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableChapterTitle")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<bool>("EnableCoverImage")
|
b.Property<bool>("EnableCoverImage")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
@ -1707,6 +1725,12 @@ namespace API.Data.Migrations
|
|||||||
b.Property<int>("Role")
|
b.Property<int>("Role")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("KavitaPlusConnection")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("OrderWeight")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.HasKey("ChapterId", "PersonId", "Role");
|
b.HasKey("ChapterId", "PersonId", "Role");
|
||||||
|
|
||||||
b.HasIndex("PersonId");
|
b.HasIndex("PersonId");
|
||||||
|
@ -47,6 +47,7 @@ public interface IChapterRepository
|
|||||||
Task<IEnumerable<string>> GetCoverImagesForLockedChaptersAsync();
|
Task<IEnumerable<string>> GetCoverImagesForLockedChaptersAsync();
|
||||||
Task<ChapterDto> AddChapterModifiers(int userId, ChapterDto chapter);
|
Task<ChapterDto> AddChapterModifiers(int userId, ChapterDto chapter);
|
||||||
IEnumerable<Chapter> GetChaptersForSeries(int seriesId);
|
IEnumerable<Chapter> GetChaptersForSeries(int seriesId);
|
||||||
|
Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId);
|
||||||
}
|
}
|
||||||
public class ChapterRepository : IChapterRepository
|
public class ChapterRepository : IChapterRepository
|
||||||
{
|
{
|
||||||
@ -298,4 +299,15 @@ public class ChapterRepository : IChapterRepository
|
|||||||
.Include(c => c.Volume)
|
.Include(c => c.Volume)
|
||||||
.AsEnumerable();
|
.AsEnumerable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId)
|
||||||
|
{
|
||||||
|
return await _context.Chapter
|
||||||
|
.Where(c => c.Volume.SeriesId == seriesId)
|
||||||
|
.OrderBy(c => c.SortOrder)
|
||||||
|
.Include(c => c.Volume)
|
||||||
|
.Include(c => c.People)
|
||||||
|
.ThenInclude(cp => cp.Person)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,8 +157,8 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||||||
.OrderByDescending(r => r.Score);
|
.OrderByDescending(r => r.Score);
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<RatingDto> ratings = new List<RatingDto>();
|
IEnumerable<RatingDto> ratings = [];
|
||||||
if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Any())
|
if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Count != 0)
|
||||||
{
|
{
|
||||||
ratings = seriesDetailDto.ExternalRatings
|
ratings = seriesDetailDto.ExternalRatings
|
||||||
.Select(r => _mapper.Map<RatingDto>(r));
|
.Select(r => _mapper.Map<RatingDto>(r));
|
||||||
|
@ -312,6 +312,11 @@ public static class Seed
|
|||||||
EnableLocalizedName = false,
|
EnableLocalizedName = false,
|
||||||
FirstLastPeopleNaming = true,
|
FirstLastPeopleNaming = true,
|
||||||
EnableCoverImage = true,
|
EnableCoverImage = true,
|
||||||
|
EnableChapterTitle = false,
|
||||||
|
EnableChapterSummary = true,
|
||||||
|
EnableChapterPublisher = true,
|
||||||
|
EnableChapterCoverImage = false,
|
||||||
|
EnableChapterReleaseDate = true,
|
||||||
PersonRoles = [PersonRole.Writer, PersonRole.CoverArtist, PersonRole.Character]
|
PersonRoles = [PersonRole.Writer, PersonRole.CoverArtist, PersonRole.Character]
|
||||||
};
|
};
|
||||||
await context.MetadataSettings.AddAsync(existing);
|
await context.MetadataSettings.AddAsync(existing);
|
||||||
|
@ -234,4 +234,25 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage
|
|||||||
PrimaryColor = string.Empty;
|
PrimaryColor = string.Empty;
|
||||||
SecondaryColor = string.Empty;
|
SecondaryColor = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsPersonRoleLocked(PersonRole role)
|
||||||
|
{
|
||||||
|
return role switch
|
||||||
|
{
|
||||||
|
PersonRole.Character => CharacterLocked,
|
||||||
|
PersonRole.Writer => WriterLocked,
|
||||||
|
PersonRole.Penciller => PencillerLocked,
|
||||||
|
PersonRole.Inker => InkerLocked,
|
||||||
|
PersonRole.Colorist => ColoristLocked,
|
||||||
|
PersonRole.Letterer => LettererLocked,
|
||||||
|
PersonRole.CoverArtist => CoverArtistLocked,
|
||||||
|
PersonRole.Editor => EditorLocked,
|
||||||
|
PersonRole.Publisher => PublisherLocked,
|
||||||
|
PersonRole.Translator => TranslatorLocked,
|
||||||
|
PersonRole.Imprint => ImprintLocked,
|
||||||
|
PersonRole.Team => TeamLocked,
|
||||||
|
PersonRole.Location => LocationLocked,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(role), role, null)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ public class ExternalSeriesMetadata
|
|||||||
public int AverageExternalRating { get; set; } = -1;
|
public int AverageExternalRating { get; set; } = -1;
|
||||||
|
|
||||||
public int AniListId { get; set; }
|
public int AniListId { get; set; }
|
||||||
|
public int CbrId { get; set; }
|
||||||
public long MalId { get; set; }
|
public long MalId { get; set; }
|
||||||
public string GoogleBooksId { get; set; }
|
public string GoogleBooksId { get; set; }
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public enum MetadataSettingField
|
public enum MetadataSettingField
|
||||||
{
|
{
|
||||||
|
#region Series Metadata
|
||||||
Summary = 1,
|
Summary = 1,
|
||||||
PublicationStatus = 2,
|
PublicationStatus = 2,
|
||||||
StartDate = 3,
|
StartDate = 3,
|
||||||
@ -13,5 +14,18 @@ public enum MetadataSettingField
|
|||||||
LocalizedName = 6,
|
LocalizedName = 6,
|
||||||
Covers = 7,
|
Covers = 7,
|
||||||
AgeRating = 8,
|
AgeRating = 8,
|
||||||
People = 9
|
People = 9,
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Chapter Metadata
|
||||||
|
|
||||||
|
ChapterTitle = 10,
|
||||||
|
ChapterSummary = 11,
|
||||||
|
ChapterReleaseDate = 12,
|
||||||
|
ChapterPublisher = 13,
|
||||||
|
ChapterCovers = 14,
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ public class MetadataSettings
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
|
#region Series Metadata
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Allow the Summary to be written
|
/// Allow the Summary to be written
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -42,6 +44,30 @@ public class MetadataSettings
|
|||||||
/// Allow setting the cover image
|
/// Allow setting the cover image
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableCoverImage { get; set; }
|
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
|
// Need to handle the Genre/tags stuff
|
||||||
public bool EnableGenres { get; set; } = true;
|
public bool EnableGenres { get; set; } = true;
|
||||||
|
@ -10,5 +10,14 @@ public class ChapterPeople
|
|||||||
public int PersonId { get; set; }
|
public int PersonId { get; set; }
|
||||||
public virtual Person Person { 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; }
|
public required PersonRole Role { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,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);
|
||||||
}
|
}
|
||||||
@ -66,7 +66,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
private readonly ICoverDbService _coverDbService;
|
private readonly ICoverDbService _coverDbService;
|
||||||
private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(30);
|
private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(30);
|
||||||
public static readonly HashSet<LibraryType> NonEligibleLibraryTypes =
|
public static readonly HashSet<LibraryType> NonEligibleLibraryTypes =
|
||||||
[LibraryType.Comic, LibraryType.Book, LibraryType.Image, LibraryType.ComicVine];
|
[LibraryType.Comic, LibraryType.Book, LibraryType.Image];
|
||||||
private readonly SeriesDetailPlusDto _defaultReturn = new()
|
private readonly SeriesDetailPlusDto _defaultReturn = new()
|
||||||
{
|
{
|
||||||
Series = null,
|
Series = null,
|
||||||
@ -203,7 +203,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);
|
||||||
@ -217,7 +217,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(),
|
||||||
@ -319,8 +319,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;
|
||||||
@ -336,15 +338,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,8 +432,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)
|
||||||
{
|
{
|
||||||
@ -482,6 +485,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
{
|
{
|
||||||
var rating = _mapper.Map<ExternalRating>(r);
|
var rating = _mapper.Map<ExternalRating>(r);
|
||||||
rating.SeriesId = externalSeriesMetadata.SeriesId;
|
rating.SeriesId = externalSeriesMetadata.SeriesId;
|
||||||
|
rating.ProviderUrl = r.ProviderUrl;
|
||||||
return rating;
|
return rating;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
@ -500,6 +504,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
|
|
||||||
if (result.MalId.HasValue) externalSeriesMetadata.MalId = result.MalId.Value;
|
if (result.MalId.HasValue) externalSeriesMetadata.MalId = result.MalId.Value;
|
||||||
if (result.AniListId.HasValue) externalSeriesMetadata.AniListId = result.AniListId.Value;
|
if (result.AniListId.HasValue) externalSeriesMetadata.AniListId = result.AniListId.Value;
|
||||||
|
if (result.CbrId.HasValue) externalSeriesMetadata.CbrId = result.CbrId.Value;
|
||||||
|
|
||||||
// If there is metadata and the user has metadata download turned on
|
// If there is metadata and the user has metadata download turned on
|
||||||
var madeMetadataModification = false;
|
var madeMetadataModification = false;
|
||||||
@ -622,6 +627,8 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
madeModification = await UpdateRelationships(series, settings, externalMetadata.Relations, defaultAdmin) || madeModification;
|
madeModification = await UpdateRelationships(series, settings, externalMetadata.Relations, defaultAdmin) || madeModification;
|
||||||
madeModification = await UpdateCoverImage(series, settings, externalMetadata) || madeModification;
|
madeModification = await UpdateCoverImage(series, settings, externalMetadata) || madeModification;
|
||||||
|
|
||||||
|
madeModification = await UpdateChapters(series, settings, externalMetadata) || madeModification;
|
||||||
|
|
||||||
return madeModification;
|
return madeModification;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -848,7 +855,6 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download the image and save it
|
|
||||||
_unitOfWork.SeriesRepository.Update(series);
|
_unitOfWork.SeriesRepository.Update(series);
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
@ -1044,6 +1050,199 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task<bool> UpdateChapters(Series series, MetadataSettingsDto settings,
|
||||||
|
ExternalSeriesDetailDto externalMetadata)
|
||||||
|
{
|
||||||
|
if (externalMetadata.PlusMediaFormat != PlusMediaFormat.Comic) return false;
|
||||||
|
|
||||||
|
if (externalMetadata.ChapterDtos == null || externalMetadata.ChapterDtos.Count == 0) return false;
|
||||||
|
|
||||||
|
// Get all volumes and chapters
|
||||||
|
var madeModification = false;
|
||||||
|
var allChapters = await _unitOfWork.ChapterRepository.GetAllChaptersForSeries(series.Id);
|
||||||
|
|
||||||
|
var matchedChapters = allChapters
|
||||||
|
.Join(
|
||||||
|
externalMetadata.ChapterDtos,
|
||||||
|
chapter => chapter.Range,
|
||||||
|
dto => dto.IssueNumber,
|
||||||
|
(chapter, dto) => (chapter, dto) // Create a tuple of matched pairs
|
||||||
|
)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var (chapter, potentialMatch) in matchedChapters)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Updating {ChapterNumber} with metadata", chapter.Range);
|
||||||
|
|
||||||
|
// Write the metadata
|
||||||
|
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);
|
||||||
|
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.IsPersonRoleLocked(role) && !settings.HasOverride(MetadataSettingField.People))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings.IsPersonAllowed(role) && role != PersonRole.Publisher)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter.People ??= [];
|
||||||
|
var people = 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 = people.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)
|
private async Task<bool> UpdateCoverImage(Series series, MetadataSettingsDto settings, ExternalSeriesDetailDto externalMetadata)
|
||||||
{
|
{
|
||||||
if (!settings.EnableCoverImage) return false;
|
if (!settings.EnableCoverImage) return false;
|
||||||
@ -1166,6 +1365,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)
|
private async Task DownloadAndSetPersonCovers(List<SeriesStaffDto> people)
|
||||||
{
|
{
|
||||||
foreach (var staff in people)
|
foreach (var staff in people)
|
||||||
|
@ -263,6 +263,7 @@ public class LicenseService(
|
|||||||
if (cacheValue.HasValue) return cacheValue.Value;
|
if (cacheValue.HasValue) return cacheValue.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: If info.IsCancelled && notActive, let's remove the license so we aren't constantly checking
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -38,6 +38,9 @@ public enum ScrobbleProvider
|
|||||||
Kavita = 0,
|
Kavita = 0,
|
||||||
AniList = 1,
|
AniList = 1,
|
||||||
Mal = 2,
|
Mal = 2,
|
||||||
|
[Obsolete]
|
||||||
|
GoogleBooks = 3,
|
||||||
|
Cbr = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IScrobblingService
|
public interface IScrobblingService
|
||||||
|
@ -65,6 +65,12 @@ public class SettingsService : ISettingsService
|
|||||||
existingMetadataSetting.FirstLastPeopleNaming = dto.FirstLastPeopleNaming;
|
existingMetadataSetting.FirstLastPeopleNaming = dto.FirstLastPeopleNaming;
|
||||||
existingMetadataSetting.EnableCoverImage = dto.EnableCoverImage;
|
existingMetadataSetting.EnableCoverImage = dto.EnableCoverImage;
|
||||||
|
|
||||||
|
existingMetadataSetting.EnableChapterPublisher = dto.EnableChapterPublisher;
|
||||||
|
existingMetadataSetting.EnableChapterSummary = dto.EnableChapterSummary;
|
||||||
|
existingMetadataSetting.EnableChapterTitle = dto.EnableChapterTitle;
|
||||||
|
existingMetadataSetting.EnableChapterReleaseDate = dto.EnableChapterReleaseDate;
|
||||||
|
existingMetadataSetting.EnableChapterCoverImage = dto.EnableChapterCoverImage;
|
||||||
|
|
||||||
existingMetadataSetting.AgeRatingMappings = dto.AgeRatingMappings ?? [];
|
existingMetadataSetting.AgeRatingMappings = dto.AgeRatingMappings ?? [];
|
||||||
|
|
||||||
existingMetadataSetting.Blacklist = (dto.Blacklist ?? []).Where(s => !string.IsNullOrWhiteSpace(s)).DistinctBy(d => d.ToNormalized()).ToList() ?? [];
|
existingMetadataSetting.Blacklist = (dto.Blacklist ?? []).Where(s => !string.IsNullOrWhiteSpace(s)).DistinctBy(d => d.ToNormalized()).ToList() ?? [];
|
||||||
|
@ -32,6 +32,7 @@ public interface ICoverDbService
|
|||||||
Task<string?> DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url);
|
Task<string?> DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url);
|
||||||
Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false);
|
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 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)
|
private async Task<string> CreateThumbnail(string url, string filename, bool fromBase64 = true)
|
||||||
{
|
{
|
||||||
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
|
@ -977,26 +977,26 @@ public class ProcessSeries : IProcessSeries
|
|||||||
chapter.ReleaseDate = new DateTime(comicInfo.Year, month, day);
|
chapter.ReleaseDate = new DateTime(comicInfo.Year, month, day);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chapter.ColoristLocked)
|
if (!chapter.IsPersonRoleLocked(PersonRole.Colorist))
|
||||||
{
|
{
|
||||||
var people = TagHelper.GetTagValues(comicInfo.Colorist);
|
var people = TagHelper.GetTagValues(comicInfo.Colorist);
|
||||||
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Colorist);
|
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Colorist);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chapter.CharacterLocked)
|
if (!chapter.IsPersonRoleLocked(PersonRole.Character))
|
||||||
{
|
{
|
||||||
var people = TagHelper.GetTagValues(comicInfo.Characters);
|
var people = TagHelper.GetTagValues(comicInfo.Characters);
|
||||||
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Character);
|
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Character);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!chapter.TranslatorLocked)
|
if (!chapter.IsPersonRoleLocked(PersonRole.Translator))
|
||||||
{
|
{
|
||||||
var people = TagHelper.GetTagValues(comicInfo.Translator);
|
var people = TagHelper.GetTagValues(comicInfo.Translator);
|
||||||
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Translator);
|
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Translator);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chapter.WriterLocked)
|
if (!chapter.IsPersonRoleLocked(PersonRole.Writer))
|
||||||
{
|
{
|
||||||
var personSw = Stopwatch.StartNew();
|
var personSw = Stopwatch.StartNew();
|
||||||
var people = TagHelper.GetTagValues(comicInfo.Writer);
|
var people = TagHelper.GetTagValues(comicInfo.Writer);
|
||||||
@ -1004,55 +1004,55 @@ public class ProcessSeries : IProcessSeries
|
|||||||
_logger.LogTrace("[TIME] Kavita took {Time} ms to process writer on Chapter: {File} for {Count} people", personSw.ElapsedMilliseconds, chapter.Files.First().FileName, people.Count);
|
_logger.LogTrace("[TIME] Kavita took {Time} ms to process writer on Chapter: {File} for {Count} people", personSw.ElapsedMilliseconds, chapter.Files.First().FileName, people.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chapter.EditorLocked)
|
if (!chapter.IsPersonRoleLocked(PersonRole.Editor))
|
||||||
{
|
{
|
||||||
var people = TagHelper.GetTagValues(comicInfo.Editor);
|
var people = TagHelper.GetTagValues(comicInfo.Editor);
|
||||||
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Editor);
|
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chapter.InkerLocked)
|
if (!chapter.IsPersonRoleLocked(PersonRole.Inker))
|
||||||
{
|
{
|
||||||
var people = TagHelper.GetTagValues(comicInfo.Inker);
|
var people = TagHelper.GetTagValues(comicInfo.Inker);
|
||||||
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Inker);
|
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Inker);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chapter.LettererLocked)
|
if (!chapter.IsPersonRoleLocked(PersonRole.Letterer))
|
||||||
{
|
{
|
||||||
var people = TagHelper.GetTagValues(comicInfo.Letterer);
|
var people = TagHelper.GetTagValues(comicInfo.Letterer);
|
||||||
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Letterer);
|
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Letterer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chapter.PencillerLocked)
|
if (!chapter.IsPersonRoleLocked(PersonRole.Penciller))
|
||||||
{
|
{
|
||||||
var people = TagHelper.GetTagValues(comicInfo.Penciller);
|
var people = TagHelper.GetTagValues(comicInfo.Penciller);
|
||||||
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Penciller);
|
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Penciller);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chapter.CoverArtistLocked)
|
if (!chapter.IsPersonRoleLocked(PersonRole.CoverArtist))
|
||||||
{
|
{
|
||||||
var people = TagHelper.GetTagValues(comicInfo.CoverArtist);
|
var people = TagHelper.GetTagValues(comicInfo.CoverArtist);
|
||||||
await UpdateChapterPeopleAsync(chapter, people, PersonRole.CoverArtist);
|
await UpdateChapterPeopleAsync(chapter, people, PersonRole.CoverArtist);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chapter.PublisherLocked)
|
if (!chapter.IsPersonRoleLocked(PersonRole.Publisher))
|
||||||
{
|
{
|
||||||
var people = TagHelper.GetTagValues(comicInfo.Publisher);
|
var people = TagHelper.GetTagValues(comicInfo.Publisher);
|
||||||
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Publisher);
|
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Publisher);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chapter.ImprintLocked)
|
if (!chapter.IsPersonRoleLocked(PersonRole.Imprint))
|
||||||
{
|
{
|
||||||
var people = TagHelper.GetTagValues(comicInfo.Imprint);
|
var people = TagHelper.GetTagValues(comicInfo.Imprint);
|
||||||
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Imprint);
|
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Imprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chapter.TeamLocked)
|
if (!chapter.IsPersonRoleLocked(PersonRole.Team))
|
||||||
{
|
{
|
||||||
var people = TagHelper.GetTagValues(comicInfo.Teams);
|
var people = TagHelper.GetTagValues(comicInfo.Teams);
|
||||||
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Team);
|
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Team);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chapter.LocationLocked)
|
if (!chapter.IsPersonRoleLocked(PersonRole.Location))
|
||||||
{
|
{
|
||||||
var people = TagHelper.GetTagValues(comicInfo.Locations);
|
var people = TagHelper.GetTagValues(comicInfo.Locations);
|
||||||
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Location);
|
await UpdateChapterPeopleAsync(chapter, people, PersonRole.Location);
|
||||||
|
@ -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;
|
||||||
|
@ -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 {MetadataSettingField} from "../admin/_models/metadata-setting-field";
|
||||||
import {translate} from "@jsverse/transloco";
|
import {translate} from "@jsverse/transloco";
|
||||||
|
|
||||||
@ -10,9 +10,19 @@ export class MetadataSettingFiledPipe implements PipeTransform {
|
|||||||
|
|
||||||
transform(value: MetadataSettingField): string {
|
transform(value: MetadataSettingField): string {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
|
case MetadataSettingField.ChapterTitle:
|
||||||
|
return translate('metadata-setting-field-pipe.chapter-title');
|
||||||
|
case MetadataSettingField.ChapterSummary:
|
||||||
|
return translate('metadata-setting-field-pipe.chapter-summary');
|
||||||
|
case MetadataSettingField.ChapterReleaseDate:
|
||||||
|
return translate('metadata-setting-field-pipe.chapter-release-date');
|
||||||
|
case MetadataSettingField.ChapterPublisher:
|
||||||
|
return translate('metadata-setting-field-pipe.chapter-publisher');
|
||||||
|
case MetadataSettingField.ChapterCovers:
|
||||||
|
return translate('metadata-setting-field-pipe.chapter-covers');
|
||||||
case MetadataSettingField.AgeRating:
|
case MetadataSettingField.AgeRating:
|
||||||
return translate('metadata-setting-field-pipe.age-rating');
|
return translate('metadata-setting-field-pipe.age-rating');
|
||||||
case MetadataSettingField.People:
|
case MetadataSettingField.People:
|
||||||
return translate('metadata-setting-field-pipe.people');
|
return translate('metadata-setting-field-pipe.people');
|
||||||
case MetadataSettingField.Covers:
|
case MetadataSettingField.Covers:
|
||||||
return translate('metadata-setting-field-pipe.covers');
|
return translate('metadata-setting-field-pipe.covers');
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core';
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
import {PlusMediaFormat} from "../_models/series-detail/external-series-detail";
|
import {PlusMediaFormat} from "../_models/series-detail/external-series-detail";
|
||||||
import {translate} from "@jsverse/transloco";
|
import {translate} from "@jsverse/transloco";
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ export class PlusMediaFormatPipe implements PipeTransform {
|
|||||||
case PlusMediaFormat.Manga:
|
case PlusMediaFormat.Manga:
|
||||||
return translate('library-type-pipe.manga');
|
return translate('library-type-pipe.manga');
|
||||||
case PlusMediaFormat.Comic:
|
case PlusMediaFormat.Comic:
|
||||||
return translate('library-type-pipe.comic');
|
return translate('library-type-pipe.comicVine');
|
||||||
case PlusMediaFormat.LightNovel:
|
case PlusMediaFormat.LightNovel:
|
||||||
return translate('library-type-pipe.lightNovel');
|
return translate('library-type-pipe.lightNovel');
|
||||||
case PlusMediaFormat.Book:
|
case PlusMediaFormat.Book:
|
||||||
|
@ -17,6 +17,8 @@ export class ProviderImagePipe implements PipeTransform {
|
|||||||
return `assets/images/ExternalServices/GoogleBooks${large ? '-lg' : ''}.png`;
|
return `assets/images/ExternalServices/GoogleBooks${large ? '-lg' : ''}.png`;
|
||||||
case ScrobbleProvider.Kavita:
|
case ScrobbleProvider.Kavita:
|
||||||
return `assets/images/logo-${large ? '64' : '32'}.png`;
|
return `assets/images/logo-${large ? '64' : '32'}.png`;
|
||||||
|
case ScrobbleProvider.Cbr:
|
||||||
|
return `assets/images/ExternalServices/ComicBookRoundup.png`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core';
|
|
||||||
import {ScrobbleProvider} from "../_services/scrobbling.service";
|
|
||||||
|
|
||||||
@Pipe({
|
|
||||||
name: 'providerName',
|
|
||||||
standalone: true
|
|
||||||
})
|
|
||||||
export class ProviderNamePipe implements PipeTransform {
|
|
||||||
|
|
||||||
transform(value: ScrobbleProvider): string {
|
|
||||||
switch (value) {
|
|
||||||
case ScrobbleProvider.AniList:
|
|
||||||
return 'AniList';
|
|
||||||
case ScrobbleProvider.Mal:
|
|
||||||
return 'MAL';
|
|
||||||
case ScrobbleProvider.Kavita:
|
|
||||||
return 'Kavita';
|
|
||||||
case ScrobbleProvider.GoogleBooks:
|
|
||||||
return 'Google Books';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -12,6 +12,7 @@ export class ScrobbleProviderNamePipe implements PipeTransform {
|
|||||||
case ScrobbleProvider.AniList: return 'AniList';
|
case ScrobbleProvider.AniList: return 'AniList';
|
||||||
case ScrobbleProvider.Mal: return 'MAL';
|
case ScrobbleProvider.Mal: return 'MAL';
|
||||||
case ScrobbleProvider.Kavita: return 'Kavita';
|
case ScrobbleProvider.Kavita: return 'Kavita';
|
||||||
|
case ScrobbleProvider.Cbr: return 'Comicbook Roundup';
|
||||||
case ScrobbleProvider.GoogleBooks: return 'Google Books';
|
case ScrobbleProvider.GoogleBooks: return 'Google Books';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,9 +12,10 @@ import {UtilityService} from "../shared/_services/utility.service";
|
|||||||
|
|
||||||
export enum ScrobbleProvider {
|
export enum ScrobbleProvider {
|
||||||
Kavita = 0,
|
Kavita = 0,
|
||||||
AniList= 1,
|
AniList = 1,
|
||||||
Mal = 2,
|
Mal = 2,
|
||||||
GoogleBooks = 3
|
GoogleBooks = 3,
|
||||||
|
Cbr = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
@ -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) {
|
||||||
|
@ -91,7 +91,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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,14 @@ export enum MetadataSettingField {
|
|||||||
LocalizedName = 6,
|
LocalizedName = 6,
|
||||||
Covers = 7,
|
Covers = 7,
|
||||||
AgeRating = 8,
|
AgeRating = 8,
|
||||||
People = 9
|
People = 9,
|
||||||
|
|
||||||
|
// Chapter fields
|
||||||
|
ChapterTitle = 10,
|
||||||
|
ChapterSummary = 11,
|
||||||
|
ChapterReleaseDate = 12,
|
||||||
|
ChapterPublisher = 13,
|
||||||
|
ChapterCovers = 14,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const allMetadataSettingField = Object.keys(MetadataSettingField)
|
export const allMetadataSettingField = Object.keys(MetadataSettingField)
|
||||||
|
@ -25,6 +25,14 @@ export interface MetadataSettings {
|
|||||||
enableStartDate: boolean;
|
enableStartDate: boolean;
|
||||||
enableCoverImage: boolean;
|
enableCoverImage: boolean;
|
||||||
enableLocalizedName: boolean;
|
enableLocalizedName: boolean;
|
||||||
|
|
||||||
|
enableChapterSummary: boolean;
|
||||||
|
enableChapterReleaseDate: boolean;
|
||||||
|
enableChapterTitle: boolean;
|
||||||
|
enableChapterPublisher: boolean;
|
||||||
|
enableChapterCoverImage: boolean;
|
||||||
|
|
||||||
|
|
||||||
enableGenres: boolean;
|
enableGenres: boolean;
|
||||||
enableTags: boolean;
|
enableTags: boolean;
|
||||||
firstLastPeopleNaming: boolean;
|
firstLastPeopleNaming: boolean;
|
||||||
|
@ -89,6 +89,70 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-section-break"></div>
|
||||||
|
|
||||||
|
<!-- Chapter-based fields -->
|
||||||
|
<h5>{{t('chapter-header')}}</h5>
|
||||||
|
<div class="row g-0 mt-4 mb-4">
|
||||||
|
@if(settingsForm.get('enableChapterTitle'); as formControl) {
|
||||||
|
<app-setting-switch [title]="t('enable-chapter-title-label')" [subtitle]="t('enable-chapter-title-tooltip')">
|
||||||
|
<ng-template #switch>
|
||||||
|
<div class="form-check form-switch float-end">
|
||||||
|
<input id="enable-chapter-title" type="checkbox" class="form-check-input" formControlName="enableChapterTitle">
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</app-setting-switch>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0 mt-4 mb-4">
|
||||||
|
@if(settingsForm.get('enableChapterSummary'); as formControl) {
|
||||||
|
<app-setting-switch [title]="t('enable-chapter-summary-label')" [subtitle]="t('enable-chapter-summary-tooltip')">
|
||||||
|
<ng-template #switch>
|
||||||
|
<div class="form-check form-switch float-end">
|
||||||
|
<input id="enable-chapter-summary" type="checkbox" class="form-check-input" formControlName="enableChapterSummary">
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</app-setting-switch>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0 mt-4 mb-4">
|
||||||
|
@if(settingsForm.get('enableChapterReleaseDate'); as formControl) {
|
||||||
|
<app-setting-switch [title]="t('enable-chapter-release-date-label')" [subtitle]="t('enable-chapter-release-date-tooltip')">
|
||||||
|
<ng-template #switch>
|
||||||
|
<div class="form-check form-switch float-end">
|
||||||
|
<input id="enable-chapter-release-date" type="checkbox" class="form-check-input" formControlName="enableChapterReleaseDate">
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</app-setting-switch>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0 mt-4 mb-4">
|
||||||
|
@if(settingsForm.get('enableChapterPublisher'); as formControl) {
|
||||||
|
<app-setting-switch [title]="t('enable-chapter-publisher-label')" [subtitle]="t('enable-chapter-publisher-tooltip')">
|
||||||
|
<ng-template #switch>
|
||||||
|
<div class="form-check form-switch float-end">
|
||||||
|
<input id="enable-chapter-publisher" type="checkbox" class="form-check-input" formControlName="enableChapterPublisher">
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</app-setting-switch>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0 mt-4 mb-4">
|
||||||
|
@if(settingsForm.get('enableChapterCoverImage'); as formControl) {
|
||||||
|
<app-setting-switch [title]="t('enable-chapter-cover-label')" [subtitle]="t('enable-chapter-cover-tooltip')">
|
||||||
|
<ng-template #switch>
|
||||||
|
<div class="form-check form-switch float-end">
|
||||||
|
<input id="enable-chapter-cover" type="checkbox" class="form-check-input" formControlName="enableChapterCoverImage">
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</app-setting-switch>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
@if(settingsForm.get('enablePeople'); as formControl) {
|
@if(settingsForm.get('enablePeople'); as formControl) {
|
||||||
<div class="setting-section-break"></div>
|
<div class="setting-section-break"></div>
|
||||||
|
|
||||||
@ -133,6 +197,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="setting-section-break"></div>
|
<div class="setting-section-break"></div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -79,6 +79,13 @@ export class ManageMetadataSettingsComponent implements OnInit {
|
|||||||
this.settingsForm.addControl('enableStartDate', new FormControl(settings.enableStartDate, []));
|
this.settingsForm.addControl('enableStartDate', new FormControl(settings.enableStartDate, []));
|
||||||
this.settingsForm.addControl('enableCoverImage', new FormControl(settings.enableCoverImage, []));
|
this.settingsForm.addControl('enableCoverImage', new FormControl(settings.enableCoverImage, []));
|
||||||
|
|
||||||
|
|
||||||
|
this.settingsForm.addControl('enableChapterTitle', new FormControl(settings.enableChapterTitle, []));
|
||||||
|
this.settingsForm.addControl('enableChapterSummary', new FormControl(settings.enableChapterSummary, []));
|
||||||
|
this.settingsForm.addControl('enableChapterReleaseDate', new FormControl(settings.enableChapterReleaseDate, []));
|
||||||
|
this.settingsForm.addControl('enableChapterPublisher', new FormControl(settings.enableChapterPublisher, []));
|
||||||
|
this.settingsForm.addControl('enableChapterCoverImage', new FormControl(settings.enableChapterCoverImage, []));
|
||||||
|
|
||||||
this.settingsForm.addControl('blacklist', new FormControl((settings.blacklist || '').join(','), []));
|
this.settingsForm.addControl('blacklist', new FormControl((settings.blacklist || '').join(','), []));
|
||||||
this.settingsForm.addControl('whitelist', new FormControl((settings.whitelist || '').join(','), []));
|
this.settingsForm.addControl('whitelist', new FormControl((settings.whitelist || '').join(','), []));
|
||||||
this.settingsForm.addControl('firstLastPeopleNaming', new FormControl((settings.firstLastPeopleNaming), []));
|
this.settingsForm.addControl('firstLastPeopleNaming', new FormControl((settings.firstLastPeopleNaming), []));
|
||||||
|
@ -43,6 +43,7 @@ import {DefaultModalOptions} from "../../../_models/default-modal-options";
|
|||||||
templateUrl: './all-collections.component.html',
|
templateUrl: './all-collections.component.html',
|
||||||
styleUrls: ['./all-collections.component.scss'],
|
styleUrls: ['./all-collections.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
imports: [SideNavCompanionBarComponent, CardDetailLayoutComponent, CardItemComponent, AsyncPipe, DecimalPipe,
|
imports: [SideNavCompanionBarComponent, CardDetailLayoutComponent, CardItemComponent, AsyncPipe, DecimalPipe,
|
||||||
TranslocoDirective, CollectionOwnerComponent, BulkOperationsComponent]
|
TranslocoDirective, CollectionOwnerComponent, BulkOperationsComponent]
|
||||||
})
|
})
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
<div class="under-image">
|
<div class="under-image">
|
||||||
<app-image [imageUrl]="collectionTag.source | providerImage"
|
<app-image [imageUrl]="collectionTag.source | providerImage"
|
||||||
width="16px" height="16px"
|
width="16px" height="16px"
|
||||||
[ngbTooltip]="collectionTag.source | providerName" tabindex="0"></app-image>
|
[ngbTooltip]="collectionTag.source | scrobbleProviderName" tabindex="0"></app-image>
|
||||||
<span class="ms-2 me-2">{{t('sync-progress', {title: series.length + ' / ' + collectionTag.totalSourceCount})}}</span>
|
<span class="ms-2 me-2">{{t('sync-progress', {title: series.length + ' / ' + collectionTag.totalSourceCount})}}</span>
|
||||||
<i class="fa-solid fa-question-circle" aria-hidden="true" [ngbTooltip]="t('last-sync', {date: collectionTag.lastSyncUtc | date: 'short' | defaultDate })"></i>
|
<i class="fa-solid fa-question-circle" aria-hidden="true" [ngbTooltip]="t('last-sync', {date: collectionTag.lastSyncUtc | date: 'short' | defaultDate })"></i>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,20 +56,20 @@ import {User} from "../../../_models/user";
|
|||||||
import {ScrobbleProvider} from "../../../_services/scrobbling.service";
|
import {ScrobbleProvider} from "../../../_services/scrobbling.service";
|
||||||
import {DefaultDatePipe} from "../../../_pipes/default-date.pipe";
|
import {DefaultDatePipe} from "../../../_pipes/default-date.pipe";
|
||||||
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
|
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
|
||||||
import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe";
|
|
||||||
import {
|
import {
|
||||||
SmartCollectionDrawerComponent
|
SmartCollectionDrawerComponent
|
||||||
} from "../../../_single-module/smart-collection-drawer/smart-collection-drawer.component";
|
} from "../../../_single-module/smart-collection-drawer/smart-collection-drawer.component";
|
||||||
import {DefaultModalOptions} from "../../../_models/default-modal-options";
|
import {DefaultModalOptions} from "../../../_models/default-modal-options";
|
||||||
|
import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.pipe";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-collection-detail',
|
selector: 'app-collection-detail',
|
||||||
templateUrl: './collection-detail.component.html',
|
templateUrl: './collection-detail.component.html',
|
||||||
styleUrls: ['./collection-detail.component.scss'],
|
styleUrls: ['./collection-detail.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [SideNavCompanionBarComponent, CardActionablesComponent, ImageComponent, ReadMoreComponent,
|
imports: [SideNavCompanionBarComponent, CardActionablesComponent, ImageComponent, ReadMoreComponent,
|
||||||
BulkOperationsComponent, CardDetailLayoutComponent, SeriesCardComponent, TranslocoDirective, NgbTooltip,
|
BulkOperationsComponent, CardDetailLayoutComponent, SeriesCardComponent, TranslocoDirective, NgbTooltip,
|
||||||
DatePipe, DefaultDatePipe, ProviderImagePipe, ProviderNamePipe, AsyncPipe]
|
DatePipe, DefaultDatePipe, ProviderImagePipe, AsyncPipe, ScrobbleProviderNamePipe]
|
||||||
})
|
})
|
||||||
export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
{{t('collection-via-label')}}
|
{{t('collection-via-label')}}
|
||||||
<app-image [imageUrl]="collection.source | providerImage"
|
<app-image [imageUrl]="collection.source | providerImage"
|
||||||
width="16px" height="16px"
|
width="16px" height="16px"
|
||||||
[ngbTooltip]="collection.source | providerName"
|
[ngbTooltip]="collection.source | scrobbleProviderName"
|
||||||
[attr.aria-label]="collection.source | providerName"></app-image>
|
[attr.aria-label]="collection.source | scrobbleProviderName"></app-image>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core';
|
import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core';
|
||||||
import {ScrobbleProvider} from "../../../_services/scrobbling.service";
|
import {ScrobbleProvider} from "../../../_services/scrobbling.service";
|
||||||
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
|
import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
|
||||||
import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe";
|
|
||||||
import {UserCollection} from "../../../_models/collection-tag";
|
import {UserCollection} from "../../../_models/collection-tag";
|
||||||
import {TranslocoDirective} from "@jsverse/transloco";
|
import {TranslocoDirective} from "@jsverse/transloco";
|
||||||
import {AsyncPipe} from "@angular/common";
|
import {AsyncPipe} from "@angular/common";
|
||||||
import {AccountService} from "../../../_services/account.service";
|
import {AccountService} from "../../../_services/account.service";
|
||||||
import {ImageComponent} from "../../../shared/image/image.component";
|
import {ImageComponent} from "../../../shared/image/image.component";
|
||||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||||
|
import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.pipe";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-collection-owner',
|
selector: 'app-collection-owner',
|
||||||
imports: [
|
imports: [
|
||||||
ProviderImagePipe,
|
ProviderImagePipe,
|
||||||
ProviderNamePipe,
|
|
||||||
TranslocoDirective,
|
TranslocoDirective,
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
ImageComponent,
|
ImageComponent,
|
||||||
NgbTooltip
|
NgbTooltip,
|
||||||
|
ScrobbleProviderNamePipe
|
||||||
],
|
],
|
||||||
templateUrl: './collection-owner.component.html',
|
templateUrl: './collection-owner.component.html',
|
||||||
styleUrl: './collection-owner.component.scss',
|
styleUrl: './collection-owner.component.scss',
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
@for (rating of ratings; track rating.provider + rating.averageScore) {
|
@for (rating of ratings; track rating.provider + rating.averageScore) {
|
||||||
<div class="col-auto custom-col clickable" [ngbPopover]="externalPopContent" [popoverContext]="{rating: rating}"
|
<div class="col-auto custom-col clickable" [ngbPopover]="externalPopContent" [popoverContext]="{rating: rating}"
|
||||||
[popoverTitle]="rating.provider | providerName" popoverClass="sm-popover">
|
[popoverTitle]="rating.provider | scrobbleProviderName" popoverClass="sm-popover">
|
||||||
<span class="badge rounded-pill me-1">
|
<span class="badge rounded-pill me-1">
|
||||||
<img class="me-1" [ngSrc]="rating.provider | providerImage:true" width="24" height="24" alt="" aria-hidden="true">
|
<img class="me-1" [ngSrc]="rating.provider | providerImage:true" width="24" height="24" alt="" aria-hidden="true">
|
||||||
{{rating.averageScore}}%
|
{{rating.averageScore}}%
|
||||||
@ -64,9 +64,11 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #externalPopContent let-rating="rating">
|
<ng-template #externalPopContent let-rating="rating">
|
||||||
<div>
|
@if (rating.favoriteCount > 0) {
|
||||||
<i class="fa-solid fa-heart" aria-hidden="true"></i> {{rating.favoriteCount}}
|
<div>
|
||||||
</div>
|
<i class="fa-solid fa-heart" aria-hidden="true"></i> {{rating.favoriteCount}}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (rating.providerUrl) {
|
@if (rating.providerUrl) {
|
||||||
<a [href]="rating.providerUrl" target="_blank" rel="noreferrer nofollow">{{t('entry-label')}}</a>
|
<a [href]="rating.providerUrl" target="_blank" rel="noreferrer nofollow">{{t('entry-label')}}</a>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component, DestroyRef,
|
Component,
|
||||||
|
DestroyRef,
|
||||||
inject,
|
inject,
|
||||||
Input,
|
Input,
|
||||||
OnInit,
|
OnInit,
|
||||||
@ -13,7 +14,6 @@ import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe";
|
|||||||
import {NgbModal, NgbPopover} from "@ng-bootstrap/ng-bootstrap";
|
import {NgbModal, NgbPopover} from "@ng-bootstrap/ng-bootstrap";
|
||||||
import {LoadingComponent} from "../../../shared/loading/loading.component";
|
import {LoadingComponent} from "../../../shared/loading/loading.component";
|
||||||
import {LibraryType} from "../../../_models/library/library";
|
import {LibraryType} from "../../../_models/library/library";
|
||||||
import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe";
|
|
||||||
import {NgxStarsModule} from "ngx-stars";
|
import {NgxStarsModule} from "ngx-stars";
|
||||||
import {ThemeService} from "../../../_services/theme.service";
|
import {ThemeService} from "../../../_services/theme.service";
|
||||||
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
||||||
@ -23,15 +23,16 @@ import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
|
|||||||
import {ImageService} from "../../../_services/image.service";
|
import {ImageService} from "../../../_services/image.service";
|
||||||
import {AsyncPipe, NgOptimizedImage, NgTemplateOutlet} from "@angular/common";
|
import {AsyncPipe, NgOptimizedImage, NgTemplateOutlet} from "@angular/common";
|
||||||
import {RatingModalComponent} from "../rating-modal/rating-modal.component";
|
import {RatingModalComponent} from "../rating-modal/rating-modal.component";
|
||||||
|
import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.pipe";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-external-rating',
|
selector: 'app-external-rating',
|
||||||
imports: [ProviderImagePipe, NgbPopover, LoadingComponent, ProviderNamePipe, NgxStarsModule, ImageComponent,
|
imports: [ProviderImagePipe, NgbPopover, LoadingComponent, NgxStarsModule, ImageComponent,
|
||||||
TranslocoDirective, SafeHtmlPipe, NgOptimizedImage, AsyncPipe, NgTemplateOutlet],
|
TranslocoDirective, SafeHtmlPipe, NgOptimizedImage, AsyncPipe, NgTemplateOutlet, ScrobbleProviderNamePipe],
|
||||||
templateUrl: './external-rating.component.html',
|
templateUrl: './external-rating.component.html',
|
||||||
styleUrls: ['./external-rating.component.scss'],
|
styleUrls: ['./external-rating.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class ExternalRatingComponent implements OnInit {
|
export class ExternalRatingComponent implements OnInit {
|
||||||
|
|
||||||
|
@ -128,6 +128,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 {
|
||||||
if (this.library === undefined) {
|
if (this.library === undefined) {
|
||||||
this.isAddLibrary = true;
|
this.isAddLibrary = true;
|
||||||
@ -141,11 +146,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(),
|
||||||
@ -208,11 +221,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)
|
||||||
@ -231,7 +249,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;
|
||||||
|
BIN
UI/Web/src/assets/images/ExternalServices/ComicBookRoundup.png
Normal file
BIN
UI/Web/src/assets/images/ExternalServices/ComicBookRoundup.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
@ -806,6 +806,17 @@
|
|||||||
"enable-cover-image-tooltip": "Allow Kavita to write the cover image for the Series",
|
"enable-cover-image-tooltip": "Allow Kavita to write the cover image for the Series",
|
||||||
"enable-start-date-label": "Start Date",
|
"enable-start-date-label": "Start Date",
|
||||||
"enable-start-date-tooltip": "Allow Start Date of Series to be written to the Series",
|
"enable-start-date-tooltip": "Allow Start Date of Series to be written to the Series",
|
||||||
|
|
||||||
|
"enable-chapter-title-label": "Title",
|
||||||
|
"enable-chapter-title-tooltip": "Allow Title of Chapter/Issue to be written",
|
||||||
|
"enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}",
|
||||||
|
"enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}",
|
||||||
|
"enable-chapter-release-date-label": "Release Date",
|
||||||
|
"enable-chapter-release-date-tooltip": "Allow Release Date of Chapter/Issue to be written",
|
||||||
|
"enable-chapter-publisher-label": "Publisher",
|
||||||
|
"enable-chapter-publisher-tooltip": "Allow Publisher of Chapter/Issue to be written",
|
||||||
|
"enable-chapter-cover-label": "Chapter Cover",
|
||||||
|
"enable-chapter-cover-tooltip": "Allow Cover of Chapter/Issue to be set",
|
||||||
"enable-genres-label": "Genres",
|
"enable-genres-label": "Genres",
|
||||||
"enable-genres-tooltip": "Allow Series Genres to be written.",
|
"enable-genres-tooltip": "Allow Series Genres to be written.",
|
||||||
"enable-tags-label": "Tags",
|
"enable-tags-label": "Tags",
|
||||||
@ -827,7 +838,8 @@
|
|||||||
"first-last-name-tooltip": "Ensure People's names are written First then Last",
|
"first-last-name-tooltip": "Ensure People's names are written First then Last",
|
||||||
"person-roles-label": "Roles",
|
"person-roles-label": "Roles",
|
||||||
"overrides-label": "Overrides",
|
"overrides-label": "Overrides",
|
||||||
"overrides-description": "Allow Kavita to write over locked fields."
|
"overrides-description": "Allow Kavita to write over locked fields.",
|
||||||
|
"chapter-header": "Chapter Fields"
|
||||||
},
|
},
|
||||||
|
|
||||||
"book-line-overlay": {
|
"book-line-overlay": {
|
||||||
@ -2686,7 +2698,12 @@
|
|||||||
"start-date": "{{manage-metadata-settings.enable-start-date-label}}",
|
"start-date": "{{manage-metadata-settings.enable-start-date-label}}",
|
||||||
"genres": "{{metadata-fields.genres-title}}",
|
"genres": "{{metadata-fields.genres-title}}",
|
||||||
"tags": "{{metadata-fields.tags-title}}",
|
"tags": "{{metadata-fields.tags-title}}",
|
||||||
"localized-name": "{{edit-series-modal.localized-name-label}}"
|
"localized-name": "{{edit-series-modal.localized-name-label}}",
|
||||||
|
"chapter-release-date": "Release Date (Chapter)",
|
||||||
|
"chapter-summary": "Summary (Chapter)",
|
||||||
|
"chapter-covers": "Covers (Chapter)",
|
||||||
|
"chapter-publisher": "{{person-role-pipe.publisher}} (Chapter)",
|
||||||
|
"chapter-title": "Title (Chapter)"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user