ComicVine Finish Line (#2779)
This commit is contained in:
parent
4ccac5f479
commit
353d44a882
57 changed files with 8108 additions and 1116 deletions
|
@ -6,17 +6,17 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.15" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.15" />
|
||||
<PackageReference Include="xunit" Version="2.6.6" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.28" />
|
||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.28" />
|
||||
<PackageReference Include="xunit" Version="2.7.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
|
|
@ -77,6 +77,53 @@ public class SeriesExtensionsTests
|
|||
{
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
.WithVolume(new VolumeBuilder(Parser.LooseLeafVolume)
|
||||
.WithName(Parser.LooseLeafVolume)
|
||||
.WithChapter(new ChapterBuilder("-1")
|
||||
.WithCoverImage("Chapter -1")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("0.5")
|
||||
.WithCoverImage("Chapter 0.5")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("2")
|
||||
.WithCoverImage("Chapter 2")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("1")
|
||||
.WithCoverImage("Chapter 1")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("3")
|
||||
.WithCoverImage("Chapter 3")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("4AU")
|
||||
.WithCoverImage("Chapter 4AU")
|
||||
.Build())
|
||||
.Build())
|
||||
|
||||
.Build();
|
||||
|
||||
|
||||
Assert.Equal("Chapter 1", series.GetCoverImage());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the case where there are specials and loose leafs, loose leaf chapters should be preferred
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetCoverImage_LooseChapters_WithSub1_Chapter_WithSpecials()
|
||||
{
|
||||
var series = new SeriesBuilder("Test 1")
|
||||
.WithFormat(MangaFormat.Archive)
|
||||
|
||||
.WithVolume(new VolumeBuilder(Parser.SpecialVolume)
|
||||
.WithName(Parser.SpecialVolume)
|
||||
.WithChapter(new ChapterBuilder("I am a Special")
|
||||
.WithCoverImage("I am a Special")
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("I am a Special 2")
|
||||
.WithCoverImage("I am a Special 2")
|
||||
.Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder(Parser.LooseLeafVolume)
|
||||
.WithName(Parser.LooseLeafVolume)
|
||||
.WithChapter(new ChapterBuilder("0.5")
|
||||
|
|
|
@ -208,8 +208,9 @@ public class ComicParsingTests
|
|||
[InlineData("Batman Beyond Omnibus (1999)", true)]
|
||||
[InlineData("Batman Beyond Omnibus", true)]
|
||||
[InlineData("01 Annual Batman Beyond", true)]
|
||||
[InlineData("Blood Syndicate Annual #001", true)]
|
||||
public void IsComicSpecialTest(string input, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.IsComicSpecial(input));
|
||||
Assert.Equal(expected, Parser.IsComicSpecial(input));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -721,6 +721,45 @@ public class DirectoryServiceTests
|
|||
|
||||
#endregion
|
||||
|
||||
#region FindLowestDirectoriesFromFiles
|
||||
|
||||
[Theory]
|
||||
[InlineData(new [] {"C:/Manga/"},
|
||||
new [] {"C:/Manga/Love Hina/Vol. 01.cbz"},
|
||||
"C:/Manga/Love Hina")]
|
||||
[InlineData(new [] {"C:/Manga/"},
|
||||
new [] {"C:/Manga/Romance/Love Hina/Vol. 01.cbz"},
|
||||
"C:/Manga/Romance/Love Hina")]
|
||||
[InlineData(new [] {"C:/Manga/Dir 1/", "c://Manga/Dir 2/"},
|
||||
new [] {"C:/Manga/Dir 1/Love Hina/Vol. 01.cbz"},
|
||||
"C:/Manga/Dir 1/Love Hina")]
|
||||
[InlineData(new [] {"C:/Manga/Dir 1/", "c://Manga/"},
|
||||
new [] {"D:/Manga/Love Hina/Vol. 01.cbz", "D:/Manga/Vol. 01.cbz"},
|
||||
null)]
|
||||
[InlineData(new [] {"C:/Manga/"},
|
||||
new [] {"C:/Manga//Love Hina/Vol. 01.cbz"},
|
||||
"C:/Manga/Love Hina")]
|
||||
[InlineData(new [] {@"C:\mount\drive\Library\Test Library\Comics\"},
|
||||
new [] {@"C:\mount\drive\Library\Test Library\Comics\Bruce Lee (1994)\Bruce Lee #001 (1994).cbz"},
|
||||
@"C:/mount/drive/Library/Test Library/Comics/Bruce Lee (1994)")]
|
||||
public void FindLowestDirectoriesFromFilesTest(string[] rootDirectories, string[] files, string expectedDirectory)
|
||||
{
|
||||
var fileSystem = new MockFileSystem();
|
||||
foreach (var directory in rootDirectories)
|
||||
{
|
||||
fileSystem.AddDirectory(directory);
|
||||
}
|
||||
foreach (var f in files)
|
||||
{
|
||||
fileSystem.AddFile(f, new MockFileData(""));
|
||||
}
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
|
||||
var actual = ds.FindLowestDirectoriesFromFiles(rootDirectories, files);
|
||||
Assert.Equal(expectedDirectory, actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region GetFoldersTillRoot
|
||||
|
||||
[Theory]
|
||||
|
|
|
@ -53,30 +53,30 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="30.1.0" />
|
||||
<PackageReference Include="MailKit" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
|
||||
<PackageReference Include="CsvHelper" Version="31.0.2" />
|
||||
<PackageReference Include="MailKit" Version="4.4.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="Docnet.Core" Version="2.6.0" />
|
||||
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
|
||||
<PackageReference Include="ExCSS" Version="4.2.4" />
|
||||
<PackageReference Include="ExCSS" Version="4.2.5" />
|
||||
<PackageReference Include="Flurl" Version="3.0.7" />
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||
<PackageReference Include="Hangfire" Version="1.8.9" />
|
||||
<PackageReference Include="Hangfire.InMemory" Version="0.7.0" />
|
||||
<PackageReference Include="Hangfire" Version="1.8.11" />
|
||||
<PackageReference Include="Hangfire.InMemory" Version="0.8.0" />
|
||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.58" />
|
||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.1" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.59" />
|
||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.9" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
|
||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||
|
@ -94,16 +94,16 @@
|
|||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||
<PackageReference Include="SharpCompress" Version="0.36.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.2" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.19.0.84025">
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.21.0.86780">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="20.0.15" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.1" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.4.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="20.0.28" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.3" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -33,13 +33,14 @@ public class CblController : BaseApiController
|
|||
/// <param name="file">FormBody with parameter name of cbl</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("validate")]
|
||||
public async Task<ActionResult<CblImportSummaryDto>> ValidateCbl([FromForm(Name = "cbl")] IFormFile file)
|
||||
public async Task<ActionResult<CblImportSummaryDto>> ValidateCbl([FromForm(Name = "cbl")] IFormFile file,
|
||||
[FromForm(Name = "comicVineMatching")] bool comicVineMatching = false)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
try
|
||||
{
|
||||
var cbl = await SaveAndLoadCblFile(file);
|
||||
var importSummary = await _readingListService.ValidateCblFile(userId, cbl);
|
||||
var importSummary = await _readingListService.ValidateCblFile(userId, cbl, comicVineMatching);
|
||||
importSummary.FileName = file.FileName;
|
||||
return Ok(importSummary);
|
||||
}
|
||||
|
@ -83,13 +84,14 @@ public class CblController : BaseApiController
|
|||
/// <param name="dryRun">If true, will only emulate the import but not perform. This should be done to preview what will happen</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("import")]
|
||||
public async Task<ActionResult<CblImportSummaryDto>> ImportCbl([FromForm(Name = "cbl")] IFormFile file, [FromForm(Name = "dryRun")] bool dryRun = false)
|
||||
public async Task<ActionResult<CblImportSummaryDto>> ImportCbl([FromForm(Name = "cbl")] IFormFile file,
|
||||
[FromForm(Name = "dryRun")] bool dryRun = false, [FromForm(Name = "comicVineMatching")] bool comicVineMatching = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var cbl = await SaveAndLoadCblFile(file);
|
||||
var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cbl, dryRun);
|
||||
var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cbl, dryRun, comicVineMatching);
|
||||
importSummary.FileName = file.FileName;
|
||||
return Ok(importSummary);
|
||||
} catch (ArgumentNullException)
|
||||
|
|
|
@ -49,6 +49,8 @@ public enum FilterField
|
|||
/// Average rating from Kavita+ - Not usable for non-licensed users
|
||||
/// </summary>
|
||||
AverageRating = 28,
|
||||
Imprint = 29
|
||||
Imprint = 29,
|
||||
Team = 30,
|
||||
Location = 31
|
||||
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ public class ChapterMetadataDto
|
|||
public ICollection<PersonDto> Letterers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Editors { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Translators { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Teams { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Locations { get; set; } = new List<PersonDto>();
|
||||
|
||||
public ICollection<GenreTagDto> Genres { get; set; } = new List<GenreTagDto>();
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Xml.Serialization;
|
||||
using API.Data.Metadata;
|
||||
|
||||
namespace API.DTOs.ReadingLists.CBL;
|
||||
|
||||
|
@ -21,6 +22,12 @@ public class CblBook
|
|||
[XmlAttribute("Year")]
|
||||
public string Year { get; set; }
|
||||
/// <summary>
|
||||
/// Main Series, Annual, Limited Series
|
||||
/// </summary>
|
||||
/// <remarks>This maps to <see cref="ComicInfo">Format</see> tag</remarks>
|
||||
[XmlAttribute("Format")]
|
||||
public string Format { get; set; }
|
||||
/// <summary>
|
||||
/// The underlying filetype
|
||||
/// </summary>
|
||||
/// <remarks>This is not part of the standard and explicitly for Kavita to support non cbz/cbr files</remarks>
|
||||
|
|
|
@ -35,6 +35,9 @@ public class SeriesMetadataDto
|
|||
public ICollection<PersonDto> Letterers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Editors { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Translators { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Teams { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Locations { get; set; } = new List<PersonDto>();
|
||||
|
||||
/// <summary>
|
||||
/// Highest Age Rating from all Chapters
|
||||
/// </summary>
|
||||
|
@ -86,6 +89,8 @@ public class SeriesMetadataDto
|
|||
public bool PencillerLocked { get; set; }
|
||||
public bool PublisherLocked { get; set; }
|
||||
public bool TranslatorLocked { get; set; }
|
||||
public bool TeamLocked { get; set; }
|
||||
public bool LocationLocked { get; set; }
|
||||
public bool CoverArtistLocked { get; set; }
|
||||
public bool ReleaseYearLocked { get; set; }
|
||||
|
||||
|
|
|
@ -129,6 +129,8 @@ public class ComicInfo
|
|||
public string Publisher { get; set; } = string.Empty;
|
||||
public string Imprint { get; set; } = string.Empty;
|
||||
public string Characters { get; set; } = string.Empty;
|
||||
public string Teams { get; set; } = string.Empty;
|
||||
public string Locations { get; set; } = string.Empty;
|
||||
|
||||
|
||||
public static AgeRating ConvertAgeRatingToEnum(string value)
|
||||
|
@ -157,6 +159,8 @@ public class ComicInfo
|
|||
info.Characters = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Characters);
|
||||
info.Translator = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Translator);
|
||||
info.CoverArtist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.CoverArtist);
|
||||
info.Teams = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Teams);
|
||||
info.Locations = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Locations);
|
||||
|
||||
// We need to convert GTIN to ISBN
|
||||
if (!string.IsNullOrEmpty(info.GTIN))
|
||||
|
|
2892
API/Data/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs
generated
Normal file
2892
API/Data/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
28
API/Data/Migrations/20240313112552_SeriesLowestFolderPath.cs
Normal file
28
API/Data/Migrations/20240313112552_SeriesLowestFolderPath.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SeriesLowestFolderPath : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "LowestFolderPath",
|
||||
table: "Series",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LowestFolderPath",
|
||||
table: "Series");
|
||||
}
|
||||
}
|
||||
}
|
2898
API/Data/Migrations/20240314194402_TeamsAndLocations.Designer.cs
generated
Normal file
2898
API/Data/Migrations/20240314194402_TeamsAndLocations.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
40
API/Data/Migrations/20240314194402_TeamsAndLocations.cs
Normal file
40
API/Data/Migrations/20240314194402_TeamsAndLocations.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class TeamsAndLocations : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "LocationLocked",
|
||||
table: "SeriesMetadata",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "TeamLocked",
|
||||
table: "SeriesMetadata",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LocationLocked",
|
||||
table: "SeriesMetadata");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TeamLocked",
|
||||
table: "SeriesMetadata");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.3");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
|
@ -1268,6 +1268,9 @@ namespace API.Data.Migrations
|
|||
b.Property<bool>("LettererLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LocationLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -1305,6 +1308,9 @@ namespace API.Data.Migrations
|
|||
b.Property<bool>("TagsLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("TeamLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TotalCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -1680,6 +1686,9 @@ namespace API.Data.Migrations
|
|||
b.Property<bool>("LocalizedNameLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LowestFolderPath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MaxHoursToRead")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
|
|
@ -318,7 +318,7 @@ public class LibraryRepository : ILibraryRepository
|
|||
/// <returns></returns>
|
||||
public async Task<bool> DoAnySeriesFoldersMatch(IEnumerable<string> folders)
|
||||
{
|
||||
var normalized = folders.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath);
|
||||
var normalized = folders.Select(Parser.NormalizePath);
|
||||
return await _context.Series.AnyAsync(s => normalized.Contains(s.FolderPath));
|
||||
}
|
||||
|
||||
|
|
|
@ -1185,6 +1185,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
FilterField.Colorist => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Inker => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Imprint => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Team => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Location => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Penciller => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Writers => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Genres => query.HasGenre(true, statement.Comparison, (IList<int>) value),
|
||||
|
@ -2053,7 +2055,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
foreach (var series in info)
|
||||
{
|
||||
if (series.FolderPath == null) continue;
|
||||
if (!map.ContainsKey(series.FolderPath))
|
||||
if (!map.TryGetValue(series.FolderPath, out var value))
|
||||
{
|
||||
map.Add(series.FolderPath, new List<SeriesModified>()
|
||||
{
|
||||
|
@ -2062,9 +2064,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
}
|
||||
else
|
||||
{
|
||||
map[series.FolderPath].Add(series);
|
||||
value.Add(series);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return map;
|
||||
|
|
|
@ -28,7 +28,7 @@ public enum PersonRole
|
|||
/// <summary>
|
||||
/// The publisher before another Publisher bought
|
||||
/// </summary>
|
||||
Imprint = 13
|
||||
|
||||
|
||||
Imprint = 13,
|
||||
Team = 14,
|
||||
Location = 15
|
||||
}
|
||||
|
|
|
@ -73,10 +73,11 @@ public class SeriesMetadata : IHasConcurrencyToken
|
|||
public bool PencillerLocked { get; set; }
|
||||
public bool PublisherLocked { get; set; }
|
||||
public bool TranslatorLocked { get; set; }
|
||||
public bool TeamLocked { get; set; }
|
||||
public bool LocationLocked { get; set; }
|
||||
public bool CoverArtistLocked { get; set; }
|
||||
public bool ReleaseYearLocked { get; set; }
|
||||
|
||||
|
||||
// Relationship
|
||||
public Series Series { get; set; } = null!;
|
||||
public int SeriesId { get; set; }
|
||||
|
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||
using API.Entities.Enums;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Entities.Metadata;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
|
||||
namespace API.Entities;
|
||||
|
||||
|
@ -65,6 +64,11 @@ public class Series : IEntityDate, IHasReadTimeEstimate
|
|||
/// <remarks><see cref="Services.Tasks.Scanner.Parser.Parser.NormalizePath"/> must be used before setting</remarks>
|
||||
public string? FolderPath { get; set; }
|
||||
/// <summary>
|
||||
/// Lowest path (that is under library root) that contains all files for the series.
|
||||
/// </summary>
|
||||
/// <remarks><see cref="Services.Tasks.Scanner.Parser.Parser.NormalizePath"/> must be used before setting</remarks>
|
||||
public string? LowestFolderPath { get; set; }
|
||||
/// <summary>
|
||||
/// Last time the folder was scanned
|
||||
/// </summary>
|
||||
public DateTime LastFolderScanned { get; set; }
|
||||
|
|
|
@ -22,6 +22,12 @@ public static class SeriesExtensions
|
|||
var firstVolume = volumes.GetCoverImage(series.Format);
|
||||
if (firstVolume == null) return null;
|
||||
|
||||
// If first volume here is specials, move to the next as specials should almost always be last.
|
||||
if (firstVolume.MinNumber.Is(Parser.SpecialVolumeNumber) && volumes.Count > 1)
|
||||
{
|
||||
firstVolume = volumes[1];
|
||||
}
|
||||
|
||||
var chapters = firstVolume.Chapters
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.ToList();
|
||||
|
|
|
@ -70,6 +70,16 @@ public static class VolumeListExtensions
|
|||
return volumes.FirstOrDefault(v => v.MinNumber.Is(Parser.DefaultChapterNumber));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first (and only) special volume or null if none
|
||||
/// </summary>
|
||||
/// <param name="volumes"></param>
|
||||
/// <returns></returns>
|
||||
public static Volume? GetSpecialVolumeOrDefault(this IEnumerable<Volume> volumes)
|
||||
{
|
||||
return volumes.FirstOrDefault(v => v.MinNumber.Is(Parser.SpecialVolumeNumber));
|
||||
}
|
||||
|
||||
public static IEnumerable<VolumeDto> WhereNotLooseLeaf(this IEnumerable<VolumeDto> volumes)
|
||||
{
|
||||
return volumes.Where(v => v.MinNumber.Is(Parser.DefaultChapterNumber));
|
||||
|
|
|
@ -128,6 +128,14 @@ public class AutoMapperProfiles : Profile
|
|||
opt =>
|
||||
opt.MapFrom(
|
||||
src => src.People.Where(p => p.Role == PersonRole.Editor).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Teams,
|
||||
opt =>
|
||||
opt.MapFrom(
|
||||
src => src.People.Where(p => p.Role == PersonRole.Team).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Locations,
|
||||
opt =>
|
||||
opt.MapFrom(
|
||||
src => src.People.Where(p => p.Role == PersonRole.Location).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Genres,
|
||||
opt =>
|
||||
opt.MapFrom(
|
||||
|
@ -174,7 +182,14 @@ public class AutoMapperProfiles : Profile
|
|||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Character).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Editors,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Editor).OrderBy(p => p.NormalizedName)));
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Editor).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Teams,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Team).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Locations,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Location).OrderBy(p => p.NormalizedName)))
|
||||
;
|
||||
|
||||
CreateMap<AppUser, UserDto>()
|
||||
.ForMember(dest => dest.AgeRestriction,
|
||||
|
|
|
@ -61,6 +61,12 @@ public static class FilterFieldValueConverter
|
|||
FilterField.Imprint => value.Split(',')
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Team => value.Split(',')
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Location => value.Split(',')
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Penciller => value.Split(',')
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
|
|
|
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
|||
using API.DTOs.System;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Kavita.Common.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -53,6 +54,8 @@ public interface IDirectoryService
|
|||
bool CopyDirectoryToDirectory(string? sourceDirName, string destDirName, string searchPattern = "");
|
||||
Dictionary<string, string> FindHighestDirectoriesFromFiles(IEnumerable<string> libraryFolders,
|
||||
IList<string> filePaths);
|
||||
string? FindLowestDirectoriesFromFiles(IEnumerable<string> libraryFolders,
|
||||
IList<string> filePaths);
|
||||
IEnumerable<string> GetFoldersTillRoot(string rootPath, string fullPath);
|
||||
IEnumerable<string> GetFiles(string path, string fileNameRegex = "", SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
||||
bool ExistOrCreate(string directoryPath);
|
||||
|
@ -584,6 +587,43 @@ public class DirectoryService : IDirectoryService
|
|||
return dirs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the lowest directory from a set of file paths. Does not return the root path, will always select the lowest non-root path.
|
||||
/// </summary>
|
||||
/// <remarks>If the file paths do not contain anything from libraryFolders, this returns an empty dictionary back</remarks>
|
||||
/// <param name="libraryFolders">List of top level folders which files belong to</param>
|
||||
/// <param name="filePaths">List of file paths that belong to libraryFolders</param>
|
||||
/// <returns></returns>
|
||||
public string? FindLowestDirectoriesFromFiles(IEnumerable<string> libraryFolders, IList<string> filePaths)
|
||||
{
|
||||
|
||||
|
||||
var stopLookingForDirectories = false;
|
||||
var dirs = new Dictionary<string, string>();
|
||||
foreach (var folder in libraryFolders.Select(Tasks.Scanner.Parser.Parser.NormalizePath))
|
||||
{
|
||||
if (stopLookingForDirectories) break;
|
||||
foreach (var file in filePaths.Select(Tasks.Scanner.Parser.Parser.NormalizePath))
|
||||
{
|
||||
if (!file.Contains(folder)) continue;
|
||||
|
||||
var lowestPath = Path.GetDirectoryName(file)?.Replace(folder, string.Empty);
|
||||
if (!string.IsNullOrEmpty(lowestPath))
|
||||
{
|
||||
dirs.TryAdd(Parser.NormalizePath(lowestPath), string.Empty);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (dirs.Keys.Count == 1) return dirs.Keys.First();
|
||||
if (dirs.Keys.Count > 1)
|
||||
{
|
||||
return dirs.Keys.Last();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a set of directories from the folder path. Automatically excludes directories that shouldn't be in scope.
|
||||
/// </summary>
|
||||
|
|
|
@ -131,8 +131,8 @@ public class MetadataService : IMetadataService
|
|||
null, series.Created, forceUpdate, series.CoverImageLocked))
|
||||
return Task.CompletedTask;
|
||||
|
||||
series.Volumes ??= new List<Volume>();
|
||||
series.CoverImage = series.GetCoverImage(); // BUG: At this point the volume or chapter hasn't regenerated the cover
|
||||
series.Volumes ??= [];
|
||||
series.CoverImage = series.GetCoverImage();
|
||||
|
||||
_updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series));
|
||||
return Task.CompletedTask;
|
||||
|
|
|
@ -36,8 +36,8 @@ public interface IReadingListService
|
|||
Task<bool> AddChaptersToReadingList(int seriesId, IList<int> chapterIds,
|
||||
ReadingList readingList);
|
||||
|
||||
Task<CblImportSummaryDto> ValidateCblFile(int userId, CblReadingList cblReading);
|
||||
Task<CblImportSummaryDto> CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false);
|
||||
Task<CblImportSummaryDto> ValidateCblFile(int userId, CblReadingList cblReading, bool useComicLibraryMatching = false);
|
||||
Task<CblImportSummaryDto> CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false, bool useComicLibraryMatching = false);
|
||||
Task CalculateStartAndEndDates(ReadingList readingListWithItems);
|
||||
/// <summary>
|
||||
/// This is expected to be called from ProcessSeries and has the Full Series present. Will generate on the default admin user.
|
||||
|
@ -530,7 +530,8 @@ public class ReadingListService : IReadingListService
|
|||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="cblReading"></param>
|
||||
public async Task<CblImportSummaryDto> ValidateCblFile(int userId, CblReadingList cblReading)
|
||||
/// <param name="useComicLibraryMatching">When true, will force ComicVine library naming conventions: Series (Year) for Series name matching.</param>
|
||||
public async Task<CblImportSummaryDto> ValidateCblFile(int userId, CblReadingList cblReading, bool useComicLibraryMatching = false)
|
||||
{
|
||||
var importSummary = new CblImportSummaryDto
|
||||
{
|
||||
|
@ -552,9 +553,14 @@ public class ReadingListService : IReadingListService
|
|||
});
|
||||
}
|
||||
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
|
||||
var uniqueSeries = GetUniqueSeries(cblReading, useComicLibraryMatching);
|
||||
var userSeries =
|
||||
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
|
||||
|
||||
// How can we match properly with ComicVine library when year is part of the series unless we do this in 2 passes and see which has a better match
|
||||
|
||||
|
||||
if (!userSeries.Any())
|
||||
{
|
||||
// Report that no series exist in the reading list
|
||||
|
@ -584,6 +590,20 @@ public class ReadingListService : IReadingListService
|
|||
return importSummary;
|
||||
}
|
||||
|
||||
private static string GetSeriesFormatting(CblBook book, bool useComicLibraryMatching)
|
||||
{
|
||||
return useComicLibraryMatching ? $"{book.Series} ({book.Volume})" : book.Series;
|
||||
}
|
||||
|
||||
private static List<string> GetUniqueSeries(CblReadingList cblReading, bool useComicLibraryMatching)
|
||||
{
|
||||
if (useComicLibraryMatching)
|
||||
{
|
||||
return cblReading.Books.Book.Select(b => Parser.Normalize(GetSeriesFormatting(b, useComicLibraryMatching))).Distinct().ToList();
|
||||
}
|
||||
return cblReading.Books.Book.Select(b => Parser.Normalize(GetSeriesFormatting(b, useComicLibraryMatching))).Distinct().ToList();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Imports (or pretends to) a cbl into a reading list. Call <see cref="ValidateCblFile"/> first!
|
||||
|
@ -591,8 +611,9 @@ public class ReadingListService : IReadingListService
|
|||
/// <param name="userId"></param>
|
||||
/// <param name="cblReading"></param>
|
||||
/// <param name="dryRun"></param>
|
||||
/// <param name="useComicLibraryMatching">When true, will force ComicVine library naming conventions: Series (Year) for Series name matching.</param>
|
||||
/// <returns></returns>
|
||||
public async Task<CblImportSummaryDto> CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false)
|
||||
public async Task<CblImportSummaryDto> CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false, bool useComicLibraryMatching = false)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.ReadingListsWithItems);
|
||||
_logger.LogDebug("Importing {ReadingListName} CBL for User {UserName}", cblReading.Name, user!.UserName);
|
||||
|
@ -604,11 +625,11 @@ public class ReadingListService : IReadingListService
|
|||
SuccessfulInserts = new List<CblBookResult>()
|
||||
};
|
||||
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
var uniqueSeries = GetUniqueSeries(cblReading, useComicLibraryMatching);
|
||||
var userSeries =
|
||||
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
|
||||
var allSeries = userSeries.ToDictionary(s => Parser.Normalize(s.Name));
|
||||
var allSeriesLocalized = userSeries.ToDictionary(s => Parser.Normalize(s.LocalizedName));
|
||||
var allSeries = userSeries.ToDictionary(s => s.NormalizedName);
|
||||
var allSeriesLocalized = userSeries.ToDictionary(s => s.NormalizedLocalizedName);
|
||||
|
||||
var readingListNameNormalized = Parser.Normalize(cblReading.Name);
|
||||
// Get all the user's reading lists
|
||||
|
@ -635,27 +656,7 @@ public class ReadingListService : IReadingListService
|
|||
readingList.Items ??= new List<ReadingListItem>();
|
||||
foreach (var (book, i) in cblReading.Books.Book.Select((value, i) => ( value, i )))
|
||||
{
|
||||
|
||||
// I want to refactor this so that we move the matching logic into a method.
|
||||
// But when I looked, we are returning statuses on different conditions, hard to keep it single responsibility
|
||||
// Either refactor to return an enum for the state, make it return the BookResult, or refactor the reasoning so it's more straightforward
|
||||
|
||||
// var match = FindMatchingCblBookSeries(book);
|
||||
// if (match == null)
|
||||
// {
|
||||
// importSummary.Results.Add(new CblBookResult(book)
|
||||
// {
|
||||
// Reason = CblImportReason.SeriesMissing,
|
||||
// Order = i
|
||||
// });
|
||||
// continue;
|
||||
// }
|
||||
|
||||
|
||||
// TODO: I need a dedicated db query to get Series name's processed if they are ComicVine.
|
||||
// In comicvine, series names are Series(Volume), but the spec just has Series="Series" Volume="Volume"
|
||||
// So we need to combine them for comics that are in ComicVine libraries.
|
||||
var normalizedSeries = Parser.Normalize(book.Series);
|
||||
var normalizedSeries = Parser.Normalize(GetSeriesFormatting(book, useComicLibraryMatching));
|
||||
if (!allSeries.TryGetValue(normalizedSeries, out var bookSeries) && !allSeriesLocalized.TryGetValue(normalizedSeries, out bookSeries))
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult(book)
|
||||
|
@ -669,7 +670,9 @@ public class ReadingListService : IReadingListService
|
|||
var bookVolume = string.IsNullOrEmpty(book.Volume)
|
||||
? Parser.LooseLeafVolume
|
||||
: book.Volume;
|
||||
var matchingVolume = bookSeries.Volumes.Find(v => bookVolume == v.Name) ?? bookSeries.Volumes.GetLooseLeafVolumeOrDefault();
|
||||
var matchingVolume = bookSeries.Volumes.Find(v => bookVolume == v.Name)
|
||||
?? bookSeries.Volumes.GetLooseLeafVolumeOrDefault()
|
||||
?? bookSeries.Volumes.GetSpecialVolumeOrDefault();
|
||||
if (matchingVolume == null)
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult(book)
|
||||
|
@ -683,9 +686,9 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
// We need to handle default chapter or empty string when it's just a volume
|
||||
var bookNumber = string.IsNullOrEmpty(book.Number)
|
||||
? Parser.DefaultChapterNumber
|
||||
: float.Parse(book.Number);
|
||||
var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.MinNumber.Is(bookNumber));
|
||||
? Parser.DefaultChapter
|
||||
: book.Number;
|
||||
var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.Range == bookNumber);
|
||||
if (chapter == null)
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult(book)
|
||||
|
@ -743,7 +746,7 @@ public class ReadingListService : IReadingListService
|
|||
private static IList<Series> FindCblImportConflicts(IEnumerable<Series> userSeries)
|
||||
{
|
||||
var dict = new HashSet<string>();
|
||||
return userSeries.Where(series => !dict.Add(Parser.Normalize(series.Name))).ToList();
|
||||
return userSeries.Where(series => !dict.Add(series.NormalizedName)).ToList();
|
||||
}
|
||||
|
||||
private static bool IsCblEmpty(CblReadingList cblReading, CblImportSummaryDto importSummary,
|
||||
|
|
|
@ -259,9 +259,19 @@ public class SeriesService : ISeriesService
|
|||
|
||||
var allImprints = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Imprint,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Imprints.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Imprint, updateSeriesMetadataDto.SeriesMetadata.Publishers, series, allImprints.AsReadOnly(),
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Imprint, updateSeriesMetadataDto.SeriesMetadata.Imprints, series, allImprints.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.ImprintLocked = true);
|
||||
|
||||
var allTeams = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Team,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Imprints.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Team, updateSeriesMetadataDto.SeriesMetadata.Teams, series, allTeams.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.TeamLocked = true);
|
||||
|
||||
var allLocations = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Location,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Imprints.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Location, updateSeriesMetadataDto.SeriesMetadata.Locations, series, allLocations.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.LocationLocked = true);
|
||||
|
||||
var allTranslators = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Translator,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Translators.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Translator, updateSeriesMetadataDto.SeriesMetadata.Translators, series, allTranslators.AsReadOnly(),
|
||||
|
|
|
@ -76,7 +76,7 @@ public class ScannedSeriesResult
|
|||
|
||||
public class SeriesModified
|
||||
{
|
||||
public required string FolderPath { get; set; }
|
||||
public required string? FolderPath { get; set; }
|
||||
public required string SeriesName { get; set; }
|
||||
public DateTime LastScanned { get; set; }
|
||||
public MangaFormat Format { get; set; }
|
||||
|
@ -416,7 +416,6 @@ public class ParseScannedFiles
|
|||
.ToList()!;
|
||||
|
||||
result.ParserInfos = infos;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
@ -453,7 +452,7 @@ public class ParseScannedFiles
|
|||
if (float.TryParse(chapter.Chapters, out var parsedChapter))
|
||||
{
|
||||
counter = parsedChapter;
|
||||
if (!string.IsNullOrEmpty(prevIssue) && parsedChapter.Is(float.Parse(prevIssue)))
|
||||
if (!string.IsNullOrEmpty(prevIssue) && float.TryParse(prevIssue, out var prevIssueFloat) && parsedChapter.Is(prevIssueFloat))
|
||||
{
|
||||
// Bump by 0.1
|
||||
counter += 0.1f;
|
||||
|
|
|
@ -24,12 +24,16 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser
|
|||
if (type != LibraryType.ComicVine) return null;
|
||||
|
||||
var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath);
|
||||
// Mylar often outputs cover.jpg, ignore it by default
|
||||
if (string.IsNullOrEmpty(fileName) || Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null;
|
||||
|
||||
var directoryName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name;
|
||||
|
||||
var info = new ParserInfo()
|
||||
{
|
||||
Filename = Path.GetFileName(filePath),
|
||||
Format = Parser.ParseFormat(filePath),
|
||||
Title = Parser.RemoveExtensionIfSupported(fileName),
|
||||
Title = Parser.RemoveExtensionIfSupported(fileName)!,
|
||||
FullFilePath = filePath,
|
||||
Series = string.Empty,
|
||||
ComicInfo = comicInfo,
|
||||
|
@ -73,7 +77,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser
|
|||
}
|
||||
}
|
||||
|
||||
// Check if this is a Special
|
||||
// Check if this is a Special/Annual
|
||||
info.IsSpecial = Parser.IsComicSpecial(info.Filename) || Parser.IsComicSpecial(info.ComicInfo?.Format);
|
||||
|
||||
// Patch in other information from ComicInfo
|
||||
|
|
|
@ -641,7 +641,7 @@ public static class Parser
|
|||
|
||||
private static readonly Regex ComicSpecialRegex = new Regex(
|
||||
// All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle.
|
||||
$@"\b(?:{CommonSpecial}|\d.+?(\W|-|^)Annual|Annual(\W|-|$)|Book \d.+?|Compendium(\W|-|$|\s.+?)|Omnibus(\W|-|$|\s.+?)|FCBD \d.+?|Absolute(\W|-|$|\s.+?)|Preview(\W|-|$|\s.+?)|Hors[ -]S[ée]rie|TPB|HS|THS)\b",
|
||||
$@"\b(?:{CommonSpecial}|\d.+?(\W|-|^)Annual|Annual(\W|-|$|\s#)|Book \d.+?|Compendium(\W|-|$|\s.+?)|Omnibus(\W|-|$|\s.+?)|FCBD \d.+?|Absolute(\W|-|$|\s.+?)|Preview(\W|-|$|\s.+?)|Hors[ -]S[ée]rie|TPB|HS|THS)\b",
|
||||
MatchOptions, RegexTimeout
|
||||
);
|
||||
|
||||
|
|
|
@ -258,8 +258,9 @@ public class ProcessSeries : IProcessSeries
|
|||
|
||||
private async Task UpdateSeriesFolderPath(IEnumerable<ParserInfo> parsedInfos, Library library, Series series)
|
||||
{
|
||||
var seriesDirs = _directoryService.FindHighestDirectoriesFromFiles(library.Folders.Select(l => l.Path),
|
||||
parsedInfos.Select(f => f.FullFilePath).ToList());
|
||||
var libraryFolders = library.Folders.Select(l => Parser.Parser.NormalizePath(l.Path)).ToList();
|
||||
var seriesFiles = parsedInfos.Select(f => Parser.Parser.NormalizePath(f.FullFilePath)).ToList();
|
||||
var seriesDirs = _directoryService.FindHighestDirectoriesFromFiles(libraryFolders, seriesFiles);
|
||||
if (seriesDirs.Keys.Count == 0)
|
||||
{
|
||||
_logger.LogCritical(
|
||||
|
@ -273,10 +274,19 @@ public class ProcessSeries : IProcessSeries
|
|||
// Don't save FolderPath if it's a library Folder
|
||||
if (!library.Folders.Select(f => f.Path).Contains(seriesDirs.Keys.First()))
|
||||
{
|
||||
// BUG: FolderPath can be a level higher than it needs to be. I'm not sure why it's like this, but I thought it should be one level lower.
|
||||
// I think it's like this because higher level is checked or not checked. But i think we can do both
|
||||
series.FolderPath = Parser.Parser.NormalizePath(seriesDirs.Keys.First());
|
||||
_logger.LogDebug("Updating {Series} FolderPath to {FolderPath}", series.Name, series.FolderPath);
|
||||
}
|
||||
}
|
||||
|
||||
var lowestFolder = _directoryService.FindLowestDirectoriesFromFiles(libraryFolders, seriesFiles);
|
||||
if (!string.IsNullOrEmpty(lowestFolder))
|
||||
{
|
||||
series.LowestFolderPath = lowestFolder;
|
||||
_logger.LogDebug("Updating {Series} LowestFolderPath to {FolderPath}", series.Name, series.LowestFolderPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -439,6 +449,22 @@ public class ProcessSeries : IProcessSeries
|
|||
}
|
||||
}
|
||||
|
||||
if (!series.Metadata.TeamLocked)
|
||||
{
|
||||
foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Team))
|
||||
{
|
||||
PersonHelper.AddPersonIfNotExists(series.Metadata.People, person);
|
||||
}
|
||||
}
|
||||
|
||||
if (!series.Metadata.LocationLocked)
|
||||
{
|
||||
foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Location))
|
||||
{
|
||||
PersonHelper.AddPersonIfNotExists(series.Metadata.People, person);
|
||||
}
|
||||
}
|
||||
|
||||
if (!series.Metadata.LettererLocked)
|
||||
{
|
||||
foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Letterer))
|
||||
|
@ -837,6 +863,14 @@ public class ProcessSeries : IProcessSeries
|
|||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Imprint);
|
||||
await UpdatePeople(chapter, people, PersonRole.Imprint);
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.Teams);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Team);
|
||||
await UpdatePeople(chapter, people, PersonRole.Team);
|
||||
|
||||
people = TagHelper.GetTagValues(comicInfo.Locations);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Location);
|
||||
await UpdatePeople(chapter, people, PersonRole.Location);
|
||||
|
||||
var genres = TagHelper.GetTagValues(comicInfo.Genre);
|
||||
GenreHelper.KeepOnlySameGenreBetweenLists(chapter.Genres,
|
||||
genres.Select(g => new GenreBuilder(g).Build()).ToList());
|
||||
|
|
|
@ -210,7 +210,7 @@ public class ScannerService : IScannerService
|
|||
return;
|
||||
}
|
||||
|
||||
var folderPath = series.FolderPath;
|
||||
var folderPath = series.LowestFolderPath ?? series.FolderPath;
|
||||
if (string.IsNullOrEmpty(folderPath) || !_directoryService.Exists(folderPath))
|
||||
{
|
||||
// We don't care if it's multiple due to new scan loop enforcing all in one root directory
|
||||
|
@ -265,7 +265,8 @@ public class ScannerService : IScannerService
|
|||
if (parsedSeries.Count == 0)
|
||||
{
|
||||
var seriesFiles = (await _unitOfWork.SeriesRepository.GetFilesForSeries(series.Id));
|
||||
if (!string.IsNullOrEmpty(series.FolderPath) && !seriesFiles.Where(f => f.FilePath.Contains(series.FolderPath)).Any(m => File.Exists(m.FilePath)))
|
||||
if (!string.IsNullOrEmpty(series.FolderPath) &&
|
||||
!seriesFiles.Where(f => f.FilePath.Contains(series.FolderPath)).Any(m => File.Exists(m.FilePath)))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
|
@ -10,15 +10,15 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Cronos" Version="0.8.2" />
|
||||
<PackageReference Include="Cronos" Version="0.8.4" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.19.0.84025">
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.21.0.86780">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="xunit.assert" Version="2.6.6" />
|
||||
<PackageReference Include="xunit.assert" Version="2.7.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -33,3 +33,7 @@ and update environment.ts to your local ip.
|
|||
## Notes:
|
||||
- injected services should be at the top of the file
|
||||
- all components must be standalone
|
||||
|
||||
# Update latest angular
|
||||
`ng update @angular/core @angular/cli @typescript-es
|
||||
lint/parser @angular/localize @angular/compiler-cli @angular/cli @angular-devkit/build-angular @angular/cdk`
|
||||
|
|
2452
UI/Web/package-lock.json
generated
2452
UI/Web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -15,16 +15,16 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.1.0",
|
||||
"@angular/cdk": "^17.1.0",
|
||||
"@angular/common": "^17.1.0",
|
||||
"@angular/compiler": "^17.1.0",
|
||||
"@angular/core": "^17.1.0",
|
||||
"@angular/forms": "^17.1.0",
|
||||
"@angular/localize": "^17.1.0",
|
||||
"@angular/platform-browser": "^17.1.0",
|
||||
"@angular/platform-browser-dynamic": "^17.1.0",
|
||||
"@angular/router": "^17.1.0",
|
||||
"@angular/animations": "^17.3.0",
|
||||
"@angular/cdk": "^17.2.2",
|
||||
"@angular/common": "^17.3.0",
|
||||
"@angular/compiler": "^17.3.0",
|
||||
"@angular/core": "^17.3.0",
|
||||
"@angular/forms": "^17.3.0",
|
||||
"@angular/localize": "^17.3.0",
|
||||
"@angular/platform-browser": "^17.3.0",
|
||||
"@angular/platform-browser-dynamic": "^17.3.0",
|
||||
"@angular/router": "^17.3.0",
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@iharbeck/ngx-virtual-scroller": "^17.0.0",
|
||||
"@iplab/ngx-file-upload": "^17.0.0",
|
||||
|
@ -37,7 +37,7 @@
|
|||
"@ngneat/transloco-preload-langs": "^5.0.1",
|
||||
"@popperjs/core": "^2.11.7",
|
||||
"@swimlane/ngx-charts": "^20.5.0",
|
||||
"@tweenjs/tween.js": "^21.0.0",
|
||||
"@tweenjs/tween.js": "^21.1.1",
|
||||
"bootstrap": "^5.3.2",
|
||||
"charts.css": "^1.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
|
@ -57,24 +57,24 @@
|
|||
"screenfull": "^6.0.2",
|
||||
"swiper": "^8.4.6",
|
||||
"tslib": "^2.6.2",
|
||||
"zone.js": "^0.14.2"
|
||||
"zone.js": "^0.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.1.0",
|
||||
"@angular-devkit/build-angular": "^17.3.0",
|
||||
"@angular-eslint/builder": "^17.2.1",
|
||||
"@angular-eslint/eslint-plugin": "^17.2.1",
|
||||
"@angular-eslint/eslint-plugin-template": "^17.2.1",
|
||||
"@angular-eslint/schematics": "^17.2.1",
|
||||
"@angular-eslint/template-parser": "^17.2.1",
|
||||
"@angular/cli": "^17.1.0",
|
||||
"@angular/compiler-cli": "^17.1.0",
|
||||
"@angular/cli": "^17.3.0",
|
||||
"@angular/compiler-cli": "^17.3.0",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/luxon": "^3.4.0",
|
||||
"@types/node": "^20.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
||||
"@typescript-eslint/parser": "^6.19.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"eslint": "^8.57.0",
|
||||
"jsonminify": "^0.4.2",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"ts-node": "~10.9.1",
|
||||
|
|
|
@ -34,6 +34,8 @@ export interface ChapterMetadata {
|
|||
letterers: Array<Person>;
|
||||
editors: Array<Person>;
|
||||
translators: Array<Person>;
|
||||
teams: Array<Person>;
|
||||
locations: Array<Person>;
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -11,7 +11,9 @@ export enum PersonRole {
|
|||
Publisher = 10,
|
||||
Character = 11,
|
||||
Translator = 12,
|
||||
Imprint = 13
|
||||
Imprint = 13,
|
||||
Team = 14,
|
||||
Location = 15
|
||||
}
|
||||
|
||||
export interface Person {
|
||||
|
|
|
@ -26,6 +26,8 @@ export interface SeriesMetadata {
|
|||
letterers: Array<Person>;
|
||||
editors: Array<Person>;
|
||||
translators: Array<Person>;
|
||||
teams: Array<Person>;
|
||||
locations: Array<Person>;
|
||||
ageRating: AgeRating;
|
||||
releaseYear: number;
|
||||
language: string;
|
||||
|
@ -46,6 +48,8 @@ export interface SeriesMetadata {
|
|||
lettererLocked: boolean;
|
||||
editorLocked: boolean;
|
||||
translatorLocked: boolean;
|
||||
teamLocked: boolean;
|
||||
locationLocked: boolean;
|
||||
ageRatingLocked: boolean;
|
||||
releaseYearLocked: boolean;
|
||||
languageLocked: boolean;
|
||||
|
|
|
@ -30,7 +30,9 @@ export enum FilterField
|
|||
WantToRead = 26,
|
||||
ReadingDate = 27,
|
||||
AverageRating = 28,
|
||||
Imprint = 29
|
||||
Imprint = 29,
|
||||
Team = 30,
|
||||
Location = 31
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -30,6 +30,10 @@ export class FilterFieldPipe implements PipeTransform {
|
|||
return translate('filter-field-pipe.inker');
|
||||
case FilterField.Imprint:
|
||||
return translate('filter-field-pipe.imprint');
|
||||
case FilterField.Team:
|
||||
return translate('filter-field-pipe.team');
|
||||
case FilterField.Location:
|
||||
return translate('filter-field-pipe.location');
|
||||
case FilterField.Languages:
|
||||
return translate('filter-field-pipe.languages');
|
||||
case FilterField.Libraries:
|
||||
|
|
|
@ -33,6 +33,12 @@ export class PersonRolePipe implements PipeTransform {
|
|||
return this.translocoService.translate('person-role-pipe.imprint');
|
||||
case PersonRole.Writer:
|
||||
return this.translocoService.translate('person-role-pipe.writer');
|
||||
case PersonRole.Team:
|
||||
return this.translocoService.translate('person-role-pipe.team');
|
||||
case PersonRole.Location:
|
||||
return this.translocoService.translate('person-role-pipe.location');
|
||||
case PersonRole.Translator:
|
||||
return this.translocoService.translate('person-role-pipe.translator');
|
||||
case PersonRole.Other:
|
||||
return this.translocoService.translate('person-role-pipe.other');
|
||||
default:
|
||||
|
|
|
@ -327,7 +327,21 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="translator" class="form-label">{{t('translator-label')}}</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Translator);metadata.translatorLocked = true;" [settings]="getPersonsSettings(PersonRole.Translator)"
|
||||
[(locked)]="metadata.translatorLocked" (onUnlock)="metadata.translatorLocked = false"
|
||||
(newItemAdded)="metadata.translatorLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
|
@ -344,12 +358,29 @@
|
|||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="translator" class="form-label">{{t('translator-label')}}</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Translator);metadata.translatorLocked = true;" [settings]="getPersonsSettings(PersonRole.Translator)"
|
||||
[(locked)]="metadata.translatorLocked" (onUnlock)="metadata.translatorLocked = false"
|
||||
(newItemAdded)="metadata.translatorLocked = true">
|
||||
<label for="team" class="form-label">{{t('team-label')}}</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Character);metadata.teamLocked = true" [settings]="getPersonsSettings(PersonRole.Team)"
|
||||
[(locked)]="metadata.teamLocked" (onUnlock)="metadata.teamLocked = false"
|
||||
(newItemAdded)="metadata.teamLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="location" class="form-label">{{t('location-label')}}</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Location);metadata.locationLocked = true" [settings]="getPersonsSettings(PersonRole.Location)"
|
||||
[(locked)]="metadata.locationLocked" (onUnlock)="metadata.locationLocked = false"
|
||||
(newItemAdded)="metadata.locationLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
|
|
|
@ -488,7 +488,9 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
this.updateFromPreset('penciller', this.metadata.pencillers, PersonRole.Penciller),
|
||||
this.updateFromPreset('publisher', this.metadata.publishers, PersonRole.Publisher),
|
||||
this.updateFromPreset('imprint', this.metadata.imprints, PersonRole.Imprint),
|
||||
this.updateFromPreset('translator', this.metadata.translators, PersonRole.Translator)
|
||||
this.updateFromPreset('translator', this.metadata.translators, PersonRole.Translator),
|
||||
this.updateFromPreset('teams', this.metadata.teams, PersonRole.Team),
|
||||
this.updateFromPreset('locations', this.metadata.locations, PersonRole.Location),
|
||||
]).pipe(map(results => {
|
||||
return of(true);
|
||||
}));
|
||||
|
@ -611,6 +613,10 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
|
||||
updatePerson(persons: Person[], role: PersonRole) {
|
||||
switch (role) {
|
||||
case PersonRole.Other:
|
||||
break;
|
||||
case PersonRole.Artist:
|
||||
break;
|
||||
case PersonRole.CoverArtist:
|
||||
this.metadata.coverArtists = persons;
|
||||
break;
|
||||
|
@ -638,11 +644,19 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
case PersonRole.Imprint:
|
||||
this.metadata.imprints = persons;
|
||||
break;
|
||||
case PersonRole.Team:
|
||||
this.metadata.teams = persons;
|
||||
break;
|
||||
case PersonRole.Location:
|
||||
this.metadata.locations = persons;
|
||||
break;
|
||||
case PersonRole.Writer:
|
||||
this.metadata.writers = persons;
|
||||
break;
|
||||
case PersonRole.Translator:
|
||||
this.metadata.translators = persons;
|
||||
break;
|
||||
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
&& chapter.colorists.length === 0 && chapter.letterers.length === 0
|
||||
&& chapter.editors.length === 0 && chapter.publishers.length === 0
|
||||
&& chapter.characters.length === 0 && chapter.translators.length === 0
|
||||
&& chapter.imprints.length === 0">
|
||||
&& chapter.imprints.length === 0 && chapter.locations.length === 0
|
||||
&& chapter.teams.length === 0">
|
||||
{{t('no-data')}}
|
||||
</span>
|
||||
<div class="container-flex row row-cols-auto row-cols-lg-5 g-2 g-lg-3 me-0 mt-2">
|
||||
|
@ -99,6 +100,25 @@
|
|||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.teams && chapter.teams.length > 0">
|
||||
<h6>{{t('teams-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.teams">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.locations && chapter.locations.length > 0">
|
||||
<h6>{{t('locations-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.locations">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.translators && chapter.translators.length > 0">
|
||||
<h6>{{t('translators-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.translators">
|
||||
|
|
|
@ -65,7 +65,7 @@ const DropdownFields = [FilterField.PublicationStatus, FilterField.Languages, Fi
|
|||
FilterField.Colorist, FilterField.Inker, FilterField.Penciller,
|
||||
FilterField.Writers, FilterField.Genres, FilterField.Libraries,
|
||||
FilterField.Formats, FilterField.CollectionTags, FilterField.Tags,
|
||||
FilterField.Imprint
|
||||
FilterField.Imprint, FilterField.Team, FilterField.Location
|
||||
];
|
||||
const BooleanFields = [FilterField.WantToRead];
|
||||
const DateFields = [FilterField.ReadingDate];
|
||||
|
@ -299,6 +299,8 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||
case FilterField.Penciller: return this.getPersonOptions(PersonRole.Penciller);
|
||||
case FilterField.Publisher: return this.getPersonOptions(PersonRole.Publisher);
|
||||
case FilterField.Imprint: return this.getPersonOptions(PersonRole.Imprint);
|
||||
case FilterField.Team: return this.getPersonOptions(PersonRole.Imprint);
|
||||
case FilterField.Location: return this.getPersonOptions(PersonRole.Imprint);
|
||||
case FilterField.Translators: return this.getPersonOptions(PersonRole.Translator);
|
||||
case FilterField.Writers: return this.getPersonOptions(PersonRole.Writer);
|
||||
}
|
||||
|
|
|
@ -148,6 +148,8 @@ export class NavHeaderComponent implements OnInit {
|
|||
this.clearSearch();
|
||||
filter = filter + '';
|
||||
switch(role) {
|
||||
case PersonRole.Other:
|
||||
break;
|
||||
case PersonRole.Writer:
|
||||
this.goTo({field: FilterField.Writers, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
|
@ -181,9 +183,16 @@ export class NavHeaderComponent implements OnInit {
|
|||
case PersonRole.Imprint:
|
||||
this.goTo({field: FilterField.Imprint, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Team:
|
||||
this.goTo({field: FilterField.Team, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Location:
|
||||
this.goTo({field: FilterField.Location, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Translator:
|
||||
this.goTo({field: FilterField.Translators, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
<ng-container *ngIf="currentStepIndex === Step.Validate">
|
||||
<p>{{t('validate-description')}}</p>
|
||||
<div class="row g-0">
|
||||
|
||||
<div ngbAccordion #accordion="ngbAccordion">
|
||||
@for(fileToProcess of filesToProcess; track fileToProcess.fileName) {
|
||||
<div ngbAccordionItem *ngIf="fileToProcess.validateSummary as summary">
|
||||
|
@ -32,26 +31,7 @@
|
|||
</h5>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
@if(summary.results.length > 0) {
|
||||
<h5>{{t('validate-warning')}}</h5>
|
||||
<ol class="list-group list-group-numbered list-group-flush" >
|
||||
<li class="list-group-item no-hover" *ngFor="let result of summary.results"
|
||||
[innerHTML]="result | cblConflictReason | safeHtml">
|
||||
</li>
|
||||
</ol>
|
||||
} @else {
|
||||
<div class="justify-content-center col">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fa-solid fa-circle-check" style="font-size: 24px" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
{{t('validate-no-issue')}}
|
||||
</div>
|
||||
</div>
|
||||
{{t('validate-no-issue-description')}}
|
||||
</div>
|
||||
}
|
||||
<ng-container [ngTemplateOutlet]="validationList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -105,6 +85,38 @@
|
|||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #validationList let-summary="summary">
|
||||
@if (summary.results.length > 0) {
|
||||
<div class="justify-content-center col">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fa-solid fa-triangle-exclamation" style="font-size: 24px" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
{{t('validate-warning')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ol class="list-group list-group-numbered list-group-flush" >
|
||||
<li class="list-group-item no-hover" *ngFor="let result of summary.results"
|
||||
[innerHTML]="result | cblConflictReason | safeHtml">
|
||||
</li>
|
||||
</ol>
|
||||
}
|
||||
@else {
|
||||
<div class="justify-content-center col">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fa-solid fa-circle-check" style="font-size: 24px" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
{{t('validate-no-issue-description')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #resultsList let-summary="summary">
|
||||
<ul class="list-group list-group-flush">
|
||||
@for(result of summary.results; track result.order) {
|
||||
|
@ -115,24 +127,47 @@
|
|||
</ng-template>
|
||||
|
||||
<ng-template #heading let-filename="filename" let-summary="summary">
|
||||
<ng-container *ngIf="summary.success | cblImportResult as success">
|
||||
<ng-container [ngSwitch]="summary.success">
|
||||
<span *ngSwitchCase="CblImportResult.Success" class="badge bg-primary me-1">{{success}}</span>
|
||||
<span *ngSwitchCase="CblImportResult.Fail" class="badge bg-danger me-1">{{success}}</span>
|
||||
<span *ngSwitchCase="CblImportResult.Partial" class="badge bg-warning me-1">{{success}}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@switch (summary.success) {
|
||||
@case (CblImportResult.Success) {
|
||||
<span class="badge heading-badge bg-primary me-1">{{summary.success | cblImportResult}}</span>
|
||||
}
|
||||
@case (CblImportResult.Fail) {
|
||||
<span class="badge heading-badge bg-danger me-1">{{summary.success | cblImportResult}}</span>
|
||||
}
|
||||
@case (CblImportResult.Partial) {
|
||||
<span class="badge heading-badge bg-warning me-1">{{summary.success | cblImportResult}}</span>
|
||||
}
|
||||
}
|
||||
<span>{{filename}}<span *ngIf="summary.cblName">: ({{summary.cblName}})</span></span>
|
||||
</ng-template>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<form [formGroup]="cblSettingsForm" class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="settings-comicvine-mode" role="switch" formControlName="comicVineMatching" class="form-check-input"
|
||||
aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="settings-comicvine-mode">{{t('comicvine-parsing-label')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Spacer -->
|
||||
<div class="col" aria-hidden="true"></div>
|
||||
<div class="col-auto">
|
||||
<a class="btn btn-icon" href="https://wiki.kavitareader.com/en/guides/get-started-using-your-library/reading-lists#creating-a-reading-list-via-cbl" target="_blank" rel="noopener noreferrer">Help</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">{{t('close')}}</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">{{t('prev')}}</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-primary" (click)="nextStep()" [disabled]="!canMoveToNextStep()">{{t(NextButtonLabel)}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.heading-badge {
|
||||
color: var(--bs-badge-color);
|
||||
}
|
||||
|
||||
::ng-deep .file-info {
|
||||
width: 83%;
|
||||
float: left;
|
||||
|
|
|
@ -46,8 +46,12 @@ enum Step {
|
|||
})
|
||||
export class ImportCblModalComponent {
|
||||
|
||||
protected readonly CblImportResult = CblImportResult;
|
||||
protected readonly Step = Step;
|
||||
|
||||
@ViewChild('fileUpload') fileUpload!: ElementRef<HTMLInputElement>;
|
||||
|
||||
|
||||
fileUploadControl = new FormControl<undefined | Array<File>>(undefined, [
|
||||
FileUploadValidators.accept(['.cbl']),
|
||||
]);
|
||||
|
@ -55,6 +59,9 @@ export class ImportCblModalComponent {
|
|||
uploadForm = new FormGroup({
|
||||
files: this.fileUploadControl
|
||||
});
|
||||
cblSettingsForm = new FormGroup({
|
||||
comicVineMatching: new FormControl(true, [])
|
||||
});
|
||||
|
||||
isLoading: boolean = false;
|
||||
|
||||
|
@ -70,10 +77,6 @@ export class ImportCblModalComponent {
|
|||
failedFiles: Array<FileStep> = [];
|
||||
|
||||
|
||||
get Breakpoint() { return Breakpoint; }
|
||||
get Step() { return Step; }
|
||||
get CblImportResult() { return CblImportResult; }
|
||||
|
||||
get NextButtonLabel() {
|
||||
switch(this.currentStepIndex) {
|
||||
case Step.DryRun:
|
||||
|
@ -105,11 +108,12 @@ export class ImportCblModalComponent {
|
|||
return;
|
||||
}
|
||||
// Load each file into filesToProcess and group their data
|
||||
let pages = [];
|
||||
const pages = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const formData = new FormData();
|
||||
formData.append('cbl', files[i]);
|
||||
formData.append('dryRun', true + '');
|
||||
formData.append('dryRun', 'true');
|
||||
formData.append('comicVineMatching', this.cblSettingsForm.get('comicVineMatching')?.value + '');
|
||||
pages.push(this.readingListService.validateCbl(formData));
|
||||
}
|
||||
forkJoin(pages).subscribe(results => {
|
||||
|
@ -195,11 +199,12 @@ export class ImportCblModalComponent {
|
|||
const filenamesAllowedToProcess = this.filesToProcess.map(p => p.fileName);
|
||||
const files = (this.uploadForm.get('files')?.value || []).filter(f => filenamesAllowedToProcess.includes(f.name));
|
||||
|
||||
let pages = [];
|
||||
const pages = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const formData = new FormData();
|
||||
formData.append('cbl', files[i]);
|
||||
formData.append('dryRun', 'true');
|
||||
formData.append('comicVineMatching', this.cblSettingsForm.get('comicVineMatching')?.value + '');
|
||||
pages.push(this.readingListService.importCbl(formData));
|
||||
}
|
||||
forkJoin(pages).subscribe(results => {
|
||||
|
@ -224,6 +229,7 @@ export class ImportCblModalComponent {
|
|||
const formData = new FormData();
|
||||
formData.append('cbl', files[i]);
|
||||
formData.append('dryRun', 'false');
|
||||
formData.append('comicVineMatching', this.cblSettingsForm.get('comicVineMatching')?.value + '');
|
||||
pages.push(this.readingListService.importCbl(formData));
|
||||
}
|
||||
forkJoin(pages).subscribe(results => {
|
||||
|
|
|
@ -76,15 +76,6 @@
|
|||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
|
||||
|
||||
<!-- @if (libraryType === LibraryType.Comic || libraryType === LibraryType.Images) {-->
|
||||
<!-- <app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterField.Writers" [heading]="t('writers-title')">-->
|
||||
<!-- <ng-template #itemTemplate let-item>-->
|
||||
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-metadata-detail>-->
|
||||
|
||||
<!-- -->
|
||||
<!-- }-->
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterField.CoverArtist" [heading]="t('cover-artists-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
|
@ -92,26 +83,6 @@
|
|||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
|
||||
<!-- <app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterField.Writers" [heading]="t('writers-title')">-->
|
||||
<!-- <ng-template #itemTemplate let-item>-->
|
||||
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-metadata-detail>-->
|
||||
|
||||
|
||||
<!-- <app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterField.CoverArtist" [heading]="t('cover-artists-title')">-->
|
||||
<!-- <ng-template #itemTemplate let-item>-->
|
||||
<!-- <app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>-->
|
||||
<!-- </ng-template>-->
|
||||
<!-- </app-metadata-detail>-->
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.characters" [libraryId]="series.libraryId" [queryParam]="FilterField.Characters" [heading]="t('characters-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.colorists" [libraryId]="series.libraryId" [queryParam]="FilterField.Colorist" [heading]="t('colorists-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
|
@ -136,13 +107,25 @@
|
|||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.translators" [libraryId]="series.libraryId" [queryParam]="FilterField.Translators" [heading]="t('translators-title')">
|
||||
<app-metadata-detail [tags]="seriesMetadata.pencillers" [libraryId]="series.libraryId" [queryParam]="FilterField.Penciller" [heading]="t('pencillers-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.pencillers" [libraryId]="series.libraryId" [queryParam]="FilterField.Penciller" [heading]="t('pencillers-title')">
|
||||
<app-metadata-detail [tags]="seriesMetadata.characters" [libraryId]="series.libraryId" [queryParam]="FilterField.Characters" [heading]="t('characters-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.teams" [libraryId]="series.libraryId" [queryParam]="FilterField.Team" [heading]="t('teams-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.locations" [libraryId]="series.libraryId" [queryParam]="FilterField.Location" [heading]="t('locations-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
|
@ -160,6 +143,12 @@
|
|||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.translators" [libraryId]="series.libraryId" [queryParam]="FilterField.Translators" [heading]="t('translators-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
|
|
|
@ -92,7 +92,7 @@ export class SeriesMetadataDetailComponent implements OnChanges, OnInit {
|
|||
+ this.seriesMetadata.letterers.length + this.seriesMetadata.pencillers.length
|
||||
+ this.seriesMetadata.publishers.length + this.seriesMetadata.characters.length
|
||||
+ this.seriesMetadata.imprints.length + this.seriesMetadata.translators.length
|
||||
+ this.seriesMetadata.writers.length) / 11;
|
||||
+ this.seriesMetadata.writers.length + this.seriesMetadata.teams.length + this.seriesMetadata.locations.length) / 13;
|
||||
if (sum > 10) {
|
||||
this.isCollapsed = true;
|
||||
}
|
||||
|
@ -112,7 +112,10 @@ export class SeriesMetadataDetailComponent implements OnChanges, OnInit {
|
|||
this.seriesMetadata.publishers.length > 0 ||
|
||||
this.seriesMetadata.characters.length > 0 ||
|
||||
this.seriesMetadata.imprints.length > 0 ||
|
||||
this.seriesMetadata.translators.length > 0;
|
||||
this.seriesMetadata.teams.length > 0 ||
|
||||
this.seriesMetadata.locations.length > 0 ||
|
||||
this.seriesMetadata.translators.length > 0
|
||||
;
|
||||
|
||||
|
||||
this.seriesSummary = (this.seriesMetadata?.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
|
||||
|
|
|
@ -172,6 +172,7 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(false);
|
||||
break;
|
||||
case LibraryType.Comic:
|
||||
case LibraryType.ComicVine:
|
||||
this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(true);
|
||||
this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(false);
|
||||
this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(false);
|
||||
|
|
|
@ -483,7 +483,10 @@
|
|||
"publisher": "Publisher",
|
||||
"writer": "Writer",
|
||||
"other": "Other",
|
||||
"imprint": "Imprint"
|
||||
"imprint": "Imprint",
|
||||
"translator": "Translator",
|
||||
"team": "{{filter-field-pipe.team}}",
|
||||
"location": "{{filter-field-pipe.location}}"
|
||||
},
|
||||
|
||||
"manga-format-pipe": {
|
||||
|
@ -760,6 +763,8 @@
|
|||
"pencillers-title": "Pencillers",
|
||||
"publishers-title": "Publishers",
|
||||
"imprints-title": "Imprints",
|
||||
"teams-title": "Teams",
|
||||
"locations-title": "Locations",
|
||||
|
||||
"promoted": "{{common.promoted}}",
|
||||
"see-more": "See More",
|
||||
|
@ -937,6 +942,8 @@
|
|||
"publishers-title": "{{series-metadata-detail.publishers-title}}",
|
||||
"imprints-title": "{{series-metadata-detail.imprints-title}}",
|
||||
"tags-title": "{{series-metadata-detail.tags-title}}",
|
||||
"teams-title": "{{series-metadata-detail.teams-title}}",
|
||||
"locations-title": "{{series-metadata-detail.locations-title}}",
|
||||
"not-defined": "Not defined",
|
||||
"read": "{{common.read}}",
|
||||
"unread": "Unread",
|
||||
|
@ -966,7 +973,9 @@
|
|||
"inkers-title": "{{series-metadata-detail.inkers-title}}",
|
||||
"pencillers-title": "{{series-metadata-detail.pencillers-title}}",
|
||||
"cover-artists-title": "{{series-metadata-detail.cover-artists-title}}",
|
||||
"editors-title": "{{series-metadata-detail.editors-title}}"
|
||||
"editors-title": "{{series-metadata-detail.editors-title}}",
|
||||
"teams-title": "{{series-metadata-detail.teams-title}}",
|
||||
"locations-title": "{{series-metadata-detail.locations-title}}"
|
||||
},
|
||||
|
||||
"cover-image-chooser": {
|
||||
|
@ -1547,7 +1556,6 @@
|
|||
"import-description": "To get started, import a .cbl file. Kavita will perform multiple checks before importing. Some steps will block moving forward due to issues with the file.",
|
||||
"validate-description": "All files have been validated to see if there are any operations to do on the list. Any lists have have failed will not move to the next step. Fix the CBL files and retry.",
|
||||
"validate-warning": "There are issues with the CBL that will prevent an import. Correct these issues then try again.",
|
||||
"validate-no-issue": "Looks good",
|
||||
"validate-no-issue-description": "No issues found with CBL, press next.",
|
||||
"dry-run-description": "This is a dry run and shows what will happen if you press Next and perform the import. All Failures will not be imported.",
|
||||
"prev": "Prev",
|
||||
|
@ -1557,7 +1565,8 @@
|
|||
"import-step": "Import CBLs",
|
||||
"validate-cbl-step": "Validate CBL",
|
||||
"dry-run-step": "Dry Run",
|
||||
"final-import-step": "Final Step"
|
||||
"final-import-step": "Final Step",
|
||||
"comicvine-parsing-label": "Use ComicVine Series matching"
|
||||
},
|
||||
|
||||
"pdf-reader": {
|
||||
|
@ -1697,6 +1706,8 @@
|
|||
"colorist-label": "Colorist",
|
||||
"character-label": "Character",
|
||||
"translator-label": "Translator",
|
||||
"team-label": "{{filter-field-pipe.team}}",
|
||||
"location-label": "{{filter-field-pipe.location}}",
|
||||
"language-label": "Language",
|
||||
"age-rating-label": "Age Rating",
|
||||
"publication-status-label": "Publication Status",
|
||||
|
@ -1946,6 +1957,8 @@
|
|||
"formats": "Formats",
|
||||
"genres": "Genres",
|
||||
"inker": "Inker",
|
||||
"team": "Team",
|
||||
"location": "Location",
|
||||
"languages": "Languages",
|
||||
"libraries": "Libraries",
|
||||
"letterer": "Letterer",
|
||||
|
|
77
openapi.json
77
openapi.json
|
@ -7,7 +7,7 @@
|
|||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.14.8"
|
||||
"version": "0.7.14.9"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
@ -1101,6 +1101,10 @@
|
|||
},
|
||||
"FileName": {
|
||||
"type": "string"
|
||||
},
|
||||
"comicVineMatching": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1122,6 +1126,9 @@
|
|||
},
|
||||
"FileName": {
|
||||
"style": "form"
|
||||
},
|
||||
"comicVineMatching": {
|
||||
"style": "form"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1191,6 +1198,10 @@
|
|||
"dryRun": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"comicVineMatching": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1215,6 +1226,9 @@
|
|||
},
|
||||
"dryRun": {
|
||||
"style": "form"
|
||||
},
|
||||
"comicVineMatching": {
|
||||
"style": "form"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3204,7 +3218,9 @@
|
|||
10,
|
||||
11,
|
||||
12,
|
||||
13
|
||||
13,
|
||||
14,
|
||||
15
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -14316,6 +14332,20 @@
|
|||
},
|
||||
"nullable": true
|
||||
},
|
||||
"teams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PersonDto"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PersonDto"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"genres": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -15615,7 +15645,9 @@
|
|||
26,
|
||||
27,
|
||||
28,
|
||||
29
|
||||
29,
|
||||
30,
|
||||
31
|
||||
],
|
||||
"type": "integer",
|
||||
"description": "Represents the field which will dictate the value type and the Extension used for filtering",
|
||||
|
@ -16548,7 +16580,9 @@
|
|||
10,
|
||||
11,
|
||||
12,
|
||||
13
|
||||
13,
|
||||
14,
|
||||
15
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -16594,7 +16628,9 @@
|
|||
10,
|
||||
11,
|
||||
12,
|
||||
13
|
||||
13,
|
||||
14,
|
||||
15
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -17789,6 +17825,11 @@
|
|||
"description": "Highest path (that is under library root) that contains the series.",
|
||||
"nullable": true
|
||||
},
|
||||
"lowestFolderPath": {
|
||||
"type": "string",
|
||||
"description": "Lowest path (that is under library root) that contains all files for the series.",
|
||||
"nullable": true
|
||||
},
|
||||
"lastFolderScanned": {
|
||||
"type": "string",
|
||||
"description": "Last time the folder was scanned",
|
||||
|
@ -18261,6 +18302,12 @@
|
|||
"translatorLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"teamLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"locationLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"coverArtistLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
@ -18394,6 +18441,20 @@
|
|||
},
|
||||
"nullable": true
|
||||
},
|
||||
"teams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PersonDto"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PersonDto"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"ageRating": {
|
||||
"enum": [
|
||||
0,
|
||||
|
@ -18504,6 +18565,12 @@
|
|||
"translatorLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"teamLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"locationLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"coverArtistLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue