Chapter filename is set only when download happens
Some checks failed
Docker Image CI / build (push) Has been cancelled

https://github.com/C9Glax/tranga/issues/466
This commit is contained in:
2025-10-09 00:14:57 +02:00
parent 18f5021457
commit 7e8f575091
7 changed files with 608 additions and 30 deletions

View File

@@ -6,7 +6,7 @@ namespace API.Controllers.DTOs;
/// <summary>
/// <see cref="API.Schema.MangaContext.Chapter"/> DTO
/// </summary>
public sealed record Chapter(string Key, string MangaId, int? Volume, string ChapterNumber, string? Title, IEnumerable<MangaConnectorId> MangaConnectorIds, bool Downloaded, string FileName) : Identifiable(Key)
public sealed record Chapter(string Key, string MangaId, int? Volume, string ChapterNumber, string? Title, IEnumerable<MangaConnectorId> MangaConnectorIds, bool Downloaded, string? FileName) : Identifiable(Key)
{
/// <summary>
/// Identifier of the Manga this Chapter belongs to
@@ -53,7 +53,6 @@ public sealed record Chapter(string Key, string MangaId, int? Volume, string Cha
/// <summary>
/// Filename of the archive
/// </summary>
[Required]
[Description("Filename of the archive")]
public string FileName { get; init; } = FileName;
public string? FileName { get; init; } = FileName;
}

View File

@@ -0,0 +1,541 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<string>("Name")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.PrimitiveCollection<string[]>("BaseUris")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("IconUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string[]>("SupportedLanguages")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("text[]");
b.HasKey("Name");
b.ToTable("MangaConnector");
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("Key");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ChapterNumber")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<bool>("Downloaded")
.HasColumnType("boolean");
b.Property<string>("FileName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("Key");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.ToTable("FileLibraries");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("CoverUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DirectoryName")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("OriginalLanguage")
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("Key");
b.HasIndex("LibraryId");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToChapter");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("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<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.Property<string>("MetadataFetcherName")
.HasColumnType("text");
b.Property<string>("Identifier")
.HasColumnType("text");
b.Property<string>("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<string>("Name")
.HasColumnType("text");
b.Property<string>("MetadataEntry")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("character varying(21)");
b.HasKey("Name");
b.ToTable("MetadataFetcher");
b.HasDiscriminator<string>("MetadataEntry").HasValue("MetadataFetcher");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("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<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("character varying(64)");
b1.Property<string>("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<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b1.Property<string>("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<API.Schema.MangaContext.Chapter>", 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<API.Schema.MangaContext.Manga>", 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
}
}
}

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.Manga
{
/// <inheritdoc />
public partial class Chapter_Filename_is_set_own_download : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "FileName",
table: "Chapters",
type: "character varying(256)",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "FileName",
table: "Chapters",
type: "character varying(256)",
maxLength: 256,
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldNullable: true);
}
}
}

View File

@@ -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<string>("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");

View File

@@ -22,7 +22,7 @@ public class Chapter : Identifiable, IComparable<Chapter>
[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<Chapter>
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<Chapter>
/// <summary>
/// EF ONLY!!!
/// </summary>
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<Chapter>
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<Chapter>
{
try
{
return Path.Join(ParentManga.FullDirectoryPath, FileName);
return Path.Join(ParentManga.FullDirectoryPath, this.FileName is null ? GetArchiveFilePath() : FileName);
}
catch (Exception)
{

View File

@@ -17,21 +17,13 @@ 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<string, string> 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<string, string> 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<BaseWorker>();
return oldPath.Select(kv => new MoveFileOrFolderWorker(manga.Chapters.First(ch => ch.Key == kv.Key).FullArchiveFilePath!, kv.Value)).ToArray<BaseWorker>();
}
public override string ToString() => $"{base.ToString()} {MangaId} {LibraryId}";

View File

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