// 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.IO; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Exceptions; 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 { /// /// Information about one or multiple . /// [Route("api/shows")] [Route("api/show", Order = AlternativeRoute)] [Route("api/movie", Order = AlternativeRoute)] [Route("api/movies", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(Show))] [ApiDefinition("Shows", Group = ResourcesGroup)] public class ShowApi : CrudThumbsApi { /// /// The library manager used to modify or retrieve information in the data store. /// private readonly ILibraryManager _libraryManager; /// /// The file manager used to send images and fonts. /// private readonly IFileSystem _files; /// /// The base URL of Kyoo. This will be used to create links for images and /// . /// private readonly Uri _baseURL; /// /// Create a new . /// /// /// The library manager used to modify or retrieve information about the data store. /// /// The file manager used to send images and fonts. /// The thumbnail manager used to retrieve images paths. /// /// Options used to retrieve the base URL of Kyoo. /// public ShowApi(ILibraryManager libraryManager, IFileSystem files, IThumbnailsManager thumbs, IOptions options) : base(libraryManager.ShowRepository, files, thumbs) { _libraryManager = libraryManager; _files = files; _baseURL = options.Value.PublicUrl; } /// /// Get seasons of this show /// /// /// List the seasons that are part of the specified show. /// /// The ID or slug of the . /// A key to sort seasons by. /// An optional list of filters. /// The number of seasons to return. /// An optional season's ID to start the query from this specific item. /// A page of seasons. /// The filters or the sort parameters are invalid. /// No show with the given ID or slug could be found. [HttpGet("{identifier:id}/seasons")] [HttpGet("{identifier:id}/season", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetSeasons(Identifier identifier, [FromQuery] string sortBy, [FromQuery] Dictionary where, [FromQuery] int limit = 20, [FromQuery] int? afterID = null) { try { ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, identifier.Matcher(x => x.ShowID, x => x.Show.Slug)), new Sort(sortBy), new Pagination(limit, afterID) ); if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); return Page(resources, limit); } catch (ArgumentException ex) { return BadRequest(new RequestError(ex.Message)); } } /// /// Get episodes of this show /// /// /// List the episodes that are part of the specified show. /// /// The ID or slug of the . /// A key to sort episodes by. /// An optional list of filters. /// The number of episodes to return. /// An optional episode's ID to start the query from this specific item. /// A page of episodes. /// The filters or the sort parameters are invalid. /// No show with the given ID or slug could be found. [HttpGet("{identifier:id}/episodes")] [HttpGet("{identifier:id}/episode", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetEpisodes(Identifier identifier, [FromQuery] string sortBy, [FromQuery] Dictionary where, [FromQuery] int limit = 50, [FromQuery] int? afterID = null) { try { ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, identifier.Matcher(x => x.ShowID, x => x.Show.Slug)), new Sort(sortBy), new Pagination(limit, afterID) ); if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); return Page(resources, limit); } catch (ArgumentException ex) { return BadRequest(new RequestError(ex.Message)); } } /// /// Get staff /// /// /// List staff members that made this show. /// /// The ID or slug of the . /// A key to sort staff members by. /// An optional list of filters. /// The number of people to return. /// An optional person's ID to start the query from this specific item. /// A page of people. /// The filters or the sort parameters are invalid. /// No show with the given ID or slug could be found. [HttpGet("{identifier:id}/staff")] [HttpGet("{identifier:id}/people", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetPeople(Identifier identifier, [FromQuery] string sortBy, [FromQuery] Dictionary where, [FromQuery] int limit = 30, [FromQuery] int? afterID = null) { try { Expression> whereQuery = ApiHelper.ParseWhere(where); Sort sort = new(sortBy); Pagination pagination = new(limit, afterID); ICollection resources = await identifier.Match( id => _libraryManager.GetPeopleFromShow(id, whereQuery, sort, pagination), slug => _libraryManager.GetPeopleFromShow(slug, whereQuery, sort, pagination) ); return Page(resources, limit); } catch (ItemNotFoundException) { return NotFound(); } catch (ArgumentException ex) { return BadRequest(new RequestError(ex.Message)); } } /// /// Get genres of this show /// /// /// List the genres that represent this show. /// /// The ID or slug of the . /// A key to sort genres by. /// An optional list of filters. /// The number of genres to return. /// An optional genre's ID to start the query from this specific item. /// A page of genres. /// The filters or the sort parameters are invalid. /// No show with the given ID or slug could be found. [HttpGet("{identifier:id}/genres")] [HttpGet("{identifier:id}/genre", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetGenres(Identifier identifier, [FromQuery] string sortBy, [FromQuery] Dictionary where, [FromQuery] int limit = 30, [FromQuery] int? afterID = null) { try { ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Shows)), new Sort(sortBy), new Pagination(limit, afterID) ); if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); return Page(resources, limit); } catch (ArgumentException ex) { return BadRequest(new RequestError(ex.Message)); } } /// /// Get studio that made the show /// /// /// Get the studio that made the show. /// /// The ID or slug of the . /// The studio that made the show. /// No show with the given ID or slug could be found. [HttpGet("{identifier:id}/studio")] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetStudio(Identifier identifier) { Studio studio = await _libraryManager.GetOrDefault(identifier.IsContainedIn(x => x.Shows)); if (studio == null) return NotFound(); return studio; } /// /// Get libraries containing this show /// /// /// List the libraries that contain this show. If this show is contained in a collection that is contained in /// a library, this library will be returned too. /// /// The ID or slug of the . /// A key to sort libraries by. /// An optional list of filters. /// The number of libraries to return. /// An optional library's ID to start the query from this specific item. /// A page of libraries. /// The filters or the sort parameters are invalid. /// No show with the given ID or slug could be found. [HttpGet("{identifier:id}/libraries")] [HttpGet("{identifier:id}/library", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetLibraries(Identifier identifier, [FromQuery] string sortBy, [FromQuery] Dictionary where, [FromQuery] int limit = 30, [FromQuery] int? afterID = null) { try { ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Shows)), new Sort(sortBy), new Pagination(limit, afterID) ); if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); return Page(resources, limit); } catch (ArgumentException ex) { return BadRequest(new RequestError(ex.Message)); } } /// /// Get collections containing this show /// /// /// List the collections that contain this show. /// /// The ID or slug of the . /// A key to sort collections by. /// An optional list of filters. /// The number of collections to return. /// An optional collection's ID to start the query from this specific item. /// A page of collections. /// The filters or the sort parameters are invalid. /// No show with the given ID or slug could be found. [HttpGet("{identifier:id}/collections")] [HttpGet("{identifier:id}/collection", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetCollections(Identifier identifier, [FromQuery] string sortBy, [FromQuery] Dictionary where, [FromQuery] int limit = 30, [FromQuery] int? afterID = null) { try { ICollection resources = await _libraryManager.GetAll( ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Shows)), new Sort(sortBy), new Pagination(limit, afterID) ); if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) return NotFound(); return Page(resources, limit); } catch (ArgumentException ex) { return BadRequest(new RequestError(ex.Message)); } } /// /// List fonts /// /// /// List available fonts for this show. /// /// The ID or slug of the . /// An object containing the name of the font followed by the url to retrieve it. [HttpGet("{identifier:id}/fonts")] [HttpGet("{identifier:id}/font", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetFonts(Identifier identifier) { Show show = await identifier.Match( id => _libraryManager.GetOrDefault(id), slug => _libraryManager.GetOrDefault(slug) ); if (show == null) return NotFound(); string path = _files.Combine(await _files.GetExtraDirectory(show), "Attachments"); return (await _files.ListFiles(path)) .ToDictionary( Path.GetFileNameWithoutExtension, x => $"{_baseURL}api/shows/{identifier}/fonts/{Path.GetFileName(x)}" ); } /// /// Get font /// /// /// Get a font file that is used in subtitles of this show. /// /// The ID or slug of the . /// The name of the font to retrieve (with it's file extension). /// A page of collections. /// The font name is invalid. /// No show with the given ID/slug could be found or the font does not exist. [HttpGet("{identifier:id}/fonts/{font}")] [HttpGet("{identifier:id}/font/{font}", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetFont(Identifier identifier, string font) { if (font.Contains('/') || font.Contains('\\')) return BadRequest(new RequestError("Invalid font name.")); Show show = await identifier.Match( id => _libraryManager.GetOrDefault(id), slug => _libraryManager.GetOrDefault(slug) ); if (show == null) return NotFound(); string path = _files.Combine(await _files.GetExtraDirectory(show), "Attachments", font); return _files.FileResult(path); } } }