// 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.Linq; using System.Text.RegularExpressions; using EntityFrameworkCore.Projectables; using JetBrains.Annotations; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; namespace Kyoo.Abstractions.Models { /// /// A class to represent a single show's episode. /// public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate { // Use absolute numbers by default and fallback to season/episodes if it does not exists. public static Sort DefaultSort => new Sort.Conglomerate( new Sort.By(x => x.AbsoluteNumber), new Sort.By(x => x.SeasonNumber), new Sort.By(x => x.EpisodeNumber) ); /// public int Id { get; set; } /// [Computed] [MaxLength(256)] public string Slug { get { if (ShowSlug != null || Show?.Slug != null) return GetSlug(ShowSlug ?? Show!.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber); return GetSlug(ShowId.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber); } [UsedImplicitly] private set { Match match = Regex.Match(value, @"(?.+)-s(?\d+)e(?\d+)"); if (match.Success) { ShowSlug = match.Groups["show"].Value; SeasonNumber = int.Parse(match.Groups["season"].Value); EpisodeNumber = int.Parse(match.Groups["episode"].Value); } else { match = Regex.Match(value, @"(?.+)-(?\d+)"); if (match.Success) { ShowSlug = match.Groups["show"].Value; AbsoluteNumber = int.Parse(match.Groups["absolute"].Value); } else ShowSlug = value; SeasonNumber = null; EpisodeNumber = null; } } } /// /// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed. /// [SerializeIgnore] public string? ShowSlug { private get; set; } /// /// The ID of the Show containing this episode. /// public int ShowId { get; set; } /// /// The show that contains this episode. /// [LoadableRelation(nameof(ShowId))] public Show? Show { get; set; } /// /// The ID of the Season containing this episode. /// public int? SeasonId { get; set; } /// /// The season that contains this episode. /// /// /// This can be null if the season is unknown and the episode is only identified /// by it's . /// [LoadableRelation(nameof(SeasonId))] public Season? Season { get; set; } /// /// The season in witch this episode is in. /// public int? SeasonNumber { get; set; } /// /// The number of this episode in it's season. /// public int? EpisodeNumber { get; set; } /// /// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season. /// public int? AbsoluteNumber { get; set; } /// /// The path of the video file for this episode. /// public string Path { get; set; } /// /// The title of this episode. /// public string? Name { get; set; } /// /// The overview of this episode. /// public string? Overview { get; set; } /// /// How long is this episode? (in minutes) /// public int Runtime { get; set; } /// /// The release date of this episode. It can be null if unknown. /// public DateTime? ReleaseDate { get; set; } /// public DateTime AddedDate { get; set; } /// public Image? Poster { get; set; } /// public Image? Thumbnail { get; set; } /// public Image? Logo { get; set; } /// public Dictionary ExternalId { get; set; } = new(); /// /// The previous episode that should be seen before viewing this one. /// [Projectable(UseMemberBody = nameof(_PreviousEpisode), OnlyOnInclude = true)] [LoadableRelation] public Episode? PreviousEpisode { get; set; } private Episode? _PreviousEpisode => Show!.Episodes! .OrderByDescending(x => x.AbsoluteNumber) .ThenByDescending(x => x.SeasonNumber) .ThenByDescending(x => x.EpisodeNumber) .FirstOrDefault(x => x.AbsoluteNumber < AbsoluteNumber || x.SeasonNumber < SeasonNumber || (x.SeasonNumber == SeasonNumber && x.EpisodeNumber < EpisodeNumber) ); /// /// The next episode to watch after this one. /// [Projectable(UseMemberBody = nameof(_NextEpisode), OnlyOnInclude = true)] [LoadableRelation] public Episode? NextEpisode { get; set; } private Episode? _NextEpisode => Show!.Episodes! .OrderBy(x => x.AbsoluteNumber) .ThenBy(x => x.SeasonNumber) .ThenBy(x => x.EpisodeNumber) .FirstOrDefault(x => x.AbsoluteNumber > AbsoluteNumber || x.SeasonNumber > SeasonNumber || (x.SeasonNumber == SeasonNumber && x.EpisodeNumber > EpisodeNumber) ); /// /// Links to watch this episode. /// public VideoLinks Links => new() { Direct = $"/video/episode/{Slug}/direct", Hls = $"/video/episode/{Slug}/master.m3u8", }; /// /// Get the slug of an episode. /// /// The slug of the show. It can't be null. /// /// The season in which the episode is. /// If this is a movie or if the episode should be referred by it's absolute number, set this to null. /// /// /// The number of the episode in it's season. /// If this is a movie or if the episode should be referred by it's absolute number, set this to null. /// /// /// The absolute number of this show. /// If you don't know it or this is a movie, use null /// /// The slug corresponding to the given arguments public static string GetSlug(string showSlug, int? seasonNumber, int? episodeNumber, int? absoluteNumber = null) { return seasonNumber switch { null when absoluteNumber == null => showSlug, null => $"{showSlug}-{absoluteNumber}", _ => $"{showSlug}-s{seasonNumber}e{episodeNumber}" }; } } }