// 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
}
}