using System; using System.Collections.Generic; using System.Globalization; using API.Entities.Enums; using API.Entities.Interfaces; using API.Entities.Metadata; using API.Entities.Person; using API.Extensions; using API.Services.Tasks.Scanner.Parser; namespace API.Entities; public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage { public int Id { get; set; } /// /// Range of numbers. Chapter 2-4 -> "2-4". Chapter 2 -> "2". If the chapter is a special, will return the Special Name /// public required string Range { get; set; } /// /// Smallest number of the Range. Can be a partial like Chapter 4.5 /// [Obsolete("Use MinNumber and MaxNumber instead")] public required string Number { get; set; } /// /// Minimum Chapter Number. /// public float MinNumber { get; set; } /// /// Maximum Chapter Number /// public float MaxNumber { get; set; } /// /// The sorting order of the Chapter. Inherits from MinNumber, but can be overridden. /// public float SortOrder { get; set; } /// /// Can the sort order be updated on scan or is it locked from UI /// public bool SortOrderLocked { get; set; } /// /// The files that represent this Chapter /// public ICollection Files { get; set; } = null!; public DateTime Created { get; set; } public DateTime LastModified { get; set; } public DateTime CreatedUtc { get; set; } public DateTime LastModifiedUtc { get; set; } public string? CoverImage { get; set; } public string PrimaryColor { get; set; } public string SecondaryColor { get; set; } public bool CoverImageLocked { get; set; } /// /// Total number of pages in all MangaFiles /// public int Pages { get; set; } /// /// If this Chapter contains files that could only be identified as Series or has Special Identifier from filename /// public bool IsSpecial { get; set; } /// /// Used for books/specials to display custom title. For non-specials/books, will be set to /// public string? Title { get; set; } /// /// Age Rating for the issue/chapter /// public AgeRating AgeRating { get; set; } /// /// Chapter title /// /// This should not be confused with Title which is used for special filenames. public string TitleName { get; set; } = string.Empty; /// /// Date which chapter was released /// public DateTime ReleaseDate { get; set; } /// /// Summary for the Chapter/Issue /// public string? Summary { get; set; } /// /// Language for the Chapter/Issue /// public string? Language { get; set; } /// /// Total number of issues or volumes in the series. This is straight from ComicInfo /// public int TotalCount { get; set; } = 0; /// /// Number of the Total Count (progress the Series is complete) /// /// This is either the highest of ComicInfo Count field and (nonparsed volume/chapter number) public int Count { get; set; } = 0; /// /// SeriesGroup tag in ComicInfo /// public string SeriesGroup { get; set; } = string.Empty; public string StoryArc { get; set; } = string.Empty; public string StoryArcNumber { get; set; } = string.Empty; public string AlternateNumber { get; set; } = string.Empty; public string AlternateSeries { get; set; } = string.Empty; /// /// Not currently used in Kavita /// public int AlternateCount { get; set; } = 0; /// /// Total Word count of all chapters in this chapter. /// /// Word Count is only available from EPUB files public long WordCount { get; set; } /// public int MinHoursToRead { get; set; } /// public int MaxHoursToRead { get; set; } /// public float AvgHoursToRead { get; set; } /// /// Comma-separated link of urls to external services that have some relation to the Chapter /// public string WebLinks { get; set; } = string.Empty; public string ISBN { get; set; } = string.Empty; /// /// (Kavita+) Average rating from Kavita+ metadata /// public float AverageExternalRating { get; set; } = 0f; #region Locks public bool AgeRatingLocked { get; set; } public bool TitleNameLocked { get; set; } public bool GenresLocked { get; set; } public bool TagsLocked { get; set; } public bool WriterLocked { get; set; } public bool CharacterLocked { get; set; } public bool ColoristLocked { get; set; } public bool EditorLocked { get; set; } public bool InkerLocked { get; set; } public bool ImprintLocked { get; set; } public bool LettererLocked { get; set; } 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 LanguageLocked { get; set; } public bool SummaryLocked { get; set; } public bool ISBNLocked { get; set; } public bool ReleaseDateLocked { get; set; } #endregion /// /// All people attached at a Chapter level. Usually Comics will have different people per issue. /// public ICollection People { get; set; } = new List(); /// /// Genres for the Chapter /// public ICollection Genres { get; set; } = new List(); public ICollection Tags { get; set; } = new List(); public ICollection Ratings { get; set; } = []; public ICollection UserProgress { get; set; } // Relationships public Volume Volume { get; set; } = null!; public int VolumeId { get; set; } public ICollection ExternalReviews { get; set; } = []; public ICollection ExternalRatings { get; set; } = null!; public void UpdateFrom(ParserInfo info) { Files ??= new List(); IsSpecial = info.IsSpecialInfo(); if (IsSpecial) { Number = Parser.DefaultChapter; MinNumber = Parser.DefaultChapterNumber; MaxNumber = Parser.DefaultChapterNumber; } Title = (IsSpecial && info.Format is MangaFormat.Epub or MangaFormat.Pdf) ? info.Title : Parser.RemoveExtensionIfSupported(Range); var specialTreatment = info.IsSpecialInfo(); Range = specialTreatment ? info.Filename : info.Chapters; } /// /// Returns the Chapter Number. If the chapter is a range, returns that, formatted. /// /// public string GetNumberTitle() { try { if (MinNumber.Is(MaxNumber)) { if (MinNumber.Is(Parser.DefaultChapterNumber) && IsSpecial) { return Parser.RemoveExtensionIfSupported(Title); } if (MinNumber.Is(0f) && !float.TryParse(Range, CultureInfo.InvariantCulture, out _)) { return $"{Range.ToString(CultureInfo.InvariantCulture)}"; } return $"{MinNumber.ToString(CultureInfo.InvariantCulture)}"; } return $"{MinNumber.ToString(CultureInfo.InvariantCulture)}-{MaxNumber.ToString(CultureInfo.InvariantCulture)}"; } catch (Exception) { return MinNumber.ToString(CultureInfo.InvariantCulture); } } /// /// Is the Chapter representing a single Volume (volume 1.cbz). If so, Min/Max will be Default and will not be special /// /// public bool IsSingleVolumeChapter() { return MinNumber.Is(Parser.DefaultChapterNumber) && !IsSpecial; } public void ResetColorScape() { PrimaryColor = string.Empty; SecondaryColor = string.Empty; } public bool IsPersonRoleLocked(PersonRole role) { return role switch { PersonRole.Character => CharacterLocked, PersonRole.Writer => WriterLocked, PersonRole.Penciller => PencillerLocked, PersonRole.Inker => InkerLocked, PersonRole.Colorist => ColoristLocked, PersonRole.Letterer => LettererLocked, PersonRole.CoverArtist => CoverArtistLocked, PersonRole.Editor => EditorLocked, PersonRole.Publisher => PublisherLocked, PersonRole.Translator => TranslatorLocked, PersonRole.Imprint => ImprintLocked, PersonRole.Team => TeamLocked, PersonRole.Location => LocationLocked, _ => throw new ArgumentOutOfRangeException(nameof(role), role, null) }; } }