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