using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; using PathIO = System.IO.Path; namespace Kyoo.Abstractions.Models { /// /// A watch item give information useful for playback. /// Information about tracks and display information that could be used by the player. /// This contains mostly data from an with another form. /// public class WatchItem { /// /// The ID of the episode associated with this item. /// public int EpisodeID { get; set; } /// /// The slug of this episode. /// public string Slug { get; set; } /// /// The title of the show containing this episode. /// public string ShowTitle { get; set; } /// /// The slug of the show containing this episode /// public string ShowSlug { get; set; } /// /// The season in witch this episode is in. /// public int? SeasonNumber { get; set; } /// /// The number of this episode is 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 title of this episode. /// public string Title { get; set; } /// /// The release date of this episode. It can be null if unknown. /// public DateTime? ReleaseDate { get; set; } /// /// The path of the video file for this episode. Any format supported by a is allowed. /// [SerializeIgnore] public string Path { get; set; } /// /// The episode that come before this one if you follow usual watch orders. /// If this is the first episode or this is a movie, it will be null. /// public Episode PreviousEpisode { get; set; } /// /// The episode that come after this one if you follow usual watch orders. /// If this is the last aired episode or this is a movie, it will be null. /// public Episode NextEpisode { get; set; } /// /// true if this is a movie, false otherwise. /// public bool IsMovie { get; set; } /// /// The path of this item's poster. /// By default, the http path for the poster is returned from the public API. /// This can be disabled using the internal query flag. /// [SerializeAs("{HOST}/api/show/{ShowSlug}/poster")] public string Poster { get; set; } /// /// The path of this item's logo. /// By default, the http path for the logo is returned from the public API. /// This can be disabled using the internal query flag. /// [SerializeAs("{HOST}/api/show/{ShowSlug}/logo")] public string Logo { get; set; } /// /// The path of this item's backdrop. /// By default, the http path for the backdrop is returned from the public API. /// This can be disabled using the internal query flag. /// [SerializeAs("{HOST}/api/show/{ShowSlug}/backdrop")] public string Backdrop { get; set; } /// /// The container of the video file of this episode. /// Common containers are mp4, mkv, avi and so on. /// public string Container { get; set; } /// /// The video track. See for more information. /// public Track Video { get; set; } /// /// The list of audio tracks. See for more information. /// public ICollection Audios { get; set; } /// /// The list of subtitles tracks. See for more information. /// public ICollection Subtitles { get; set; } /// /// The list of chapters. See for more information. /// public ICollection Chapters { get; set; } /// /// Create a from an . /// /// The episode to transform. /// /// A library manager to retrieve the next and previous episode and load the show & tracks of the episode. /// /// A new WatchItem representing the given episode. public static async Task FromEpisode(Episode ep, ILibraryManager library) { Episode previous = null; Episode next = null; await library.Load(ep, x => x.Show); await library.Load(ep, x => x.Tracks); if (!ep.Show.IsMovie && ep.SeasonNumber != null && ep.EpisodeNumber != null) { if (ep.EpisodeNumber > 1) previous = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value - 1); else if (ep.SeasonNumber > 1) { previous = (await library.GetAll(x => x.ShowID == ep.ShowID && x.SeasonNumber == ep.SeasonNumber.Value - 1, limit: 1, sort: new Sort(x => x.EpisodeNumber, true)) ).FirstOrDefault(); } if (ep.EpisodeNumber >= await library.GetCount(x => x.SeasonID == ep.SeasonID)) next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value + 1, 1); else next = await library.GetOrDefault(ep.ShowID, ep.SeasonNumber.Value, ep.EpisodeNumber.Value + 1); } else if (!ep.Show.IsMovie && ep.AbsoluteNumber != null) { previous = await library.GetOrDefault(x => x.ShowID == ep.ShowID && x.AbsoluteNumber == ep.EpisodeNumber + 1); next = await library.GetOrDefault(x => x.ShowID == ep.ShowID && x.AbsoluteNumber == ep.AbsoluteNumber + 1); } return new WatchItem { EpisodeID = ep.ID, Slug = ep.Slug, ShowSlug = ep.Show.Slug, ShowTitle = ep.Show.Title, SeasonNumber = ep.SeasonNumber, EpisodeNumber = ep.EpisodeNumber, AbsoluteNumber = ep.AbsoluteNumber, Title = ep.Title, ReleaseDate = ep.ReleaseDate, Path = ep.Path, Container = PathIO.GetExtension(ep.Path)![1..], Video = ep.Tracks.FirstOrDefault(x => x.Type == StreamType.Video), Audios = ep.Tracks.Where(x => x.Type == StreamType.Audio).ToArray(), Subtitles = ep.Tracks.Where(x => x.Type == StreamType.Subtitle).ToArray(), PreviousEpisode = previous, NextEpisode = next, Chapters = await GetChapters(ep.Path) }; } // TODO move this method in a controller to support abstraction. // TODO use a IFileManager to retrieve and read files. private static async Task> GetChapters(string episodePath) { string path = PathIO.Combine( PathIO.GetDirectoryName(episodePath)!, "Chapters", PathIO.GetFileNameWithoutExtension(episodePath) + ".txt" ); if (!File.Exists(path)) return Array.Empty(); try { return (await File.ReadAllLinesAsync(path)) .Select(x => { string[] values = x.Split(' '); return new Chapter(float.Parse(values[0]), float.Parse(values[1]), string.Join(' ', values.Skip(2))); }) .ToArray(); } catch { await Console.Error.WriteLineAsync($"Invalid chapter file at {path}"); return Array.Empty(); } } } }