// Kyoo - A portable and vast media library solution. // Copyright (c) Kyoo. // // See AUTHORS.md and LICENSE file in the project root for full license information. // // Kyoo is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // // Kyoo is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text.Json.Serialization; using EntityFrameworkCore.Projectables; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Utils; namespace Kyoo.Abstractions.Models { /// /// A series or a movie. /// public class Show : IQuery, IResource, IMetadata, IOnMerge, IThumbnails, IAddedDate, ILibraryItem, IWatchlist { public static Sort DefaultSort => new Sort.By(x => x.Name); /// public Guid Id { get; set; } /// [MaxLength(256)] public string Slug { get; set; } /// /// The title of this show. /// public string Name { get; set; } /// /// A catchphrase for this show. /// public string? Tagline { get; set; } /// /// The list of alternative titles of this show. /// public List Aliases { get; set; } = new(); /// /// The summary of this show. /// public string? Overview { get; set; } /// /// A list of tags that match this movie. /// public List Tags { get; set; } = new(); /// /// The list of genres (themes) this show has. /// public List Genres { get; set; } = new(); /// /// Is this show airing, not aired yet or finished? /// public Status Status { get; set; } /// /// How well this item is rated? (from 0 to 100). /// public int Rating { get; set; } /// /// The date this show started airing. It can be null if this is unknown. /// public DateTime? StartAir { get; set; } /// /// The date this show finished airing. /// It can also be null if this is unknown. /// public DateTime? EndAir { get; set; } /// public DateTime AddedDate { get; set; } /// public Image? Poster { get; set; } /// public Image? Thumbnail { get; set; } /// public Image? Logo { get; set; } /// /// A video of a few minutes that tease the content. /// public string? Trailer { get; set; } [JsonIgnore] [Column("start_air")] public DateTime? AirDate => StartAir; /// public Dictionary ExternalId { get; set; } = new(); /// /// The ID of the Studio that made this show. /// public Guid? StudioId { get; set; } /// /// The Studio that made this show. /// [LoadableRelation(nameof(StudioId))] public Studio? Studio { get; set; } /// /// The different seasons in this show. If this is a movie, this list is always null or empty. /// [JsonIgnore] public ICollection? Seasons { get; set; } /// /// The list of episodes in this show. /// If this is a movie, there will be a unique episode (with the seasonNumber and episodeNumber set to null). /// Having an episode is necessary to store metadata and tracks. /// [JsonIgnore] public ICollection? Episodes { get; set; } /// /// The list of collections that contains this show. /// [JsonIgnore] public ICollection? Collections { get; set; } /// /// The first episode of this show. /// [Projectable(UseMemberBody = nameof(_FirstEpisode), OnlyOnInclude = true)] [LoadableRelation( // language=PostgreSQL Sql = """ select fe.* -- Episode as fe from ( select e.*, row_number() over (partition by e.show_id order by e.absolute_number, e.season_number, e.episode_number) as number from episodes as e) as "fe" where fe.number <= 1 """, On = "show_id = \"this\".id" )] public Episode? FirstEpisode { get; set; } private Episode? _FirstEpisode => Episodes! .OrderBy(x => x.AbsoluteNumber) .ThenBy(x => x.SeasonNumber) .ThenBy(x => x.EpisodeNumber) .FirstOrDefault(); /// /// The number of episodes in this show. /// [Projectable(UseMemberBody = nameof(_EpisodesCount), OnlyOnInclude = true)] [NotMapped] [LoadableRelation( // language=PostgreSQL Projected = """ ( select count(*)::int from episodes as e where e.show_id = "this".id) as episodes_count """ )] public int EpisodesCount { get; set; } private int _EpisodesCount => Episodes!.Count; [JsonIgnore] public ICollection? Watched { get; set; } /// /// Metadata of what an user as started/planned to watch. /// [Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)] [LoadableRelation( Sql = "show_watch_status", On = "show_id = \"this\".id and \"relation\".user_id = [current_user]" )] public ShowWatchStatus? WatchStatus { get; set; } // There is a global query filter to filter by user so we just need to do single. private ShowWatchStatus? _WatchStatus => Watched!.FirstOrDefault(); /// public void OnMerge(object merged) { if (Seasons != null) { foreach (Season season in Seasons) season.Show = this; } if (Episodes != null) { foreach (Episode episode in Episodes) episode.Show = this; } } public Show() { } [JsonConstructor] public Show(string name) { if (name != null) { Slug = Utility.ToSlug(name); Name = name; } } } /// /// The enum containing show's status. /// public enum Status { /// /// The status of the show is not known. /// Unknown, /// /// The show has finished airing. /// Finished, /// /// The show is still actively airing. /// Airing, /// /// This show has not aired yet but has been announced. /// Planned } }