using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using API.MangaDownloadClients; using API.Schema.Jobs; using API.Schema.MangaConnectors; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using static System.IO.UnixFileMode; namespace API.Schema; [PrimaryKey("MangaId")] public class Manga { [StringLength(64)] [Required] public string MangaId { get; init; } [StringLength(128)] [Required] public string IdOnConnectorSite { get; init; } [StringLength(256)] [Required] public string Name { get; internal set; } [Required] public string Description { get; internal set; } [Url] [StringLength(256)] [Required] public string WebsiteUrl { get; internal set; } [JsonIgnore] [Url] public string CoverUrl { get; internal set; } [JsonIgnore] public string? CoverFileNameInCache { get; internal set; } [Required] public uint Year { get; internal set; } [StringLength(8)] [Required] public string? OriginalLanguage { get; internal set; } [Required] public MangaReleaseStatus ReleaseStatus { get; internal set; } [StringLength(256)] [Required] public string DirectoryName { get; private set; } public LocalLibrary? Library { get; internal set; } [JsonIgnore] [NotMapped] public string LibraryPath => Library is null ? TrangaSettings.downloadLocation : Library.BasePath; [JsonIgnore] [NotMapped] public string FullDirectoryPath => Path.Join(LibraryPath, DirectoryName); [Required] public float IgnoreChapterBefore { get; internal set; } [StringLength(64)] [Required] public string MangaConnectorId { get; private set; } [JsonIgnore] public MangaConnector? MangaConnector { get; private set; } [JsonIgnore] public ICollection? Authors { get; internal set; } [NotMapped] [StringLength(64)] [Required] public IEnumerable AuthorIds => Authors?.Select(a => a.AuthorId) ?? []; [JsonIgnore] public ICollection? MangaTags { get; internal set; } [NotMapped] [StringLength(64)] [Required] public IEnumerable Tags => MangaTags?.Select(t => t.Tag) ?? []; [JsonIgnore] public ICollection? Links { get; internal set; } [NotMapped] [StringLength(64)] [Required] public IEnumerable LinkIds => Links?.Select(l => l.LinkId) ?? []; [JsonIgnore] public ICollection? AltTitles { get; internal set; } [NotMapped] [StringLength(64)] [Required] public IEnumerable AltTitleIds => AltTitles?.Select(a => a.AltTitleId) ?? []; public Manga(string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl, string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus, float ignoreChapterBefore, MangaConnector mangaConnector, ICollection authors, ICollection mangaTags, ICollection links, ICollection altTitles, LocalLibrary? library = null) : this(idOnConnectorSite, name, description, websiteUrl, coverUrl, coverFileNameInCache, year, originalLanguage, releaseStatus, ignoreChapterBefore, mangaConnector.Name) { this.Authors = authors; this.MangaTags = mangaTags; this.Links = links; this.AltTitles = altTitles; this.Library = library; } public Manga(string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl, string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus, float ignoreChapterBefore, string mangaConnectorId) { MangaId = TokenGen.CreateToken(typeof(Manga), mangaConnectorId, idOnConnectorSite); IdOnConnectorSite = idOnConnectorSite; Name = name; Description = description; WebsiteUrl = websiteUrl; CoverUrl = coverUrl; CoverFileNameInCache = coverFileNameInCache; Year = year; OriginalLanguage = originalLanguage; ReleaseStatus = releaseStatus; IgnoreChapterBefore = ignoreChapterBefore; MangaConnectorId = mangaConnectorId; DirectoryName = BuildFolderName(name); } public MoveFileOrFolderJob UpdateFolderName(string downloadLocation, string newName) { string oldName = this.DirectoryName; this.DirectoryName = newName; return new MoveFileOrFolderJob(Path.Join(downloadLocation, oldName), Path.Join(downloadLocation, this.DirectoryName)); } internal void UpdateWithInfo(Manga other) { this.Name = other.Name; this.Year = other.Year; this.Description = other.Description; this.CoverUrl = other.CoverUrl; this.OriginalLanguage = other.OriginalLanguage; this.Authors = other.Authors; this.Links = other.Links; this.MangaTags = other.MangaTags; this.AltTitles = other.AltTitles; this.ReleaseStatus = other.ReleaseStatus; } private static string BuildFolderName(string mangaName) { return mangaName; } internal string? SaveCoverImageToCache(int retries = 3) { if(retries < 0) return null; Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))"); //https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains Match match = urlRex.Match(CoverUrl); string filename = $"{match.Groups[1].Value}-{MangaId}.{match.Groups[3].Value}"; string saveImagePath = Path.Join(TrangaSettings.coverImageCache, filename); if (File.Exists(saveImagePath)) return saveImagePath; RequestResult coverResult = new HttpDownloadClient().MakeRequest(CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}"); if (coverResult.statusCode is < HttpStatusCode.OK or >= HttpStatusCode.Ambiguous) return SaveCoverImageToCache(--retries); using MemoryStream ms = new(); coverResult.result.CopyTo(ms); Directory.CreateDirectory(TrangaSettings.coverImageCache); File.WriteAllBytes(saveImagePath, ms.ToArray()); return saveImagePath; } public string CreatePublicationFolder() { string publicationFolder = Path.Join(LibraryPath, this.DirectoryName); if(!Directory.Exists(publicationFolder)) Directory.CreateDirectory(publicationFolder); if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) File.SetUnixFileMode(publicationFolder, GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute | UserRead | UserWrite | UserExecute); return publicationFolder; } //TODO onchanges create job to update metadata files in archives, etc. }