mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Feature/local metadata more tags (#832)
* Stashing code * removed some debug code on series detail page. Now detail is collapsed by default. * Added AgeRating * Fixed a crash when NetVips tries to write a cover file and cover directory is not existing. * When a card is selected for bulk actions, show an outline in addition to select box * Added AgeRating into the metadata parsing. Added a hack where ComicInfo uses Number in ComicInfo rather than Volume. This is to test out the effects on users libraries. * Added AgeRating and ReleaseDate to the metadata implelentation.
This commit is contained in:
parent
46f37069db
commit
af24c928d7
36
API.Tests/Entities/ComicInfoTests.cs
Normal file
36
API.Tests/Entities/ComicInfoTests.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using API.Data.Metadata;
|
||||
using API.Entities.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Entities;
|
||||
|
||||
public class ComicInfoTests
|
||||
{
|
||||
#region ConvertAgeRatingToEnum
|
||||
|
||||
[Theory]
|
||||
[InlineData("G", AgeRating.G)]
|
||||
[InlineData("Everyone", AgeRating.Everyone)]
|
||||
[InlineData("Mature", AgeRating.Mature)]
|
||||
[InlineData("Teen", AgeRating.Teen)]
|
||||
[InlineData("Adults Only 18+", AgeRating.AdultsOnly)]
|
||||
[InlineData("Early Childhood", AgeRating.EarlyChildhood)]
|
||||
[InlineData("Everyone 10+", AgeRating.Everyone10Plus)]
|
||||
[InlineData("Mature 15+", AgeRating.Mature15Plus)]
|
||||
[InlineData("Mature 17+", AgeRating.Mature17Plus)]
|
||||
[InlineData("Rating Pending", AgeRating.RatingPending)]
|
||||
[InlineData("X 18+", AgeRating.X18Plus)]
|
||||
[InlineData("Kids to Adults", AgeRating.KidsToAdults)]
|
||||
[InlineData("NotValid", AgeRating.Unknown)]
|
||||
public void ConvertAgeRatingToEnum_ShouldConvertCorrectly(string input, AgeRating expected)
|
||||
{
|
||||
Assert.Equal(expected, ComicInfo.ConvertAgeRatingToEnum(input));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertAgeRatingToEnum_ShouldCompareCaseInsensitive()
|
||||
{
|
||||
Assert.Equal(AgeRating.Mature, ComicInfo.ConvertAgeRatingToEnum("mature"));
|
||||
}
|
||||
#endregion
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
using API.Entities.Enums;
|
||||
using Xunit;
|
||||
using static API.Parser.Parser;
|
||||
|
||||
@ -5,7 +6,14 @@ namespace API.Tests.Parser
|
||||
{
|
||||
public class ParserTests
|
||||
{
|
||||
|
||||
[Theory]
|
||||
[InlineData("Joe Shmo, Green Blue", "Joe Shmo, Green Blue")]
|
||||
[InlineData("Shmo, Joe", "Shmo, Joe")]
|
||||
[InlineData(" Joe Shmo ", "Joe Shmo")]
|
||||
public void CleanAuthorTest(string input, string expected)
|
||||
{
|
||||
Assert.Equal(expected, CleanAuthor(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Beastars - SP01", true)]
|
||||
@ -140,14 +148,6 @@ namespace API.Tests.Parser
|
||||
Assert.Equal(expected, IsImage(filename));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Joe Smo", "Joe Smo")]
|
||||
[InlineData("Smo, Joe", "Joe Smo")]
|
||||
public void CleanAuthorTest(string author, string expected)
|
||||
{
|
||||
Assert.Equal(expected, CleanAuthor(expected));
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Theory]
|
||||
|
@ -29,6 +29,13 @@ namespace API.Tests.Services
|
||||
_archiveService = new ArchiveService(_logger, _directoryService, new ImageService(Substitute.For<ILogger<ImageService>>(), _directoryService));
|
||||
}
|
||||
|
||||
// [Fact]
|
||||
// public void CleanComicInfo_ShouldMapVolumeAndChapterNormally()
|
||||
// {
|
||||
// // TODO: Implement this
|
||||
// Assert.False(true);
|
||||
// }
|
||||
|
||||
[Theory]
|
||||
[InlineData("flat file.zip", false)]
|
||||
[InlineData("file in folder in folder.zip", true)]
|
||||
|
@ -3,15 +3,18 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Metadata;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Filtering;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
@ -392,5 +395,13 @@ namespace API.Controllers
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, userId));
|
||||
}
|
||||
|
||||
[HttpGet("age-rating")]
|
||||
public ActionResult<string> GetAgeRating(int ageRating)
|
||||
{
|
||||
var val = (AgeRating) ageRating;
|
||||
|
||||
return Ok(val.ToDescription());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,8 +51,14 @@ namespace API.DTOs
|
||||
/// </summary>
|
||||
public DateTime Created { get; init; }
|
||||
/// <summary>
|
||||
/// When the chapter was released.
|
||||
/// </summary>
|
||||
/// <remarks>Metadata field</remarks>
|
||||
public DateTime ReleaseDate { get; init; }
|
||||
/// <summary>
|
||||
/// Title of the Chapter/Issue
|
||||
/// </summary>
|
||||
/// <remarks>Metadata field</remarks>
|
||||
public string TitleName { get; set; }
|
||||
public ICollection<PersonDto> Writers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Penciller { get; set; } = new List<PersonDto>();
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.DTOs.Metadata;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs
|
||||
{
|
||||
@ -19,6 +20,14 @@ namespace API.DTOs
|
||||
public ICollection<PersonDto> Colorists { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Letterers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Editors { get; set; } = new List<PersonDto>();
|
||||
/// <summary>
|
||||
/// Highest Age Rating from all Chapters
|
||||
/// </summary>
|
||||
public AgeRating AgeRating { get; set; } = AgeRating.Unknown;
|
||||
/// <summary>
|
||||
/// Earliest Year from all chapters
|
||||
/// </summary>
|
||||
public int ReleaseYear { get; set; }
|
||||
|
||||
public int SeriesId { get; set; }
|
||||
}
|
||||
|
@ -1,4 +1,9 @@
|
||||
namespace API.Data.Metadata
|
||||
using System;
|
||||
using System.Linq;
|
||||
using API.Entities.Enums;
|
||||
using Kavita.Common.Extensions;
|
||||
|
||||
namespace API.Data.Metadata
|
||||
{
|
||||
/// <summary>
|
||||
/// A representation of a ComicInfo.xml file
|
||||
@ -16,13 +21,25 @@
|
||||
public int PageCount { get; set; }
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public string LanguageISO { get; set; }
|
||||
/// <summary>
|
||||
/// This is the link to where the data was scraped from
|
||||
/// </summary>
|
||||
public string Web { get; set; }
|
||||
public int Day { get; set; }
|
||||
public int Month { get; set; }
|
||||
public int Year { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Rating based on the content. Think PG-13, R for movies
|
||||
/// Rating based on the content. Think PG-13, R for movies. See <see cref="AgeRating"/> for valid types
|
||||
/// </summary>
|
||||
public string AgeRating { get; set; }
|
||||
|
||||
// public AgeRating AgeRating
|
||||
// {
|
||||
// get => ConvertAgeRatingToEnum(_AgeRating);
|
||||
// set => ConvertAgeRatingToEnum(value);
|
||||
// }
|
||||
/// <summary>
|
||||
/// User's rating of the content
|
||||
/// </summary>
|
||||
@ -55,5 +72,11 @@
|
||||
public string Editor { get; set; }
|
||||
public string Publisher { get; set; }
|
||||
|
||||
public static AgeRating ConvertAgeRatingToEnum(string value)
|
||||
{
|
||||
return Enum.GetValues<AgeRating>()
|
||||
.SingleOrDefault(t => t.ToDescription().ToUpperInvariant().Equals(value.ToUpperInvariant()), Entities.Enums.AgeRating.Unknown);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
1199
API/Data/Migrations/20211205185207_MetadataAgeRating.Designer.cs
generated
Normal file
1199
API/Data/Migrations/20211205185207_MetadataAgeRating.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
API/Data/Migrations/20211205185207_MetadataAgeRating.cs
Normal file
26
API/Data/Migrations/20211205185207_MetadataAgeRating.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class MetadataAgeRating : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AgeRating",
|
||||
table: "SeriesMetadata",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AgeRating",
|
||||
table: "SeriesMetadata");
|
||||
}
|
||||
}
|
||||
}
|
1208
API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs
generated
Normal file
1208
API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
public partial class AgeRatingAndReleaseDate : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ReleaseYear",
|
||||
table: "SeriesMetadata",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AgeRating",
|
||||
table: "Chapter",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "ReleaseDate",
|
||||
table: "Chapter",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ReleaseYear",
|
||||
table: "SeriesMetadata");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AgeRating",
|
||||
table: "Chapter");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ReleaseDate",
|
||||
table: "Chapter");
|
||||
}
|
||||
}
|
||||
}
|
@ -289,6 +289,9 @@ namespace API.Data.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AgeRating")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CoverImage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -316,6 +319,9 @@ namespace API.Data.Migrations
|
||||
b.Property<string>("Range")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ReleaseDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -477,6 +483,12 @@ namespace API.Data.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AgeRating")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ReleaseYear")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
@ -41,6 +41,10 @@ namespace API.Entities
|
||||
/// Used for books/specials to display custom title. For non-specials/books, will be set to <see cref="Range"/>
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
/// <summary>
|
||||
/// Age Rating for the issue/chapter
|
||||
/// </summary>
|
||||
public AgeRating AgeRating { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
@ -48,7 +52,10 @@ namespace API.Entities
|
||||
/// </summary>
|
||||
/// <remarks>This should not be confused with Title which is used for special filenames.</remarks>
|
||||
public string TitleName { get; set; } = string.Empty;
|
||||
// public string Year { get; set; } // Only time I can think this will be more than 1 year is for a volume which will be a spread
|
||||
/// <summary>
|
||||
/// Date which chapter was released
|
||||
/// </summary>
|
||||
public DateTime ReleaseDate { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
35
API/Entities/Enums/AgeRating.cs
Normal file
35
API/Entities/Enums/AgeRating.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace API.Entities.Enums;
|
||||
|
||||
public enum AgeRating
|
||||
{
|
||||
[Description("Unknown")]
|
||||
Unknown = 0,
|
||||
[Description("Rating Pending")]
|
||||
RatingPending = 1,
|
||||
[Description("Early Childhood")]
|
||||
EarlyChildhood = 2,
|
||||
[Description("Everyone")]
|
||||
Everyone = 3,
|
||||
[Description("G")]
|
||||
G = 4,
|
||||
[Description("Everyone 10+")]
|
||||
Everyone10Plus = 5,
|
||||
[Description("Kids to Adults")]
|
||||
KidsToAdults = 6,
|
||||
[Description("Teen")]
|
||||
Teen = 7,
|
||||
[Description("Mature 15+")]
|
||||
Mature15Plus = 8,
|
||||
[Description("Mature 17+")]
|
||||
Mature17Plus = 9,
|
||||
[Description("Mature")]
|
||||
Mature = 10,
|
||||
[Description("Adults Only 18+")]
|
||||
AdultsOnly = 11,
|
||||
[Description("X 18+")]
|
||||
X18Plus = 12
|
||||
|
||||
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@ -21,6 +22,14 @@ namespace API.Entities.Metadata
|
||||
/// </summary>
|
||||
public ICollection<Person> People { get; set; } = new List<Person>();
|
||||
|
||||
/// <summary>
|
||||
/// Highest Age Rating from all Chapters
|
||||
/// </summary>
|
||||
public AgeRating AgeRating { get; set; }
|
||||
/// <summary>
|
||||
/// Earliest Year from all chapters
|
||||
/// </summary>
|
||||
public int ReleaseYear { get; set; }
|
||||
|
||||
// Relationship
|
||||
public Series Series { get; set; }
|
||||
|
@ -1071,13 +1071,13 @@ namespace API.Parser
|
||||
/// <summary>
|
||||
/// Cleans an author's name
|
||||
/// </summary>
|
||||
/// <remarks>If the author is Last, First, this will reverse</remarks>
|
||||
/// <remarks>If the author is Last, First, this will not reverse</remarks>
|
||||
/// <param name="author"></param>
|
||||
/// <returns></returns>
|
||||
public static string CleanAuthor(string author)
|
||||
{
|
||||
if (string.IsNullOrEmpty(author)) return string.Empty;
|
||||
return string.Join(" ", author.Split(",").Reverse().Select(s => s.Trim()));
|
||||
return author.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NetVips;
|
||||
|
||||
namespace API
|
||||
{
|
||||
@ -33,19 +32,10 @@ namespace API
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
var isDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker;
|
||||
|
||||
// var migrateLogger = LoggerFactory.Create(builder =>
|
||||
// {
|
||||
// builder
|
||||
// //.AddConfiguration(Configuration.GetSection("Logging"))
|
||||
// .AddFilter("Microsoft", LogLevel.Warning)
|
||||
// .AddFilter("System", LogLevel.Warning)
|
||||
// .AddFilter("SampleApp.Program", LogLevel.Debug)
|
||||
// .AddConsole()
|
||||
// .AddEventLog();
|
||||
// });
|
||||
// var mLogger = migrateLogger.CreateLogger<DirectoryService>();
|
||||
|
||||
// TODO: Figure out a solution for this migration and logger.
|
||||
MigrateConfigFiles.Migrate(isDocker, new DirectoryService(null, new FileSystem()));
|
||||
var directoryService = new DirectoryService(null, new FileSystem());
|
||||
MigrateConfigFiles.Migrate(isDocker, directoryService);
|
||||
|
||||
// Before anything, check if JWT has been generated properly or if user still has default
|
||||
if (!Configuration.CheckIfJwtTokenSet() &&
|
||||
@ -76,7 +66,7 @@ namespace API
|
||||
|
||||
// This doesn't work either
|
||||
//var directoryService = services.GetRequiredService<DirectoryService>();
|
||||
var directoryService = new DirectoryService(null, new FileSystem());
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -335,6 +335,33 @@ namespace API.Services
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void CleanComicInfo(ComicInfo info)
|
||||
{
|
||||
if (info != null)
|
||||
{
|
||||
info.Writer = Parser.Parser.CleanAuthor(info.Writer);
|
||||
info.Colorist = Parser.Parser.CleanAuthor(info.Colorist);
|
||||
info.Editor = Parser.Parser.CleanAuthor(info.Editor);
|
||||
info.Inker = Parser.Parser.CleanAuthor(info.Inker);
|
||||
info.Letterer = Parser.Parser.CleanAuthor(info.Letterer);
|
||||
info.Penciller = Parser.Parser.CleanAuthor(info.Penciller);
|
||||
info.Publisher = Parser.Parser.CleanAuthor(info.Publisher);
|
||||
|
||||
if (!string.IsNullOrEmpty(info.Web))
|
||||
{
|
||||
// TODO: Validate this works through testing
|
||||
// ComicVine stores the Issue number in Number field and does not use Volume.
|
||||
if (info.Web.Contains("https://comicvine.gamespot.com/"))
|
||||
{
|
||||
if (info.Volume.Equals("1"))
|
||||
{
|
||||
info.Volume = Parser.Parser.DefaultVolume;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This can be null if nothing is found or any errors occur during access
|
||||
/// </summary>
|
||||
@ -365,16 +392,7 @@ namespace API.Services
|
||||
using var stream = entry.Open();
|
||||
var serializer = new XmlSerializer(typeof(ComicInfo));
|
||||
var info = (ComicInfo) serializer.Deserialize(stream);
|
||||
if (info != null)
|
||||
{
|
||||
info.Writer = Parser.Parser.CleanAuthor(info.Writer);
|
||||
info.Colorist = Parser.Parser.CleanAuthor(info.Colorist);
|
||||
info.Editor = Parser.Parser.CleanAuthor(info.Editor);
|
||||
info.Inker = Parser.Parser.CleanAuthor(info.Inker);
|
||||
info.Letterer = Parser.Parser.CleanAuthor(info.Letterer);
|
||||
info.Penciller = Parser.Parser.CleanAuthor(info.Penciller);
|
||||
info.Publisher = Parser.Parser.CleanAuthor(info.Publisher);
|
||||
}
|
||||
CleanComicInfo(info);
|
||||
return info;
|
||||
}
|
||||
|
||||
@ -394,16 +412,7 @@ namespace API.Services
|
||||
.Parser
|
||||
.MacOsMetadataFileStartsWith)
|
||||
&& Parser.Parser.IsXml(entry.Key)));
|
||||
if (info != null)
|
||||
{
|
||||
info.Writer = Parser.Parser.CleanAuthor(info.Writer);
|
||||
info.Colorist = Parser.Parser.CleanAuthor(info.Colorist);
|
||||
info.Editor = Parser.Parser.CleanAuthor(info.Editor);
|
||||
info.Inker = Parser.Parser.CleanAuthor(info.Inker);
|
||||
info.Letterer = Parser.Parser.CleanAuthor(info.Letterer);
|
||||
info.Penciller = Parser.Parser.CleanAuthor(info.Penciller);
|
||||
info.Publisher = Parser.Parser.CleanAuthor(info.Publisher);
|
||||
}
|
||||
CleanComicInfo(info);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
@ -133,7 +133,8 @@ public class ImageService : IImageService
|
||||
{
|
||||
using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth);
|
||||
var filename = fileName + ".png";
|
||||
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName + ".png"));
|
||||
_directoryService.ExistOrCreate(_directoryService.CoverImageDirectory);
|
||||
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, filename));
|
||||
return filename;
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.Data;
|
||||
using API.Data.Metadata;
|
||||
using API.Data.Repositories;
|
||||
using API.Data.Scanner;
|
||||
using API.Entities;
|
||||
@ -90,11 +91,20 @@ public class MetadataService : IMetadataService
|
||||
var comicInfo = _readingItemService.GetComicInfo(firstFile.FilePath, firstFile.Format);
|
||||
if (comicInfo == null) return;
|
||||
|
||||
chapter.AgeRating = ComicInfo.ConvertAgeRatingToEnum(comicInfo.AgeRating);
|
||||
|
||||
if (!string.IsNullOrEmpty(comicInfo.Title))
|
||||
{
|
||||
chapter.TitleName = comicInfo.Title.Trim();
|
||||
}
|
||||
|
||||
if (comicInfo.Year > 0 && comicInfo.Month > 0)
|
||||
{
|
||||
var day = Math.Max(comicInfo.Day, 1);
|
||||
var month = Math.Max(comicInfo.Month, 1);
|
||||
chapter.ReleaseDate = DateTime.Parse($"{month}/{day}/{comicInfo.Year}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(comicInfo.Colorist))
|
||||
{
|
||||
var people = comicInfo.Colorist.Split(",");
|
||||
@ -230,7 +240,8 @@ public class MetadataService : IMetadataService
|
||||
// Summary Info
|
||||
if (!string.IsNullOrEmpty(comicInfo.Summary))
|
||||
{
|
||||
series.Metadata.Summary = comicInfo.Summary; // NOTE: I can move this to the bottom as I have a comicInfo selection, save me an extra read
|
||||
// PERF: I can move this to the bottom as I have a comicInfo selection, save me an extra read
|
||||
series.Metadata.Summary = comicInfo.Summary;
|
||||
}
|
||||
|
||||
foreach (var chapter in series.Volumes.SelectMany(volume => volume.Chapters))
|
||||
@ -270,6 +281,13 @@ public class MetadataService : IMetadataService
|
||||
.Where(ci => ci != null)
|
||||
.ToList();
|
||||
|
||||
//var firstComicInfo = comicInfos.First(i => i.)
|
||||
|
||||
// Set the AgeRating as highest in all the comicInfos
|
||||
series.Metadata.AgeRating = comicInfos.Max(i => ComicInfo.ConvertAgeRatingToEnum(comicInfo.AgeRating));
|
||||
series.Metadata.ReleaseYear = series.Volumes
|
||||
.SelectMany(volume => volume.Chapters).Min(c => c.ReleaseDate.Year);
|
||||
|
||||
var genres = comicInfos.SelectMany(i => i.Genre.Split(",")).Distinct().ToList();
|
||||
var people = series.Volumes.SelectMany(volume => volume.Chapters).SelectMany(c => c.People).ToList();
|
||||
|
||||
@ -280,7 +298,6 @@ public class MetadataService : IMetadataService
|
||||
GenreHelper.UpdateGenre(allGenres, genres, false, genre => GenreHelper.AddGenreIfNotExists(series.Metadata.Genres, genre));
|
||||
GenreHelper.KeepOnlySameGenreBetweenLists(series.Metadata.Genres, genres.Select(g => DbFactory.Genre(g, false)).ToList(),
|
||||
genre => series.Metadata.Genres.Remove(genre));
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -324,6 +341,7 @@ public class MetadataService : IMetadataService
|
||||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||
public async Task RefreshMetadata(int libraryId, bool forceUpdate = false)
|
||||
{
|
||||
// TODO: Think about splitting the comicinfo stuff into a separate task
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
||||
_logger.LogInformation("[MetadataService] Beginning metadata refresh of {LibraryName}", library.Name);
|
||||
|
||||
|
@ -19,7 +19,10 @@ export interface Chapter {
|
||||
created: string;
|
||||
|
||||
titleName: string;
|
||||
year: string;
|
||||
/**
|
||||
* This is only Year and Month, Day is not supported from underlying sources
|
||||
*/
|
||||
releaseDate: string;
|
||||
writers: Array<Person>;
|
||||
penciller: Array<Person>;
|
||||
inker: Array<Person>;
|
||||
|
15
UI/Web/src/app/_models/metadata/age-rating.ts
Normal file
15
UI/Web/src/app/_models/metadata/age-rating.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export enum AgeRating {
|
||||
Unknown = 0,
|
||||
AdultsOnly = 1,
|
||||
EarlyChildhood = 2,
|
||||
Everyone = 3,
|
||||
Everyone10Plus = 4,
|
||||
G = 5,
|
||||
KidsToAdults = 6,
|
||||
Mature = 7,
|
||||
Mature15Plus = 8,
|
||||
Mature17Plus = 9,
|
||||
RatingPending = 10,
|
||||
Teen = 11,
|
||||
X18Plus = 12
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { CollectionTag } from "./collection-tag";
|
||||
import { Genre } from "./genre";
|
||||
import { AgeRating } from "./metadata/age-rating";
|
||||
import { Person } from "./person";
|
||||
|
||||
export interface SeriesMetadata {
|
||||
@ -16,6 +17,7 @@ export interface SeriesMetadata {
|
||||
colorists: Array<Person>;
|
||||
letterers: Array<Person>;
|
||||
editors: Array<Person>;
|
||||
|
||||
ageRating: AgeRating;
|
||||
releaseYear: number;
|
||||
seriesId: number;
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { ChapterMetadata } from '../_models/chapter-metadata';
|
||||
import { AgeRating } from '../_models/metadata/age-rating';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -10,9 +13,25 @@ export class MetadataService {
|
||||
|
||||
baseUrl = environment.apiUrl;
|
||||
|
||||
private ageRatingTypes: {[key: number]: string} | undefined = undefined;
|
||||
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
|
||||
getChapterMetadata(chapterId: number) {
|
||||
return this.httpClient.get<ChapterMetadata>(this.baseUrl + 'series/chapter-metadata?chapterId=' + chapterId);
|
||||
// getChapterMetadata(chapterId: number) {
|
||||
// return this.httpClient.get<ChapterMetadata>(this.baseUrl + 'series/chapter-metadata?chapterId=' + chapterId);
|
||||
// }
|
||||
|
||||
getAgeRating(ageRating: AgeRating) {
|
||||
if (this.ageRatingTypes != undefined && this.ageRatingTypes.hasOwnProperty(ageRating)) {
|
||||
return of(this.ageRatingTypes[ageRating]);
|
||||
}
|
||||
return this.httpClient.get<string>(this.baseUrl + 'series/age-rating?ageRating=' + ageRating, {responseType: 'text' as 'json'}).pipe(map(l => {
|
||||
if (this.ageRatingTypes === undefined) {
|
||||
this.ageRatingTypes = {};
|
||||
}
|
||||
|
||||
this.ageRatingTypes[ageRating] = l;
|
||||
return this.ageRatingTypes[ageRating];
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="card">
|
||||
<div class="card {{selected ? 'selected-highlight' : ''}}">
|
||||
<div class="overlay" (click)="handleClick($event)">
|
||||
<img *ngIf="total > 0 || supressArchiveWarning" class="img-top lazyload" [src]="imageService.placeholderImage" [attr.data-src]="imageUrl"
|
||||
(error)="imageService.updateErroredImage($event)" aria-hidden="true" height="230px" width="158px">
|
||||
|
@ -38,6 +38,9 @@ $image-width: 160px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.selected-highlight {
|
||||
outline: 2px solid colors.$primary-color;
|
||||
}
|
||||
|
||||
|
||||
.img-top {
|
||||
|
@ -1,14 +1,14 @@
|
||||
<ng-container *ngIf="chapter !== undefined">
|
||||
<div class="container-fluid">
|
||||
<h4>{{chapter.range}}</h4>
|
||||
Title: {{chapter.titleName || '-'}}
|
||||
<!-- Year: {{metadata.year || '-'}} -->
|
||||
Arc Information
|
||||
<!-- <h4>{{libraryType !== LibraryType.Comic ? 'Chapter ' : 'Issue #'}} {{chapter.number}} <span title="Id">({{chapter.id}})</span></h4> -->
|
||||
|
||||
|
||||
<!-- Arc Information -->
|
||||
|
||||
|
||||
<div class="row no-gutters">
|
||||
<div class="col">
|
||||
Id: {{chapter.id}}
|
||||
Title: {{chapter.titleName || '-'}}
|
||||
</div>
|
||||
<div class="col">
|
||||
Pages: {{chapter.pages}}
|
||||
@ -20,7 +20,7 @@
|
||||
Added: {{(chapter.created | date: 'short') || '-'}}
|
||||
</div>
|
||||
<div class="col">
|
||||
Pages: {{chapter.pages}}
|
||||
Release Date: {{(chapter.releaseDate | date: 'shortDate') || '-'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -33,7 +33,8 @@
|
||||
<div class="media-body">
|
||||
<h5 class="mt-0 mb-1">
|
||||
<span *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
<!-- <span>
|
||||
<!-- TODO: Add back in
|
||||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions" [labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
</span> -->
|
||||
|
@ -55,16 +55,16 @@
|
||||
<app-read-more [text]="seriesSummary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
<div *ngIf="seriesMetadata" class="mt-2">
|
||||
<app-series-metadata-detail [seriesMetadata]="seriesMetadata"></app-series-metadata-detail>
|
||||
<app-series-metadata-detail [seriesMetadata]="seriesMetadata" [series]="series"></app-series-metadata-detail>
|
||||
|
||||
<div class="row no-gutters mt-1" *ngIf="series.format != MangaFormat.UNKNOWN">
|
||||
<!-- <div class="row no-gutters mt-1" *ngIf="series.format != MangaFormat.UNKNOWN">
|
||||
<div class="col-md-4">
|
||||
<h5>Type</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-tag-badge [selectionMode]="TagBadgeCursor.NotAllowed"><app-series-format [format]="series.format">{{utilityService.mangaFormat(series.format)}}</app-series-format></app-tag-badge>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -1,3 +1,16 @@
|
||||
<!-- This first row will have random information about the series-->
|
||||
<div class="row no-gutters" *ngIf="seriesMetadata.ageRating">
|
||||
<app-tag-badge title="Age Rating">{{ageRatingName}}</app-tag-badge>
|
||||
<ng-container *ngIf="series">
|
||||
<!-- Maybe we can put the library this resides in to make it easier to get back -->
|
||||
<!-- tooltip here explaining how this is year of first issue -->
|
||||
<app-tag-badge *ngIf="seriesMetadata.releaseYear > 0" title="Release date">{{seriesMetadata.releaseYear}}</app-tag-badge>
|
||||
<app-tag-badge [selectionMode]="TagBadgeCursor.NotAllowed">
|
||||
<app-series-format [format]="series.format">{{utilityService.mangaFormat(series.format)}}</app-series-format>
|
||||
</app-tag-badge>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters" *ngIf="seriesMetadata.genres && seriesMetadata.genres.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Genres</h5>
|
||||
@ -31,7 +44,6 @@
|
||||
</div>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
|
||||
Stuff
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.artists && seriesMetadata.artists.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Artists</h5>
|
||||
@ -41,15 +53,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.publishers && seriesMetadata.publishers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Publishers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.publishers" [person]="person"></app-person-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.characters && seriesMetadata.characters.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Characters</h5>
|
||||
@ -59,12 +62,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.pencillers && seriesMetadata.pencillers.length > 0">
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.colorists && seriesMetadata.colorists.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Pencillers</h5>
|
||||
<h5>Colorists</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.pencillers" [person]="person"></app-person-badge>
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.colorists" [person]="person"></app-person-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.editors && seriesMetadata.editors.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Editors</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.editors" [person]="person"></app-person-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -77,15 +89,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.colorists && seriesMetadata.colorists.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Colorists</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.colorists" [person]="person"></app-person-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.letterers && seriesMetadata.letterers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Letterers</h5>
|
||||
@ -95,12 +98,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.editors && seriesMetadata.editors.length > 0">
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.pencillers && seriesMetadata.pencillers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Editors</h5>
|
||||
<h5>Pencillers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.editors" [person]="person"></app-person-badge>
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.pencillers" [person]="person"></app-person-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.publishers && seriesMetadata.publishers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Publishers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-person-badge *ngFor="let person of seriesMetadata.publishers" [person]="person"></app-person-badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -2,7 +2,9 @@ import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/cor
|
||||
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
|
||||
import { UtilityService } from '../shared/_services/utility.service';
|
||||
import { MangaFormat } from '../_models/manga-format';
|
||||
import { Series } from '../_models/series';
|
||||
import { SeriesMetadata } from '../_models/series-metadata';
|
||||
import { MetadataService } from '../_services/metadata.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-metadata-detail',
|
||||
@ -12,10 +14,16 @@ import { SeriesMetadata } from '../_models/series-metadata';
|
||||
export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input() seriesMetadata!: SeriesMetadata;
|
||||
@Input() series!: Series;
|
||||
|
||||
isCollapsed: boolean = false;
|
||||
isCollapsed: boolean = true;
|
||||
hasExtendedProperites: boolean = false;
|
||||
|
||||
/**
|
||||
* String representation of AgeRating enum
|
||||
*/
|
||||
ageRatingName: string = '';
|
||||
|
||||
get MangaFormat(): typeof MangaFormat {
|
||||
return MangaFormat;
|
||||
}
|
||||
@ -24,7 +32,7 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
||||
return TagBadgeCursor;
|
||||
}
|
||||
|
||||
constructor(public utilityService: UtilityService) { }
|
||||
constructor(public utilityService: UtilityService, private metadataService: MetadataService) { }
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.hasExtendedProperites = this.seriesMetadata.colorists.length > 0 ||
|
||||
@ -34,6 +42,10 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
||||
this.seriesMetadata.letterers.length > 0 ||
|
||||
this.seriesMetadata.pencillers.length > 0 ||
|
||||
this.seriesMetadata.publishers.length > 0;
|
||||
|
||||
this.metadataService.getAgeRating(this.seriesMetadata.ageRating).subscribe(rating => {
|
||||
this.ageRatingName = rating;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -43,4 +55,5 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { AgeRating } from 'src/app/_models/metadata/age-rating';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user