From 7e8f575091531b951585c78403f699cfec2ee7e6 Mon Sep 17 00:00:00 2001 From: glax Date: Thu, 9 Oct 2025 00:14:57 +0200 Subject: [PATCH] Chapter filename is set only when download happens https://github.com/C9Glax/tranga/issues/466 --- API/Controllers/DTOs/Chapter.cs | 5 +- ...r_Filename_is_set_own_download.Designer.cs | 541 ++++++++++++++++++ ...07_Chapter_Filename_is_set_own_download.cs | 40 ++ .../Manga/MangaContextModelSnapshot.cs | 24 +- API/Schema/MangaContext/Chapter.cs | 9 +- API/Workers/MoveMangaLibraryWorker.cs | 14 +- API/openapi/API_v2.json | 5 +- 7 files changed, 608 insertions(+), 30 deletions(-) create mode 100644 API/Migrations/Manga/20251008220207_Chapter_Filename_is_set_own_download.Designer.cs create mode 100644 API/Migrations/Manga/20251008220207_Chapter_Filename_is_set_own_download.cs diff --git a/API/Controllers/DTOs/Chapter.cs b/API/Controllers/DTOs/Chapter.cs index f9eae85..2d20a8a 100644 --- a/API/Controllers/DTOs/Chapter.cs +++ b/API/Controllers/DTOs/Chapter.cs @@ -6,7 +6,7 @@ namespace API.Controllers.DTOs; /// /// DTO /// -public sealed record Chapter(string Key, string MangaId, int? Volume, string ChapterNumber, string? Title, IEnumerable MangaConnectorIds, bool Downloaded, string FileName) : Identifiable(Key) +public sealed record Chapter(string Key, string MangaId, int? Volume, string ChapterNumber, string? Title, IEnumerable MangaConnectorIds, bool Downloaded, string? FileName) : Identifiable(Key) { /// /// Identifier of the Manga this Chapter belongs to @@ -53,7 +53,6 @@ public sealed record Chapter(string Key, string MangaId, int? Volume, string Cha /// /// Filename of the archive /// - [Required] [Description("Filename of the archive")] - public string FileName { get; init; } = FileName; + public string? FileName { get; init; } = FileName; } \ No newline at end of file diff --git a/API/Migrations/Manga/20251008220207_Chapter_Filename_is_set_own_download.Designer.cs b/API/Migrations/Manga/20251008220207_Chapter_Filename_is_set_own_download.Designer.cs new file mode 100644 index 0000000..9631089 --- /dev/null +++ b/API/Migrations/Manga/20251008220207_Chapter_Filename_is_set_own_download.Designer.cs @@ -0,0 +1,541 @@ +// +using API.Schema.MangaContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations.Manga +{ + [DbContext(typeof(MangaContext))] + [Migration("20251008220207_Chapter_Filename_is_set_own_download")] + partial class Chapter_Filename_is_set_own_download + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("API.MangaConnectors.MangaConnector", b => + { + b.Property("Name") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.PrimitiveCollection("BaseUris") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("text[]"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IconUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.PrimitiveCollection("SupportedLanguages") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("text[]"); + + b.HasKey("Name"); + + b.ToTable("MangaConnector"); + + b.HasDiscriminator("Name").HasValue("MangaConnector"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.MangaContext.Author", b => + { + b.Property("Key") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Key"); + + b.ToTable("Authors"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.Chapter", b => + { + b.Property("Key") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ChapterNumber") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Downloaded") + .HasColumnType("boolean"); + + b.Property("FileName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ParentMangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("VolumeNumber") + .HasColumnType("integer"); + + b.HasKey("Key"); + + b.HasIndex("ParentMangaId"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b => + { + b.Property("Key") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("BasePath") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LibraryName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Key"); + + b.ToTable("FileLibraries"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.Manga", b => + { + b.Property("Key") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CoverFileNameInCache") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("CoverUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DirectoryName") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("IgnoreChaptersBefore") + .HasColumnType("real"); + + b.Property("LibraryId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("OriginalLanguage") + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("ReleaseStatus") + .HasColumnType("smallint"); + + b.Property("Year") + .HasColumnType("bigint"); + + b.HasKey("Key"); + + b.HasIndex("LibraryId"); + + b.ToTable("Mangas"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId", b => + { + b.Property("Key") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IdOnConnectorSite") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("MangaConnectorName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ObjId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UseForDownload") + .HasColumnType("boolean"); + + b.Property("WebsiteUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Key"); + + b.HasIndex("ObjId"); + + b.ToTable("MangaConnectorToChapter"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId", b => + { + b.Property("Key") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IdOnConnectorSite") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("MangaConnectorName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ObjId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UseForDownload") + .HasColumnType("boolean"); + + b.Property("WebsiteUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Key"); + + b.HasIndex("ObjId"); + + b.ToTable("MangaConnectorToManga"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b => + { + b.Property("Tag") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Tag"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b => + { + b.Property("MetadataFetcherName") + .HasColumnType("text"); + + b.Property("Identifier") + .HasColumnType("text"); + + b.Property("MangaId") + .IsRequired() + .HasColumnType("character varying(64)"); + + b.HasKey("MetadataFetcherName", "Identifier"); + + b.HasIndex("MangaId"); + + b.ToTable("MetadataEntries"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("MetadataEntry") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("character varying(21)"); + + b.HasKey("Name"); + + b.ToTable("MetadataFetcher"); + + b.HasDiscriminator("MetadataEntry").HasValue("MetadataFetcher"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("AuthorToManga", b => + { + b.Property("AuthorIds") + .HasColumnType("character varying(64)"); + + b.Property("MangaIds") + .HasColumnType("character varying(64)"); + + b.HasKey("AuthorIds", "MangaIds"); + + b.HasIndex("MangaIds"); + + b.ToTable("AuthorToManga"); + }); + + modelBuilder.Entity("MangaTagToManga", b => + { + b.Property("MangaTagIds") + .HasColumnType("character varying(64)"); + + b.Property("MangaIds") + .HasColumnType("character varying(64)"); + + b.HasKey("MangaTagIds", "MangaIds"); + + b.HasIndex("MangaIds"); + + b.ToTable("MangaTagToManga"); + }); + + modelBuilder.Entity("API.MangaConnectors.Global", b => + { + b.HasBaseType("API.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Global"); + }); + + modelBuilder.Entity("API.MangaConnectors.MangaDex", b => + { + b.HasBaseType("API.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaDex"); + }); + + modelBuilder.Entity("API.MangaConnectors.MangaPark", b => + { + b.HasBaseType("API.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaPark"); + }); + + modelBuilder.Entity("API.MangaConnectors.Mangaworld", b => + { + b.HasBaseType("API.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Mangaworld"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b => + { + b.HasBaseType("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher"); + + b.HasDiscriminator().HasValue("MyAnimeList"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.Chapter", b => + { + b.HasOne("API.Schema.MangaContext.Manga", "ParentManga") + .WithMany("Chapters") + .HasForeignKey("ParentMangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ParentManga"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.Manga", b => + { + b.HasOne("API.Schema.MangaContext.FileLibrary", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 => + { + b1.Property("Key") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b1.Property("MangaKey") + .IsRequired() + .HasColumnType("character varying(64)"); + + b1.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b1.HasKey("Key"); + + b1.HasIndex("MangaKey"); + + b1.ToTable("AltTitle"); + + b1.WithOwner() + .HasForeignKey("MangaKey"); + }); + + b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 => + { + b1.Property("Key") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkProvider") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("LinkUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("MangaKey") + .IsRequired() + .HasColumnType("character varying(64)"); + + b1.HasKey("Key"); + + b1.HasIndex("MangaKey"); + + b1.ToTable("Link"); + + b1.WithOwner() + .HasForeignKey("MangaKey"); + }); + + b.Navigation("AltTitles"); + + b.Navigation("Library"); + + b.Navigation("Links"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId", b => + { + b.HasOne("API.Schema.MangaContext.Chapter", "Obj") + .WithMany("MangaConnectorIds") + .HasForeignKey("ObjId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Obj"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId", b => + { + b.HasOne("API.Schema.MangaContext.Manga", "Obj") + .WithMany("MangaConnectorIds") + .HasForeignKey("ObjId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Obj"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b => + { + b.HasOne("API.Schema.MangaContext.Manga", "Manga") + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher") + .WithMany() + .HasForeignKey("MetadataFetcherName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Manga"); + + b.Navigation("MetadataFetcher"); + }); + + modelBuilder.Entity("AuthorToManga", b => + { + b.HasOne("API.Schema.MangaContext.Author", null) + .WithMany() + .HasForeignKey("AuthorIds") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.MangaContext.Manga", null) + .WithMany() + .HasForeignKey("MangaIds") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MangaTagToManga", b => + { + b.HasOne("API.Schema.MangaContext.Manga", null) + .WithMany() + .HasForeignKey("MangaIds") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.MangaContext.MangaTag", null) + .WithMany() + .HasForeignKey("MangaTagIds") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Schema.MangaContext.Chapter", b => + { + b.Navigation("MangaConnectorIds"); + }); + + modelBuilder.Entity("API.Schema.MangaContext.Manga", b => + { + b.Navigation("Chapters"); + + b.Navigation("MangaConnectorIds"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/Manga/20251008220207_Chapter_Filename_is_set_own_download.cs b/API/Migrations/Manga/20251008220207_Chapter_Filename_is_set_own_download.cs new file mode 100644 index 0000000..6d6aa06 --- /dev/null +++ b/API/Migrations/Manga/20251008220207_Chapter_Filename_is_set_own_download.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Migrations.Manga +{ + /// + public partial class Chapter_Filename_is_set_own_download : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "FileName", + table: "Chapters", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "FileName", + table: "Chapters", + type: "character varying(256)", + maxLength: 256, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + } + } +} diff --git a/API/Migrations/Manga/MangaContextModelSnapshot.cs b/API/Migrations/Manga/MangaContextModelSnapshot.cs index aded9d6..ec904a7 100644 --- a/API/Migrations/Manga/MangaContextModelSnapshot.cs +++ b/API/Migrations/Manga/MangaContextModelSnapshot.cs @@ -16,7 +16,7 @@ namespace API.Migrations.Manga { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("ProductVersion", "9.0.9") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -85,7 +85,6 @@ namespace API.Migrations.Manga .HasColumnType("boolean"); b.Property("FileName") - .IsRequired() .HasMaxLength(256) .HasColumnType("character varying(256)"); @@ -331,13 +330,6 @@ namespace API.Migrations.Manga b.ToTable("MangaTagToManga"); }); - modelBuilder.Entity("API.MangaConnectors.ComickIo", b => - { - b.HasBaseType("API.MangaConnectors.MangaConnector"); - - b.HasDiscriminator().HasValue("ComickIo"); - }); - modelBuilder.Entity("API.MangaConnectors.Global", b => { b.HasBaseType("API.MangaConnectors.MangaConnector"); @@ -352,6 +344,20 @@ namespace API.Migrations.Manga b.HasDiscriminator().HasValue("MangaDex"); }); + modelBuilder.Entity("API.MangaConnectors.MangaPark", b => + { + b.HasBaseType("API.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaPark"); + }); + + modelBuilder.Entity("API.MangaConnectors.Mangaworld", b => + { + b.HasBaseType("API.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Mangaworld"); + }); + modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b => { b.HasBaseType("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher"); diff --git a/API/Schema/MangaContext/Chapter.cs b/API/Schema/MangaContext/Chapter.cs index 0c0c80c..6be5d88 100644 --- a/API/Schema/MangaContext/Chapter.cs +++ b/API/Schema/MangaContext/Chapter.cs @@ -22,7 +22,7 @@ public class Chapter : Identifiable, IComparable [StringLength(256)] public string? Title { get; private set; } - [StringLength(256)] public string FileName { get; private set; } + [StringLength(256)] public string? FileName { get; private set; } public bool Downloaded { get; internal set; } @@ -43,7 +43,6 @@ public class Chapter : Identifiable, IComparable this.MangaConnectorIds = []; this.VolumeNumber = volumeNumber; this.Title = title; - this.FileName = GetArchiveFilePath().CleanNameForWindows(); this.Downloaded = false; this.MangaConnectorIds = []; } @@ -51,7 +50,7 @@ public class Chapter : Identifiable, IComparable /// /// EF ONLY!!! /// - internal Chapter(string key, int? volumeNumber, string chapterNumber, string? title, string fileName, bool downloaded) + internal Chapter(string key, int? volumeNumber, string chapterNumber, string? title, string? fileName, bool downloaded) : base(key) { this.VolumeNumber = volumeNumber; @@ -91,6 +90,8 @@ public class Chapter : Identifiable, IComparable if (chapter.ParentManga.Library is null) return false; + if (chapter.FileName is null) + return false; this.Downloaded = File.Exists(chapter.FullArchiveFilePath); await context.Sync(token??CancellationToken.None, GetType(), $"CheckDownloaded {this} {this.Downloaded}"); @@ -169,7 +170,7 @@ public class Chapter : Identifiable, IComparable { try { - return Path.Join(ParentManga.FullDirectoryPath, FileName); + return Path.Join(ParentManga.FullDirectoryPath, this.FileName is null ? GetArchiveFilePath() : FileName); } catch (Exception) { diff --git a/API/Workers/MoveMangaLibraryWorker.cs b/API/Workers/MoveMangaLibraryWorker.cs index 815ab94..bab52d0 100644 --- a/API/Workers/MoveMangaLibraryWorker.cs +++ b/API/Workers/MoveMangaLibraryWorker.cs @@ -17,20 +17,12 @@ public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumera // Get Manga (with and Library) if (await DbContext.Mangas .Include(m => m.Library) + .Include(m => m.Chapters) .FirstOrDefaultAsync(m => m.Key == MangaId, CancellationToken) is not { } manga) { Log.Error("Could not find Manga."); return []; } - - if (await DbContext.Chapters - .Include(ch => ch.ParentManga).ThenInclude(m => m.Library) - .Where(ch => ch.ParentMangaId == MangaId) - .ToListAsync(CancellationToken) is not { } chapters) - { - Log.Error("Could not find chapters."); - return []; - } // Get new Library if (await DbContext.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, CancellationToken) is not { } toLibrary) @@ -40,7 +32,7 @@ public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumera } // Save old Path (to later move chapters) - Dictionary oldPath = manga.Chapters.ToDictionary(c => c.Key, c => c.FullArchiveFilePath).Where(kv => kv.Value is not null).ToDictionary(x => x.Key, x => x.Value)!; + Dictionary oldPath = manga.Chapters.Where(c => c.FileName != null).ToDictionary(c => c.Key, c => c.FullArchiveFilePath)!; // Set new Path manga.Library = toLibrary; @@ -48,7 +40,7 @@ public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IEnumera return []; // Create Jobs to move chapters from old to new Path - return manga.Chapters.Select(c => new MoveFileOrFolderWorker(c.FullArchiveFilePath, oldPath[c.Key])).ToArray(); + return oldPath.Select(kv => new MoveFileOrFolderWorker(manga.Chapters.First(ch => ch.Key == kv.Key).FullArchiveFilePath!, kv.Value)).ToArray(); } public override string ToString() => $"{base.ToString()} {MangaId} {LibraryId}"; diff --git a/API/openapi/API_v2.json b/API/openapi/API_v2.json index d725b20..866ca40 100644 --- a/API/openapi/API_v2.json +++ b/API/openapi/API_v2.json @@ -3406,7 +3406,6 @@ "required": [ "chapterNumber", "downloaded", - "fileName", "key", "mangaConnectorIds", "mangaId", @@ -3447,9 +3446,9 @@ "description": "Ids of the Manga on MangaConnectors" }, "fileName": { - "minLength": 1, "type": "string", - "description": "Filename of the archive" + "description": "Filename of the archive", + "nullable": true }, "key": { "maxLength": 64,