diff --git a/AUTHORS.md b/AUTHORS.md index b7f88d9d..ad32290a 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,4 +1,4 @@ # Authors -Alphabetical order by first name. +Ordered by the date of the first commit. * Zoe Roux ([@AnonymusRaccoon](http://github.com/AnonymusRaccoon)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e36365a..6dbaad61 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,7 @@ Here are a few things you can do that will increase the likelihood of your pull ## Resources +- [Why should you indent with tabs](https://www.reddit.com/r/javascript/comments/c8drjo/nobody_talks_about_the_real_reason_to_use_tabs/) - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [Using Pull Requests](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) - [GitHub Help](https://docs.github.com/en) diff --git a/src/Kyoo.Abstractions/Models/Resources/Track.cs b/src/Kyoo.Abstractions/Models/Resources/Track.cs index e6d5d7e1..14f471b0 100644 --- a/src/Kyoo.Abstractions/Models/Resources/Track.cs +++ b/src/Kyoo.Abstractions/Models/Resources/Track.cs @@ -73,7 +73,7 @@ namespace Kyoo.Abstractions.Models { string type = Type.ToString().ToLower(); string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty; - string episode = EpisodeSlug ?? Episode?.Slug ?? EpisodeID.ToString(); + string episode = _episodeSlug ?? Episode?.Slug ?? EpisodeID.ToString(); return $"{episode}.{Language ?? "und"}{index}{(IsForced ? ".forced" : string.Empty)}.{type}"; } @@ -90,7 +90,7 @@ namespace Kyoo.Abstractions.Models "Format: {episodeSlug}.{language}[-{index}][.forced].{type}[.{extension}]"); } - EpisodeSlug = match.Groups["ep"].Value; + _episodeSlug = match.Groups["ep"].Value; Language = match.Groups["lang"].Value; if (Language == "und") Language = null; @@ -100,11 +100,6 @@ namespace Kyoo.Abstractions.Models } } - /// - /// The slug of the episode that contain this track. If this is not set, this track is ill-formed. - /// - [SerializeIgnore] public string EpisodeSlug { private get; set; } - /// /// The title of the stream. /// @@ -153,7 +148,16 @@ namespace Kyoo.Abstractions.Models /// /// The episode that uses this track. /// - [LoadableRelation(nameof(EpisodeID))] public Episode Episode { get; set; } + [LoadableRelation(nameof(EpisodeID))] public Episode Episode + { + get => _episode; + set + { + _episode = value; + if (_episode != null) + _episodeSlug = _episode.Slug; + } + } /// /// The index of this track on the episode. @@ -184,6 +188,17 @@ namespace Kyoo.Abstractions.Models } } + /// + /// The slug of the episode that contain this track. If this is not set, this track is ill-formed. + /// + [SerializeIgnore] private string _episodeSlug; + + /// + /// The episode that uses this track. + /// This is the baking field of . + /// + [SerializeIgnore] private Episode _episode; + // Converting mkv track language to c# system language tag. private static string _GetLanguage(string mkvLanguage) { diff --git a/src/Kyoo.Abstractions/Models/Utils/Constants.cs b/src/Kyoo.Abstractions/Models/Utils/Constants.cs index dd0ac925..5267347b 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Constants.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Constants.cs @@ -34,6 +34,6 @@ namespace Kyoo.Abstractions.Models.Utils /// /// A group name for . It should be used for every . /// - public const string ResourceGroup = "Resources"; + public const string ResourcesGroup = "Resources"; } } diff --git a/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index f2042c65..2ceebd9c 100644 --- a/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -169,7 +169,6 @@ namespace Kyoo.Core.Controllers resource.Tracks = await resource.Tracks.SelectAsync(x => { x.Episode = resource; - x.EpisodeSlug = resource.Slug; return _tracks.Create(x); }).ToListAsync(); _database.Tracks.AttachRange(resource.Tracks); diff --git a/src/Kyoo.Core/Views/CollectionApi.cs b/src/Kyoo.Core/Views/CollectionApi.cs index 65f49a90..705fe745 100644 --- a/src/Kyoo.Core/Views/CollectionApi.cs +++ b/src/Kyoo.Core/Views/CollectionApi.cs @@ -40,7 +40,7 @@ namespace Kyoo.Core.Api [Route("api/collection", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(CollectionApi))] - [ApiDefinition("Collections", Group = ResourceGroup)] + [ApiDefinition("Collections", Group = ResourcesGroup)] public class CollectionApi : CrudThumbsApi { /// diff --git a/src/Kyoo.Core/Views/EpisodeApi.cs b/src/Kyoo.Core/Views/EpisodeApi.cs index 37591b99..fc5b03ae 100644 --- a/src/Kyoo.Core/Views/EpisodeApi.cs +++ b/src/Kyoo.Core/Views/EpisodeApi.cs @@ -22,216 +22,145 @@ using System.Linq; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Exceptions; +using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Permissions; +using Kyoo.Abstractions.Models.Utils; using Kyoo.Core.Models.Options; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Core.Api { - [Route("api/episode")] + /// + /// Information about one or multiple . + /// [Route("api/episodes")] + [Route("api/episode", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(EpisodeApi))] - public class EpisodeApi : CrudApi + [ApiDefinition("Episodes", Group = ResourcesGroup)] + public class EpisodeApi : CrudThumbsApi { + /// + /// The library manager used to modify or retrieve information in the data store. + /// private readonly ILibraryManager _libraryManager; - private readonly IThumbnailsManager _thumbnails; - private readonly IFileSystem _files; + /// + /// Create a new . + /// + /// + /// The library manager used to modify or retrieve information in the data store. + /// + /// The file manager used to send images. + /// The thumbnail manager used to retrieve images paths. + /// + /// Options used to retrieve the base URL of Kyoo. + /// public EpisodeApi(ILibraryManager libraryManager, - IOptions options, IFileSystem files, - IThumbnailsManager thumbnails) - : base(libraryManager.EpisodeRepository, options.Value.PublicUrl) + IThumbnailsManager thumbnails, + IOptions options) + : base(libraryManager.EpisodeRepository, files, thumbnails, options.Value.PublicUrl) { _libraryManager = libraryManager; - _files = files; - _thumbnails = thumbnails; } - [HttpGet("{episodeID:int}/show")] + /// + /// Get episode's show + /// + /// + /// Get the show that this episode is part of. + /// + /// The ID or slug of the . + /// The show that contains this episode. + /// No episode with the given ID or slug could be found. + [HttpGet("{identifier:id}/show")] [PartialPermission(Kind.Read)] - public async Task> GetShow(int episodeID) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetShow(Identifier identifier) { - Show ret = await _libraryManager.GetOrDefault(x => x.Episodes.Any(y => y.ID == episodeID)); + Show ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn(x => x.Episodes)); if (ret == null) return NotFound(); return ret; } - [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/show")] + /// + /// Get episode's season + /// + /// + /// Get the season that this episode is part of. + /// + /// The ID or slug of the . + /// The season that contains this episode. + /// The episode is not part of a season. + /// No episode with the given ID or slug could be found. + [HttpGet("{identifier:id}/season")] [PartialPermission(Kind.Read)] - public async Task> GetShow(string showSlug, int seasonNumber, int episodeNumber) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetSeason(Identifier identifier) { - Show ret = await _libraryManager.GetOrDefault(showSlug); - if (ret == null) - return NotFound(); - return ret; + Season ret = await _libraryManager.GetOrDefault(identifier.IsContainedIn(x => x.Episodes)); + if (ret != null) + return ret; + Episode episode = await identifier.Match( + id => _libraryManager.GetOrDefault(id), + slug => _libraryManager.GetOrDefault(slug) + ); + return episode == null + ? NotFound() + : NoContent(); } - [HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/show")] + /// + /// Get tracks + /// + /// + /// List the tracks (video, audio and subtitles) available for this episode. + /// This endpoint provide the list of raw tracks, without transcode on it. To get a schema easier to watch + /// on a player, see the [/watch endpoint](#/watch). + /// + /// The ID or slug of the . + /// A key to sort tracks by. + /// An optional list of filters. + /// The number of tracks to return. + /// An optional track's ID to start the query from this specific item. + /// A page of tracks. + /// The filters or the sort parameters are invalid. + /// No track with the given ID or slug could be found. + /// TODO fix the /watch endpoint link (when operations ID are specified). + [HttpGet("{identifier:id}/tracks")] + [HttpGet("{identifier:id}/track", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] - public async Task> GetShow(int showID, int seasonNumber, int episodeNumber) - { - Show ret = await _libraryManager.GetOrDefault(showID); - if (ret == null) - return NotFound(); - return ret; - } - - [HttpGet("{episodeID:int}/season")] - [PartialPermission(Kind.Read)] - public async Task> GetSeason(int episodeID) - { - Season ret = await _libraryManager.GetOrDefault(x => x.Episodes.Any(y => y.ID == episodeID)); - if (ret == null) - return NotFound(); - return ret; - } - - [HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/season")] - [PartialPermission(Kind.Read)] - public async Task> GetSeason(string showSlug, int seasonNumber, int episodeNumber) - { - try - { - return await _libraryManager.Get(showSlug, seasonNumber); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("{showID:int}-{seasonNumber:int}e{episodeNumber:int}/season")] - [PartialPermission(Kind.Read)] - public async Task> GetSeason(int showID, int seasonNumber, int episodeNumber) - { - try - { - return await _libraryManager.Get(showID, seasonNumber); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("{episodeID:int}/track")] - [HttpGet("{episodeID:int}/tracks")] - [PartialPermission(Kind.Read)] - public async Task>> GetEpisode(int episodeID, + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetEpisode(Identifier identifier, [FromQuery] string sortBy, - [FromQuery] int afterID, [FromQuery] Dictionary where, - [FromQuery] int limit = 30) + [FromQuery] int limit = 30, + [FromQuery] int? afterID = null) { try { ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Episode.ID == episodeID), + ApiHelper.ParseWhere(where, identifier.Matcher(x => x.EpisodeID, x => x.Episode.Slug)), new Sort(sortBy), new Pagination(limit, afterID)); - if (!resources.Any() && await _libraryManager.GetOrDefault(episodeID) == null) + if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); return Page(resources, limit); } catch (ArgumentException ex) { - return BadRequest(new { Error = ex.Message }); - } - } - - [HttpGet("{showID:int}-s{seasonNumber:int}e{episodeNumber:int}/track")] - [HttpGet("{showID:int}-s{seasonNumber:int}e{episodeNumber:int}/tracks")] - [PartialPermission(Kind.Read)] - public async Task>> GetEpisode(int showID, - int seasonNumber, - int episodeNumber, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 30) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Episode.ShowID == showID - && x.Episode.SeasonNumber == seasonNumber - && x.Episode.EpisodeNumber == episodeNumber), - new Sort(sortBy), - new Pagination(limit, afterID)); - - if (!resources.Any() && await _libraryManager.GetOrDefault(showID, seasonNumber, episodeNumber) == null) - return NotFound(); - return Page(resources, limit); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - - [HttpGet("{slug}-s{seasonNumber:int}e{episodeNumber:int}/track")] - [HttpGet("{slug}-s{seasonNumber:int}e{episodeNumber:int}/tracks")] - [PartialPermission(Kind.Read)] - public async Task>> GetEpisode(string slug, - int seasonNumber, - int episodeNumber, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 30) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Episode.Show.Slug == slug - && x.Episode.SeasonNumber == seasonNumber - && x.Episode.EpisodeNumber == episodeNumber), - new Sort(sortBy), - new Pagination(limit, afterID)); - - if (!resources.Any() && await _libraryManager.GetOrDefault(slug, seasonNumber, episodeNumber) == null) - return NotFound(); - return Page(resources, limit); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - - [HttpGet("{id:int}/thumbnail")] - [HttpGet("{id:int}/backdrop")] - public async Task GetThumb(int id) - { - try - { - Episode episode = await _libraryManager.Get(id); - return _files.FileResult(await _thumbnails.GetImagePath(episode, Images.Thumbnail)); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - } - - [HttpGet("{slug}/thumbnail")] - [HttpGet("{slug}/backdrop")] - public async Task GetThumb(string slug) - { - try - { - Episode episode = await _libraryManager.Get(slug); - return _files.FileResult(await _thumbnails.GetImagePath(episode, Images.Thumbnail)); - } - catch (ItemNotFoundException) - { - return NotFound(); + return BadRequest(new RequestError(ex.Message)); } } } diff --git a/src/Kyoo.Core/Views/SeasonApi.cs b/src/Kyoo.Core/Views/SeasonApi.cs index 99bac0dc..75da206d 100644 --- a/src/Kyoo.Core/Views/SeasonApi.cs +++ b/src/Kyoo.Core/Views/SeasonApi.cs @@ -40,7 +40,7 @@ namespace Kyoo.Core.Api [Route("api/season", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(SeasonApi))] - [ApiDefinition("Seasons", Group = ResourceGroup)] + [ApiDefinition("Seasons", Group = ResourcesGroup)] public class SeasonApi : CrudThumbsApi { /// diff --git a/src/Kyoo.Core/Views/ShowApi.cs b/src/Kyoo.Core/Views/ShowApi.cs index 306de40b..42bde2ae 100644 --- a/src/Kyoo.Core/Views/ShowApi.cs +++ b/src/Kyoo.Core/Views/ShowApi.cs @@ -44,7 +44,7 @@ namespace Kyoo.Core.Api [Route("api/movies", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(ShowApi))] - [ApiDefinition("Shows", Group = ResourceGroup)] + [ApiDefinition("Shows", Group = ResourcesGroup)] public class ShowApi : CrudThumbsApi { ///